lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

mod.rs (19009B)


      1 pub mod decode;
      2 pub mod encode;
      3 pub mod list_sets;
      4 
      5 #[cfg(test)]
      6 mod tests {
      7     use crate::error::EventEncodeError;
      8     #[cfg(feature = "json")]
      9     use crate::error::EventParseError;
     10     #[cfg(feature = "json")]
     11     use crate::farm::decode::{farm_from_event, parsed_from_event};
     12     use crate::farm::encode::{farm_build_tags, farm_ref_tags};
     13     use crate::farm::list_sets::{
     14         farm_members_list_set, farm_operational_listings_list_set_from_listings,
     15         farm_plots_list_set_from_plots, member_of_farms_list_set,
     16     };
     17     use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
     18     #[cfg(feature = "json")]
     19     use radroots_event::envelope::kind::KIND_FARM;
     20     use radroots_event::farm::plot::Plot;
     21     use radroots_event::farm::{Farm, FarmPublicLocation, FarmRef};
     22     use radroots_event::id::{DTag, InventoryBinId};
     23     use radroots_event::listing::operational::{
     24         OperationalListing, OperationalListingBin, OperationalListingProduct,
     25     };
     26 
     27     #[cfg(feature = "json")]
     28     const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     29     #[cfg(feature = "json")]
     30     const AUTHOR: &str = crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX;
     31     #[cfg(feature = "json")]
     32     const EVENT_SIG: &str = concat!(
     33         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
     34         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
     35     );
     36 
     37     fn d_tag(raw: &str) -> DTag {
     38         raw.parse().unwrap()
     39     }
     40 
     41     fn bin_id(raw: &str) -> InventoryBinId {
     42         raw.parse().unwrap()
     43     }
     44 
     45     #[test]
     46     fn farm_tags_include_required_fields() {
     47         let farm = Farm {
     48             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
     49             name: "Test Farm".to_string(),
     50             about: None,
     51             website: None,
     52             picture: None,
     53             banner: None,
     54             location: Some(FarmPublicLocation {
     55                 primary: "Test Farm".to_string(),
     56                 city: Some("Santa Cruz".to_string()),
     57                 region: Some("California".to_string()),
     58                 country: Some("US".to_string()),
     59                 geohash: "9q8yy".to_string(),
     60             }),
     61             tags: Some(vec!["orchard".to_string()]),
     62         };
     63 
     64         let tags = farm_build_tags(&farm).expect("tags");
     65         assert!(tags.iter().any(|tag| tag.first() == Some(&"d".to_string())));
     66         assert!(tags.iter().any(|tag| tag.first() == Some(&"t".to_string())));
     67         assert!(tags.iter().any(|tag| tag.first() == Some(&"g".to_string())));
     68     }
     69 
     70     #[test]
     71     fn farm_tags_allow_missing_optional_fields() {
     72         let farm = Farm {
     73             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
     74             name: "Test Farm".to_string(),
     75             about: None,
     76             website: None,
     77             picture: None,
     78             banner: None,
     79             location: None,
     80             tags: None,
     81         };
     82 
     83         let tags = farm_build_tags(&farm).expect("tags without optional fields");
     84         assert!(
     85             tags.iter()
     86                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("d"))
     87         );
     88         assert!(
     89             !tags
     90                 .iter()
     91                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("t"))
     92         );
     93         assert!(
     94             !tags
     95                 .iter()
     96                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("g"))
     97         );
     98     }
     99 
    100     #[test]
    101     fn farm_build_tags_rejects_invalid_d_tag() {
    102         let farm = Farm {
    103             d_tag: "farm:invalid".to_string(),
    104             name: "Test Farm".to_string(),
    105             about: None,
    106             website: None,
    107             picture: None,
    108             banner: None,
    109             location: None,
    110             tags: None,
    111         };
    112 
    113         let err = farm_build_tags(&farm).expect_err("expected invalid d_tag");
    114         assert!(matches!(err, EventEncodeError::InvalidField("d_tag")));
    115     }
    116 
    117     #[test]
    118     #[cfg_attr(coverage_nightly, coverage(off))]
    119     #[cfg(feature = "json")]
    120     fn farm_decode_rejects_empty_d_tag_and_content() {
    121         let farm = Farm {
    122             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    123             name: "Test Farm".to_string(),
    124             about: None,
    125             website: None,
    126             picture: None,
    127             banner: None,
    128             location: None,
    129             tags: None,
    130         };
    131         let content = serde_json::to_string(&farm).expect("farm content");
    132 
    133         let empty_d = farm_from_event(
    134             KIND_FARM,
    135             &[vec!["d".to_string(), " ".to_string()]],
    136             &content,
    137         )
    138         .expect_err("empty d tag");
    139         assert!(matches!(
    140             empty_d,
    141             crate::error::EventParseError::InvalidTag("d")
    142         ));
    143 
    144         let empty_content = farm_from_event(
    145             KIND_FARM,
    146             &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    147             " ",
    148         )
    149         .expect_err("empty content");
    150         assert!(matches!(
    151             empty_content,
    152             crate::error::EventParseError::InvalidJson("content")
    153         ));
    154 
    155         let with_empty_tag = farm_from_event(
    156             KIND_FARM,
    157             &[
    158                 Vec::new(),
    159                 vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()],
    160             ],
    161             &content,
    162         )
    163         .expect("empty unrelated tags are ignored");
    164         assert_eq!(with_empty_tag.name, "Test Farm");
    165 
    166         let parsed_error = parsed_from_event(
    167             EVENT_ID.to_string(),
    168             AUTHOR.to_string(),
    169             42,
    170             KIND_FARM + 1,
    171             content,
    172             vec![vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    173             EVENT_SIG.to_string(),
    174         )
    175         .expect_err("parsed wrapper propagates decode failures");
    176         assert!(matches!(
    177             parsed_error,
    178             crate::error::EventParseError::InvalidKind { .. }
    179         ));
    180     }
    181 
    182     #[test]
    183     fn farm_ref_tags_include_p_and_a() {
    184         let farm = FarmRef {
    185             pubkey: "farm_pubkey".to_string(),
    186             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    187         };
    188 
    189         let tags = farm_ref_tags(&farm).expect("farm ref tags");
    190         let has_a = tags
    191             .iter()
    192             .any(|tag| tag.first().map(|v| v.as_str()) == Some("a"));
    193         let has_p = tags
    194             .iter()
    195             .any(|tag| tag.first().map(|v| v.as_str()) == Some("p"));
    196         assert!(has_a);
    197         assert!(has_p);
    198 
    199         let err = farm_ref_tags(&FarmRef {
    200             pubkey: "farm_pubkey".to_string(),
    201             d_tag: "invalid".to_string(),
    202         })
    203         .expect_err("expected invalid farm.d_tag");
    204         assert!(matches!(err, EventEncodeError::InvalidField("farm.d_tag")));
    205     }
    206 
    207     #[test]
    208     fn farm_encode_rejects_empty_required_fields() {
    209         let mut farm = Farm {
    210             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    211             name: "Test Farm".to_string(),
    212             about: None,
    213             website: None,
    214             picture: None,
    215             banner: None,
    216             location: Some(FarmPublicLocation {
    217                 primary: "Test Farm".to_string(),
    218                 city: Some("Santa Cruz".to_string()),
    219                 region: Some("California".to_string()),
    220                 country: Some("US".to_string()),
    221                 geohash: "9q8yy".to_string(),
    222             }),
    223             tags: None,
    224         };
    225 
    226         farm.d_tag = " ".to_string();
    227         let err = farm_build_tags(&farm).expect_err("expected empty d_tag");
    228         assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag")));
    229 
    230         farm.d_tag = "AAAAAAAAAAAAAAAAAAAAAA".to_string();
    231         farm.name = " ".to_string();
    232         let err = farm_build_tags(&farm).expect_err("expected empty name");
    233         assert!(matches!(err, EventEncodeError::EmptyRequiredField("name")));
    234 
    235         farm.name = "Test Farm".to_string();
    236         farm.location.as_mut().expect("location").geohash = " ".to_string();
    237         let err = farm_build_tags(&farm).expect_err("expected empty geohash");
    238         assert!(matches!(
    239             err,
    240             EventEncodeError::EmptyRequiredField("location.geohash")
    241         ));
    242 
    243         farm.location.as_mut().expect("location").geohash = "9q8yy6".to_string();
    244         let err = farm_build_tags(&farm).expect_err("expected invalid geohash");
    245         assert!(matches!(
    246             err,
    247             EventEncodeError::InvalidField("location.geohash")
    248         ));
    249 
    250         let err = farm_ref_tags(&FarmRef {
    251             pubkey: " ".to_string(),
    252             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    253         })
    254         .expect_err("expected empty farm.pubkey");
    255         assert!(matches!(
    256             err,
    257             EventEncodeError::EmptyRequiredField("farm.pubkey")
    258         ));
    259 
    260         let err = farm_ref_tags(&FarmRef {
    261             pubkey: "farm_pubkey".to_string(),
    262             d_tag: " ".to_string(),
    263         })
    264         .expect_err("expected empty farm.d_tag");
    265         assert!(matches!(
    266             err,
    267             EventEncodeError::EmptyRequiredField("farm.d_tag")
    268         ));
    269 
    270         let mut farm = Farm {
    271             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    272             name: "Test Farm".to_string(),
    273             about: None,
    274             website: None,
    275             picture: None,
    276             banner: None,
    277             location: Some(FarmPublicLocation {
    278                 primary: " ".to_string(),
    279                 city: Some("null".to_string()),
    280                 region: None,
    281                 country: None,
    282                 geohash: "9q8yy".to_string(),
    283             }),
    284             tags: None,
    285         };
    286         let err = farm_build_tags(&farm).expect_err("expected missing locality");
    287         assert!(matches!(
    288             err,
    289             EventEncodeError::EmptyRequiredField("location.locality")
    290         ));
    291 
    292         let location = farm.location.as_mut().expect("location");
    293         location.primary = "Test Farm".to_string();
    294         location.city = Some("Santa Cruz".to_string());
    295         let tags = farm_build_tags(&farm).expect("valid location after locality repair");
    296         assert!(
    297             tags.iter()
    298                 .any(|tag| tag.first().map(|value| value.as_str()) == Some("g"))
    299         );
    300     }
    301 
    302     #[test]
    303     #[cfg_attr(coverage_nightly, coverage(off))]
    304     #[cfg(feature = "json")]
    305     fn farm_decode_rejects_private_location_and_ops_shapes() {
    306         let farm = Farm {
    307             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    308             name: "Test Farm".to_string(),
    309             about: None,
    310             website: None,
    311             picture: None,
    312             banner: None,
    313             location: Some(FarmPublicLocation {
    314                 primary: "Test Farm".to_string(),
    315                 city: Some("Santa Cruz".to_string()),
    316                 region: Some("California".to_string()),
    317                 country: Some("US".to_string()),
    318                 geohash: "9q8yy".to_string(),
    319             }),
    320             tags: None,
    321         };
    322         let content = serde_json::to_string(&farm).expect("farm content");
    323         let tags = vec![
    324             vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()],
    325             vec!["g".to_string(), "9q8yy".to_string()],
    326         ];
    327         let parsed = parsed_from_event(
    328             EVENT_ID.to_string(),
    329             AUTHOR.to_string(),
    330             42,
    331             KIND_FARM,
    332             content.clone(),
    333             tags,
    334             EVENT_SIG.to_string(),
    335         )
    336         .expect("parsed farm");
    337         assert_eq!(parsed.event.signature_hex(), EVENT_SIG);
    338         assert_eq!(parsed.data.data.name, "Test Farm");
    339 
    340         for (tag, expected) in [
    341             (vec!["g".to_string()], "g"),
    342             (vec!["g".to_string(), "9q8ya".to_string()], "g"),
    343             (vec!["dd".to_string(), "secret".to_string()], "dd"),
    344             (vec!["dd.lat".to_string(), "1".to_string()], "dd.lat"),
    345             (vec!["dd.lon".to_string(), "1".to_string()], "dd.lon"),
    346             (vec!["l".to_string(), "private".to_string()], "l"),
    347             (vec!["L".to_string(), "private".to_string()], "L"),
    348         ] {
    349             let tags = vec![
    350                 vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()],
    351                 tag,
    352             ];
    353             let err = farm_from_event(KIND_FARM, &tags, &content).unwrap_err();
    354             assert!(matches!(err, EventParseError::InvalidTag(found) if found == expected));
    355         }
    356 
    357         let invalid_geohash_content = r#"{"d_tag":"AAAAAAAAAAAAAAAAAAAAAA","name":"Test Farm","location":{"primary":"Test Farm","city":"Santa Cruz","region":"California","country":"US","geohash":"9q8ya"}}"#;
    358         let err = farm_from_event(
    359             KIND_FARM,
    360             &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    361             invalid_geohash_content,
    362         )
    363         .unwrap_err();
    364         assert!(matches!(err, EventParseError::InvalidTag("g")));
    365 
    366         let missing_locality_content = r#"{"d_tag":"AAAAAAAAAAAAAAAAAAAAAA","name":"Test Farm","location":{"primary":" ","city":"null","region":null,"country":null,"geohash":"9q8yy"}}"#;
    367         let err = farm_from_event(
    368             KIND_FARM,
    369             &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    370             missing_locality_content,
    371         )
    372         .unwrap_err();
    373         assert!(matches!(err, EventParseError::InvalidTag("g")));
    374 
    375         let err = farm_from_event(
    376             KIND_FARM,
    377             &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    378             "[]",
    379         )
    380         .unwrap_err();
    381         assert!(matches!(err, EventParseError::InvalidJson("content")));
    382 
    383         for key in [
    384             "workspace",
    385             "farm_group_id",
    386             "document_id",
    387             "document_kind",
    388             "crdt_backend",
    389             "encoded_change",
    390             "semantic_kind",
    391             "owner_document_kind",
    392             "owner_document_id",
    393             "relays",
    394             "media_servers",
    395             "supported_kinds",
    396             "protocol_version",
    397         ] {
    398             let content =
    399                 format!(r#"{{"d_tag":"AAAAAAAAAAAAAAAAAAAAAA","name":"Test Farm","{key}":"x"}}"#);
    400             let err = farm_from_event(
    401                 KIND_FARM,
    402                 &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    403                 &content,
    404             )
    405             .unwrap_err();
    406             assert!(matches!(err, EventParseError::InvalidJson("content")));
    407         }
    408 
    409         for key in [
    410             "gcs",
    411             "lat",
    412             "lng",
    413             "lon",
    414             "point",
    415             "polygon",
    416             "coordinates",
    417             "accuracy",
    418             "altitude",
    419             "label",
    420             "tag_0",
    421         ] {
    422             let content = format!(
    423                 r#"{{"d_tag":"AAAAAAAAAAAAAAAAAAAAAA","name":"Test Farm","location":{{"primary":"Test Farm","city":"Santa Cruz","region":"California","country":"US","geohash":"9q8yy","{key}":"x"}}}}"#
    424             );
    425             let err = farm_from_event(
    426                 KIND_FARM,
    427                 &[vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAA".to_string()]],
    428                 &content,
    429             )
    430             .unwrap_err();
    431             assert!(matches!(err, EventParseError::InvalidJson("content")));
    432         }
    433     }
    434 
    435     #[test]
    436     fn farm_list_sets_include_expected_tags() {
    437         let members = farm_members_list_set("AAAAAAAAAAAAAAAAAAAAAA", ["owner_pubkey"])
    438             .expect("members list");
    439         assert_eq!(members.d_tag, "farm:AAAAAAAAAAAAAAAAAAAAAA:members");
    440         assert_eq!(members.entries.len(), 1);
    441         assert_eq!(members.entries[0].tag, "p");
    442 
    443         let claims = member_of_farms_list_set(["farm_pubkey"]).expect("claims list");
    444         assert_eq!(claims.d_tag, "member_of.farms");
    445         assert_eq!(claims.entries.len(), 1);
    446         assert_eq!(claims.entries[0].tag, "p");
    447     }
    448 
    449     #[test]
    450     fn farm_plots_list_set_uses_plot_addresses() {
    451         let plots = vec![Plot {
    452             d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
    453             farm: FarmRef {
    454                 pubkey: "farm_pubkey".to_string(),
    455                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    456             },
    457             name: "Plot 1".to_string(),
    458             about: None,
    459             location: None,
    460             tags: None,
    461         }];
    462 
    463         let plots_list =
    464             farm_plots_list_set_from_plots("AAAAAAAAAAAAAAAAAAAAAA", "farm_pubkey", &plots)
    465                 .expect("plots list");
    466         assert_eq!(plots_list.d_tag, "farm:AAAAAAAAAAAAAAAAAAAAAA:plots");
    467         assert_eq!(plots_list.entries.len(), 1);
    468         assert_eq!(plots_list.entries[0].tag, "a");
    469         assert_eq!(
    470             plots_list.entries[0].values[0],
    471             "30350:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ"
    472         );
    473     }
    474 
    475     #[test]
    476     fn farm_listings_list_set_uses_listing_addresses() {
    477         let listings = vec![OperationalListing {
    478             d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"),
    479             published_at: None,
    480             farm: FarmRef {
    481                 pubkey: "farm_pubkey".to_string(),
    482                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
    483             },
    484             product: OperationalListingProduct {
    485                 key: "coffee".to_string(),
    486                 title: "Coffee".to_string(),
    487                 category: "coffee".to_string(),
    488                 summary: None,
    489                 process: None,
    490                 lot: None,
    491                 location: None,
    492                 profile: None,
    493                 year: None,
    494             },
    495             primary_bin_id: bin_id("bin-1"),
    496             bins: vec![OperationalListingBin {
    497                 bin_id: bin_id("bin-1"),
    498                 quantity: Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(),
    499                 price_per_canonical_unit: QuantityPrice::try_new(
    500                     Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(),
    501                     Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(),
    502                 )
    503                 .unwrap(),
    504                 display_amount: None,
    505                 display_unit: None,
    506                 display_label: None,
    507                 display_price: None,
    508                 display_price_unit: None,
    509             }],
    510             resource_area: None,
    511             plot: None,
    512             discounts: None,
    513             inventory_available: None,
    514             availability: None,
    515             delivery_method: None,
    516             location: None,
    517             images: None,
    518         }];
    519 
    520         let listings_list = farm_operational_listings_list_set_from_listings(
    521             "AAAAAAAAAAAAAAAAAAAAAA",
    522             "farm_pubkey",
    523             &listings,
    524         )
    525         .expect("listings list");
    526         assert_eq!(listings_list.d_tag, "farm:AAAAAAAAAAAAAAAAAAAAAA:listings");
    527         assert_eq!(listings_list.entries.len(), 1);
    528         assert_eq!(listings_list.entries[0].tag, "a");
    529         assert_eq!(
    530             listings_list.entries[0].values[0],
    531             "30402:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAg"
    532         );
    533     }
    534 }