listing.rs (42133B)
1 #![cfg(feature = "serde_json")] 2 3 use radroots_core::{ 4 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountScope, 5 RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, 6 RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, 7 }; 8 use radroots_events::{ 9 RadrootsNostrEvent, 10 farm::RadrootsFarmRef, 11 ids::{RadrootsDTag, RadrootsInventoryBinId}, 12 kinds::{ 13 KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_PLOT, KIND_POST, KIND_RESOURCE_AREA, 14 }, 15 listing::{ 16 RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, 17 RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize, 18 RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus, 19 }, 20 plot::RadrootsPlotRef, 21 resource_area::RadrootsResourceAreaRef, 22 tags::{TAG_D, TAG_PUBLISHED_AT}, 23 }; 24 use radroots_events_codec::error::{EventEncodeError, EventParseError}; 25 use radroots_events_codec::listing::decode::{ 26 data_from_event, data_from_nostr_event, listing_from_event, parsed_from_event, 27 parsed_from_nostr_event, 28 }; 29 use radroots_events_codec::listing::encode::{ 30 listing_build_tags, to_wire_parts, to_wire_parts_with_kind, 31 }; 32 use radroots_events_codec::listing::tags::{ 33 ListingTagOptions, listing_tags_full, listing_tags_with_options, 34 }; 35 use std::str::FromStr; 36 37 fn listing_d_tag(raw: &str) -> RadrootsDTag { 38 raw.parse().unwrap() 39 } 40 41 fn bin_id(raw: &str) -> RadrootsInventoryBinId { 42 raw.parse().unwrap() 43 } 44 45 fn sample_listing_tags() -> Vec<Vec<String>> { 46 listing_build_tags(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg")).unwrap() 47 } 48 49 fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) { 50 tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some(name)); 51 } 52 53 fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<&str>) { 54 let tag = tags 55 .iter_mut() 56 .find(|tag| tag.first().map(|value| value.as_str()) == Some(name)) 57 .expect("tag"); 58 *tag = replacement.into_iter().map(str::to_string).collect(); 59 } 60 61 fn assert_missing_tag(tags: Vec<Vec<String>>, expected: &'static str) { 62 match listing_from_event(KIND_LISTING, &tags, "# Widget") { 63 Err(EventParseError::MissingTag(tag)) => assert_eq!(tag, expected), 64 other => panic!("expected missing tag {expected}: {other:?}"), 65 } 66 } 67 68 fn assert_invalid_tag(tags: Vec<Vec<String>>, expected: &'static str) { 69 match listing_from_event(KIND_LISTING, &tags, "# Widget") { 70 Err(EventParseError::InvalidTag(tag)) => assert_eq!(tag, expected), 71 other => panic!("expected invalid tag {expected}: {other:?}"), 72 } 73 } 74 75 fn sample_listing(d_tag: &str) -> RadrootsListing { 76 let quantity = 77 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each); 78 let price = RadrootsCoreQuantityPrice::new( 79 RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD), 80 quantity.clone(), 81 ); 82 83 RadrootsListing { 84 d_tag: listing_d_tag(d_tag), 85 published_at: None, 86 farm: RadrootsFarmRef { 87 pubkey: "farm_pubkey".to_string(), 88 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 89 }, 90 product: RadrootsListingProduct { 91 key: "sku".to_string(), 92 title: "Widget".to_string(), 93 category: "Tools".to_string(), 94 summary: None, 95 process: None, 96 lot: None, 97 location: None, 98 profile: None, 99 year: None, 100 }, 101 primary_bin_id: bin_id("bin-1"), 102 bins: vec![RadrootsListingBin { 103 bin_id: bin_id("bin-1"), 104 quantity, 105 price_per_canonical_unit: price, 106 display_amount: None, 107 display_unit: None, 108 display_label: None, 109 display_price: None, 110 display_price_unit: None, 111 }], 112 resource_area: None, 113 plot: None, 114 discounts: None, 115 inventory_available: None, 116 availability: None, 117 delivery_method: None, 118 location: None, 119 images: None, 120 } 121 } 122 123 fn sample_listing_full(d_tag: &str) -> RadrootsListing { 124 let qty_amount = RadrootsCoreDecimal::from_str("1000").unwrap(); 125 let price_amount = RadrootsCoreDecimal::from_str("0.01").unwrap(); 126 let display_qty = RadrootsCoreDecimal::from_str("1").unwrap(); 127 let display_price = RadrootsCoreDecimal::from_str("10").unwrap(); 128 129 RadrootsListing { 130 d_tag: listing_d_tag(d_tag), 131 published_at: None, 132 farm: RadrootsFarmRef { 133 pubkey: "farm_pubkey".to_string(), 134 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), 135 }, 136 product: RadrootsListingProduct { 137 key: "sku".to_string(), 138 title: "Widget".to_string(), 139 category: "Tools".to_string(), 140 summary: Some("Compact widget".to_string()), 141 process: Some("milled".to_string()), 142 lot: Some("lot-1".to_string()), 143 location: Some("Warehouse".to_string()), 144 profile: Some("standard".to_string()), 145 year: Some("2024".to_string()), 146 }, 147 primary_bin_id: bin_id("bin-1"), 148 bins: vec![RadrootsListingBin { 149 bin_id: bin_id("bin-1"), 150 quantity: RadrootsCoreQuantity::new(qty_amount, RadrootsCoreUnit::MassG), 151 price_per_canonical_unit: RadrootsCoreQuantityPrice::new( 152 RadrootsCoreMoney::new(price_amount, RadrootsCoreCurrency::USD), 153 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::MassG), 154 ), 155 display_amount: Some(display_qty), 156 display_unit: Some(RadrootsCoreUnit::MassKg), 157 display_label: Some("bag".to_string()), 158 display_price: Some(RadrootsCoreMoney::new( 159 display_price, 160 RadrootsCoreCurrency::USD, 161 )), 162 display_price_unit: Some(RadrootsCoreUnit::MassKg), 163 }], 164 resource_area: None, 165 plot: None, 166 discounts: Some(vec![RadrootsCoreDiscount { 167 scope: RadrootsCoreDiscountScope::Bin, 168 threshold: RadrootsCoreDiscountThreshold::BinCount { 169 bin_id: "bin-1".to_string(), 170 min: 5, 171 }, 172 value: RadrootsCoreDiscountValue::MoneyPerBin(RadrootsCoreMoney::new( 173 RadrootsCoreDecimal::from_str("2").unwrap(), 174 RadrootsCoreCurrency::USD, 175 )), 176 }]), 177 inventory_available: None, 178 availability: None, 179 delivery_method: None, 180 location: Some(RadrootsListingLocation { 181 primary: "Moyobamba".to_string(), 182 city: Some("Moyobamba".to_string()), 183 region: Some("San Martin".to_string()), 184 country: Some("PE".to_string()), 185 lat: Some(-6.0346), 186 lng: Some(-76.9714), 187 geohash: None, 188 }), 189 images: Some(vec![RadrootsListingImage { 190 url: "http://example.com/widget.jpg".to_string(), 191 size: Some(RadrootsListingImageSize { w: 1200, h: 800 }), 192 }]), 193 } 194 } 195 196 #[test] 197 fn listing_build_tags_requires_d_tag() { 198 assert!(RadrootsDTag::parse("").is_err()); 199 } 200 201 #[test] 202 fn listing_build_tags_rejects_invalid_d_tag() { 203 let listing = sample_listing("invalid:tag"); 204 let err = listing_build_tags(&listing).unwrap_err(); 205 assert!(matches!(err, EventEncodeError::InvalidField("d"))); 206 } 207 208 #[test] 209 fn listing_roundtrip_from_event() { 210 let listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); 211 let parts = to_wire_parts(&listing).unwrap(); 212 213 assert_eq!(parts.content, "# Widget"); 214 215 let decoded = listing_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 216 assert_eq!(decoded.d_tag, listing.d_tag); 217 assert_eq!(decoded.product.key, listing.product.key); 218 assert_eq!(decoded.product.title, listing.product.title); 219 assert_eq!(decoded.primary_bin_id, listing.primary_bin_id); 220 assert_eq!(decoded.bins.len(), listing.bins.len()); 221 } 222 223 #[test] 224 fn listing_from_event_reconstructs_from_tags_with_markdown_content() { 225 let listing = sample_listing_full("FAAAAAAAAAAAAAAAAAAAAA"); 226 let tags = listing_build_tags(&listing).unwrap(); 227 228 let decoded = listing_from_event(KIND_LISTING, &tags, "### Markdown listing").unwrap(); 229 assert_eq!(decoded.d_tag, listing.d_tag); 230 assert_eq!(decoded.product.summary, listing.product.summary); 231 assert_eq!(decoded.primary_bin_id, listing.primary_bin_id); 232 assert_eq!( 233 decoded 234 .location 235 .as_ref() 236 .map(|location| location.primary.as_str()), 237 Some("Moyobamba") 238 ); 239 } 240 241 #[test] 242 fn listing_from_event_rejects_invalid_d_tag() { 243 let mut tags = listing_build_tags(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg")).unwrap(); 244 let d_tag = tags 245 .iter_mut() 246 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_D)) 247 .expect("d tag"); 248 d_tag[1] = "invalid:tag".to_string(); 249 250 let err = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap_err(); 251 assert!(matches!(err, EventParseError::InvalidTag(TAG_D))); 252 } 253 254 #[test] 255 fn listing_from_event_rejects_wrong_kind() { 256 let tags = listing_build_tags(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg")).unwrap(); 257 258 let err = listing_from_event(KIND_POST, &tags, "# Widget").unwrap_err(); 259 assert!(matches!( 260 err, 261 EventParseError::InvalidKind { 262 expected: "30402 or 30403", 263 got: KIND_POST 264 } 265 )); 266 } 267 268 #[test] 269 fn listing_from_event_covers_reference_tag_error_paths() { 270 let mut tags = sample_listing_tags(); 271 remove_tags(&mut tags, TAG_D); 272 assert_missing_tag(tags, TAG_D); 273 274 let mut tags = sample_listing_tags(); 275 replace_first_tag(&mut tags, TAG_D, vec![TAG_D]); 276 assert_invalid_tag(tags, TAG_D); 277 278 let mut tags = sample_listing_tags(); 279 replace_first_tag(&mut tags, TAG_D, vec![TAG_D, " "]); 280 assert_invalid_tag(tags, TAG_D); 281 282 let mut tags = sample_listing_tags(); 283 remove_tags(&mut tags, "a"); 284 assert_missing_tag(tags, "a"); 285 286 let mut tags = sample_listing_tags(); 287 replace_first_tag(&mut tags, "a", vec!["a"]); 288 assert_invalid_tag(tags, "a"); 289 290 let mut tags = sample_listing_tags(); 291 replace_first_tag(&mut tags, "a", vec!["a", "bad:farm_pubkey:farm"]); 292 assert_invalid_tag(tags, "a"); 293 294 let mut tags = sample_listing_tags(); 295 replace_first_tag(&mut tags, "a", vec!["a", "30340"]); 296 assert_invalid_tag(tags, "a"); 297 298 let mut tags = sample_listing_tags(); 299 replace_first_tag(&mut tags, "a", vec!["a", "30340::farm"]); 300 assert_invalid_tag(tags, "a"); 301 302 let mut tags = sample_listing_tags(); 303 replace_first_tag(&mut tags, "a", vec!["a", "30340:farm_pubkey:"]); 304 assert_invalid_tag(tags, "a"); 305 306 let mut tags = sample_listing_tags(); 307 replace_first_tag(&mut tags, "a", vec!["a", "30340:farm_pubkey:bad d"]); 308 assert_invalid_tag(tags, "a"); 309 310 let mut tags = sample_listing_tags(); 311 replace_first_tag(&mut tags, "a", vec!["a", "30023:other:article"]); 312 assert_missing_tag(tags, "a"); 313 314 let mut tags = sample_listing_tags(); 315 remove_tags(&mut tags, "p"); 316 assert_missing_tag(tags, "p"); 317 318 let mut tags = sample_listing_tags(); 319 replace_first_tag(&mut tags, "p", vec!["p"]); 320 assert_invalid_tag(tags, "p"); 321 322 let mut tags = sample_listing_tags(); 323 replace_first_tag(&mut tags, "p", vec!["p", " "]); 324 assert_invalid_tag(tags, "p"); 325 326 let mut tags = sample_listing_tags(); 327 replace_first_tag(&mut tags, "p", vec!["p", "other_pubkey"]); 328 assert_invalid_tag(tags, "p"); 329 } 330 331 #[test] 332 fn listing_from_event_covers_resource_and_plot_reference_paths() { 333 let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAw"); 334 listing.resource_area = Some(RadrootsResourceAreaRef { 335 pubkey: "resource_pubkey".to_string(), 336 d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(), 337 }); 338 listing.plot = Some(RadrootsPlotRef { 339 pubkey: "plot_pubkey".to_string(), 340 d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), 341 }); 342 let tags = listing_build_tags(&listing).unwrap(); 343 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 344 assert_eq!( 345 decoded 346 .resource_area 347 .as_ref() 348 .map(|area| area.d_tag.as_str()), 349 Some("AAAAAAAAAAAAAAAAAAAABQ") 350 ); 351 assert_eq!( 352 decoded.plot.as_ref().map(|plot| plot.d_tag.as_str()), 353 Some("AAAAAAAAAAAAAAAAAAAAAw") 354 ); 355 356 let mut tags = sample_listing_tags(); 357 tags.push(vec!["radroots:resource_area".to_string()]); 358 assert_invalid_tag(tags, "radroots:resource_area"); 359 360 let mut tags = sample_listing_tags(); 361 tags.push(vec![ 362 "radroots:resource_area".to_string(), 363 format!("{KIND_FARM}:resource_pubkey:resource-area-1"), 364 ]); 365 assert_invalid_tag(tags, "radroots:resource_area"); 366 367 let mut tags = sample_listing_tags(); 368 tags.push(vec![ 369 "radroots:resource_area".to_string(), 370 format!("{KIND_RESOURCE_AREA}::resource-area-1"), 371 ]); 372 assert_invalid_tag(tags, "radroots:resource_area"); 373 374 let mut tags = sample_listing_tags(); 375 tags.push(vec![ 376 "radroots:resource_area".to_string(), 377 format!("{KIND_RESOURCE_AREA}:resource_pubkey:"), 378 ]); 379 assert_invalid_tag(tags, "radroots:resource_area"); 380 381 let mut tags = sample_listing_tags(); 382 tags.push(vec![ 383 "radroots:resource_area".to_string(), 384 format!("{KIND_RESOURCE_AREA}:resource_pubkey:bad d"), 385 ]); 386 assert_invalid_tag(tags, "radroots:resource_area"); 387 388 let mut tags = sample_listing_tags(); 389 tags.push(vec!["radroots:plot".to_string()]); 390 assert_invalid_tag(tags, "radroots:plot"); 391 392 let mut tags = sample_listing_tags(); 393 tags.push(vec![ 394 "radroots:plot".to_string(), 395 format!("{KIND_RESOURCE_AREA}:plot_pubkey:plot-1"), 396 ]); 397 assert_invalid_tag(tags, "radroots:plot"); 398 399 let mut tags = sample_listing_tags(); 400 tags.push(vec![ 401 "radroots:plot".to_string(), 402 format!("{KIND_PLOT}:plot_pubkey:"), 403 ]); 404 assert_invalid_tag(tags, "radroots:plot"); 405 406 let mut tags = sample_listing_tags(); 407 tags.push(vec![ 408 "radroots:plot".to_string(), 409 format!("{KIND_PLOT}:plot_pubkey:bad d"), 410 ]); 411 assert_invalid_tag(tags, "radroots:plot"); 412 } 413 414 #[test] 415 fn listing_from_event_covers_bin_and_price_error_paths() { 416 let mut tags = sample_listing_tags(); 417 remove_tags(&mut tags, "radroots:primary_bin"); 418 assert_missing_tag(tags, "radroots:primary_bin"); 419 420 let mut tags = sample_listing_tags(); 421 tags.push(vec![ 422 "radroots:primary_bin".to_string(), 423 "bin-1".to_string(), 424 ]); 425 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 426 assert_eq!(decoded.primary_bin_id.as_str(), "bin-1"); 427 428 let mut tags = sample_listing_tags(); 429 tags.push(vec![ 430 "radroots:primary_bin".to_string(), 431 "bin-2".to_string(), 432 ]); 433 assert_invalid_tag(tags, "radroots:primary_bin"); 434 435 let mut tags = sample_listing_tags(); 436 replace_first_tag( 437 &mut tags, 438 "radroots:primary_bin", 439 vec!["radroots:primary_bin", "bin-2"], 440 ); 441 assert_invalid_tag(tags, "radroots:primary_bin"); 442 443 let mut tags = sample_listing_tags(); 444 remove_tags(&mut tags, "radroots:bin"); 445 assert_missing_tag(tags, "radroots:bin"); 446 447 let mut tags = sample_listing_tags(); 448 remove_tags(&mut tags, "radroots:price"); 449 assert_missing_tag(tags, "radroots:price"); 450 451 let mut tags = sample_listing_tags(); 452 replace_first_tag(&mut tags, "radroots:bin", vec!["radroots:bin"]); 453 assert_invalid_tag(tags, "radroots:bin"); 454 455 let mut tags = sample_listing_tags(); 456 replace_first_tag( 457 &mut tags, 458 "radroots:bin", 459 vec!["radroots:bin", "bin-1", "1", "kg"], 460 ); 461 assert_invalid_tag(tags, "radroots:bin"); 462 463 let mut tags = sample_listing_tags(); 464 replace_first_tag( 465 &mut tags, 466 "radroots:bin", 467 vec!["radroots:bin", "bin-1", "1", "not-a-unit"], 468 ); 469 assert_invalid_tag(tags, "radroots:bin"); 470 471 let mut tags = sample_listing_tags(); 472 replace_first_tag( 473 &mut tags, 474 "radroots:bin", 475 vec!["radroots:bin", "bin-1", "1", "each", "1"], 476 ); 477 assert_invalid_tag(tags, "radroots:bin"); 478 479 let mut tags = sample_listing_tags(); 480 replace_first_tag( 481 &mut tags, 482 "radroots:bin", 483 vec![ 484 "radroots:bin", 485 "bin-1", 486 "1", 487 "each", 488 "1", 489 "each", 490 "label", 491 "extra", 492 ], 493 ); 494 assert_invalid_tag(tags, "radroots:bin"); 495 496 let mut tags = sample_listing_tags(); 497 replace_first_tag( 498 &mut tags, 499 "radroots:bin", 500 vec!["radroots:bin", "bin-1", "1", "each", "1", "each"], 501 ); 502 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 503 assert_eq!( 504 decoded.bins[0].display_amount, 505 Some(RadrootsCoreDecimal::from(1u32)) 506 ); 507 assert_eq!(decoded.bins[0].display_unit, Some(RadrootsCoreUnit::Each)); 508 assert_eq!(decoded.bins[0].display_label, None); 509 510 let mut tags = sample_listing_tags(); 511 tags.push(vec![ 512 "radroots:bin".to_string(), 513 "bin-1".to_string(), 514 "1".to_string(), 515 "each".to_string(), 516 ]); 517 assert_invalid_tag(tags, "radroots:bin"); 518 519 let mut tags = sample_listing_tags(); 520 replace_first_tag(&mut tags, "radroots:price", vec!["radroots:price"]); 521 assert_invalid_tag(tags, "radroots:price"); 522 523 let mut tags = sample_listing_tags(); 524 replace_first_tag( 525 &mut tags, 526 "radroots:price", 527 vec!["radroots:price", "bin-1", "10", "USD", "1", "kg"], 528 ); 529 assert_invalid_tag(tags, "radroots:price"); 530 531 let mut tags = sample_listing_tags(); 532 replace_first_tag( 533 &mut tags, 534 "radroots:price", 535 vec!["radroots:price", "bin-1", "10", "not-currency", "1", "each"], 536 ); 537 assert_invalid_tag(tags, "radroots:price"); 538 539 let mut tags = sample_listing_tags(); 540 replace_first_tag( 541 &mut tags, 542 "radroots:price", 543 vec!["radroots:price", "bin-1", "10", "USD", "1", "each", "10"], 544 ); 545 assert_invalid_tag(tags, "radroots:price"); 546 547 let mut tags = sample_listing_tags(); 548 replace_first_tag( 549 &mut tags, 550 "radroots:price", 551 vec![ 552 "radroots:price", 553 "bin-1", 554 "10", 555 "USD", 556 "1", 557 "each", 558 "10", 559 "each", 560 "extra", 561 ], 562 ); 563 assert_invalid_tag(tags, "radroots:price"); 564 565 let mut tags = sample_listing_tags(); 566 tags.push(vec![ 567 "radroots:price".to_string(), 568 "bin-1".to_string(), 569 "10".to_string(), 570 "USD".to_string(), 571 "1".to_string(), 572 "each".to_string(), 573 ]); 574 assert_invalid_tag(tags, "radroots:price"); 575 576 let mut tags = sample_listing_tags(); 577 replace_first_tag( 578 &mut tags, 579 "radroots:price", 580 vec!["radroots:price", "bin-1", "10", "USD", "1", "g"], 581 ); 582 assert_invalid_tag(tags, "radroots:price"); 583 } 584 585 #[test] 586 fn listing_from_event_covers_trade_location_delivery_and_image_paths() { 587 let mut tags = sample_listing_tags(); 588 tags.push(vec!["location".to_string(), "Farm shelf".to_string()]); 589 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 590 assert_eq!( 591 decoded 592 .location 593 .as_ref() 594 .map(|location| location.primary.as_str()), 595 Some("Farm shelf") 596 ); 597 598 let mut tags = sample_listing_tags(); 599 tags.push(vec!["location".to_string(), "Farm shelf".to_string()]); 600 tags.push(vec![ 601 "location".to_string(), 602 "Peru".to_string(), 603 "Moyobamba".to_string(), 604 "San Martin".to_string(), 605 "PE".to_string(), 606 ]); 607 tags.push(vec!["g".to_string(), "6gkzwgjzn".to_string()]); 608 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 609 assert_eq!(decoded.product.location.as_deref(), Some("Farm shelf")); 610 assert_eq!( 611 decoded.location.as_ref().map(|location| { 612 ( 613 location.primary.as_str(), 614 location.city.as_deref(), 615 location.geohash.as_deref(), 616 ) 617 }), 618 Some(("Peru", Some("Moyobamba"), Some("6gkzwgjzn"))) 619 ); 620 621 let mut tags = sample_listing_tags(); 622 tags.push(vec![ 623 "location".to_string(), 624 " ".to_string(), 625 "Moyobamba".to_string(), 626 ]); 627 assert_invalid_tag(tags, "location"); 628 629 let mut tags = sample_listing_tags(); 630 tags.push(vec!["inventory".to_string()]); 631 assert_invalid_tag(tags, "inventory"); 632 633 let mut tags = sample_listing_tags(); 634 tags.push(vec!["inventory".to_string(), "bad".to_string()]); 635 assert_invalid_tag(tags, "inventory"); 636 637 let mut tags = sample_listing_tags(); 638 tags.push(vec!["inventory".to_string(), "12.5".to_string()]); 639 tags.push(vec![ 640 "radroots:availability_start".to_string(), 641 "1730".to_string(), 642 ]); 643 tags.push(vec!["expires_at".to_string(), "1740".to_string()]); 644 tags.push(vec!["delivery".to_string(), "pickup".to_string()]); 645 tags.push(vec!["image".to_string(), " ".to_string()]); 646 tags.push(vec!["g".to_string(), " ".to_string()]); 647 tags.push(vec![ 648 "image".to_string(), 649 "https://example.test/a.jpg".to_string(), 650 ]); 651 tags.push(vec![ 652 "image".to_string(), 653 "https://example.test/b.jpg".to_string(), 654 "bad-size".to_string(), 655 ]); 656 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 657 let Some(RadrootsListingAvailability::Window { start, end }) = decoded.availability else { 658 panic!("expected availability window"); 659 }; 660 assert_eq!(start, Some(1730)); 661 assert_eq!(end, Some(1740)); 662 assert!(matches!( 663 decoded.delivery_method, 664 Some(RadrootsListingDeliveryMethod::Pickup) 665 )); 666 assert_eq!(decoded.images.as_ref().map(Vec::len), Some(2)); 667 assert!(decoded.images.as_ref().unwrap()[1].size.is_none()); 668 669 let mut tags = sample_listing_tags(); 670 tags.push(vec!["delivery".to_string(), "local_delivery".to_string()]); 671 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 672 assert!(matches!( 673 decoded.delivery_method, 674 Some(RadrootsListingDeliveryMethod::LocalDelivery) 675 )); 676 677 let mut tags = sample_listing_tags(); 678 tags.push(vec!["delivery".to_string(), "shipping".to_string()]); 679 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 680 assert!(matches!( 681 decoded.delivery_method, 682 Some(RadrootsListingDeliveryMethod::Shipping) 683 )); 684 685 let mut tags = sample_listing_tags(); 686 tags.push(vec![ 687 "delivery".to_string(), 688 "other".to_string(), 689 "bike courier".to_string(), 690 ]); 691 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 692 let Some(RadrootsListingDeliveryMethod::Other { method }) = decoded.delivery_method else { 693 panic!("expected other delivery method"); 694 }; 695 assert_eq!(method, "bike courier"); 696 697 let mut tags = sample_listing_tags(); 698 tags.push(vec!["delivery".to_string(), "drone".to_string()]); 699 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 700 let Some(RadrootsListingDeliveryMethod::Other { method }) = decoded.delivery_method else { 701 panic!("expected fallback delivery method"); 702 }; 703 assert_eq!(method, "drone"); 704 705 let mut tags = sample_listing_tags(); 706 tags.push(vec!["status".to_string(), "active".to_string()]); 707 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 708 assert!(matches!( 709 decoded.availability, 710 Some(RadrootsListingAvailability::Status { 711 status: RadrootsListingStatus::Active 712 }) 713 )); 714 715 let mut tags = sample_listing_tags(); 716 tags.push(vec!["status".to_string(), "sold".to_string()]); 717 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 718 assert!(matches!( 719 decoded.availability, 720 Some(RadrootsListingAvailability::Status { 721 status: RadrootsListingStatus::Sold 722 }) 723 )); 724 725 let mut tags = sample_listing_tags(); 726 tags.push(vec!["status".to_string(), "paused".to_string()]); 727 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 728 let Some(RadrootsListingAvailability::Status { 729 status: RadrootsListingStatus::Other { value }, 730 }) = decoded.availability 731 else { 732 panic!("expected other availability status"); 733 }; 734 assert_eq!(value, "paused"); 735 } 736 737 #[test] 738 fn listing_from_event_covers_remaining_edge_paths() { 739 let mut tags = sample_listing_tags(); 740 tags.insert(0, Vec::new()); 741 tags.push(vec!["location".to_string()]); 742 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 743 assert_eq!(decoded.product.location, None); 744 745 let mut tags = sample_listing_tags(); 746 tags.push(vec![ 747 "radroots:plot".to_string(), 748 format!("{KIND_PLOT}::AAAAAAAAAAAAAAAAAAAAAw"), 749 ]); 750 assert_invalid_tag(tags, "radroots:plot"); 751 752 let mut tags = sample_listing_tags(); 753 tags.push(vec![ 754 "radroots:primary_bin".to_string(), 755 "bin-2".to_string(), 756 ]); 757 assert_invalid_tag(tags, "radroots:primary_bin"); 758 759 let mut tags = sample_listing_tags(); 760 let primary_position = tags 761 .iter() 762 .position(|tag| tag.first().map(String::as_str) == Some("radroots:primary_bin")) 763 .expect("primary bin tag"); 764 tags.insert( 765 primary_position + 1, 766 vec!["radroots:primary_bin".to_string(), "bin-2".to_string()], 767 ); 768 assert_invalid_tag(tags, "radroots:primary_bin"); 769 770 let mut tags = sample_listing_tags(); 771 tags.insert(0, vec!["key".to_string(), " ".to_string()]); 772 tags.push(vec!["key".to_string(), "ignored".to_string()]); 773 tags.insert(0, vec!["summary".to_string(), " ".to_string()]); 774 tags.push(vec!["summary".to_string(), "first summary".to_string()]); 775 tags.push(vec!["summary".to_string(), "ignored summary".to_string()]); 776 tags.push(vec!["process".to_string(), "null".to_string()]); 777 tags.push(vec!["lot".to_string(), " null ".to_string()]); 778 tags.push(vec!["profile".to_string(), "null".to_string()]); 779 tags.push(vec!["year".to_string(), "null".to_string()]); 780 let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap(); 781 assert_eq!(decoded.product.key, "sku"); 782 assert_eq!(decoded.product.summary.as_deref(), Some("first summary")); 783 assert_eq!(decoded.product.process, None); 784 assert_eq!(decoded.product.lot, None); 785 assert_eq!(decoded.product.profile, None); 786 assert_eq!(decoded.product.year, None); 787 788 let mut tags = sample_listing_tags(); 789 tags.push(vec!["radroots:availability_start".to_string()]); 790 assert_invalid_tag(tags, "radroots:availability_start"); 791 792 let mut tags = sample_listing_tags(); 793 tags.push(vec![ 794 "radroots:availability_start".to_string(), 795 "bad".to_string(), 796 ]); 797 assert_invalid_tag(tags, "radroots:availability_start"); 798 } 799 800 #[test] 801 fn listing_parsed_wrappers_preserve_event_metadata() { 802 let listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAQ"); 803 let parts = to_wire_parts(&listing).unwrap(); 804 let data = data_from_event( 805 "event-id".to_string(), 806 "author-pubkey".to_string(), 807 7, 808 parts.kind, 809 parts.content.clone(), 810 parts.tags.clone(), 811 ) 812 .unwrap(); 813 assert_eq!(data.id, "event-id"); 814 assert_eq!(data.author, "author-pubkey"); 815 assert_eq!(data.published_at, 7); 816 assert_eq!(data.kind, KIND_LISTING); 817 assert_eq!(data.data.d_tag, listing.d_tag); 818 819 let parsed = parsed_from_event( 820 "event-id".to_string(), 821 "author-pubkey".to_string(), 822 7, 823 parts.kind, 824 parts.content.clone(), 825 parts.tags.clone(), 826 "sig".to_string(), 827 ) 828 .unwrap(); 829 assert_eq!(parsed.event.id, "event-id"); 830 assert_eq!(parsed.event.author, "author-pubkey"); 831 assert_eq!(parsed.event.created_at, 7); 832 assert_eq!(parsed.event.sig, "sig"); 833 assert_eq!(parsed.data.data.d_tag, listing.d_tag); 834 835 let event = RadrootsNostrEvent { 836 id: "event-id".to_string(), 837 author: "author-pubkey".to_string(), 838 created_at: 7, 839 kind: parts.kind, 840 tags: parts.tags, 841 content: parts.content, 842 sig: "sig".to_string(), 843 }; 844 let data = data_from_nostr_event(&event).unwrap(); 845 assert_eq!(data.data.d_tag, listing.d_tag); 846 let parsed = parsed_from_nostr_event(&event).unwrap(); 847 assert_eq!(parsed.event.sig, "sig"); 848 assert_eq!(parsed.data.data.d_tag, listing.d_tag); 849 850 let err = parsed_from_event( 851 "event-id".to_string(), 852 "author-pubkey".to_string(), 853 7, 854 KIND_POST, 855 event.content, 856 event.tags, 857 "sig".to_string(), 858 ) 859 .unwrap_err(); 860 assert!(matches!( 861 err, 862 EventParseError::InvalidKind { 863 expected: "30402 or 30403", 864 got: KIND_POST 865 } 866 )); 867 } 868 869 #[test] 870 fn draft_listing_roundtrip_from_event() { 871 let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAQ"); 872 listing.published_at = Some(1_781_895_600); 873 let parts = to_wire_parts_with_kind(&listing, KIND_LISTING_DRAFT).unwrap(); 874 875 let decoded = listing_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 876 assert_eq!(parts.kind, KIND_LISTING_DRAFT); 877 assert_eq!(parts.content, "# Widget"); 878 assert_eq!(decoded.d_tag, listing.d_tag); 879 assert_eq!(decoded.published_at, Some(1_781_895_600)); 880 } 881 882 #[test] 883 fn listing_roundtrips_published_at_for_active_and_rejects_bad_value() { 884 let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); 885 listing.published_at = Some(1_781_895_600); 886 let parts = to_wire_parts_with_kind(&listing, KIND_LISTING).unwrap(); 887 assert!(parts.tags.iter().any(|tag| { 888 tag.first().map(|value| value.as_str()) == Some(TAG_PUBLISHED_AT) 889 && tag.get(1).map(|value| value.as_str()) == Some("1781895600") 890 })); 891 892 let decoded = listing_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); 893 assert_eq!(decoded.published_at, Some(1_781_895_600)); 894 895 let mut tags = parts.tags; 896 let published_at = tags 897 .iter_mut() 898 .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_PUBLISHED_AT)) 899 .expect("published_at tag"); 900 published_at[1] = "bad".to_string(); 901 let err = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap_err(); 902 assert!(matches!(err, EventParseError::InvalidTag(TAG_PUBLISHED_AT))); 903 } 904 905 #[test] 906 fn to_wire_parts_rejects_non_listing_kind() { 907 let err = 908 to_wire_parts_with_kind(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg"), KIND_POST).unwrap_err(); 909 assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST))); 910 } 911 912 #[test] 913 fn listing_build_tags_includes_listing_fields() { 914 let listing = sample_listing_full("AAAAAAAAAAAAAAAAAAAAAg"); 915 let tags = listing_build_tags(&listing).unwrap(); 916 917 assert!(tags.iter().any(|t| { 918 t.get(0).map(|s| s.as_str()) == Some(TAG_D) 919 && t.get(1).map(|s| s.as_str()) == Some("AAAAAAAAAAAAAAAAAAAAAg") 920 })); 921 assert!(tags.iter().any(|t| { 922 t.get(0).map(|s| s.as_str()) == Some("p") 923 && t.get(1).map(|s| s.as_str()) == Some("farm_pubkey") 924 })); 925 assert!(tags.iter().any(|t| { 926 t.get(0).map(|s| s.as_str()) == Some("a") 927 && t.get(1).map(|s| s.as_str()) == Some("30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAA") 928 })); 929 assert!(tags.iter().any(|t| { 930 t.get(0).map(|s| s.as_str()) == Some("key") && t.get(1).map(|s| s.as_str()) == Some("sku") 931 })); 932 assert!(tags.iter().any(|t| { 933 t.get(0).map(|s| s.as_str()) == Some("title") 934 && t.get(1).map(|s| s.as_str()) == Some("Widget") 935 })); 936 937 let primary_tag = tags 938 .iter() 939 .find(|t| t.get(0).map(|s| s.as_str()) == Some("radroots:primary_bin")) 940 .expect("primary bin tag"); 941 assert_eq!(primary_tag.get(1).map(|s| s.as_str()), Some("bin-1")); 942 943 let bin_tag = tags 944 .iter() 945 .find(|t| t.get(0).map(|s| s.as_str()) == Some("radroots:bin")) 946 .expect("bin tag"); 947 assert_eq!(bin_tag.get(1).map(|s| s.as_str()), Some("bin-1")); 948 assert_eq!(bin_tag.get(2).map(|s| s.as_str()), Some("1000")); 949 assert_eq!(bin_tag.get(3).map(|s| s.as_str()), Some("g")); 950 assert_eq!(bin_tag.get(4).map(|s| s.as_str()), Some("1")); 951 assert_eq!(bin_tag.get(5).map(|s| s.as_str()), Some("kg")); 952 assert_eq!(bin_tag.get(6).map(|s| s.as_str()), Some("bag")); 953 954 let price_tag = tags 955 .iter() 956 .find(|t| t.get(0).map(|s| s.as_str()) == Some("radroots:price")) 957 .expect("radroots price tag"); 958 assert_eq!(price_tag.get(1).map(|s| s.as_str()), Some("bin-1")); 959 assert_eq!(price_tag.get(2).map(|s| s.as_str()), Some("0.01")); 960 assert_eq!(price_tag.get(3).map(|s| s.as_str()), Some("USD")); 961 assert_eq!(price_tag.get(4).map(|s| s.as_str()), Some("1")); 962 assert_eq!(price_tag.get(5).map(|s| s.as_str()), Some("g")); 963 assert_eq!(price_tag.get(6).map(|s| s.as_str()), Some("10")); 964 assert_eq!(price_tag.get(7).map(|s| s.as_str()), Some("kg")); 965 966 let generic_price_tag = tags 967 .iter() 968 .find(|t| { 969 t.get(0).map(|s| s.as_str()) == Some("price") 970 && t.get(1).map(|s| s.as_str()) == Some("10") 971 }) 972 .expect("generic price tag"); 973 assert_eq!(generic_price_tag.get(2).map(|s| s.as_str()), Some("USD")); 974 975 let discount_tag = tags 976 .iter() 977 .find(|t| t.get(0).map(|s| s.as_str()) == Some("radroots:discount")) 978 .expect("discount tag"); 979 assert!( 980 discount_tag 981 .get(1) 982 .map(|s| s.contains("\"scope\":\"bin\"")) 983 .unwrap_or(false) 984 ); 985 986 assert!(tags.iter().any(|t| { 987 t.get(0).map(|s| s.as_str()) == Some("location") 988 && t.get(1).map(|s| s.as_str()) == Some("Moyobamba") 989 })); 990 991 let g_tags: Vec<&Vec<String>> = tags 992 .iter() 993 .filter(|t| t.get(0).map(|s| s.as_str()) == Some("g")) 994 .collect(); 995 assert!(!g_tags.is_empty()); 996 let full_len = g_tags[0][1].len(); 997 assert_eq!(g_tags.len(), full_len); 998 for (idx, tag) in g_tags.iter().enumerate() { 999 assert_eq!(tag[1].len(), full_len - idx); 1000 } 1001 assert!(tags.iter().any(|t| { 1002 t.get(0).map(|s| s.as_str()) == Some("L") && t.get(1).map(|s| s.as_str()) == Some("dd.lat") 1003 })); 1004 assert!(tags.iter().any(|t| { 1005 t.get(0).map(|s| s.as_str()) == Some("L") && t.get(1).map(|s| s.as_str()) == Some("dd.lon") 1006 })); 1007 assert!(tags.iter().any(|t| { 1008 t.get(0).map(|s| s.as_str()) == Some("l") && t.get(2).map(|s| s.as_str()) == Some("dd.lat") 1009 })); 1010 assert!(tags.iter().any(|t| { 1011 t.get(0).map(|s| s.as_str()) == Some("l") && t.get(2).map(|s| s.as_str()) == Some("dd.lon") 1012 })); 1013 1014 assert!(tags.iter().any(|t| { 1015 t.get(0).map(|s| s.as_str()) == Some("image") 1016 && t.get(1).map(|s| s.as_str()) == Some("http://example.com/widget.jpg") 1017 && t.get(2).map(|s| s.as_str()) == Some("1200x800") 1018 })); 1019 } 1020 1021 #[test] 1022 fn listing_tags_full_uses_single_generic_price_for_primary_bin() { 1023 let mut listing = sample_listing_full("AAAAAAAAAAAAAAAAAAAAAw"); 1024 listing.bins.push(RadrootsListingBin { 1025 bin_id: bin_id("bin-2"), 1026 quantity: RadrootsCoreQuantity::new( 1027 RadrootsCoreDecimal::from_str("500").unwrap(), 1028 RadrootsCoreUnit::MassG, 1029 ), 1030 price_per_canonical_unit: RadrootsCoreQuantityPrice::new( 1031 RadrootsCoreMoney::new( 1032 RadrootsCoreDecimal::from_str("0.02").unwrap(), 1033 RadrootsCoreCurrency::USD, 1034 ), 1035 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::MassG), 1036 ), 1037 display_amount: Some(RadrootsCoreDecimal::from(500u32)), 1038 display_unit: Some(RadrootsCoreUnit::MassG), 1039 display_label: Some("sample".to_string()), 1040 display_price: Some(RadrootsCoreMoney::new( 1041 RadrootsCoreDecimal::from_str("10").unwrap(), 1042 RadrootsCoreCurrency::USD, 1043 )), 1044 display_price_unit: Some(RadrootsCoreUnit::MassG), 1045 }); 1046 1047 let tags = listing_tags_full(&listing).unwrap(); 1048 let generic_price_tags: Vec<&Vec<String>> = tags 1049 .iter() 1050 .filter(|tag| tag.first().map(|value| value.as_str()) == Some("price")) 1051 .collect(); 1052 assert_eq!(generic_price_tags.len(), 1); 1053 assert_eq!( 1054 generic_price_tags[0].get(1).map(|value| value.as_str()), 1055 Some("10") 1056 ); 1057 assert_eq!( 1058 generic_price_tags[0].get(2).map(|value| value.as_str()), 1059 Some("USD") 1060 ); 1061 } 1062 1063 #[test] 1064 fn listing_tags_full_includes_trade_fields() { 1065 let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); 1066 let inventory = RadrootsCoreDecimal::from_str("12.5").unwrap(); 1067 let inventory_value = inventory.to_string(); 1068 listing.inventory_available = Some(inventory); 1069 listing.availability = Some(RadrootsListingAvailability::Window { 1070 start: Some(1730000000), 1071 end: Some(1731000000), 1072 }); 1073 listing.delivery_method = Some(RadrootsListingDeliveryMethod::Shipping); 1074 1075 let tags = listing_tags_full(&listing).unwrap(); 1076 1077 assert!(tags.iter().any(|t| { 1078 t.get(0).map(|s| s.as_str()) == Some("inventory") 1079 && t.get(1).map(|s| s.as_str()) == Some(inventory_value.as_str()) 1080 })); 1081 assert!(tags.iter().any(|t| { 1082 t.get(0).map(|s| s.as_str()) == Some("radroots:availability_start") 1083 && t.get(1).map(|s| s.as_str()) == Some("1730000000") 1084 })); 1085 assert!(tags.iter().any(|t| { 1086 t.get(0).map(|s| s.as_str()) == Some("expires_at") 1087 && t.get(1).map(|s| s.as_str()) == Some("1731000000") 1088 })); 1089 assert!(tags.iter().any(|t| { 1090 t.get(0).map(|s| s.as_str()) == Some("delivery") 1091 && t.get(1).map(|s| s.as_str()) == Some("shipping") 1092 })); 1093 } 1094 1095 #[test] 1096 fn listing_tags_full_includes_status_tag() { 1097 let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); 1098 listing.availability = Some(RadrootsListingAvailability::Status { 1099 status: RadrootsListingStatus::Active, 1100 }); 1101 1102 let tags = listing_tags_full(&listing).unwrap(); 1103 1104 assert!(tags.iter().any(|t| { 1105 t.get(0).map(|s| s.as_str()) == Some("status") 1106 && t.get(1).map(|s| s.as_str()) == Some("active") 1107 })); 1108 } 1109 1110 #[test] 1111 fn listing_build_tags_ignores_null_strings() { 1112 let mut listing = sample_listing_full("AAAAAAAAAAAAAAAAAAAAAg"); 1113 listing.product.summary = Some("null".to_string()); 1114 listing.product.process = Some("null".to_string()); 1115 listing.product.lot = Some("null".to_string()); 1116 listing.product.location = Some("null".to_string()); 1117 listing.product.profile = Some("null".to_string()); 1118 listing.product.year = Some("null".to_string()); 1119 listing.location = Some(RadrootsListingLocation { 1120 primary: "Moyobamba".to_string(), 1121 city: Some("null".to_string()), 1122 region: Some("San Martin".to_string()), 1123 country: Some("null".to_string()), 1124 lat: Some(-6.0346), 1125 lng: Some(-76.9714), 1126 geohash: None, 1127 }); 1128 listing.images = Some(vec![RadrootsListingImage { 1129 url: "null".to_string(), 1130 size: None, 1131 }]); 1132 1133 let tags = listing_build_tags(&listing).unwrap(); 1134 assert!( 1135 !tags 1136 .iter() 1137 .any(|tag| tag.iter().any(|value| value == "null")) 1138 ); 1139 } 1140 1141 #[test] 1142 fn listing_tags_with_options_cover_location_fallback_paths() { 1143 let mut geohash_only = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); 1144 geohash_only.location = Some(RadrootsListingLocation { 1145 primary: "Moyobamba".to_string(), 1146 city: None, 1147 region: None, 1148 country: None, 1149 lat: None, 1150 lng: None, 1151 geohash: Some("6gkzwgjzn".to_string()), 1152 }); 1153 let tags = listing_tags_with_options(&geohash_only, ListingTagOptions::default()).unwrap(); 1154 assert!( 1155 tags.iter() 1156 .any(|tag| tag.get(0).map(|value| value.as_str()) == Some("g")) 1157 ); 1158 assert!(tags.iter().any(|tag| { 1159 tag.get(0).map(|value| value.as_str()) == Some("l") 1160 && tag.get(2).map(|value| value.as_str()) == Some("dd") 1161 })); 1162 1163 let mut no_coordinates = sample_listing("AAAAAAAAAAAAAAAAAAAAAQ"); 1164 no_coordinates.location = Some(RadrootsListingLocation { 1165 primary: "Moyobamba".to_string(), 1166 city: None, 1167 region: None, 1168 country: None, 1169 lat: None, 1170 lng: None, 1171 geohash: None, 1172 }); 1173 let tags = listing_tags_with_options(&no_coordinates, ListingTagOptions::default()).unwrap(); 1174 assert!( 1175 !tags 1176 .iter() 1177 .any(|tag| tag.get(0).map(|value| value.as_str()) == Some("L")) 1178 ); 1179 assert!( 1180 !tags 1181 .iter() 1182 .any(|tag| tag.get(0).map(|value| value.as_str()) == Some("g")) 1183 ); 1184 1185 let mut no_gps = sample_listing("AAAAAAAAAAAAAAAAAAAAAw"); 1186 no_gps.location = Some(RadrootsListingLocation { 1187 primary: "Moyobamba".to_string(), 1188 city: None, 1189 region: None, 1190 country: None, 1191 lat: Some(-6.0346), 1192 lng: Some(-76.9714), 1193 geohash: None, 1194 }); 1195 let tags = listing_tags_with_options( 1196 &no_gps, 1197 ListingTagOptions { 1198 include_gps: false, 1199 ..ListingTagOptions::default() 1200 }, 1201 ) 1202 .unwrap(); 1203 assert!( 1204 tags.iter() 1205 .any(|tag| tag.get(0).map(|value| value.as_str()) == Some("g")) 1206 ); 1207 assert!( 1208 !tags 1209 .iter() 1210 .any(|tag| tag.get(0).map(|value| value.as_str()) == Some("L")) 1211 ); 1212 }