commit e54c7594882c7536f04e51dc28d643dcdd5c05d9
parent a92da1b808d0f7455296cd62a1eeb17b41ff3dcc
Author: triesap <tyson@radroots.org>
Date: Sun, 21 Jun 2026 19:40:41 +0000
events-codec: expand listing coverage
- Add listing decode cases for malformed reference, resource, plot, bin, and price tags.
- Cover trade metadata, location, delivery, image, status, and availability parse paths.
- Validate listing test target, codec check, diff hygiene, and refreshed codec coverage run.
- Leave radroots_events_codec gate failing for follow-on codec family slices.
Diffstat:
1 file changed, 412 insertions(+), 1 deletion(-)
diff --git a/crates/events_codec/tests/listing.rs b/crates/events_codec/tests/listing.rs
@@ -9,12 +9,16 @@ use radroots_events::tags::{TAG_D, TAG_PUBLISHED_AT};
use radroots_events::{
farm::RadrootsFarmRef,
ids::{RadrootsDTag, RadrootsInventoryBinId},
- kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_POST},
+ kinds::{
+ KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_PLOT, KIND_POST, KIND_RESOURCE_AREA,
+ },
listing::{
RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingImageSize,
RadrootsListingLocation, RadrootsListingProduct, RadrootsListingStatus,
},
+ plot::RadrootsPlotRef,
+ resource_area::RadrootsResourceAreaRef,
};
use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::listing::decode::listing_from_event;
@@ -34,6 +38,36 @@ fn bin_id(raw: &str) -> RadrootsInventoryBinId {
raw.parse().unwrap()
}
+fn sample_listing_tags() -> Vec<Vec<String>> {
+ listing_build_tags(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg")).unwrap()
+}
+
+fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) {
+ tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some(name));
+}
+
+fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<&str>) {
+ let tag = tags
+ .iter_mut()
+ .find(|tag| tag.first().map(|value| value.as_str()) == Some(name))
+ .expect("tag");
+ *tag = replacement.into_iter().map(str::to_string).collect();
+}
+
+fn assert_missing_tag(tags: Vec<Vec<String>>, expected: &'static str) {
+ match listing_from_event(KIND_LISTING, &tags, "# Widget") {
+ Err(EventParseError::MissingTag(tag)) => assert_eq!(tag, expected),
+ other => panic!("expected missing tag {expected}: {other:?}"),
+ }
+}
+
+fn assert_invalid_tag(tags: Vec<Vec<String>>, expected: &'static str) {
+ match listing_from_event(KIND_LISTING, &tags, "# Widget") {
+ Err(EventParseError::InvalidTag(tag)) => assert_eq!(tag, expected),
+ other => panic!("expected invalid tag {expected}: {other:?}"),
+ }
+}
+
fn sample_listing(d_tag: &str) -> RadrootsListing {
let quantity =
RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
@@ -228,6 +262,383 @@ fn listing_from_event_rejects_wrong_kind() {
}
#[test]
+fn listing_from_event_covers_reference_tag_error_paths() {
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, TAG_D);
+ assert_missing_tag(tags, TAG_D);
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, TAG_D, vec![TAG_D]);
+ assert_invalid_tag(tags, TAG_D);
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, TAG_D, vec![TAG_D, " "]);
+ assert_invalid_tag(tags, TAG_D);
+
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, "a");
+ assert_missing_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a"]);
+ assert_invalid_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a", "bad:farm_pubkey:farm"]);
+ assert_invalid_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a", "30340"]);
+ assert_invalid_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a", "30340::farm"]);
+ assert_invalid_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a", "30340:farm_pubkey:bad d"]);
+ assert_invalid_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "a", vec!["a", "30023:other:article"]);
+ assert_missing_tag(tags, "a");
+
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, "p");
+ assert_missing_tag(tags, "p");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "p", vec!["p"]);
+ assert_invalid_tag(tags, "p");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "p", vec!["p", " "]);
+ assert_invalid_tag(tags, "p");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "p", vec!["p", "other_pubkey"]);
+ assert_invalid_tag(tags, "p");
+}
+
+#[test]
+fn listing_from_event_covers_resource_and_plot_reference_paths() {
+ let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAw");
+ listing.resource_area = Some(RadrootsResourceAreaRef {
+ pubkey: "resource_pubkey".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ });
+ listing.plot = Some(RadrootsPlotRef {
+ pubkey: "plot_pubkey".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ });
+ let tags = listing_build_tags(&listing).unwrap();
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert_eq!(
+ decoded
+ .resource_area
+ .as_ref()
+ .map(|area| area.d_tag.as_str()),
+ Some("AAAAAAAAAAAAAAAAAAAABQ")
+ );
+ assert_eq!(
+ decoded.plot.as_ref().map(|plot| plot.d_tag.as_str()),
+ Some("AAAAAAAAAAAAAAAAAAAAAw")
+ );
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["radroots:resource_area".to_string()]);
+ assert_invalid_tag(tags, "radroots:resource_area");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:resource_area".to_string(),
+ format!("{KIND_FARM}:resource_pubkey:resource-area-1"),
+ ]);
+ assert_invalid_tag(tags, "radroots:resource_area");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:resource_area".to_string(),
+ format!("{KIND_RESOURCE_AREA}::resource-area-1"),
+ ]);
+ assert_invalid_tag(tags, "radroots:resource_area");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:resource_area".to_string(),
+ format!("{KIND_RESOURCE_AREA}:resource_pubkey:bad d"),
+ ]);
+ assert_invalid_tag(tags, "radroots:resource_area");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["radroots:plot".to_string()]);
+ assert_invalid_tag(tags, "radroots:plot");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:plot".to_string(),
+ format!("{KIND_RESOURCE_AREA}:plot_pubkey:plot-1"),
+ ]);
+ assert_invalid_tag(tags, "radroots:plot");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:plot".to_string(),
+ format!("{KIND_PLOT}:plot_pubkey:bad d"),
+ ]);
+ assert_invalid_tag(tags, "radroots:plot");
+}
+
+#[test]
+fn listing_from_event_covers_bin_and_price_error_paths() {
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, "radroots:primary_bin");
+ assert_missing_tag(tags, "radroots:primary_bin");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:primary_bin".to_string(),
+ "bin-2".to_string(),
+ ]);
+ assert_invalid_tag(tags, "radroots:primary_bin");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:primary_bin",
+ vec!["radroots:primary_bin", "bin-2"],
+ );
+ assert_invalid_tag(tags, "radroots:primary_bin");
+
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, "radroots:bin");
+ assert_missing_tag(tags, "radroots:bin");
+
+ let mut tags = sample_listing_tags();
+ remove_tags(&mut tags, "radroots:price");
+ assert_missing_tag(tags, "radroots:price");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "radroots:bin", vec!["radroots:bin"]);
+ assert_invalid_tag(tags, "radroots:bin");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:bin",
+ vec!["radroots:bin", "bin-1", "1", "kg"],
+ );
+ assert_invalid_tag(tags, "radroots:bin");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:bin",
+ vec!["radroots:bin", "bin-1", "1", "each", "1"],
+ );
+ assert_invalid_tag(tags, "radroots:bin");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:bin".to_string(),
+ "bin-1".to_string(),
+ "1".to_string(),
+ "each".to_string(),
+ ]);
+ assert_invalid_tag(tags, "radroots:bin");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(&mut tags, "radroots:price", vec!["radroots:price"]);
+ assert_invalid_tag(tags, "radroots:price");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:price",
+ vec!["radroots:price", "bin-1", "10", "USD", "1", "kg"],
+ );
+ assert_invalid_tag(tags, "radroots:price");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:price",
+ vec!["radroots:price", "bin-1", "10", "USD", "1", "each", "10"],
+ );
+ assert_invalid_tag(tags, "radroots:price");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "radroots:price".to_string(),
+ "bin-1".to_string(),
+ "10".to_string(),
+ "USD".to_string(),
+ "1".to_string(),
+ "each".to_string(),
+ ]);
+ assert_invalid_tag(tags, "radroots:price");
+
+ let mut tags = sample_listing_tags();
+ replace_first_tag(
+ &mut tags,
+ "radroots:price",
+ vec!["radroots:price", "bin-1", "10", "USD", "1", "g"],
+ );
+ assert_invalid_tag(tags, "radroots:price");
+}
+
+#[test]
+fn listing_from_event_covers_trade_location_delivery_and_image_paths() {
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["location".to_string(), "Farm shelf".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert_eq!(
+ decoded
+ .location
+ .as_ref()
+ .map(|location| location.primary.as_str()),
+ Some("Farm shelf")
+ );
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["location".to_string(), "Farm shelf".to_string()]);
+ tags.push(vec![
+ "location".to_string(),
+ "Peru".to_string(),
+ "Moyobamba".to_string(),
+ "San Martin".to_string(),
+ "PE".to_string(),
+ ]);
+ tags.push(vec!["g".to_string(), "6gkzwgjzn".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert_eq!(decoded.product.location.as_deref(), Some("Farm shelf"));
+ assert_eq!(
+ decoded.location.as_ref().map(|location| {
+ (
+ location.primary.as_str(),
+ location.city.as_deref(),
+ location.geohash.as_deref(),
+ )
+ }),
+ Some(("Peru", Some("Moyobamba"), Some("6gkzwgjzn")))
+ );
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "location".to_string(),
+ " ".to_string(),
+ "Moyobamba".to_string(),
+ ]);
+ assert_invalid_tag(tags, "location");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["inventory".to_string()]);
+ assert_invalid_tag(tags, "inventory");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["inventory".to_string(), "bad".to_string()]);
+ assert_invalid_tag(tags, "inventory");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["inventory".to_string(), "12.5".to_string()]);
+ tags.push(vec![
+ "radroots:availability_start".to_string(),
+ "1730".to_string(),
+ ]);
+ tags.push(vec!["expires_at".to_string(), "1740".to_string()]);
+ tags.push(vec!["delivery".to_string(), "pickup".to_string()]);
+ tags.push(vec!["image".to_string(), " ".to_string()]);
+ tags.push(vec![
+ "image".to_string(),
+ "https://example.test/a.jpg".to_string(),
+ ]);
+ tags.push(vec![
+ "image".to_string(),
+ "https://example.test/b.jpg".to_string(),
+ "bad-size".to_string(),
+ ]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ let Some(RadrootsListingAvailability::Window { start, end }) = decoded.availability else {
+ panic!("expected availability window");
+ };
+ assert_eq!(start, Some(1730));
+ assert_eq!(end, Some(1740));
+ assert!(matches!(
+ decoded.delivery_method,
+ Some(RadrootsListingDeliveryMethod::Pickup)
+ ));
+ assert_eq!(decoded.images.as_ref().map(Vec::len), Some(2));
+ assert!(decoded.images.as_ref().unwrap()[1].size.is_none());
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["delivery".to_string(), "local_delivery".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert!(matches!(
+ decoded.delivery_method,
+ Some(RadrootsListingDeliveryMethod::LocalDelivery)
+ ));
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["delivery".to_string(), "shipping".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert!(matches!(
+ decoded.delivery_method,
+ Some(RadrootsListingDeliveryMethod::Shipping)
+ ));
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec![
+ "delivery".to_string(),
+ "other".to_string(),
+ "bike courier".to_string(),
+ ]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ let Some(RadrootsListingDeliveryMethod::Other { method }) = decoded.delivery_method else {
+ panic!("expected other delivery method");
+ };
+ assert_eq!(method, "bike courier");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["delivery".to_string(), "drone".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ let Some(RadrootsListingDeliveryMethod::Other { method }) = decoded.delivery_method else {
+ panic!("expected fallback delivery method");
+ };
+ assert_eq!(method, "drone");
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["status".to_string(), "active".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert!(matches!(
+ decoded.availability,
+ Some(RadrootsListingAvailability::Status {
+ status: RadrootsListingStatus::Active
+ })
+ ));
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["status".to_string(), "sold".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ assert!(matches!(
+ decoded.availability,
+ Some(RadrootsListingAvailability::Status {
+ status: RadrootsListingStatus::Sold
+ })
+ ));
+
+ let mut tags = sample_listing_tags();
+ tags.push(vec!["status".to_string(), "paused".to_string()]);
+ let decoded = listing_from_event(KIND_LISTING, &tags, "# Widget").unwrap();
+ let Some(RadrootsListingAvailability::Status {
+ status: RadrootsListingStatus::Other { value },
+ }) = decoded.availability
+ else {
+ panic!("expected other availability status");
+ };
+ assert_eq!(value, "paused");
+}
+
+#[test]
fn draft_listing_roundtrip_from_event() {
let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAQ");
listing.published_at = Some(1_781_895_600);