lib

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

commit 8b9c6547686f77d88e62dce121c0c89af8d9b6b3
parent e3aacd796e728b7549c463ad6995cf38cea2d984
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 23:56:27 +0000

tests: add list_set and listing edge coverage and tighten discount cfg usage

Diffstat:
Mcrates/events-codec/src/coop/list_sets.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/src/list_set/mod.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/src/listing/tags.rs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 180 insertions(+), 9 deletions(-)

diff --git a/crates/events-codec/src/coop/list_sets.rs b/crates/events-codec/src/coop/list_sets.rs @@ -181,3 +181,53 @@ where image: None, }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn coop_list_set_id_validates_suffix_and_coop_id() { + let err = coop_list_set_id("AAAAAAAAAAAAAAAAAAAAAQ", " ") + .expect_err("expected suffix validation error"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("list_set_suffix") + )); + + let err = coop_list_set_id(" ", "members").expect_err("expected coop_id validation error"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("coop_id") + )); + } + + #[test] + fn list_entries_rejects_blank_values() { + let err = list_entries("p", [" "]).expect_err("expected blank entry error"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("entry.values") + )); + } + + #[test] + fn farm_address_rejects_empty_and_invalid_d_tag() { + let err = farm_address(&RadrootsFarmRef { + pubkey: "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(), + d_tag: " ".to_string(), + }) + .expect_err("expected empty d_tag error"); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("farm.d_tag") + )); + + let err = farm_address(&RadrootsFarmRef { + pubkey: "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62".to_string(), + d_tag: "invalid".to_string(), + }) + .expect_err("expected invalid d_tag error"); + assert!(matches!(err, EventEncodeError::InvalidField("farm.d_tag"))); + } +} diff --git a/crates/events-codec/src/list_set/mod.rs b/crates/events-codec/src/list_set/mod.rs @@ -81,4 +81,48 @@ mod tests { .expect_err("expected invalid d_tag"); assert!(matches!(err, EventParseError::InvalidTag("d"))); } + + #[test] + fn list_set_accepts_resource_base64_d_tag() { + let list = RadrootsListSet { + d_tag: "resource:AAAAAAAAAAAAAAAAAAAAAA:members".to_string(), + content: "".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec!["pubkey".to_string()], + }], + title: None, + description: None, + image: None, + }; + let tags = list_set_build_tags(&list).expect("build tags"); + let parsed = list_set_from_tags(KIND_LIST_SET_FOLLOW, list.content.clone(), &tags) + .expect("parse list set"); + assert_eq!(parsed.d_tag, list.d_tag); + } + + #[test] + fn list_set_rejects_empty_prefixed_id_or_suffix() { + let list = RadrootsListSet { + d_tag: "farm::members".to_string(), + content: "".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec!["pubkey".to_string()], + }], + title: None, + description: None, + image: None, + }; + let err = list_set_build_tags(&list).expect_err("expected invalid d_tag"); + assert!(matches!(err, EventEncodeError::InvalidField("d_tag"))); + + let tags = vec![ + vec!["d".to_string(), "coop:AAAAAAAAAAAAAAAAAAAAAA:".to_string()], + vec!["p".to_string(), "pubkey".to_string()], + ]; + let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &tags) + .expect_err("expected invalid d_tag"); + assert!(matches!(err, EventParseError::InvalidTag("d"))); + } } diff --git a/crates/events-codec/src/listing/tags.rs b/crates/events-codec/src/listing/tags.rs @@ -10,7 +10,9 @@ use alloc::{ use core::cmp; -use radroots_core::{RadrootsCoreDiscount, RadrootsCoreMoney}; +#[cfg(any(feature = "serde_json", test))] +use radroots_core::RadrootsCoreDiscount; +use radroots_core::RadrootsCoreMoney; use radroots_events::kinds::{KIND_FARM, KIND_PLOT, KIND_RESOURCE_AREA}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -27,6 +29,7 @@ use crate::error::EventEncodeError; const TAG_PRICE: &str = "price"; const TAG_RADROOTS_BIN: &str = "radroots:bin"; const TAG_RADROOTS_PRICE: &str = "radroots:price"; +#[cfg(feature = "serde_json")] const TAG_RADROOTS_DISCOUNT: &str = "radroots:discount"; const TAG_RADROOTS_PRIMARY_BIN: &str = "radroots:primary_bin"; const TAG_RADROOTS_RESOURCE_AREA: &str = "radroots:resource_area"; @@ -601,6 +604,7 @@ fn clean_value(value: &str) -> Option<String> { } } +#[cfg(any(feature = "serde_json", test))] fn discount_tag_payload(discount: &RadrootsCoreDiscount) -> Result<String, EventEncodeError> { #[cfg(feature = "serde_json")] { @@ -888,11 +892,30 @@ mod tests { }, ); assert!(find_tag(&invalid_with_geohash_enabled, "g").is_some()); - assert!( - !invalid_with_geohash_enabled - .iter() - .any(|tag| tag.first().map(|v| v.as_str()) == Some("l")) + assert!(!invalid_with_geohash_enabled + .iter() + .any(|tag| tag.first().map(|v| v.as_str()) == Some("l"))); + + let mut no_coordinate_tags = Vec::new(); + let no_coordinate_location = RadrootsListingLocation { + primary: "Test".to_string(), + city: None, + region: None, + country: None, + lat: None, + lng: None, + geohash: None, + }; + push_location_geotags( + &mut no_coordinate_tags, + &no_coordinate_location, + ListingTagOptions { + include_geohash: false, + include_gps: true, + ..ListingTagOptions::default() + }, ); + assert!(find_tag(&no_coordinate_tags, "l").is_none()); } #[test] @@ -1132,14 +1155,16 @@ mod tests { }]); #[cfg(feature = "serde_json")] { - let tags = listing_tags_with_options(&listing_with_discount, ListingTagOptions::default()) - .expect("discount serialization works"); + let tags = + listing_tags_with_options(&listing_with_discount, ListingTagOptions::default()) + .expect("discount serialization works"); assert!(find_tag(&tags, "radroots:discount").is_some()); } #[cfg(not(feature = "serde_json"))] { - let err = listing_tags_with_options(&listing_with_discount, ListingTagOptions::default()) - .expect_err("discount serialization requires serde_json"); + let err = + listing_tags_with_options(&listing_with_discount, ListingTagOptions::default()) + .expect_err("discount serialization requires serde_json"); assert!(matches!(err, EventEncodeError::Json)); } @@ -1204,6 +1229,23 @@ mod tests { assert!(find_tag(&no_availability_tags, "published_at").is_none()); assert!(find_tag(&no_availability_tags, "expires_at").is_none()); + let mut empty_window_availability = base_listing(); + empty_window_availability.discounts = None; + empty_window_availability.availability = Some(RadrootsListingAvailability::Window { + start: None, + end: None, + }); + let empty_window_tags = listing_tags_with_options( + &empty_window_availability, + ListingTagOptions { + include_availability: true, + ..ListingTagOptions::default() + }, + ) + .expect("availability window without bounds"); + assert!(find_tag(&empty_window_tags, "published_at").is_none()); + assert!(find_tag(&empty_window_tags, "expires_at").is_none()); + let mut no_delivery = base_listing(); no_delivery.discounts = None; no_delivery.delivery_method = None; @@ -1403,6 +1445,25 @@ mod tests { .expect("first bin tag"); assert_eq!(first_bin.get(1).map(|v| v.as_str()), Some("bin-1")); assert_eq!(first_bin.get(6).map(|v| v.as_str()), Some("fallback-label")); + + let mut listing_without_primary_match = base_listing(); + listing_without_primary_match.discounts = None; + let mut first = base_bin(); + first.bin_id = "bin-2".to_string(); + first.display_label = None; + first.quantity = RadrootsCoreQuantity::new(decimal("1000"), RadrootsCoreUnit::MassG); + let mut second = base_bin(); + second.bin_id = "bin-1".to_string(); + listing_without_primary_match.primary_bin_id = "bin-missing".to_string(); + listing_without_primary_match.bins = vec![first, second]; + + let tags = listing_tags(&listing_without_primary_match).expect("listing tags"); + let first_bin = tags + .iter() + .find(|tag| tag.first().map(|v| v.as_str()) == Some("radroots:bin")) + .expect("first bin tag"); + assert_eq!(first_bin.get(1).map(|v| v.as_str()), Some("bin-2")); + assert_eq!(first_bin.len(), 6); } #[test] @@ -1425,6 +1486,22 @@ mod tests { assert_eq!(location.get(1).map(|v| v.as_str()), Some("Moyobamba")); assert_eq!(location.get(2).map(|v| v.as_str()), Some("San Martin")); assert_eq!(location.len(), 3); + + listing.location = Some(RadrootsListingLocation { + primary: "Moyobamba".to_string(), + city: Some("Moyobamba".to_string()), + region: Some(" ".to_string()), + country: Some("PE".to_string()), + lat: Some(-6.03), + lng: Some(-76.97), + geohash: None, + }); + let tags = listing_tags(&listing).expect("listing tags"); + let location = find_tag(&tags, "location").expect("location tag"); + assert_eq!(location.get(1).map(|v| v.as_str()), Some("Moyobamba")); + assert_eq!(location.get(2).map(|v| v.as_str()), Some("Moyobamba")); + assert_eq!(location.get(3).map(|v| v.as_str()), Some("PE")); + assert_eq!(location.len(), 4); } #[test]