lib

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

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:
Mcrates/events-codec/tests/event_ref.rs | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/follow.rs | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/gift_wrap.rs | 33++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/job_util.rs | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/message.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/reaction.rs | 13+++++++++++++
Mcrates/events-codec/tests/seal.rs | 15++++++++++++++-
Acrates/events-codec/tests/structured_encode_default.rs | 489+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"); +}