commit 095300a45c594bc8ad1d8bae99f1de5ad5b73cd0
parent 617f7b0d8c141e3c7b8a1db20fb3248f945b72a3
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Feb 2026 23:08:45 +0000
tests: broaden branch coverage across codec helpers
- add event_ref nip10 roundtrip and failure-path tests covering missing, invalid, and relay precedence logic
- expand follow, gift_wrap, message, reaction, and seal suites for optional-field and error-kind encode branches
- add additional job_util input and amount or bid parsing scenarios to cover marker and invalid-shape paths
- add structured default encode tests for farm, coop, document, plot, resource area, resource cap, and list-set builders
Diffstat:
8 files changed, 855 insertions(+), 4 deletions(-)
diff --git a/crates/events-codec/tests/event_ref.rs b/crates/events-codec/tests/event_ref.rs
@@ -3,7 +3,8 @@ mod common;
use radroots_events::kinds::KIND_POST;
use radroots_events_codec::error::EventParseError;
use radroots_events_codec::event_ref::{
- build_event_ref_tag, find_event_ref_tag, parse_event_ref_tag,
+ build_event_ref_tag, find_event_ref_tag, parse_event_ref_tag, parse_nip10_ref_tags,
+ push_nip10_ref_tags,
};
#[test]
@@ -82,3 +83,101 @@ fn find_event_ref_tag_locates_first_match() {
assert_eq!(found[0], "e");
assert_eq!(found[1], "id");
}
+
+#[test]
+fn push_and_parse_nip10_ref_tags_roundtrip_with_and_without_a_tag() {
+ let event = common::event_ref_with_d(
+ "id",
+ "author",
+ KIND_POST,
+ "AAAAAAAAAAAAAAAAAAAAAA",
+ Some(vec!["wss://relay.example.com".to_string()]),
+ );
+ let mut tags = Vec::new();
+ push_nip10_ref_tags(&mut tags, &event, "e", "p", "k", "a");
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert_eq!(parsed.id, event.id);
+ assert_eq!(parsed.author, event.author);
+ assert_eq!(parsed.kind, event.kind);
+ assert_eq!(parsed.d_tag, event.d_tag);
+ assert_eq!(parsed.relays, event.relays);
+
+ let event = common::event_ref("id2", "author2", KIND_POST);
+ let mut tags = Vec::new();
+ push_nip10_ref_tags(&mut tags, &event, "e", "p", "k", "a");
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert_eq!(parsed.id, event.id);
+ assert_eq!(parsed.author, event.author);
+ assert_eq!(parsed.kind, event.kind);
+ assert!(parsed.d_tag.is_none());
+}
+
+#[test]
+fn parse_nip10_ref_tags_rejects_missing_or_invalid_required_tags() {
+ let err = parse_nip10_ref_tags(&[], "e", "p", "k", "a").unwrap_err();
+ assert!(matches!(err, EventParseError::MissingTag("e")));
+
+ let tags = vec![
+ vec!["e".to_string(), "".to_string()],
+ vec!["p".to_string(), "author".to_string()],
+ vec!["k".to_string(), KIND_POST.to_string()],
+ ];
+ let err = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("e")));
+
+ let tags = vec![
+ vec!["e".to_string(), "id".to_string()],
+ vec!["p".to_string(), "".to_string()],
+ vec!["k".to_string(), KIND_POST.to_string()],
+ ];
+ let err = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("p")));
+
+ let tags = vec![
+ vec!["e".to_string(), "id".to_string()],
+ vec!["p".to_string(), "author".to_string()],
+ vec!["k".to_string(), "bad-kind".to_string()],
+ ];
+ let err = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidNumber("k", _)));
+}
+
+#[test]
+fn parse_nip10_ref_tags_prefers_e_relays_and_can_fall_back_to_a_relays() {
+ let tags = vec![
+ vec!["e".to_string(), "id".to_string()],
+ vec!["p".to_string(), "author".to_string()],
+ vec!["k".to_string(), KIND_POST.to_string()],
+ vec![
+ "a".to_string(),
+ format!("{}:{}:{}", KIND_POST, "author", "AAAAAAAAAAAAAAAAAAAAAA"),
+ "wss://relay.a.example.com".to_string(),
+ ],
+ ];
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert_eq!(parsed.d_tag.as_deref(), Some("AAAAAAAAAAAAAAAAAAAAAA"));
+ assert_eq!(
+ parsed.relays,
+ Some(vec!["wss://relay.a.example.com".to_string()])
+ );
+
+ let tags = vec![
+ vec![
+ "e".to_string(),
+ "id".to_string(),
+ "wss://relay.e.example.com".to_string(),
+ ],
+ vec!["p".to_string(), "author".to_string()],
+ vec!["k".to_string(), KIND_POST.to_string()],
+ vec![
+ "a".to_string(),
+ format!("{}:{}:{}", KIND_POST, "author", "AAAAAAAAAAAAAAAAAAAAAA"),
+ "wss://relay.a.example.com".to_string(),
+ ],
+ ];
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert_eq!(
+ parsed.relays,
+ Some(vec!["wss://relay.e.example.com".to_string()])
+ );
+}
diff --git a/crates/events-codec/tests/follow.rs b/crates/events-codec/tests/follow.rs
@@ -7,7 +7,10 @@ use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::follow::decode::{
follow_from_tags, index_from_event, metadata_from_event,
};
-use radroots_events_codec::follow::encode::{follow_apply, to_wire_parts, FollowMutation};
+use radroots_events_codec::follow::encode::{
+ follow_apply, follow_to_wire_parts_after, to_wire_parts, to_wire_parts_with_kind,
+ FollowMutation,
+};
#[test]
fn follow_to_wire_parts_builds_p_tags() {
@@ -297,3 +300,46 @@ fn follow_apply_rejects_empty_pubkey() {
EventEncodeError::EmptyRequiredField("follow.public_key")
));
}
+
+#[test]
+fn follow_build_tags_normalizes_empty_optional_values() {
+ let follow = RadrootsFollow {
+ list: vec![RadrootsFollowProfile {
+ published_at: 1,
+ public_key: "pubkey".to_string(),
+ relay_url: Some("".to_string()),
+ contact_name: Some(" ".to_string()),
+ }],
+ };
+ let parts = to_wire_parts(&follow).unwrap();
+ assert_eq!(
+ parts.tags,
+ vec![vec!["p".to_string(), "pubkey".to_string(), " ".to_string()]]
+ );
+}
+
+#[test]
+fn follow_to_wire_parts_with_kind_and_after_mutation_work() {
+ let follow = RadrootsFollow {
+ list: vec![RadrootsFollowProfile {
+ published_at: 1,
+ public_key: "pubkey-a".to_string(),
+ relay_url: None,
+ contact_name: None,
+ }],
+ };
+ let parts = to_wire_parts_with_kind(&follow, KIND_POST).unwrap();
+ assert_eq!(parts.kind, KIND_POST);
+
+ let toggled = follow_to_wire_parts_after(
+ &follow,
+ FollowMutation::Toggle {
+ public_key: "pubkey-b".to_string(),
+ relay_url: Some("wss://relay.example.com".to_string()),
+ contact_name: Some("alice".to_string()),
+ },
+ )
+ .unwrap();
+ assert_eq!(toggled.kind, KIND_FOLLOW);
+ assert_eq!(toggled.tags.len(), 2);
+}
diff --git a/crates/events-codec/tests/gift_wrap.rs b/crates/events-codec/tests/gift_wrap.rs
@@ -5,7 +5,9 @@ use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::gift_wrap::decode::{
gift_wrap_from_tags, index_from_event, metadata_from_event,
};
-use radroots_events_codec::gift_wrap::encode::{gift_wrap_build_tags, to_wire_parts};
+use radroots_events_codec::gift_wrap::encode::{
+ gift_wrap_build_tags, to_wire_parts, to_wire_parts_with_kind,
+};
fn sample_gift_wrap() -> RadrootsGiftWrap {
RadrootsGiftWrap {
@@ -157,3 +159,32 @@ fn gift_wrap_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.gift_wrap.recipient.public_key, "pubkey");
}
+
+#[test]
+fn gift_wrap_build_tags_handles_optional_expiration_and_invalid_relay() {
+ let mut gift_wrap = sample_gift_wrap();
+ gift_wrap.expiration = None;
+ let tags = gift_wrap_build_tags(&gift_wrap).unwrap();
+ assert_eq!(
+ tags,
+ vec![vec![
+ "p".to_string(),
+ "pubkey".to_string(),
+ "wss://relay.example".to_string()
+ ]]
+ );
+
+ let mut gift_wrap = sample_gift_wrap();
+ gift_wrap.recipient.relay_url = Some(" ".to_string());
+ let err = gift_wrap_build_tags(&gift_wrap).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("recipient.relay_url")
+ ));
+}
+
+#[test]
+fn gift_wrap_to_wire_parts_with_kind_rejects_wrong_kind() {
+ let err = to_wire_parts_with_kind(&sample_gift_wrap(), KIND_MESSAGE).unwrap_err();
+ assert!(matches!(err, EventEncodeError::InvalidKind(KIND_MESSAGE)));
+}
diff --git a/crates/events-codec/tests/job_util.rs b/crates/events-codec/tests/job_util.rs
@@ -62,6 +62,60 @@ fn parse_i_tags_handles_multiple_shapes() {
}
#[test]
+fn parse_i_tags_covers_marker_and_fallback_shapes() {
+ let tags = vec![
+ vec!["i".to_string()],
+ vec!["i".to_string(), "marker-only".to_string()],
+ vec![
+ "i".to_string(),
+ "event-id".to_string(),
+ "marker".to_string(),
+ ],
+ vec![
+ "i".to_string(),
+ "event-id".to_string(),
+ "event".to_string(),
+ "marker-4".to_string(),
+ ],
+ vec![
+ "i".to_string(),
+ "event-id".to_string(),
+ "event".to_string(),
+ "wss://relay.example.com".to_string(),
+ ],
+ vec![
+ "i".to_string(),
+ "event-id".to_string(),
+ "event".to_string(),
+ "marker-5".to_string(),
+ "fallback-marker".to_string(),
+ ],
+ vec![
+ "i".to_string(),
+ "event-id".to_string(),
+ "event".to_string(),
+ "wss://relay.example.com".to_string(),
+ "final-marker".to_string(),
+ ],
+ ];
+
+ let inputs = parse_i_tags(&tags);
+ assert_eq!(inputs.len(), 6);
+ assert_eq!(inputs[0].marker.as_deref(), Some("marker-only"));
+ assert_eq!(inputs[0].data, "");
+ assert_eq!(inputs[1].marker.as_deref(), Some("marker"));
+ assert_eq!(inputs[1].data, "event-id");
+ assert_eq!(inputs[2].marker.as_deref(), Some("marker-4"));
+ assert_eq!(inputs[2].relay, None);
+ assert_eq!(inputs[3].relay.as_deref(), Some("wss://relay.example.com"));
+ assert_eq!(inputs[3].marker, None);
+ assert_eq!(inputs[4].marker.as_deref(), Some("marker-5"));
+ assert_eq!(inputs[4].relay, None);
+ assert_eq!(inputs[5].relay.as_deref(), Some("wss://relay.example.com"));
+ assert_eq!(inputs[5].marker.as_deref(), Some("final-marker"));
+}
+
+#[test]
fn parse_params_extracts_key_value_pairs() {
let tags = vec![
vec!["param".to_string(), "k".to_string(), "v".to_string()],
@@ -88,6 +142,17 @@ fn parse_amount_tag_sat_accepts_msat_and_bolt11() {
}
#[test]
+fn parse_amount_tag_sat_handles_none_and_invalid_shapes() {
+ assert!(parse_amount_tag_sat(&[]).unwrap().is_none());
+
+ let err = parse_amount_tag_sat(&[vec!["amount".to_string()]]).unwrap_err();
+ assert!(matches!(err, JobParseError::InvalidTag("amount")));
+
+ let err = parse_amount_tag_sat(&[vec!["amount".to_string(), "abc".to_string()]]).unwrap_err();
+ assert!(matches!(err, JobParseError::InvalidNumber("amount", _)));
+}
+
+#[test]
fn parse_amount_tag_sat_rejects_non_whole_sats() {
let tags = vec![vec!["amount".to_string(), "1500".to_string()]];
let err = parse_amount_tag_sat(&tags).unwrap_err();
@@ -124,6 +189,14 @@ fn parse_bid_tag_sat_accepts_sat() {
}
#[test]
+fn parse_bid_tag_sat_handles_none_and_invalid_shape() {
+ assert!(parse_bid_tag_sat(&[]).unwrap().is_none());
+
+ let err = parse_bid_tag_sat(&[vec!["bid".to_string()]]).unwrap_err();
+ assert!(matches!(err, JobParseError::InvalidTag("bid")));
+}
+
+#[test]
fn parse_bid_tag_sat_rejects_non_numeric() {
let tags = vec![vec!["bid".to_string(), "not-a-number".to_string()]];
let err = parse_bid_tag_sat(&tags).unwrap_err();
diff --git a/crates/events-codec/tests/message.rs b/crates/events-codec/tests/message.rs
@@ -211,3 +211,90 @@ fn message_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.message.recipients.len(), 2);
}
+
+#[test]
+fn message_build_tags_rejects_invalid_optional_fields() {
+ let message = RadrootsMessage {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: "pub".to_string(),
+ relay_url: Some(" ".to_string()),
+ }],
+ content: "hello".to_string(),
+ reply_to: None,
+ subject: None,
+ };
+ let err = message_build_tags(&message).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("recipients.relay_url")
+ ));
+
+ let message = RadrootsMessage {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: "pub".to_string(),
+ relay_url: None,
+ }],
+ content: "hello".to_string(),
+ reply_to: Some(RadrootsNostrEventPtr {
+ id: " ".to_string(),
+ relays: None,
+ }),
+ subject: None,
+ };
+ let err = message_build_tags(&message).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("reply_to.id")
+ ));
+
+ let message = RadrootsMessage {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: "pub".to_string(),
+ relay_url: None,
+ }],
+ content: "hello".to_string(),
+ reply_to: None,
+ subject: Some(" ".to_string()),
+ };
+ let err = message_build_tags(&message).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("subject")
+ ));
+}
+
+#[test]
+fn message_from_tags_rejects_invalid_optional_tags() {
+ let err = message_from_tags(
+ KIND_MESSAGE,
+ &[
+ vec!["p".to_string(), "pub".to_string(), " ".to_string()],
+ vec!["e".to_string(), "reply".to_string()],
+ ],
+ "hello",
+ )
+ .unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("p")));
+
+ let err = message_from_tags(
+ KIND_MESSAGE,
+ &[
+ vec!["p".to_string(), "pub".to_string()],
+ vec!["e".to_string(), " ".to_string()],
+ ],
+ "hello",
+ )
+ .unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("e")));
+
+ let err = message_from_tags(
+ KIND_MESSAGE,
+ &[
+ vec!["p".to_string(), "pub".to_string()],
+ vec!["subject".to_string(), " ".to_string()],
+ ],
+ "hello",
+ )
+ .unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("subject")));
+}
diff --git a/crates/events-codec/tests/reaction.rs b/crates/events-codec/tests/reaction.rs
@@ -125,3 +125,16 @@ fn reaction_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.reaction.content, "+");
}
+
+#[test]
+fn reaction_build_tags_supports_root_without_d_tag() {
+ let reaction = RadrootsReaction {
+ root: common::event_ref("root", "author", KIND_POST),
+ content: "+".to_string(),
+ };
+ let tags = reaction_build_tags(&reaction).unwrap();
+ assert_eq!(tags.len(), 3);
+ assert_eq!(tags[0][0], "e");
+ assert_eq!(tags[1][0], "p");
+ assert_eq!(tags[2][0], "k");
+}
diff --git a/crates/events-codec/tests/seal.rs b/crates/events-codec/tests/seal.rs
@@ -3,7 +3,9 @@ use radroots_events::seal::RadrootsSeal;
use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::seal::decode::{index_from_event, metadata_from_event, seal_from_parts};
-use radroots_events_codec::seal::encode::to_wire_parts;
+use radroots_events_codec::seal::encode::{
+ seal_build_tags, to_wire_parts, to_wire_parts_with_kind,
+};
#[test]
fn seal_to_wire_parts_requires_content() {
@@ -90,3 +92,14 @@ fn seal_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.seal.content, "payload");
}
+
+#[test]
+fn seal_build_tags_and_kind_validation_cover_paths() {
+ let seal = RadrootsSeal {
+ content: "payload".to_string(),
+ };
+ assert!(seal_build_tags(&seal).unwrap().is_empty());
+
+ let err = to_wire_parts_with_kind(&seal, KIND_MESSAGE).unwrap_err();
+ assert!(matches!(err, EventEncodeError::InvalidKind(KIND_MESSAGE)));
+}
diff --git a/crates/events-codec/tests/structured_encode_default.rs b/crates/events-codec/tests/structured_encode_default.rs
@@ -0,0 +1,489 @@
+use radroots_core::{
+ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
+ RadrootsCoreQuantityPrice, RadrootsCoreUnit,
+};
+use radroots_events::coop::{RadrootsCoop, RadrootsCoopLocation, RadrootsCoopRef};
+use radroots_events::document::{RadrootsDocument, RadrootsDocumentSubject};
+use radroots_events::farm::{
+ RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef, RadrootsGcsLocation, RadrootsGeoJsonPoint,
+ RadrootsGeoJsonPolygon,
+};
+use radroots_events::list_set::RadrootsListSet;
+use radroots_events::listing::{
+ RadrootsListing, RadrootsListingBin, RadrootsListingFarmRef, RadrootsListingProduct,
+};
+use radroots_events::plot::{RadrootsPlot, RadrootsPlotLocation, RadrootsPlotRef};
+use radroots_events::resource_area::{
+ RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef,
+};
+use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
+use radroots_events_codec::coop::encode::{coop_build_tags, coop_ref_tags};
+use radroots_events_codec::coop::list_sets::{
+ coop_admins_list_set, coop_items_list_set, coop_members_farms_list_set, coop_members_list_set,
+ coop_owners_list_set, member_of_coops_list_set,
+};
+use radroots_events_codec::document::encode::document_build_tags;
+use radroots_events_codec::error::EventEncodeError;
+use radroots_events_codec::farm::encode::{farm_build_tags, farm_ref_tags};
+use radroots_events_codec::farm::list_sets::{
+ farm_listings_list_set, farm_listings_list_set_from_listings, farm_members_list_set,
+ farm_owners_list_set, farm_plots_list_set, farm_plots_list_set_from_plots,
+ farm_workers_list_set, member_of_farms_list_set,
+};
+use radroots_events_codec::plot::encode::{plot_address, plot_build_tags};
+use radroots_events_codec::resource_area::encode::{
+ resource_area_build_tags, resource_area_ref_tags,
+};
+use radroots_events_codec::resource_area::list_sets::{
+ resource_area_members_farms_list_set, resource_area_members_plots_list_set,
+ resource_area_stewards_list_set,
+};
+use radroots_events_codec::resource_cap::encode::resource_harvest_cap_build_tags;
+
+const TEST_PUBKEY_HEX: &str = "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62";
+
+fn sample_gcs() -> RadrootsGcsLocation {
+ RadrootsGcsLocation {
+ lat: 37.0,
+ lng: -122.0,
+ geohash: "9q8yy".to_string(),
+ point: RadrootsGeoJsonPoint {
+ r#type: "Point".to_string(),
+ coordinates: [-122.0, 37.0],
+ },
+ polygon: RadrootsGeoJsonPolygon {
+ r#type: "Polygon".to_string(),
+ coordinates: vec![vec![
+ [-122.0, 37.0],
+ [-122.0, 37.0001],
+ [-122.0001, 37.0001],
+ [-122.0, 37.0],
+ ]],
+ },
+ accuracy: None,
+ altitude: None,
+ tag_0: None,
+ label: None,
+ area: None,
+ elevation: None,
+ soil: None,
+ climate: None,
+ gc_id: None,
+ gc_name: None,
+ gc_admin1_id: None,
+ gc_admin1_name: None,
+ gc_country_id: None,
+ gc_country_name: None,
+ }
+}
+
+fn sample_listing(d_tag: &str) -> RadrootsListing {
+ let quantity =
+ RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
+ let price = RadrootsCoreQuantityPrice::new(
+ RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD),
+ quantity.clone(),
+ );
+ RadrootsListing {
+ d_tag: d_tag.to_string(),
+ farm: RadrootsListingFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ product: RadrootsListingProduct {
+ key: "sku".to_string(),
+ title: "Widget".to_string(),
+ category: "Tools".to_string(),
+ summary: None,
+ process: None,
+ lot: None,
+ location: None,
+ profile: None,
+ year: None,
+ },
+ primary_bin_id: "bin-1".to_string(),
+ bins: vec![RadrootsListingBin {
+ bin_id: "bin-1".to_string(),
+ quantity,
+ price_per_canonical_unit: price,
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ display_price: None,
+ display_price_unit: None,
+ }],
+ resource_area: None,
+ plot: None,
+ discounts: None,
+ inventory_available: None,
+ availability: None,
+ delivery_method: None,
+ location: None,
+ images: None,
+ }
+}
+
+#[test]
+fn structured_build_tags_cover_optional_and_error_paths() {
+ let farm = RadrootsFarm {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ name: "Farm".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location: Some(RadrootsFarmLocation {
+ primary: Some("farm".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(),
+ }),
+ tags: Some(vec!["organic".to_string(), " ".to_string()]),
+ };
+ let farm_tags = farm_build_tags(&farm).unwrap();
+ assert!(farm_tags.iter().any(|tag| tag[0] == "d"));
+ assert!(farm_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "organic"));
+ assert!(farm_tags.iter().any(|tag| tag[0] == "g"));
+
+ let mut invalid_farm = farm.clone();
+ invalid_farm.location.as_mut().unwrap().gcs.geohash = " ".to_string();
+ let err = farm_build_tags(&invalid_farm).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("location.gcs.geohash")
+ ));
+
+ let farm_ref_tags = farm_ref_tags(&RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ })
+ .unwrap();
+ assert_eq!(farm_ref_tags.len(), 2);
+
+ let coop = RadrootsCoop {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ name: "Coop".to_string(),
+ about: None,
+ website: None,
+ picture: None,
+ banner: None,
+ location: Some(RadrootsCoopLocation {
+ primary: Some("coop".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(),
+ }),
+ tags: Some(vec!["co-op".to_string(), " ".to_string()]),
+ };
+ let coop_tags = coop_build_tags(&coop).unwrap();
+ assert!(coop_tags.iter().any(|tag| tag[0] == "g"));
+ assert!(coop_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "co-op"));
+ let coop_ref_tags = coop_ref_tags(&RadrootsCoopRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
+ })
+ .unwrap();
+ assert_eq!(coop_ref_tags.len(), 2);
+
+ let document = RadrootsDocument {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
+ doc_type: "charter".to_string(),
+ title: "Charter".to_string(),
+ version: "1.0.0".to_string(),
+ summary: None,
+ effective_at: None,
+ body_markdown: None,
+ subject: RadrootsDocumentSubject {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ address: Some("30340:58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62:AAAAAAAAAAAAAAAAAAAAAA".to_string()),
+ },
+ tags: Some(vec!["policy".to_string(), " ".to_string()]),
+ };
+ let doc_tags = document_build_tags(&document).unwrap();
+ assert!(doc_tags.iter().any(|tag| tag[0] == "p"));
+ assert!(doc_tags.iter().any(|tag| tag[0] == "a"));
+ assert!(doc_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "policy"));
+
+ let mut invalid_document = document.clone();
+ invalid_document.subject.address = Some(" ".to_string());
+ let err = document_build_tags(&invalid_document).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("subject.address")
+ ));
+
+ let plot = RadrootsPlot {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ farm: RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ name: "Plot".to_string(),
+ about: None,
+ location: Some(RadrootsPlotLocation {
+ primary: Some("plot".to_string()),
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(),
+ }),
+ tags: Some(vec!["shade-grown".to_string(), " ".to_string()]),
+ };
+ let plot_tags = plot_build_tags(&plot).unwrap();
+ assert!(plot_tags.iter().any(|tag| tag[0] == "a"));
+ assert!(plot_tags.iter().any(|tag| tag[0] == "p"));
+ assert!(plot_tags.iter().any(|tag| tag[0] == "g"));
+ assert!(plot_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "shade-grown"));
+
+ let mut invalid_plot = plot.clone();
+ invalid_plot.location.as_mut().unwrap().gcs.geohash = " ".to_string();
+ let err = plot_build_tags(&invalid_plot).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("location.gcs.geohash")
+ ));
+
+ let err = plot_address("", "AAAAAAAAAAAAAAAAAAAABQ").unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("plot.author_pubkey")
+ ));
+
+ let area = RadrootsResourceArea {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ name: "Area".to_string(),
+ about: None,
+ location: RadrootsResourceAreaLocation {
+ primary: None,
+ city: None,
+ region: None,
+ country: None,
+ gcs: sample_gcs(),
+ },
+ tags: Some(vec!["orchard".to_string(), " ".to_string()]),
+ };
+ let area_tags = resource_area_build_tags(&area).unwrap();
+ assert!(area_tags.iter().any(|tag| tag[0] == "d"));
+ assert!(area_tags.iter().any(|tag| tag[0] == "g"));
+ assert!(area_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "orchard"));
+ let area_ref_tags = resource_area_ref_tags(&RadrootsResourceAreaRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ })
+ .unwrap();
+ assert_eq!(area_ref_tags.len(), 2);
+
+ let mut invalid_area = area.clone();
+ invalid_area.location.gcs.geohash = " ".to_string();
+ let err = resource_area_build_tags(&invalid_area).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("location.gcs.geohash")
+ ));
+
+ let cap = RadrootsResourceHarvestCap {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
+ resource_area: RadrootsResourceAreaRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ },
+ product: RadrootsResourceHarvestProduct {
+ key: "nutmeg".to_string(),
+ category: Some("spice".to_string()),
+ },
+ start: 1,
+ end: 2,
+ cap_quantity: RadrootsCoreQuantity::new(
+ RadrootsCoreDecimal::from(1000u32),
+ RadrootsCoreUnit::MassG,
+ ),
+ display_amount: None,
+ display_unit: None,
+ display_label: None,
+ tags: Some(vec!["seasonal".to_string(), " ".to_string()]),
+ };
+ let cap_tags = resource_harvest_cap_build_tags(&cap).unwrap();
+ assert!(cap_tags
+ .iter()
+ .any(|tag| tag[0] == "category" && tag[1] == "spice"));
+ assert!(cap_tags
+ .iter()
+ .any(|tag| tag[0] == "t" && tag[1] == "seasonal"));
+
+ let mut invalid_cap = cap.clone();
+ invalid_cap.product.key = " ".to_string();
+ let err = resource_harvest_cap_build_tags(&invalid_cap).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("product.key")
+ ));
+}
+
+#[test]
+fn structured_list_sets_cover_success_and_error_paths() {
+ let farm_id = "AAAAAAAAAAAAAAAAAAAAAA";
+ let members = farm_members_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(members.d_tag, format!("farm:{farm_id}:members"));
+ let owners = farm_owners_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(owners.d_tag, format!("farm:{farm_id}:members.owners"));
+ let workers = farm_workers_list_set(farm_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(workers.d_tag, format!("farm:{farm_id}:members.workers"));
+
+ let plots = farm_plots_list_set(farm_id, TEST_PUBKEY_HEX, ["AAAAAAAAAAAAAAAAAAAABQ"]).unwrap();
+ assert_eq!(plots.d_tag, format!("farm:{farm_id}:plots"));
+ assert_eq!(plots.entries.len(), 1);
+
+ let listings =
+ farm_listings_list_set(farm_id, TEST_PUBKEY_HEX, ["AAAAAAAAAAAAAAAAAAAAAg"]).unwrap();
+ assert_eq!(listings.d_tag, format!("farm:{farm_id}:listings"));
+ assert_eq!(listings.entries.len(), 1);
+
+ let listings_from = farm_listings_list_set_from_listings(
+ farm_id,
+ TEST_PUBKEY_HEX,
+ [sample_listing("AAAAAAAAAAAAAAAAAAAAAg")].iter(),
+ )
+ .unwrap();
+ assert_eq!(listings_from.entries.len(), 1);
+
+ let plots_from = farm_plots_list_set_from_plots(
+ farm_id,
+ TEST_PUBKEY_HEX,
+ [RadrootsPlot {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ farm: RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ },
+ name: "plot".to_string(),
+ about: None,
+ location: None,
+ tags: None,
+ }]
+ .iter(),
+ )
+ .unwrap();
+ assert_eq!(plots_from.entries.len(), 1);
+
+ let member_of_farms = member_of_farms_list_set([TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(member_of_farms.d_tag, "member_of.farms");
+
+ let err = farm_members_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("farm_id")
+ ));
+ let err = farm_members_list_set(farm_id, [" "]).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("entry.values")
+ ));
+ let err = farm_listings_list_set(farm_id, TEST_PUBKEY_HEX, [" "]).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("listing_id")
+ ));
+
+ let coop_id = "AAAAAAAAAAAAAAAAAAAAAQ";
+ let coop_members = coop_members_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(coop_members.d_tag, format!("coop:{coop_id}:members"));
+ let coop_owners = coop_owners_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(coop_owners.d_tag, format!("coop:{coop_id}:members.owners"));
+ let coop_admins = coop_admins_list_set(coop_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(coop_admins.d_tag, format!("coop:{coop_id}:members.admins"));
+ let coop_items = coop_items_list_set(coop_id, ["30340:pubkey:AAAAAAAAAAAAAAAAAAAAAA"]).unwrap();
+ assert_eq!(coop_items.d_tag, format!("coop:{coop_id}:items"));
+ let member_of_coops = member_of_coops_list_set([TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(member_of_coops.d_tag, "member_of.coops");
+
+ let coop_farms = coop_members_farms_list_set(
+ coop_id,
+ [RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ }],
+ )
+ .unwrap();
+ assert_eq!(coop_farms.entries.len(), 2);
+
+ let err = coop_members_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("coop_id")
+ ));
+ let err = coop_members_farms_list_set(
+ coop_id,
+ [RadrootsFarmRef {
+ pubkey: "".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ }],
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("farm.pubkey")
+ ));
+
+ let area_id = "AAAAAAAAAAAAAAAAAAAAAw";
+ let resource_farms = resource_area_members_farms_list_set(
+ area_id,
+ [RadrootsFarmRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ }],
+ )
+ .unwrap();
+ assert_eq!(resource_farms.entries.len(), 2);
+
+ let resource_plots = resource_area_members_plots_list_set(
+ area_id,
+ [RadrootsPlotRef {
+ pubkey: TEST_PUBKEY_HEX.to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ }],
+ )
+ .unwrap();
+ assert_eq!(resource_plots.entries.len(), 2);
+
+ let resource_stewards = resource_area_stewards_list_set(area_id, [TEST_PUBKEY_HEX]).unwrap();
+ assert_eq!(resource_stewards.entries.len(), 1);
+
+ let err = resource_area_stewards_list_set("", [TEST_PUBKEY_HEX]).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("area_id")
+ ));
+ let err = resource_area_members_plots_list_set(
+ area_id,
+ [RadrootsPlotRef {
+ pubkey: "".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAABQ".to_string(),
+ }],
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("plot.pubkey")
+ ));
+}
+
+#[test]
+fn structured_list_set_outputs_remain_deterministic() {
+ let list_set: RadrootsListSet =
+ farm_members_list_set("AAAAAAAAAAAAAAAAAAAAAA", [TEST_PUBKEY_HEX, TEST_PUBKEY_HEX])
+ .unwrap();
+ assert_eq!(list_set.entries.len(), 2);
+ assert_eq!(list_set.entries[0].tag, "p");
+}