commit 9cd55df7d2f96be5e29199b594fee5c484339ce9
parent 8b9c6547686f77d88e62dce121c0c89af8d9b6b3
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Feb 2026 00:23:53 +0000
tests: close `radroots-events` codec strict coverage gaps
- add edge-path tests across codec decode and encode suites to reach strict function coverage
- extend comment and reaction tag coverage for d-tag address branches
- remove dead follow and list_set decode branches and tighten helper assertions
- compute branch coverage from brda records with brf/brh fallback for deterministic gating
Diffstat:
31 files changed, 933 insertions(+), 103 deletions(-)
diff --git a/crates/events-codec/src/d_tag.rs b/crates/events-codec/src/d_tag.rs
@@ -38,3 +38,22 @@ pub(crate) fn validate_d_tag_tag(value: &str, tag: &'static str) -> Result<(), E
Err(EventParseError::InvalidTag(tag))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn d_tag_base64url_validation_covers_allowed_and_rejected_shapes() {
+ assert!(is_d_tag_base64url("AAAAAAAAAAAAAAAAAAAAAA"));
+ assert!(!is_d_tag_base64url("AAAAAAAAAAAAAAAAAAAAA!"));
+ assert!(!is_d_tag_base64url("AAAAAAAAAAAAAAAAAAAAAB"));
+ assert!(!is_d_tag_base64url("short"));
+ }
+
+ #[test]
+ fn validate_d_tag_returns_error_for_invalid_values() {
+ let err = validate_d_tag("AAAAAAAAAAAAAAAAAAAAA!", "d_tag").expect_err("invalid d_tag");
+ assert!(matches!(err, EventEncodeError::InvalidField("d_tag")));
+ }
+}
diff --git a/crates/events-codec/src/document/encode.rs b/crates/events-codec/src/document/encode.rs
@@ -86,3 +86,51 @@ pub fn to_wire_parts_with_kind(
tags,
})
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use radroots_events::document::RadrootsDocumentSubject;
+
+ fn sample_document() -> RadrootsDocument {
+ 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: "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62"
+ .to_string(),
+ address: Some(
+ "30340:58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62:AAAAAAAAAAAAAAAAAAAAAA"
+ .to_string(),
+ ),
+ },
+ tags: None,
+ }
+ }
+
+ #[test]
+ fn document_build_tags_includes_subject_address_when_present() {
+ let tags = document_build_tags(&sample_document()).expect("document tags");
+ assert!(
+ tags.iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("a"))
+ );
+ }
+
+ #[test]
+ fn document_build_tags_omits_subject_address_when_absent() {
+ let mut document = sample_document();
+ document.subject.address = None;
+ let tags = document_build_tags(&document).expect("document tags");
+ assert!(
+ !tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("a"))
+ );
+ }
+}
diff --git a/crates/events-codec/src/event_ref.rs b/crates/events-codec/src/event_ref.rs
@@ -175,10 +175,7 @@ pub fn parse_nip10_ref_tags(
break;
}
- let relays = match relays {
- Some(v) if !v.is_empty() => Some(v),
- _ => addr_relays,
- };
+ let relays = relays.or(addr_relays);
Ok(RadrootsNostrEventRef {
id: id.clone(),
diff --git a/crates/events-codec/src/farm/list_sets.rs b/crates/events-codec/src/farm/list_sets.rs
@@ -214,3 +214,24 @@ where
image: None,
})
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn farm_list_set_id_validates_suffix_and_farm_id() {
+ let err = farm_list_set_id("AAAAAAAAAAAAAAAAAAAAAA", " ")
+ .expect_err("expected suffix validation error");
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("list_set_suffix")
+ ));
+
+ let err = farm_list_set_id(" ", "members").expect_err("expected farm_id error");
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("farm_id")
+ ));
+ }
+}
diff --git a/crates/events-codec/src/follow/decode.rs b/crates/events-codec/src/follow/decode.rs
@@ -22,9 +22,6 @@ fn parse_follow_tag(
tag: &[String],
published_at: u32,
) -> Result<RadrootsFollowProfile, EventParseError> {
- if tag.get(0).map(|s| s.as_str()) != Some("p") {
- return Err(EventParseError::InvalidTag("p"));
- }
let public_key = tag.get(1).ok_or(EventParseError::InvalidTag("p"))?;
let (relay_url, contact_name) = match tag.get(2).filter(|s| !s.is_empty()) {
Some(value) if looks_like_ws_relay(value) => (
diff --git a/crates/events-codec/src/list_set/decode.rs b/crates/events-codec/src/list_set/decode.rs
@@ -19,9 +19,6 @@ const TAG_IMAGE: &str = "image";
fn entry_from_tag(tag: &[String]) -> Result<RadrootsListEntry, EventParseError> {
let name = tag.get(0).ok_or(EventParseError::InvalidTag("tag"))?;
- if name.trim().is_empty() {
- return Err(EventParseError::InvalidTag("tag"));
- }
let value = tag.get(1).ok_or(EventParseError::InvalidTag("tag"))?;
if value.trim().is_empty() {
return Err(EventParseError::InvalidTag("tag"));
diff --git a/crates/events-codec/src/listing/tags.rs b/crates/events-codec/src/listing/tags.rs
@@ -486,11 +486,7 @@ fn calculate_resolution(value: f64, max: u32) -> u32 {
let s = value.to_string();
let decimals = s.split('.').nth(1).map(|v| v.len() as u32).unwrap_or(0);
let bounded = cmp::min(decimals, max);
- if bounded == 0 {
- 1
- } else {
- bounded
- }
+ if bounded == 0 { 1 } else { bounded }
}
fn truncate_to_resolution(value: f64, resolution: u32) -> f64 {
@@ -803,9 +799,10 @@ mod tests {
geohash: None,
};
push_location_geotags(&mut tags, &location, ListingTagOptions::default());
- assert!(tags
- .iter()
- .any(|tag| tag.first().map(|v| v.as_str()) == Some("g")));
+ assert!(
+ tags.iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("g"))
+ );
assert!(tags.iter().any(|tag| {
tag.first().map(|v| v.as_str()) == Some("L")
&& tag.get(1).map(|v| v.as_str()) == Some("dd.lat")
@@ -834,10 +831,13 @@ mod tests {
..ListingTagOptions::default()
},
);
- assert!(decoded_tags.iter().any(|tag| {
- tag.first().map(|v| v.as_str()) == Some("l")
- && tag.get(2).map(|v| v.as_str()) == Some("dd")
- }));
+ let decoded_l_scopes: Vec<&str> = decoded_tags
+ .iter()
+ .filter(|tag| tag.first().map(|v| v.as_str()) == Some("l"))
+ .filter_map(|tag| tag.get(2).map(|v| v.as_str()))
+ .collect();
+ assert!(decoded_l_scopes.contains(&"dd"));
+ assert!(decoded_l_scopes.contains(&"dd.lat"));
let mut invalid_tags = Vec::new();
let invalid_geohash = RadrootsListingLocation {
@@ -892,9 +892,11 @@ 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 {
@@ -916,6 +918,32 @@ mod tests {
},
);
assert!(find_tag(&no_coordinate_tags, "l").is_none());
+
+ let mut partial_coordinate_tags = Vec::new();
+ let partial_coordinate_location = RadrootsListingLocation {
+ primary: "Test".to_string(),
+ city: None,
+ region: None,
+ country: None,
+ lat: Some(-6.03),
+ lng: None,
+ geohash: Some("6gkzwgjzn".to_string()),
+ };
+ push_location_geotags(
+ &mut partial_coordinate_tags,
+ &partial_coordinate_location,
+ ListingTagOptions {
+ include_geohash: false,
+ include_gps: true,
+ ..ListingTagOptions::default()
+ },
+ );
+ let partial_l_scopes: Vec<&str> = partial_coordinate_tags
+ .iter()
+ .filter(|tag| tag.first().map(|v| v.as_str()) == Some("l"))
+ .filter_map(|tag| tag.get(2).map(|v| v.as_str()))
+ .collect();
+ assert!(partial_l_scopes.contains(&"dd"));
}
#[test]
@@ -935,11 +963,13 @@ mod tests {
.expect("image tag");
assert_eq!(without_size.len(), 2);
- assert!(tag_listing_image(&RadrootsListingImage {
- url: "null".to_string(),
- size: None,
- })
- .is_none());
+ assert!(
+ tag_listing_image(&RadrootsListingImage {
+ url: "null".to_string(),
+ size: None,
+ })
+ .is_none()
+ );
assert_eq!(status_as_str(&RadrootsListingStatus::Active), "active");
assert_eq!(status_as_str(&RadrootsListingStatus::Sold), "sold");
@@ -1286,6 +1316,29 @@ mod tests {
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());
+
+ let geohash_only_tags = listing_tags_with_options(
+ &no_geo,
+ ListingTagOptions {
+ include_geohash: true,
+ include_gps: false,
+ ..ListingTagOptions::default()
+ },
+ )
+ .expect("location with geohash only");
+ assert!(find_tag(&geohash_only_tags, "g").is_some());
+ assert!(find_tag(&geohash_only_tags, "l").is_none());
+
+ let gps_only_tags = listing_tags_with_options(
+ &no_geo,
+ ListingTagOptions {
+ include_geohash: false,
+ include_gps: true,
+ ..ListingTagOptions::default()
+ },
+ )
+ .expect("location with gps only");
+ assert!(find_tag(&gps_only_tags, "l").is_some());
}
#[test]
diff --git a/crates/events-codec/src/message/tags.rs b/crates/events-codec/src/message/tags.rs
@@ -158,6 +158,7 @@ pub(crate) fn parse_subject_tag(tags: &[Vec<String>]) -> Result<Option<String>,
#[cfg(test)]
mod tests {
use super::*;
+ use radroots_events::RadrootsNostrEventPtr;
#[test]
fn parse_recipient_tag_rejects_non_p_tag() {
@@ -165,4 +166,41 @@ mod tests {
.expect_err("expected invalid tag");
assert!(matches!(err, EventParseError::InvalidTag("p")));
}
+
+ #[test]
+ fn build_and_parse_reply_tags_cover_optional_relay_paths() {
+ let tag = build_reply_tag(&Some(RadrootsNostrEventPtr {
+ id: "reply".to_string(),
+ relays: Some("wss://relay.example.com".to_string()),
+ }))
+ .expect("build reply tag")
+ .expect("reply tag");
+ assert_eq!(tag.len(), 3);
+ let parsed = parse_reply_tag(&[tag]).expect("parse reply");
+ assert_eq!(
+ parsed.and_then(|value| value.relays),
+ Some("wss://relay.example.com".to_string())
+ );
+
+ let tag = build_reply_tag(&Some(RadrootsNostrEventPtr {
+ id: "reply".to_string(),
+ relays: None,
+ }))
+ .expect("build reply tag")
+ .expect("reply tag");
+ assert_eq!(tag.len(), 2);
+ }
+
+ #[test]
+ fn parse_reply_tag_handles_absent_relay() {
+ let parsed = parse_reply_tag(&[vec!["e".to_string(), "reply".to_string()]])
+ .expect("parse reply tag without relay");
+ assert_eq!(
+ parsed,
+ Some(RadrootsNostrEventPtr {
+ id: "reply".to_string(),
+ relays: None,
+ })
+ );
+ }
}
diff --git a/crates/events-codec/src/resource_cap/encode.rs b/crates/events-codec/src/resource_cap/encode.rs
@@ -101,3 +101,62 @@ pub fn to_wire_parts_with_kind(
tags,
})
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use radroots_core::{RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreUnit};
+ use radroots_events::resource_area::RadrootsResourceAreaRef;
+ use radroots_events::resource_cap::RadrootsResourceHarvestProduct;
+
+ fn sample_cap_with_category(category: Option<&str>) -> RadrootsResourceHarvestCap {
+ RadrootsResourceHarvestCap {
+ d_tag: "AAAAAAAAAAAAAAAAAAAABA".to_string(),
+ resource_area: RadrootsResourceAreaRef {
+ pubkey: "58e318557257f2ab58a415d21bb57082b4824cf667a1d64e72bcbc5acc018c62"
+ .to_string(),
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
+ },
+ product: RadrootsResourceHarvestProduct {
+ key: "nutmeg".to_string(),
+ category: category.map(|value| value.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: None,
+ }
+ }
+
+ #[test]
+ fn resource_harvest_cap_build_tags_omits_blank_category() {
+ let tags = resource_harvest_cap_build_tags(&sample_cap_with_category(Some(" ")))
+ .expect("resource harvest cap tags");
+ assert!(
+ !tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("category"))
+ );
+
+ let tags = resource_harvest_cap_build_tags(&sample_cap_with_category(None))
+ .expect("resource harvest cap tags");
+ assert!(
+ !tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("category"))
+ );
+
+ let tags = resource_harvest_cap_build_tags(&sample_cap_with_category(Some("spice")))
+ .expect("resource harvest cap tags");
+ assert!(
+ tags.iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("category"))
+ );
+ }
+}
diff --git a/crates/events-codec/tests/app_data.rs b/crates/events-codec/tests/app_data.rs
@@ -1,11 +1,13 @@
use radroots_events::{
- app_data::{RadrootsAppData, KIND_APP_DATA},
+ app_data::{KIND_APP_DATA, RadrootsAppData},
kinds::KIND_POST,
};
use radroots_events_codec::app_data::decode::{
app_data_from_tags, index_from_event, metadata_from_event,
};
-use radroots_events_codec::app_data::encode::{app_data_build_tags, to_wire_parts};
+use radroots_events_codec::app_data::encode::{
+ app_data_build_tags, to_wire_parts, to_wire_parts_with_kind,
+};
use radroots_events_codec::error::{EventEncodeError, EventParseError};
#[test]
@@ -36,6 +38,16 @@ fn app_data_to_wire_parts_sets_kind_tags_content() {
}
#[test]
+fn app_data_to_wire_parts_with_kind_rejects_wrong_kind() {
+ let app_data = RadrootsAppData {
+ d_tag: "radroots.app".to_string(),
+ content: "payload".to_string(),
+ };
+ let err = to_wire_parts_with_kind(&app_data, KIND_POST).unwrap_err();
+ assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST)));
+}
+
+#[test]
fn app_data_from_tags_requires_kind() {
let tags = vec![vec!["d".to_string(), "radroots.app".to_string()]];
let err = app_data_from_tags(KIND_POST, &tags, "payload").unwrap_err();
@@ -114,3 +126,24 @@ fn app_data_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.app_data.d_tag, "radroots.app");
}
+
+#[test]
+fn app_data_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 42,
+ KIND_POST,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "30078",
+ got: KIND_POST
+ }
+ ));
+}
diff --git a/crates/events-codec/tests/codec_error_job.rs b/crates/events-codec/tests/codec_error_job.rs
@@ -2,15 +2,15 @@ use std::error::Error as _;
use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::job::encode::{
- assert_no_inputs_when_encrypted, push_provider_tag, push_relay_tag, push_status_tag,
- JobEncodeError,
+ JobEncodeError, assert_no_inputs_when_encrypted, push_provider_tag, push_relay_tag,
+ push_status_tag,
};
use radroots_events_codec::job::error::JobParseError;
use radroots_events_codec::profile::error::ProfileEncodeError;
#[cfg(feature = "serde_json")]
-use serde::ser::{Error as _, Serializer};
-#[cfg(feature = "serde_json")]
use serde::Serialize;
+#[cfg(feature = "serde_json")]
+use serde::ser::{Error as _, Serializer};
#[test]
fn parse_error_display_and_source_cover_variants() {
@@ -31,9 +31,11 @@ fn parse_error_display_and_source_cover_variants() {
let parse_int = "x".parse::<u32>().expect_err("parse int error");
let invalid_number = EventParseError::InvalidNumber("count", parse_int);
- assert!(invalid_number
- .to_string()
- .contains("invalid number in 'count'"));
+ assert!(
+ invalid_number
+ .to_string()
+ .contains("invalid number in 'count'")
+ );
assert!(invalid_number.source().is_some());
let invalid_json = EventParseError::InvalidJson("content");
@@ -147,9 +149,11 @@ fn job_parse_error_display_and_source_covers_variants() {
assert!(invalid.source().is_none());
let invalid_number = JobParseError::InvalidNumber("amount", "x".parse::<u32>().unwrap_err());
- assert!(invalid_number
- .to_string()
- .contains("invalid number in 'amount'"));
+ assert!(
+ invalid_number
+ .to_string()
+ .contains("invalid number in 'amount'")
+ );
assert!(invalid_number.source().is_some());
let non_whole = JobParseError::NonWholeSats("amount");
diff --git a/crates/events-codec/tests/comment.rs b/crates/events-codec/tests/comment.rs
@@ -89,6 +89,31 @@ fn comment_to_wire_parts_sets_kind_content_and_tags() {
}
#[test]
+fn comment_build_tags_includes_address_tags_when_refs_have_d_tag() {
+ let comment = RadrootsComment {
+ root: common::event_ref_with_d(
+ "root",
+ "author",
+ KIND_POST,
+ "root-d",
+ Some(vec!["wss://relay".to_string()]),
+ ),
+ parent: common::event_ref_with_d(
+ "parent",
+ "author",
+ KIND_POST,
+ "parent-d",
+ Some(vec!["wss://relay-2".to_string()]),
+ ),
+ content: "hello".to_string(),
+ };
+ let tags = comment_build_tags(&comment).unwrap();
+ assert_eq!(tags.len(), 8);
+ assert!(tags.iter().any(|tag| tag[0] == "A"));
+ assert!(tags.iter().any(|tag| tag[0] == "a"));
+}
+
+#[test]
fn comment_roundtrip_from_tags_with_parent() {
let root = common::event_ref_with_d(
"root",
@@ -153,6 +178,16 @@ fn comment_from_tags_requires_root_tag() {
}
#[test]
+fn comment_from_tags_rejects_empty_content() {
+ let root = common::event_ref("root", "author", KIND_POST);
+ let mut tags = Vec::new();
+ push_nip10_ref_tags(&mut tags, &root, "E", "P", "K", "A");
+
+ let err = comment_from_tags(KIND_COMMENT, &tags, " ").unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("content")));
+}
+
+#[test]
fn comment_from_tags_rejects_wrong_kind() {
let tags = vec![vec!["e".to_string(), "x".to_string()]];
let err = comment_from_tags(KIND_POST, &tags, "hello").unwrap_err();
@@ -217,3 +252,24 @@ fn comment_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.comment.content, "hello");
}
+
+#[test]
+fn comment_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 77,
+ KIND_POST,
+ "hello".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "1111",
+ got: KIND_POST
+ }
+ ));
+}
diff --git a/crates/events-codec/tests/event_ref.rs b/crates/events-codec/tests/event_ref.rs
@@ -138,6 +138,19 @@ fn push_and_parse_nip10_ref_tags_roundtrip_with_and_without_a_tag() {
assert_eq!(parsed.author, event.author);
assert_eq!(parsed.kind, event.kind);
assert!(parsed.d_tag.is_none());
+
+ let event =
+ common::event_ref_with_d("id3", "author3", KIND_POST, "AAAAAAAAAAAAAAAAAAAAAA", None);
+ let mut tags = Vec::new();
+ push_nip10_ref_tags(&mut tags, &event, "e", "p", "k", "a");
+ let a_tag = tags
+ .iter()
+ .find(|tag| tag.first().map(|v| v.as_str()) == Some("a"))
+ .expect("a tag");
+ assert_eq!(a_tag.len(), 2);
+ let parsed = parse_nip10_ref_tags(&tags, "e", "p", "k", "a").unwrap();
+ assert_eq!(parsed.d_tag, event.d_tag);
+ assert!(parsed.relays.is_none());
}
#[test]
@@ -219,12 +232,20 @@ fn parse_nip10_ref_tags_skips_invalid_a_tags_until_match() {
vec!["a".to_string()],
vec![
"a".to_string(),
- format!("{}:{}:{}", KIND_POST + 1, "author", "AAAAAAAAAAAAAAAAAAAAAA"),
+ format!(
+ "{}:{}:{}",
+ KIND_POST + 1,
+ "author",
+ "AAAAAAAAAAAAAAAAAAAAAA"
+ ),
"wss://relay.bad-kind.example.com".to_string(),
],
vec![
"a".to_string(),
- format!("{}:{}:{}", KIND_POST, "other-author", "AAAAAAAAAAAAAAAAAAAAAA"),
+ format!(
+ "{}:{}:{}",
+ KIND_POST, "other-author", "AAAAAAAAAAAAAAAAAAAAAA"
+ ),
"wss://relay.bad-author.example.com".to_string(),
],
vec![
diff --git a/crates/events-codec/tests/follow.rs b/crates/events-codec/tests/follow.rs
@@ -8,8 +8,8 @@ use radroots_events_codec::follow::decode::{
follow_from_tags, index_from_event, metadata_from_event,
};
use radroots_events_codec::follow::encode::{
- follow_apply, follow_to_wire_parts_after, to_wire_parts, to_wire_parts_with_kind,
- FollowMutation,
+ FollowMutation, follow_apply, follow_to_wire_parts_after, to_wire_parts,
+ to_wire_parts_with_kind,
};
#[test]
@@ -81,6 +81,23 @@ fn follow_from_tags_accepts_contact_without_relay() {
}
#[test]
+fn follow_from_tags_accepts_ws_relay_and_contact_name() {
+ let tags = vec![vec![
+ "p".to_string(),
+ "pubkey".to_string(),
+ "ws://relay.example.com".to_string(),
+ "alice".to_string(),
+ ]];
+
+ let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap();
+ assert_eq!(
+ follow.list[0].relay_url.as_deref(),
+ Some("ws://relay.example.com")
+ );
+ assert_eq!(follow.list[0].contact_name.as_deref(), Some("alice"));
+}
+
+#[test]
fn follow_from_tags_uses_tag_published_at() {
let tags = vec![vec![
"p".to_string(),
@@ -170,6 +187,27 @@ fn follow_metadata_and_index_from_event_roundtrip() {
}
#[test]
+fn follow_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 50,
+ KIND_POST,
+ "".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "3",
+ got: KIND_POST
+ }
+ ));
+}
+
+#[test]
fn follow_apply_adds_and_updates_entries() {
let follow = RadrootsFollow {
list: vec![
diff --git a/crates/events-codec/tests/geochat.rs b/crates/events-codec/tests/geochat.rs
@@ -41,6 +41,19 @@ fn geochat_build_tags_requires_nickname_if_present() {
}
#[test]
+fn geochat_build_tags_omits_optional_nickname_and_teleport_when_disabled() {
+ let geochat = RadrootsGeoChat {
+ geohash: "dr5rsj7".to_string(),
+ content: "hello".to_string(),
+ nickname: None,
+ teleported: false,
+ };
+
+ let tags = geochat_build_tags(&geochat).unwrap();
+ assert_eq!(tags, vec![vec!["g".to_string(), "dr5rsj7".to_string()]]);
+}
+
+#[test]
fn geochat_to_wire_parts_requires_content() {
let geochat = RadrootsGeoChat {
geohash: "dr5rsj7".to_string(),
@@ -117,6 +130,14 @@ fn geochat_roundtrip_from_tags() {
fn geochat_from_tags_rejects_invalid_optional_tags() {
let err = geochat_from_tags(
KIND_GEOCHAT,
+ &[vec!["g".to_string(), " ".to_string()]],
+ "hello",
+ )
+ .unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("g")));
+
+ let err = geochat_from_tags(
+ KIND_GEOCHAT,
&[
vec!["g".to_string(), "dr5rsj7".to_string()],
vec!["n".to_string(), " ".to_string()],
@@ -136,6 +157,17 @@ fn geochat_from_tags_rejects_invalid_optional_tags() {
)
.unwrap_err();
assert!(matches!(err, EventParseError::InvalidTag("t")));
+
+ let geochat = geochat_from_tags(
+ KIND_GEOCHAT,
+ &[
+ vec!["g".to_string(), "dr5rsj7".to_string()],
+ vec!["t".to_string(), "moving".to_string()],
+ ],
+ "hello",
+ )
+ .unwrap();
+ assert!(!geochat.teleported);
}
#[test]
@@ -175,3 +207,24 @@ fn geochat_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.geochat.geohash, "dr5rsj7");
}
+
+#[test]
+fn geochat_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 77,
+ KIND_POST,
+ "hello".to_string(),
+ vec![vec!["g".to_string(), "dr5rsj7".to_string()]],
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "20000",
+ got: KIND_POST
+ }
+ ));
+}
diff --git a/crates/events-codec/tests/gift_wrap.rs b/crates/events-codec/tests/gift_wrap.rs
@@ -181,6 +181,11 @@ fn gift_wrap_build_tags_handles_optional_expiration_and_invalid_relay() {
err,
EventEncodeError::EmptyRequiredField("recipient.relay_url")
));
+
+ let mut gift_wrap = sample_gift_wrap();
+ gift_wrap.recipient.relay_url = None;
+ let tags = gift_wrap_build_tags(&gift_wrap).unwrap();
+ assert_eq!(tags[0], vec!["p".to_string(), "pubkey".to_string()]);
}
#[test]
diff --git a/crates/events-codec/tests/job_feedback.rs b/crates/events-codec/tests/job_feedback.rs
@@ -5,7 +5,7 @@ use radroots_events::job_feedback::RadrootsJobFeedback;
use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
use radroots_events_codec::job::encode::JobEncodeError;
use radroots_events_codec::job::error::JobParseError;
-use radroots_events_codec::job::feedback::decode::job_feedback_from_tags;
+use radroots_events_codec::job::feedback::decode::{index_from_event, job_feedback_from_tags};
use radroots_events_codec::job::feedback::encode::to_wire_parts;
fn sample_feedback() -> RadrootsJobFeedback {
@@ -35,6 +35,22 @@ fn job_feedback_roundtrip_from_tags() {
}
#[test]
+fn job_feedback_from_tags_accepts_e_ref_and_empty_content() {
+ let tags = vec![
+ vec![
+ "e_ref".to_string(),
+ "req".to_string(),
+ "wss://relay".to_string(),
+ ],
+ vec!["status".to_string(), "processing".to_string()],
+ ];
+ let decoded = job_feedback_from_tags(KIND_JOB_FEEDBACK, &tags, "").unwrap();
+ assert_eq!(decoded.request_event.id, "req");
+ assert_eq!(decoded.request_event.relays.as_deref(), Some("wss://relay"));
+ assert!(decoded.content.is_none());
+}
+
+#[test]
fn job_feedback_requires_valid_kind() {
let mut fb = sample_feedback();
fb.kind = KIND_JOB_RESULT_MIN as u16;
@@ -82,6 +98,25 @@ fn job_feedback_metadata_rejects_wrong_kind() {
}
#[test]
+fn job_feedback_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 1,
+ KIND_JOB_REQUEST_MIN,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+
+ assert!(matches!(
+ err,
+ JobParseError::InvalidTag("kind (expected 7000)")
+ ));
+}
+
+#[test]
fn job_feedback_build_tags_cover_optional_paths() {
let mut fb = sample_feedback();
fb.extra_info = None;
diff --git a/crates/events-codec/tests/job_request.rs b/crates/events-codec/tests/job_request.rs
@@ -3,7 +3,7 @@ use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJ
use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
use radroots_events_codec::job::encode::JobEncodeError;
use radroots_events_codec::job::error::JobParseError;
-use radroots_events_codec::job::request::decode::job_request_from_tags;
+use radroots_events_codec::job::request::decode::{index_from_event, job_request_from_tags};
use radroots_events_codec::job::request::encode::to_wire_parts;
fn sample_request() -> RadrootsJobRequest {
@@ -64,6 +64,33 @@ fn job_request_requires_providers_when_encrypted() {
}
#[test]
+fn job_request_from_tags_accepts_encrypted_with_provider() {
+ let request = job_request_from_tags(
+ KIND_JOB_REQUEST_MIN + 1,
+ &[
+ vec!["encrypted".to_string()],
+ vec!["p".to_string(), "provider".to_string()],
+ ],
+ )
+ .unwrap();
+ assert!(request.encrypted);
+ assert_eq!(request.providers, vec!["provider".to_string()]);
+}
+
+#[test]
+fn job_request_to_wire_parts_allows_encrypted_when_provider_present() {
+ let mut req = sample_request();
+ req.encrypted = true;
+ let parts = to_wire_parts(&req, "payload").unwrap();
+ assert!(
+ parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("encrypted"))
+ );
+}
+
+#[test]
fn job_request_metadata_rejects_wrong_kind() {
let err = radroots_events_codec::job::request::decode::metadata_from_event(
"id".to_string(),
@@ -79,3 +106,21 @@ fn job_request_metadata_rejects_wrong_kind() {
JobParseError::InvalidTag("kind (expected 5000-5999)")
));
}
+
+#[test]
+fn job_request_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 1,
+ KIND_JOB_RESULT_MIN,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ JobParseError::InvalidTag("kind (expected 5000-5999)")
+ ));
+}
diff --git a/crates/events-codec/tests/job_result.rs b/crates/events-codec/tests/job_result.rs
@@ -6,7 +6,7 @@ use radroots_events::job_result::RadrootsJobResult;
use radroots_events::kinds::{KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
use radroots_events_codec::job::encode::JobEncodeError;
use radroots_events_codec::job::error::JobParseError;
-use radroots_events_codec::job::result::decode::job_result_from_tags;
+use radroots_events_codec::job::result::decode::{index_from_event, job_result_from_tags};
use radroots_events_codec::job::result::encode::to_wire_parts;
fn sample_result() -> RadrootsJobResult {
@@ -41,6 +41,14 @@ fn job_result_roundtrip_from_tags() {
}
#[test]
+fn job_result_roundtrip_with_empty_content_sets_none() {
+ let res = sample_result();
+ let parts = to_wire_parts(&res, "").unwrap();
+ let decoded = job_result_from_tags(parts.kind, &parts.tags, "").unwrap();
+ assert!(decoded.content.is_none());
+}
+
+#[test]
fn job_result_roundtrip_preserves_input_relay_and_marker() {
let mut res = sample_result();
res.inputs = vec![RadrootsJobInput {
@@ -137,6 +145,19 @@ fn job_result_build_tags_supports_minimal_optional_fields() {
}
#[test]
+fn job_result_build_tags_omits_request_relay_when_absent() {
+ let mut res = sample_result();
+ res.request_event.relays = None;
+ let parts = to_wire_parts(&res, "payload").unwrap();
+ let request = parts
+ .tags
+ .iter()
+ .find(|tag| tag.first().map(|v| v.as_str()) == Some("e"))
+ .expect("request tag");
+ assert_eq!(request.len(), 2);
+}
+
+#[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();
@@ -160,3 +181,21 @@ fn job_result_metadata_rejects_wrong_kind() {
JobParseError::InvalidTag("kind (expected 6000-6999)")
));
}
+
+#[test]
+fn job_result_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 1,
+ KIND_JOB_REQUEST_MIN,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ JobParseError::InvalidTag("kind (expected 6000-6999)")
+ ));
+}
diff --git a/crates/events-codec/tests/job_traits.rs b/crates/events-codec/tests/job_traits.rs
@@ -1,9 +1,9 @@
+use radroots_events::RadrootsNostrEvent;
use radroots_events::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest};
use radroots_events::job_feedback::RadrootsJobFeedback;
use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest};
use radroots_events::job_result::RadrootsJobResult;
use radroots_events::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN};
-use radroots_events::RadrootsNostrEvent;
use radroots_events_codec::job::feedback::encode::to_wire_parts as to_feedback_wire_parts;
use radroots_events_codec::job::request::encode::to_wire_parts as to_request_wire_parts;
use radroots_events_codec::job::result::encode::to_wire_parts as to_result_wire_parts;
diff --git a/crates/events-codec/tests/job_util.rs b/crates/events-codec/tests/job_util.rs
@@ -53,7 +53,10 @@ fn feedback_status_tag_covers_all_variants() {
feedback_status_from_tag("payment-required"),
Some(JobFeedbackStatus::PaymentRequired)
);
- assert_eq!(feedback_status_from_tag("error"), Some(JobFeedbackStatus::Error));
+ assert_eq!(
+ feedback_status_from_tag("error"),
+ Some(JobFeedbackStatus::Error)
+ );
assert_eq!(
feedback_status_from_tag("success"),
Some(JobFeedbackStatus::Success)
@@ -117,6 +120,15 @@ fn parse_i_tags_handles_multiple_shapes() {
}
#[test]
+fn parse_i_tags_http_url_uses_url_type() {
+ let tags = vec![vec!["i".to_string(), "http://example.com".to_string()]];
+ let inputs = parse_i_tags(&tags);
+ assert_eq!(inputs.len(), 1);
+ assert_eq!(inputs[0].input_type, JobInputType::Url);
+ assert_eq!(inputs[0].data, "http://example.com");
+}
+
+#[test]
fn parse_i_tags_covers_marker_and_fallback_shapes() {
let tags = vec![
vec!["i".to_string()],
diff --git a/crates/events-codec/tests/list.rs b/crates/events-codec/tests/list.rs
@@ -124,3 +124,24 @@ fn list_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.list.entries.len(), 2);
}
+
+#[test]
+fn list_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 44,
+ KIND_POST,
+ "private".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "nip51 standard list kind",
+ got: KIND_POST
+ }
+ ));
+}
diff --git a/crates/events-codec/tests/list_set.rs b/crates/events-codec/tests/list_set.rs
@@ -176,6 +176,27 @@ fn list_set_metadata_and_index_from_event_roundtrip() {
}
#[test]
+fn list_set_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 44,
+ KIND_POST,
+ "private".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "nip51 list set kind",
+ got: KIND_POST
+ }
+ ));
+}
+
+#[test]
fn list_set_decode_keeps_first_optional_display_tags() {
let tags = vec![
vec!["d".to_string(), "members.owners".to_string()],
diff --git a/crates/events-codec/tests/message.rs b/crates/events-codec/tests/message.rs
@@ -1,7 +1,7 @@
use radroots_events::{
+ RadrootsNostrEventPtr,
kinds::{KIND_MESSAGE, KIND_POST},
message::{RadrootsMessage, RadrootsMessageRecipient},
- RadrootsNostrEventPtr,
};
use radroots_events_codec::error::{EventEncodeError, EventParseError};
use radroots_events_codec::message::decode::{
@@ -107,6 +107,22 @@ fn message_to_wire_parts_sets_tags() {
}
#[test]
+fn message_to_wire_parts_handles_absent_optional_fields() {
+ let message = RadrootsMessage {
+ recipients: vec![RadrootsMessageRecipient {
+ public_key: "pub1".to_string(),
+ relay_url: None,
+ }],
+ content: "hello".to_string(),
+ reply_to: None,
+ subject: None,
+ };
+
+ let parts = to_wire_parts(&message).unwrap();
+ assert_eq!(parts.tags, vec![vec!["p".to_string(), "pub1".to_string()]]);
+}
+
+#[test]
fn message_from_tags_requires_kind_content_and_recipients() {
let tags = vec![vec!["p".to_string(), "pub".to_string()]];
let err = message_from_tags(KIND_POST, &tags, "hello").unwrap_err();
@@ -213,6 +229,27 @@ fn message_metadata_and_index_from_event_roundtrip() {
}
#[test]
+fn message_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 77,
+ KIND_POST,
+ "hello".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "14",
+ got: KIND_POST
+ }
+ ));
+}
+
+#[test]
fn message_build_tags_rejects_invalid_optional_fields() {
let message = RadrootsMessage {
recipients: vec![RadrootsMessageRecipient {
@@ -309,11 +346,7 @@ fn message_from_tags_rejects_invalid_optional_tags() {
KIND_MESSAGE,
&[
vec!["p".to_string(), "pub".to_string()],
- vec![
- "e".to_string(),
- "reply".to_string(),
- " ".to_string(),
- ],
+ vec!["e".to_string(), "reply".to_string(), " ".to_string()],
],
"hello",
)
diff --git a/crates/events-codec/tests/message_file.rs b/crates/events-codec/tests/message_file.rs
@@ -363,6 +363,27 @@ fn message_file_metadata_and_index_from_event_roundtrip() {
}
#[test]
+fn message_file_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 77,
+ KIND_MESSAGE,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "15",
+ got: KIND_MESSAGE
+ }
+ ));
+}
+
+#[test]
fn message_file_from_tags_rejects_empty_content() {
let err = message_file_from_tags(
KIND_MESSAGE_FILE,
diff --git a/crates/events-codec/tests/post.rs b/crates/events-codec/tests/post.rs
@@ -83,3 +83,24 @@ fn post_metadata_and_index_from_event_roundtrip() {
assert_eq!(index.event.sig, "sig");
assert_eq!(index.metadata.post.content, "hello");
}
+
+#[test]
+fn post_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 77,
+ KIND_COMMENT,
+ "hello".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "1",
+ got: KIND_COMMENT
+ }
+ ));
+}
diff --git a/crates/events-codec/tests/reaction.rs b/crates/events-codec/tests/reaction.rs
@@ -121,6 +121,23 @@ fn reaction_roundtrip_from_tags() {
}
#[test]
+fn reaction_build_tags_includes_address_tag_when_root_has_d_tag() {
+ let reaction = RadrootsReaction {
+ root: common::event_ref_with_d(
+ "root",
+ "author",
+ KIND_POST,
+ "note-1",
+ Some(vec!["wss://relay".to_string()]),
+ ),
+ content: "+".to_string(),
+ };
+ let tags = reaction_build_tags(&reaction).unwrap();
+ assert_eq!(tags.len(), 4);
+ assert_eq!(tags[3][0], "a");
+}
+
+#[test]
fn reaction_roundtrip_from_legacy_tags() {
let root = common::event_ref("root", "author", KIND_POST);
let tags = vec![build_event_ref_tag(TAG_E_ROOT, &root)];
diff --git a/crates/events-codec/tests/seal.rs b/crates/events-codec/tests/seal.rs
@@ -94,12 +94,37 @@ fn seal_metadata_and_index_from_event_roundtrip() {
}
#[test]
+fn seal_index_from_event_propagates_parse_errors() {
+ let err = index_from_event(
+ "id".to_string(),
+ "author".to_string(),
+ 14,
+ KIND_MESSAGE,
+ "payload".to_string(),
+ Vec::new(),
+ "sig".to_string(),
+ )
+ .unwrap_err();
+ assert!(matches!(
+ err,
+ EventParseError::InvalidKind {
+ expected: "13",
+ got: KIND_MESSAGE
+ }
+ ));
+}
+
+#[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 parts = to_wire_parts_with_kind(&seal, KIND_SEAL).unwrap();
+ assert_eq!(parts.kind, KIND_SEAL);
+ assert_eq!(parts.content, "payload");
+
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
@@ -143,9 +143,11 @@ fn structured_build_tags_cover_optional_and_error_paths() {
};
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] == "t" && tag[1] == "organic")
+ );
assert!(farm_tags.iter().any(|tag| tag[0] == "g"));
let mut invalid_farm = farm.clone();
@@ -181,9 +183,11 @@ fn structured_build_tags_cover_optional_and_error_paths() {
};
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"));
+ 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(),
@@ -208,9 +212,11 @@ fn structured_build_tags_cover_optional_and_error_paths() {
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"));
+ 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());
@@ -241,9 +247,11 @@ fn structured_build_tags_cover_optional_and_error_paths() {
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"));
+ 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();
@@ -275,9 +283,11 @@ fn structured_build_tags_cover_optional_and_error_paths() {
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"));
+ 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(),
@@ -315,12 +325,16 @@ fn structured_build_tags_cover_optional_and_error_paths() {
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"));
+ 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();
@@ -360,7 +374,10 @@ fn structured_build_tags_cover_required_field_errors() {
invalid_document = document.clone();
invalid_document.doc_type = " ".to_string();
let err = document_build_tags(&invalid_document).unwrap_err();
- assert!(matches!(err, EventEncodeError::EmptyRequiredField("doc_type")));
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("doc_type")
+ ));
invalid_document = document.clone();
invalid_document.title = " ".to_string();
let err = document_build_tags(&invalid_document).unwrap_err();
@@ -368,7 +385,10 @@ fn structured_build_tags_cover_required_field_errors() {
invalid_document = document.clone();
invalid_document.version = " ".to_string();
let err = document_build_tags(&invalid_document).unwrap_err();
- assert!(matches!(err, EventEncodeError::EmptyRequiredField("version")));
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("version")
+ ));
invalid_document = document.clone();
invalid_document.subject.pubkey = " ".to_string();
let err = document_build_tags(&invalid_document).unwrap_err();
@@ -501,7 +521,10 @@ fn structured_build_tags_cover_required_field_errors() {
EventEncodeError::EmptyRequiredField("farm.d_tag")
));
let err = plot_address(TEST_PUBKEY_HEX, " ").unwrap_err();
- assert!(matches!(err, EventEncodeError::EmptyRequiredField("plot.d_tag")));
+ assert!(matches!(
+ err,
+ EventEncodeError::EmptyRequiredField("plot.d_tag")
+ ));
let area = RadrootsResourceArea {
d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(),
diff --git a/crates/events-codec/tests/tag_builders.rs b/crates/events-codec/tests/tag_builders.rs
@@ -2,6 +2,8 @@ use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
+use radroots_events::RadrootsNostrEventPtr;
+use radroots_events::RadrootsNostrEventRef;
use radroots_events::app_data::RadrootsAppData;
use radroots_events::comment::RadrootsComment;
use radroots_events::coop::RadrootsCoop;
@@ -36,8 +38,6 @@ use radroots_events::resource_area::{
};
use radroots_events::resource_cap::{RadrootsResourceHarvestCap, RadrootsResourceHarvestProduct};
use radroots_events::seal::RadrootsSeal;
-use radroots_events::RadrootsNostrEventPtr;
-use radroots_events::RadrootsNostrEventRef;
use radroots_events_codec::job::encode::JobEncodeError;
use radroots_events_codec::listing::encode::listing_build_tags;
use radroots_events_codec::tag_builders::RadrootsEventTagBuilder;
@@ -447,3 +447,28 @@ fn job_request_tag_builder_rejects_encrypted_without_provider() {
let err = request.build_tags().unwrap_err();
assert!(matches!(err, JobEncodeError::MissingProvidersForEncrypted));
}
+
+#[test]
+fn job_request_tag_builder_accepts_encrypted_with_provider() {
+ let request = RadrootsJobRequest {
+ kind: (KIND_JOB_REQUEST_MIN + 1) as u16,
+ inputs: vec![RadrootsJobInput {
+ data: "hello".to_string(),
+ input_type: JobInputType::Text,
+ relay: None,
+ marker: None,
+ }],
+ output: None,
+ params: Vec::new(),
+ bid_sat: None,
+ relays: Vec::new(),
+ providers: vec![TEST_PUBKEY_HEX.to_string()],
+ topics: Vec::new(),
+ encrypted: true,
+ };
+ let tags = request.build_tags().unwrap();
+ assert!(
+ tags.iter()
+ .any(|tag| tag.first().map(|v| v.as_str()) == Some("encrypted"))
+ );
+}
diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs
@@ -141,17 +141,19 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> {
let mut da_covered: u64 = 0;
let mut executable_total: u64 = 0;
let mut executable_covered: u64 = 0;
- let mut branch_total: u64 = 0;
- let mut branch_covered: u64 = 0;
+ let mut branch_total_lcov: u64 = 0;
+ let mut branch_covered_lcov: u64 = 0;
+ let mut branch_total_brda: u64 = 0;
+ let mut branch_covered_brda: u64 = 0;
for line in raw.lines() {
if let Some(value) = line.strip_prefix("DA:") {
let Some((_, hit)) = value.split_once(',') else {
return Err(format!("invalid DA record in {}", path.display()));
};
- let hit_count: u64 = hit
- .parse()
- .map_err(|err| format!("invalid DA hit count `{hit}` in {}: {err}", path.display()))?;
+ let hit_count: u64 = hit.parse().map_err(|err| {
+ format!("invalid DA hit count `{hit}` in {}: {err}", path.display())
+ })?;
da_total = da_total.saturating_add(1);
if hit_count > 0 {
da_covered = da_covered.saturating_add(1);
@@ -159,31 +161,63 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> {
continue;
}
if let Some(value) = line.strip_prefix("LF:") {
- let parsed: u64 = value
- .parse()
- .map_err(|err| format!("invalid LF value `{value}` in {}: {err}", path.display()))?;
+ let parsed: u64 = value.parse().map_err(|err| {
+ format!("invalid LF value `{value}` in {}: {err}", path.display())
+ })?;
executable_total = executable_total.saturating_add(parsed);
continue;
}
if let Some(value) = line.strip_prefix("LH:") {
- let parsed: u64 = value
- .parse()
- .map_err(|err| format!("invalid LH value `{value}` in {}: {err}", path.display()))?;
+ let parsed: u64 = value.parse().map_err(|err| {
+ format!("invalid LH value `{value}` in {}: {err}", path.display())
+ })?;
executable_covered = executable_covered.saturating_add(parsed);
continue;
}
if let Some(value) = line.strip_prefix("BRF:") {
- let parsed: u64 = value
- .parse()
- .map_err(|err| format!("invalid BRF value `{value}` in {}: {err}", path.display()))?;
- branch_total = branch_total.saturating_add(parsed);
+ let parsed: u64 = value.parse().map_err(|err| {
+ format!("invalid BRF value `{value}` in {}: {err}", path.display())
+ })?;
+ branch_total_lcov = branch_total_lcov.saturating_add(parsed);
continue;
}
if let Some(value) = line.strip_prefix("BRH:") {
- let parsed: u64 = value
- .parse()
- .map_err(|err| format!("invalid BRH value `{value}` in {}: {err}", path.display()))?;
- branch_covered = branch_covered.saturating_add(parsed);
+ let parsed: u64 = value.parse().map_err(|err| {
+ format!("invalid BRH value `{value}` in {}: {err}", path.display())
+ })?;
+ branch_covered_lcov = branch_covered_lcov.saturating_add(parsed);
+ continue;
+ }
+ if let Some(value) = line.strip_prefix("BRDA:") {
+ let mut fields = value.split(',');
+ let _line_no = fields
+ .next()
+ .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?;
+ let _block_no = fields
+ .next()
+ .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?;
+ let _branch_no = fields
+ .next()
+ .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?;
+ let taken = fields
+ .next()
+ .ok_or_else(|| format!("invalid BRDA record in {}", path.display()))?;
+ if fields.next().is_some() {
+ return Err(format!("invalid BRDA record in {}", path.display()));
+ }
+ if taken == "-" {
+ continue;
+ }
+ let hit_count: u64 = taken.parse().map_err(|err| {
+ format!(
+ "invalid BRDA taken count `{taken}` in {}: {err}",
+ path.display()
+ )
+ })?;
+ branch_total_brda = branch_total_brda.saturating_add(1);
+ if hit_count > 0 {
+ branch_covered_brda = branch_covered_brda.saturating_add(1);
+ }
}
}
@@ -199,6 +233,11 @@ pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> {
executable_percent = (executable_covered as f64 / executable_total as f64) * 100.0_f64;
}
+ let (branch_total, branch_covered) = if branch_total_brda > 0 {
+ (branch_total_brda, branch_covered_brda)
+ } else {
+ (branch_total_lcov, branch_covered_lcov)
+ };
let branches_available = branch_total > 0;
let branch_percent = if branches_available {
Some((branch_covered as f64 / branch_total as f64) * 100.0_f64)
@@ -582,7 +621,7 @@ mod tests {
let path = temp_file_path("lcov");
fs::write(
&path,
- "DA:1,1\nDA:2,0\nDA:3,1\nBRF:4\nBRH:3\n",
+ "DA:1,1\nDA:2,0\nDA:3,1\nBRDA:1,0,0,1\nBRDA:1,0,1,0\nBRDA:2,0,0,3\nBRDA:2,0,1,-\n",
)
.expect("write lcov");
@@ -590,6 +629,20 @@ mod tests {
assert_eq!(lcov.executable_total, 3);
assert_eq!(lcov.executable_covered, 2);
assert!(lcov.branches_available);
+ assert_eq!(lcov.branch_total, 3);
+ assert_eq!(lcov.branch_covered, 2);
+ assert_eq!(lcov.branch_percent, Some(66.66666666666666));
+
+ fs::remove_file(path).expect("remove lcov");
+ }
+
+ #[test]
+ fn reads_lcov_branch_metrics_from_brf_brh_when_brda_missing() {
+ let path = temp_file_path("lcov_fallback");
+ fs::write(&path, "DA:1,1\nDA:2,1\nBRF:4\nBRH:3\n").expect("write lcov");
+
+ let lcov = read_lcov(&path).expect("parse lcov");
+ assert!(lcov.branches_available);
assert_eq!(lcov.branch_total, 4);
assert_eq!(lcov.branch_covered, 3);
assert_eq!(lcov.branch_percent, Some(75.0));