commit 3a1a072c888551fa1d3bde8f459d41c57436b365
parent b16fd1880772aae5b8b6834c52fdaae60d82cac5
Author: triesap <tyson@radroots.org>
Date: Sat, 21 Feb 2026 23:34:58 +0000
refactor: remove dead parser branches and extend high-gap tag coverage
- simplify job util and message tag parsing by removing unreachable branch guards while preserving behavior
- gate listing discount serialization by feature and add targeted listing tag branch-path tests
- expand event ref, job result, job util, list set, and resource area helper tests for uncovered edge cases
- verify with cargo check, cargo test, and sdk coverage progress reporting for `radroots-events-codec`
Diffstat:
8 files changed, 215 insertions(+), 26 deletions(-)
diff --git a/crates/events-codec/src/job/util.rs b/crates/events-codec/src/job/util.rs
@@ -132,27 +132,19 @@ pub fn parse_i_tags(tags: &[Vec<String>]) -> Vec<RadrootsJobInput> {
let v = &t[3];
if looks_like_ws_relay(v) {
relay = Some(v.clone());
- } else if marker.is_none() {
+ } else {
marker = Some(v.clone());
}
}
_ => {
data = t[1].clone();
input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text);
- if let Some(v) = t.get(3) {
- if looks_like_ws_relay(v) {
- relay = Some(v.clone());
- if let Some(m) = t.get(4) {
- marker = Some(m.clone());
- }
- } else {
- marker = Some(v.clone());
- }
- }
- if marker.is_none() {
- if let Some(m) = t.get(4) {
- marker = Some(m.clone());
- }
+ let relay_or_marker = &t[3];
+ if looks_like_ws_relay(relay_or_marker) {
+ relay = Some(relay_or_marker.clone());
+ marker = Some(t[4].clone());
+ } else {
+ marker = Some(relay_or_marker.clone());
}
}
}
diff --git a/crates/events-codec/src/listing/tags.rs b/crates/events-codec/src/listing/tags.rs
@@ -166,12 +166,17 @@ pub fn listing_tags_with_options(
tags.push(tag_listing_price_generic(&total));
}
+ #[cfg(feature = "serde_json")]
if let Some(discounts) = &listing.discounts {
for discount in discounts {
let payload = discount_tag_payload(discount)?;
tags.push(vec![TAG_RADROOTS_DISCOUNT.to_string(), payload]);
}
}
+ #[cfg(not(feature = "serde_json"))]
+ if listing.discounts.as_ref().is_some() {
+ return Err(EventEncodeError::Json);
+ }
if options.include_inventory {
if let Some(inventory) = &listing.inventory_available {
@@ -871,6 +876,23 @@ mod tests {
},
);
assert!(find_tag(&geohash_only_tags, "g").is_some());
+
+ let mut invalid_with_geohash_enabled = Vec::new();
+ push_location_geotags(
+ &mut invalid_with_geohash_enabled,
+ &invalid_geohash,
+ ListingTagOptions {
+ include_geohash: true,
+ include_gps: true,
+ ..ListingTagOptions::default()
+ },
+ );
+ 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"))
+ );
}
#[test]
@@ -1194,6 +1216,34 @@ mod tests {
)
.expect("delivery option without value");
assert!(find_tag(&no_delivery_tags, "delivery").is_none());
+
+ let mut no_inventory = base_listing();
+ no_inventory.discounts = None;
+ no_inventory.inventory_available = None;
+ let no_inventory_tags = listing_tags_with_options(
+ &no_inventory,
+ ListingTagOptions {
+ include_inventory: true,
+ ..ListingTagOptions::default()
+ },
+ )
+ .expect("inventory option without value");
+ assert!(find_tag(&no_inventory_tags, "inventory").is_none());
+
+ let mut no_geo = base_listing();
+ no_geo.discounts = None;
+ let no_geo_tags = listing_tags_with_options(
+ &no_geo,
+ ListingTagOptions {
+ include_geohash: false,
+ include_gps: false,
+ ..ListingTagOptions::default()
+ },
+ )
+ .expect("location without geotags");
+ assert!(find_tag(&no_geo_tags, "location").is_some());
+ assert!(find_tag(&no_geo_tags, "g").is_none());
+ assert!(find_tag(&no_geo_tags, "l").is_none());
}
#[test]
@@ -1332,6 +1382,52 @@ mod tests {
}
#[test]
+ fn listing_tags_reorders_primary_bin_and_falls_back_to_quantity_label() {
+ let mut listing = base_listing();
+ listing.discounts = None;
+
+ let mut primary = base_bin();
+ primary.bin_id = "bin-1".to_string();
+ primary.display_label = None;
+ primary.quantity = primary.quantity.clone().with_label("fallback-label");
+
+ let mut secondary = base_bin();
+ secondary.bin_id = "bin-2".to_string();
+ listing.primary_bin_id = "bin-1".to_string();
+ listing.bins = vec![secondary, primary];
+
+ let tags = listing_tags(&listing).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-1"));
+ assert_eq!(first_bin.get(6).map(|v| v.as_str()), Some("fallback-label"));
+ }
+
+ #[test]
+ fn listing_tags_location_handles_partial_optional_components() {
+ let mut listing = base_listing();
+ listing.discounts = None;
+ listing.location = Some(RadrootsListingLocation {
+ primary: "Moyobamba".to_string(),
+ city: Some(" ".to_string()),
+ region: Some("San Martin".to_string()),
+ country: Some(" ".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(0).map(|v| v.as_str()), Some("location"));
+ 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);
+ }
+
+ #[test]
fn listing_tags_supports_npub_farm_pubkey() {
let mut listing = base_listing();
listing.discounts = None;
diff --git a/crates/events-codec/src/message/tags.rs b/crates/events-codec/src/message/tags.rs
@@ -125,9 +125,6 @@ pub(crate) fn parse_reply_tag(
Some(tag) => tag,
None => return Ok(None),
};
- if tag.get(0).map(|s| s.as_str()) != Some("e") {
- return Err(EventParseError::InvalidTag("e"));
- }
let id = tag.get(1).ok_or(EventParseError::InvalidTag("e"))?;
if id.trim().is_empty() {
return Err(EventParseError::InvalidTag("e"));
@@ -151,12 +148,21 @@ pub(crate) fn parse_subject_tag(tags: &[Vec<String>]) -> Result<Option<String>,
Some(tag) => tag,
None => return Ok(None),
};
- if tag.get(0).map(|s| s.as_str()) != Some("subject") {
- return Err(EventParseError::InvalidTag("subject"));
- }
let subject = tag.get(1).ok_or(EventParseError::InvalidTag("subject"))?;
if subject.trim().is_empty() {
return Err(EventParseError::InvalidTag("subject"));
}
Ok(Some(subject.clone()))
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parse_recipient_tag_rejects_non_p_tag() {
+ let err = parse_recipient_tag(&["x".to_string(), "pub".to_string()])
+ .expect_err("expected invalid tag");
+ assert!(matches!(err, EventParseError::InvalidTag("p")));
+ }
+}
diff --git a/crates/events-codec/src/resource_area/list_sets.rs b/crates/events-codec/src/resource_area/list_sets.rs
@@ -184,6 +184,16 @@ mod tests {
#[test]
fn farm_and_plot_address_helpers_reject_empty_d_tags() {
let err = farm_address(&RadrootsFarmRef {
+ pubkey: " ".to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
+ })
+ .expect_err("expected farm pubkey error");
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("farm.pubkey")
+ ));
+
+ let err = farm_address(&RadrootsFarmRef {
pubkey: "farm_pubkey".to_string(),
d_tag: " ".to_string(),
})
diff --git a/crates/events-codec/tests/event_ref.rs b/crates/events-codec/tests/event_ref.rs
@@ -56,6 +56,17 @@ fn parse_event_ref_tag_allows_relay_only_fifth_entry() {
let parsed = parse_event_ref_tag(&tag, "e").unwrap();
assert!(parsed.d_tag.is_none());
assert_eq!(parsed.relays, Some(vec!["wss://relay".to_string()]));
+
+ let ws_tag = vec![
+ "e".to_string(),
+ "id".to_string(),
+ "author".to_string(),
+ KIND_POST.to_string(),
+ "ws://relay".to_string(),
+ ];
+ let parsed = parse_event_ref_tag(&ws_tag, "e").unwrap();
+ assert!(parsed.d_tag.is_none());
+ assert_eq!(parsed.relays, Some(vec!["ws://relay".to_string()]));
}
#[test]
@@ -229,4 +240,14 @@ fn parse_nip10_ref_tags_skips_invalid_a_tags_until_match() {
parsed.relays,
Some(vec!["wss://relay.empty-d.example.com".to_string()])
);
+
+ 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")],
+ ];
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert!(parsed.d_tag.is_none());
+ assert!(parsed.relays.is_none());
}
diff --git a/crates/events-codec/tests/job_result.rs b/crates/events-codec/tests/job_result.rs
@@ -97,6 +97,46 @@ fn job_result_encrypted_adds_flag_and_rejects_inputs() {
}
#[test]
+fn job_result_build_tags_supports_minimal_optional_fields() {
+ let mut res = sample_result();
+ res.request_json = None;
+ res.inputs.clear();
+ res.customer_pubkey = None;
+ res.payment = None;
+ let parts = to_wire_parts(&res, "payload").unwrap();
+ assert!(
+ parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("e"))
+ );
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("request"))
+ );
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("i"))
+ );
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("p"))
+ );
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("amount"))
+ );
+}
+
+#[test]
fn job_result_requires_request_event_tag() {
let tags = vec![vec!["p".to_string(), "customer".to_string()]];
let err = job_result_from_tags(KIND_JOB_RESULT_MIN + 1, &tags, "payload").unwrap_err();
diff --git a/crates/events-codec/tests/job_util.rs b/crates/events-codec/tests/job_util.rs
@@ -75,6 +75,10 @@ fn parse_i_tags_handles_multiple_shapes() {
],
vec![
"i".to_string(),
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
+ ],
+ vec![
+ "i".to_string(),
"job-id".to_string(),
"job".to_string(),
"wss://relay".to_string(),
@@ -83,7 +87,7 @@ fn parse_i_tags_handles_multiple_shapes() {
];
let inputs = parse_i_tags(&tags);
- assert_eq!(inputs.len(), 4);
+ assert_eq!(inputs.len(), 5);
assert_eq!(inputs[0].data, "https://example.com");
assert_eq!(inputs[0].input_type, JobInputType::Url);
@@ -98,10 +102,18 @@ fn parse_i_tags_handles_multiple_shapes() {
assert!(inputs[2].relay.is_none());
assert!(inputs[2].marker.is_none());
- assert_eq!(inputs[3].data, "job-id");
- assert_eq!(inputs[3].input_type, JobInputType::Job);
- assert_eq!(inputs[3].relay.as_deref(), Some("wss://relay"));
- assert_eq!(inputs[3].marker.as_deref(), Some("marker"));
+ assert_eq!(
+ inputs[3].data,
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ );
+ assert_eq!(inputs[3].input_type, JobInputType::Event);
+ assert!(inputs[3].relay.is_none());
+ assert!(inputs[3].marker.is_none());
+
+ assert_eq!(inputs[4].data, "job-id");
+ assert_eq!(inputs[4].input_type, JobInputType::Job);
+ assert_eq!(inputs[4].relay.as_deref(), Some("wss://relay"));
+ assert_eq!(inputs[4].marker.as_deref(), Some("marker"));
}
#[test]
diff --git a/crates/events-codec/tests/list_set.rs b/crates/events-codec/tests/list_set.rs
@@ -203,3 +203,15 @@ fn list_set_decode_keeps_first_d_tag() {
let decoded = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).unwrap();
assert_eq!(decoded.d_tag, "members.owners");
}
+
+#[test]
+fn list_set_build_tags_omits_blank_optional_metadata() {
+ let mut list_set = sample_list_set();
+ list_set.title = Some(" ".to_string());
+ list_set.description = Some(" ".to_string());
+ list_set.image = Some(" ".to_string());
+ let tags = list_set_build_tags(&list_set).unwrap();
+ assert!(!tags.iter().any(|tag| tag[0] == "title"));
+ assert!(!tags.iter().any(|tag| tag[0] == "description"));
+ assert!(!tags.iter().any(|tag| tag[0] == "image"));
+}