commit ab1eb2941dee184bf7a8c2d01abf01f3f51bcd5e
parent 6ab7fe71a96aa8741f95eaf0c8b577622f0091c7
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Jul 2026 08:16:26 +0000
validation: close final runtime gates
- Remove clippy-only allocations from event contract tag validation tests.
- Build frozen listing mutation drafts through codec-owned JSON wire parts.
- Align authority and event-ref validation fixtures with strict draft contracts.
- Keep release-preflight coverage gates green for final closeout.
Diffstat:
6 files changed, 84 insertions(+), 20 deletions(-)
diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs
@@ -607,7 +607,15 @@ mod tests {
"radroots.order.request.v1",
KIND_ORDER_REQUEST,
1_700_000_000,
- Vec::new(),
+ vec![
+ vec!["d".to_owned(), "order-1".to_owned()],
+ vec!["p".to_owned(), pubkey.clone()],
+ vec![
+ "a".to_owned(),
+ format!("30402:{}:listing-a", pubkey.as_str()),
+ ],
+ vec!["listing_event".to_owned(), hex_64('b')],
+ ],
"{}",
pubkey.as_str(),
)
diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs
@@ -4344,7 +4344,7 @@ mod tests {
);
assert_eq!(
validate_contract_tags_parts(
- &vec![vec!["test_many".to_owned(), "one".to_owned()]],
+ &[vec!["test_many".to_owned(), "one".to_owned()]],
&required_many
),
Ok(())
@@ -4355,7 +4355,7 @@ mod tests {
assert_eq!(validate_contract_tags_parts(&[], &optional_one), Ok(()));
assert_eq!(
validate_contract_tags_parts(
- &vec![
+ &[
vec!["test_optional".to_owned(), "one".to_owned()],
vec!["test_optional".to_owned(), "two".to_owned()],
],
@@ -4373,7 +4373,7 @@ mod tests {
);
assert_eq!(
validate_contract_tags_parts(
- &vec![
+ &[
vec!["test_required".to_owned(), "one".to_owned()],
vec!["test_required".to_owned(), "two".to_owned()],
],
@@ -4388,7 +4388,7 @@ mod tests {
);
assert_eq!(
validate_contract_tags_parts(
- &vec![
+ &[
vec!["test_optional".to_owned(), "one".to_owned()],
vec!["test_optional".to_owned(), "two".to_owned()],
],
diff --git a/crates/events_codec/src/event_ref.rs b/crates/events_codec/src/event_ref.rs
@@ -36,25 +36,24 @@ pub fn parse_event_ref_tag(
if tag.len() < 5 {
return Err(EventParseError::InvalidTag(tag_name));
}
- let id = tag.get(1).ok_or(EventParseError::InvalidTag(tag_name))?;
+ let id = &tag[1];
RadrootsEventId::parse(id).map_err(|_| EventParseError::InvalidTag(tag_name))?;
- let author = tag.get(2).ok_or(EventParseError::InvalidTag(tag_name))?;
+ let author = &tag[2];
RadrootsPublicKey::parse(author).map_err(|_| EventParseError::InvalidTag(tag_name))?;
- let kind_s = tag.get(3).ok_or(EventParseError::InvalidTag(tag_name))?;
+ let kind_s = &tag[3];
let kind: u32 = kind_s
.parse()
.map_err(|e| EventParseError::InvalidNumber(tag_name, e))?;
- let d_tag = match tag.get(4) {
- Some(value) if value.is_empty() => None,
- Some(value) => {
- if RadrootsRelayUrl::parse(value).is_ok() {
- return Err(EventParseError::InvalidTag(tag_name));
- }
- RadrootsDTag::parse(value).map_err(|_| EventParseError::InvalidTag(tag_name))?;
- Some(value.clone())
+ let d_tag_value = &tag[4];
+ let d_tag = if d_tag_value.is_empty() {
+ None
+ } else {
+ if RadrootsRelayUrl::parse(d_tag_value).is_ok() {
+ return Err(EventParseError::InvalidTag(tag_name));
}
- None => return Err(EventParseError::InvalidTag(tag_name)),
+ RadrootsDTag::parse(d_tag_value).map_err(|_| EventParseError::InvalidTag(tag_name))?;
+ Some(d_tag_value.clone())
};
let relays = if tag.len() > 5 {
diff --git a/crates/events_codec/src/listing/encode.rs b/crates/events_codec/src/listing/encode.rs
@@ -46,6 +46,23 @@ pub fn to_wire_parts_with_kind(
}
#[cfg(feature = "serde_json")]
+pub fn to_json_wire_parts_with_kind(
+ listing: &RadrootsListing,
+ kind: u32,
+) -> Result<WireEventParts, EventEncodeError> {
+ if !is_listing_kind(kind) {
+ return Err(EventEncodeError::InvalidKind(kind));
+ }
+ let tags = listing_tags_full(listing)?;
+ let content = serde_json::to_string(listing).map_err(|_| EventEncodeError::Json)?;
+ Ok(WireEventParts {
+ kind,
+ content,
+ tags,
+ })
+}
+
+#[cfg(feature = "serde_json")]
fn listing_markdown_content(listing: &RadrootsListing) -> String {
let title = listing.product.title.trim();
let summary = listing
diff --git a/crates/events_codec/tests/listing.rs b/crates/events_codec/tests/listing.rs
@@ -27,7 +27,7 @@ use radroots_events_codec::listing::decode::{
parsed_from_nostr_event,
};
use radroots_events_codec::listing::encode::{
- listing_build_tags, to_wire_parts, to_wire_parts_with_kind,
+ listing_build_tags, to_json_wire_parts_with_kind, to_wire_parts, to_wire_parts_with_kind,
};
use radroots_events_codec::listing::tags::{
ListingTagOptions, listing_tags_full, listing_tags_with_options,
@@ -219,6 +219,40 @@ fn listing_roundtrip_from_event() {
}
#[test]
+fn listing_json_wire_parts_with_kind_serializes_listing_object() {
+ let listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg");
+ let parts = to_json_wire_parts_with_kind(&listing, KIND_LISTING_DRAFT).unwrap();
+
+ assert_eq!(parts.kind, KIND_LISTING_DRAFT);
+ let decoded: RadrootsListing = serde_json::from_str(&parts.content).unwrap();
+ assert_eq!(decoded.d_tag, listing.d_tag);
+ assert_eq!(decoded.product.title, listing.product.title);
+ assert_eq!(decoded.primary_bin_id, listing.primary_bin_id);
+}
+
+#[test]
+fn listing_json_wire_parts_with_kind_rejects_wrong_kind() {
+ let err = to_json_wire_parts_with_kind(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg"), KIND_POST)
+ .unwrap_err();
+ assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST)));
+}
+
+#[test]
+fn listing_json_wire_parts_with_kind_reports_tag_errors() {
+ let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg");
+ listing.resource_area = Some(RadrootsResourceAreaRef {
+ pubkey: "resource_pubkey".to_string(),
+ d_tag: "bad d".to_string(),
+ });
+
+ let err = to_json_wire_parts_with_kind(&listing, KIND_LISTING).unwrap_err();
+ assert!(matches!(
+ err,
+ EventEncodeError::InvalidField("resource_area.d_tag")
+ ));
+}
+
+#[test]
fn listing_from_event_reconstructs_from_tags_with_markdown_content() {
let listing = sample_listing_full("FAAAAAAAAAAAAAAAAAAAAA");
let tags = listing_build_tags(&listing).unwrap();
diff --git a/crates/trade/src/listing/mutation.rs b/crates/trade/src/listing/mutation.rs
@@ -21,7 +21,7 @@ use radroots_events::{
kinds::{KIND_LISTING, KIND_LISTING_DRAFT},
};
#[cfg(feature = "serde_json")]
-use radroots_events_codec::{listing::encode::to_wire_parts_with_kind, wire::to_frozen_draft};
+use radroots_events_codec::{listing::encode::to_json_wire_parts_with_kind, wire::to_frozen_draft};
use thiserror::Error;
use crate::listing::draft::RadrootsCanonicalListingDraft;
@@ -133,7 +133,7 @@ pub fn build_listing_mutation_draft(
return Err(RadrootsListingMutationError::UnsupportedMutation);
}
};
- let parts = to_wire_parts_with_kind(draft.listing(), kind)
+ let parts = to_json_wire_parts_with_kind(draft.listing(), kind)
.map_err(|error| RadrootsListingMutationError::EncodeListing(error.to_string()))?;
to_frozen_draft(
parts,
@@ -368,6 +368,9 @@ mod tests {
assert_eq!(publish_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID);
assert_eq!(publish_draft.expected_pubkey, SELLER);
assert_eq!(publish_draft.created_at, 1_700_000_000);
+ let published_content: RadrootsListing =
+ serde_json::from_str(&publish_draft.content).expect("listing json");
+ assert_eq!(published_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
assert_eq!(update_draft.kind, KIND_LISTING);
assert_eq!(update_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID);
assert_eq!(update_draft.expected_pubkey, SELLER);
@@ -383,6 +386,9 @@ mod tests {
assert_eq!(draft.contract_id, LISTING_DRAFT_CONTRACT_ID);
assert_eq!(draft.expected_pubkey, SELLER);
assert_eq!(draft.created_at, 1_700_000_000);
+ let draft_content: RadrootsListing =
+ serde_json::from_str(&draft.content).expect("listing json");
+ assert_eq!(draft_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
}
#[test]