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 }