lib

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

commit 26a5c9cfa6c231a0ddbdbb9816d2102da8fe32dc
parent 9ff218a66a6a8aad8c6b8735a3bc60334a258db4
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 22:38:37 +0000

tests: expand default decode coverage across event metadata wrappers


- add metadata/index coverage tests for app_data, comment, follow, geochat, gift_wrap, message, message_file, post, reaction, and seal
- add list and list_set integration suites for encode/decode error branches and metadata/index roundtrips
- cover invalid optional tag parsing branches for message_file, gift_wrap, geochat, and follow
- validate with cargo check, cargo test, and xtask coverage preflight showing strict metric improvement

Diffstat:
Mcrates/events-codec/tests/app_data.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events-codec/tests/comment.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/follow.rs | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events-codec/tests/geochat.rs | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/gift_wrap.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Acrates/events-codec/tests/list.rs | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/events-codec/tests/list_set.rs | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/events-codec/tests/message.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events-codec/tests/message_file.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/events-codec/tests/post.rs | 40+++++++++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/reaction.rs | 47++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events-codec/tests/seal.rs | 40+++++++++++++++++++++++++++++++++++++++-
12 files changed, 878 insertions(+), 13 deletions(-)

diff --git a/crates/events-codec/tests/app_data.rs b/crates/events-codec/tests/app_data.rs @@ -1,8 +1,10 @@ use radroots_events::{ - app_data::{KIND_APP_DATA, RadrootsAppData}, + app_data::{RadrootsAppData, KIND_APP_DATA}, kinds::KIND_POST, }; -use radroots_events_codec::app_data::decode::app_data_from_tags; +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::error::{EventEncodeError, EventParseError}; @@ -60,3 +62,55 @@ fn app_data_roundtrip_from_tags() { assert_eq!(app_data.d_tag, "radroots.app"); assert_eq!(app_data.content, "payload"); } + +#[test] +fn app_data_from_tags_rejects_invalid_d_tag_shape() { + let err = app_data_from_tags(KIND_APP_DATA, &[vec!["d".to_string()]], "payload").unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("d"))); + + let err = app_data_from_tags( + KIND_APP_DATA, + &[vec!["d".to_string(), " ".to_string()]], + "payload", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("d"))); +} + +#[test] +fn app_data_metadata_and_index_from_event_roundtrip() { + let tags = vec![vec!["d".to_string(), "radroots.app".to_string()]]; + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 42, + KIND_APP_DATA, + "payload".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 42); + assert_eq!(metadata.kind, KIND_APP_DATA); + assert_eq!(metadata.app_data.d_tag, "radroots.app"); + assert_eq!(metadata.app_data.content, "payload"); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 42, + KIND_APP_DATA, + "payload".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.id, "id"); + assert_eq!(index.event.author, "author"); + assert_eq!(index.event.created_at, 42); + assert_eq!(index.event.kind, KIND_APP_DATA); + assert_eq!(index.event.content, "payload"); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.app_data.d_tag, "radroots.app"); +} diff --git a/crates/events-codec/tests/comment.rs b/crates/events-codec/tests/comment.rs @@ -7,6 +7,7 @@ use radroots_events::{ }; use radroots_events_codec::comment::decode::comment_from_tags; +use radroots_events_codec::comment::decode::{index_from_event, metadata_from_event}; use radroots_events_codec::comment::encode::{comment_build_tags, to_wire_parts}; use radroots_events_codec::error::{EventEncodeError, EventParseError}; use radroots_events_codec::event_ref::{build_event_ref_tag, push_nip10_ref_tags}; @@ -143,3 +144,56 @@ fn comment_from_tags_rejects_wrong_kind() { } )); } + +#[test] +fn comment_metadata_and_index_from_event_roundtrip() { + let root = common::event_ref_with_d( + "root", + "author", + KIND_POST, + "root-d", + Some(vec!["wss://relay".to_string()]), + ); + let parent = common::event_ref_with_d( + "parent", + "author", + KIND_POST, + "parent-d", + Some(vec!["wss://relay-2".to_string()]), + ); + + let mut tags = Vec::new(); + push_nip10_ref_tags(&mut tags, &root, "E", "P", "K", "A"); + push_nip10_ref_tags(&mut tags, &parent, "e", "p", "k", "a"); + + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_COMMENT, + "hello".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 77); + assert_eq!(metadata.comment.content, "hello"); + assert_event_ref_fields(&metadata.comment.root, &root); + assert_event_ref_fields(&metadata.comment.parent, &parent); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_COMMENT, + "hello".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.created_at, 77); + assert_eq!(index.event.kind, KIND_COMMENT); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.comment.content, "hello"); +} diff --git a/crates/events-codec/tests/follow.rs b/crates/events-codec/tests/follow.rs @@ -4,8 +4,10 @@ use radroots_events::{ }; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::follow::decode::follow_from_tags; -use radroots_events_codec::follow::encode::{FollowMutation, follow_apply, to_wire_parts}; +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}; #[test] fn follow_to_wire_parts_builds_p_tags() { @@ -103,6 +105,68 @@ fn follow_from_tags_rejects_wrong_kind() { } #[test] +fn follow_from_tags_rejects_invalid_published_at_number() { + let tags = vec![vec![ + "p".to_string(), + "pubkey".to_string(), + "".to_string(), + "".to_string(), + "not-a-number".to_string(), + ]]; + let err = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap_err(); + assert!(matches!(err, EventParseError::InvalidNumber("p", _))); +} + +#[test] +fn follow_metadata_and_index_from_event_roundtrip() { + let tags = vec![vec![ + "p".to_string(), + "pubkey".to_string(), + "wss://relay.example.com".to_string(), + "alice".to_string(), + "88".to_string(), + ]]; + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 50, + KIND_FOLLOW, + "".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 50); + assert_eq!(metadata.kind, KIND_FOLLOW); + assert_eq!(metadata.follow.list.len(), 1); + assert_eq!(metadata.follow.list[0].published_at, 88); + assert_eq!(metadata.follow.list[0].public_key, "pubkey"); + assert_eq!( + metadata.follow.list[0].relay_url.as_deref(), + Some("wss://relay.example.com") + ); + assert_eq!( + metadata.follow.list[0].contact_name.as_deref(), + Some("alice") + ); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 50, + KIND_FOLLOW, + "".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_FOLLOW); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.follow.list.len(), 1); +} + +#[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 @@ -3,7 +3,9 @@ use radroots_events::{ kinds::{KIND_GEOCHAT, KIND_POST}, }; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::geochat::decode::geochat_from_tags; +use radroots_events_codec::geochat::decode::{ + geochat_from_tags, index_from_event, metadata_from_event, +}; use radroots_events_codec::geochat::encode::{geochat_build_tags, to_wire_parts}; #[test] @@ -110,3 +112,66 @@ fn geochat_roundtrip_from_tags() { assert_eq!(geochat.nickname.as_deref(), Some("alex")); assert!(geochat.teleported); } + +#[test] +fn geochat_from_tags_rejects_invalid_optional_tags() { + let err = geochat_from_tags( + KIND_GEOCHAT, + &[ + vec!["g".to_string(), "dr5rsj7".to_string()], + vec!["n".to_string(), " ".to_string()], + ], + "hello", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("n"))); + + let err = geochat_from_tags( + KIND_GEOCHAT, + &[ + vec!["g".to_string(), "dr5rsj7".to_string()], + vec!["t".to_string(), " ".to_string()], + ], + "hello", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("t"))); +} + +#[test] +fn geochat_metadata_and_index_from_event_roundtrip() { + let tags = vec![ + vec!["g".to_string(), "dr5rsj7".to_string()], + vec!["n".to_string(), "alex".to_string()], + vec!["t".to_string(), "teleport".to_string()], + ]; + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_GEOCHAT, + "hello".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 77); + assert_eq!(metadata.kind, KIND_GEOCHAT); + assert_eq!(metadata.geochat.geohash, "dr5rsj7"); + assert!(metadata.geochat.teleported); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_GEOCHAT, + "hello".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_GEOCHAT); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.geochat.geohash, "dr5rsj7"); +} diff --git a/crates/events-codec/tests/gift_wrap.rs b/crates/events-codec/tests/gift_wrap.rs @@ -2,7 +2,9 @@ use radroots_events::gift_wrap::{RadrootsGiftWrap, RadrootsGiftWrapRecipient}; use radroots_events::kinds::{KIND_GIFT_WRAP, KIND_MESSAGE}; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::gift_wrap::decode::gift_wrap_from_tags; +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}; fn sample_gift_wrap() -> RadrootsGiftWrap { @@ -68,3 +70,90 @@ fn gift_wrap_from_tags_requires_p_tag() { let err = gift_wrap_from_tags(KIND_GIFT_WRAP, &[], "payload").unwrap_err(); assert!(matches!(err, EventParseError::MissingTag("p"))); } + +#[test] +fn gift_wrap_from_tags_rejects_invalid_expiration_and_relay() { + let err = gift_wrap_from_tags( + KIND_GIFT_WRAP, + &[ + vec![ + "p".to_string(), + "pubkey".to_string(), + "wss://relay.example".to_string(), + ], + vec!["expiration".to_string(), " ".to_string()], + ], + "payload", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("expiration"))); + + let err = gift_wrap_from_tags( + KIND_GIFT_WRAP, + &[ + vec![ + "p".to_string(), + "pubkey".to_string(), + "wss://relay.example".to_string(), + ], + vec!["expiration".to_string(), "invalid".to_string()], + ], + "payload", + ) + .unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidNumber("expiration", _) + )); + + let err = gift_wrap_from_tags( + KIND_GIFT_WRAP, + &[vec!["p".to_string(), "pubkey".to_string(), " ".to_string()]], + "payload", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("p"))); +} + +#[test] +fn gift_wrap_metadata_and_index_from_event_roundtrip() { + let gift_wrap = sample_gift_wrap(); + let parts = to_wire_parts(&gift_wrap).unwrap(); + + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 11, + parts.kind, + parts.content.clone(), + parts.tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 11); + assert_eq!( + metadata.gift_wrap.recipient.public_key, + gift_wrap.recipient.public_key + ); + assert_eq!( + metadata.gift_wrap.recipient.relay_url, + gift_wrap.recipient.relay_url + ); + assert_eq!(metadata.gift_wrap.content, gift_wrap.content); + assert_eq!(metadata.gift_wrap.expiration, gift_wrap.expiration); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 11, + parts.kind, + parts.content, + parts.tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_GIFT_WRAP); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.gift_wrap.recipient.public_key, "pubkey"); +} diff --git a/crates/events-codec/tests/list.rs b/crates/events-codec/tests/list.rs @@ -0,0 +1,126 @@ +use radroots_events::{ + kinds::{KIND_LIST_MUTE, KIND_POST}, + list::{RadrootsList, RadrootsListEntry}, +}; +use radroots_events_codec::error::{EventEncodeError, EventParseError}; +use radroots_events_codec::list::decode::{ + index_from_event, list_entries_from_tags, list_from_tags, metadata_from_event, +}; +use radroots_events_codec::list::encode::{list_build_tags, to_wire_parts_with_kind}; + +fn sample_list() -> RadrootsList { + RadrootsList { + content: "private".to_string(), + entries: vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["pubkey".to_string()], + }, + RadrootsListEntry { + tag: "t".to_string(), + values: vec!["orchard".to_string()], + }, + ], + } +} + +#[test] +fn list_build_tags_and_decode_roundtrip() { + let list = sample_list(); + let tags = list_build_tags(&list).unwrap(); + let decoded = list_from_tags(KIND_LIST_MUTE, list.content.clone(), &tags).unwrap(); + assert_eq!(decoded.content, list.content); + assert_eq!(decoded.entries.len(), list.entries.len()); + assert_eq!(decoded.entries[0].tag, list.entries[0].tag); + assert_eq!(decoded.entries[0].values, list.entries[0].values); + assert_eq!(decoded.entries[1].tag, list.entries[1].tag); + assert_eq!(decoded.entries[1].values, list.entries[1].values); +} + +#[test] +fn list_encode_and_decode_reject_invalid_inputs() { + let invalid = RadrootsList { + content: "".to_string(), + entries: vec![RadrootsListEntry { + tag: " ".to_string(), + values: vec!["pubkey".to_string()], + }], + }; + let err = list_build_tags(&invalid).unwrap_err(); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("entry.tag") + )); + + let invalid = RadrootsList { + content: "".to_string(), + entries: vec![RadrootsListEntry { + tag: "p".to_string(), + values: vec![" ".to_string()], + }], + }; + let err = list_build_tags(&invalid).unwrap_err(); + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField("entry.values") + )); + + let err = to_wire_parts_with_kind(&sample_list(), KIND_POST).unwrap_err(); + assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST))); + + let err = list_from_tags(KIND_POST, "private".to_string(), &[]).unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidKind { + expected: "nip51 standard list kind", + got: KIND_POST + } + )); +} + +#[test] +fn list_entries_from_tags_rejects_empty_entry_fields() { + let err = list_entries_from_tags(&[vec!["".to_string(), "x".to_string()]]).unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("tag"))); + + let err = list_entries_from_tags(&[vec!["p".to_string(), " ".to_string()]]).unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("tag"))); +} + +#[test] +fn list_metadata_and_index_from_event_roundtrip() { + let list = sample_list(); + let parts = to_wire_parts_with_kind(&list, KIND_LIST_MUTE).unwrap(); + + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 44, + KIND_LIST_MUTE, + parts.content.clone(), + parts.tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 44); + assert_eq!(metadata.kind, KIND_LIST_MUTE); + assert_eq!(metadata.list.content, list.content); + assert_eq!(metadata.list.entries.len(), list.entries.len()); + assert_eq!(metadata.list.entries[0].tag, list.entries[0].tag); + assert_eq!(metadata.list.entries[0].values, list.entries[0].values); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 44, + KIND_LIST_MUTE, + parts.content, + parts.tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_LIST_MUTE); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.list.entries.len(), 2); +} diff --git a/crates/events-codec/tests/list_set.rs b/crates/events-codec/tests/list_set.rs @@ -0,0 +1,159 @@ +use radroots_events::{ + kinds::{KIND_LIST_SET_FOLLOW, KIND_POST}, + list::RadrootsListEntry, + list_set::RadrootsListSet, +}; +use radroots_events_codec::error::{EventEncodeError, EventParseError}; +use radroots_events_codec::list_set::decode::{ + index_from_event, list_set_from_tags, metadata_from_event, +}; +use radroots_events_codec::list_set::encode::{list_set_build_tags, to_wire_parts_with_kind}; + +fn sample_list_set() -> RadrootsListSet { + RadrootsListSet { + d_tag: "members.owners".to_string(), + content: "private".to_string(), + entries: vec![ + RadrootsListEntry { + tag: "p".to_string(), + values: vec!["owner".to_string()], + }, + RadrootsListEntry { + tag: "t".to_string(), + values: vec!["orchard".to_string()], + }, + ], + title: Some("owners".to_string()), + description: Some("core team".to_string()), + image: Some("https://example.com/team.png".to_string()), + } +} + +#[test] +fn list_set_build_tags_and_decode_roundtrip() { + let list_set = sample_list_set(); + let tags = list_set_build_tags(&list_set).unwrap(); + let decoded = + list_set_from_tags(KIND_LIST_SET_FOLLOW, list_set.content.clone(), &tags).unwrap(); + assert_eq!(decoded.d_tag, list_set.d_tag); + assert_eq!(decoded.title, list_set.title); + assert_eq!(decoded.description, list_set.description); + assert_eq!(decoded.image, list_set.image); + assert_eq!(decoded.entries.len(), list_set.entries.len()); + assert_eq!(decoded.entries[0].tag, list_set.entries[0].tag); + assert_eq!(decoded.entries[0].values, list_set.entries[0].values); + assert_eq!(decoded.entries[1].tag, list_set.entries[1].tag); + assert_eq!(decoded.entries[1].values, list_set.entries[1].values); +} + +#[test] +fn list_set_encode_and_decode_reject_invalid_inputs() { + let invalid = RadrootsListSet { + d_tag: " ".to_string(), + content: "".to_string(), + entries: Vec::new(), + title: None, + description: None, + image: None, + }; + let err = list_set_build_tags(&invalid).unwrap_err(); + assert!(matches!(err, EventEncodeError::EmptyRequiredField("d_tag"))); + + let invalid = RadrootsListSet { + d_tag: "farm:invalid:owners".to_string(), + content: "".to_string(), + entries: Vec::new(), + title: None, + description: None, + image: None, + }; + let err = list_set_build_tags(&invalid).unwrap_err(); + assert!(matches!(err, EventEncodeError::InvalidField("d_tag"))); + + let err = to_wire_parts_with_kind(&sample_list_set(), KIND_POST).unwrap_err(); + assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST))); + + let err = list_set_from_tags(KIND_POST, "".to_string(), &[]).unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidKind { + expected: "nip51 list set kind", + got: KIND_POST + } + )); +} + +#[test] +fn list_set_decode_rejects_invalid_tag_shapes() { + let err = list_set_from_tags(KIND_LIST_SET_FOLLOW, "".to_string(), &[]).unwrap_err(); + assert!(matches!(err, EventParseError::MissingTag("d"))); + + let err = list_set_from_tags( + KIND_LIST_SET_FOLLOW, + "".to_string(), + &[vec!["d".to_string(), " ".to_string()]], + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("d"))); + + let err = list_set_from_tags( + KIND_LIST_SET_FOLLOW, + "".to_string(), + &[vec!["".to_string(), "value".to_string()]], + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("tag"))); +} + +#[test] +fn list_set_metadata_and_index_from_event_roundtrip() { + let list_set = sample_list_set(); + let parts = to_wire_parts_with_kind(&list_set, KIND_LIST_SET_FOLLOW).unwrap(); + + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 44, + KIND_LIST_SET_FOLLOW, + parts.content.clone(), + parts.tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 44); + assert_eq!(metadata.kind, KIND_LIST_SET_FOLLOW); + assert_eq!(metadata.list_set.d_tag, "members.owners"); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 44, + KIND_LIST_SET_FOLLOW, + parts.content, + parts.tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_LIST_SET_FOLLOW); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.list_set.entries.len(), 2); +} + +#[test] +fn list_set_decode_keeps_first_optional_display_tags() { + let tags = vec![ + vec!["d".to_string(), "members.owners".to_string()], + vec!["title".to_string(), "owners".to_string()], + vec!["title".to_string(), "ignored".to_string()], + vec!["description".to_string(), "team".to_string()], + vec!["description".to_string(), "ignored".to_string()], + vec!["image".to_string(), "https://example.com/a.png".to_string()], + vec!["image".to_string(), "https://example.com/b.png".to_string()], + vec!["p".to_string(), "owner".to_string()], + ]; + let decoded = list_set_from_tags(KIND_LIST_SET_FOLLOW, "private".to_string(), &tags).unwrap(); + assert_eq!(decoded.title.as_deref(), Some("owners")); + assert_eq!(decoded.description.as_deref(), Some("team")); + assert_eq!(decoded.image.as_deref(), Some("https://example.com/a.png")); +} diff --git a/crates/events-codec/tests/message.rs b/crates/events-codec/tests/message.rs @@ -1,10 +1,12 @@ 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::message_from_tags; +use radroots_events_codec::message::decode::{ + index_from_event, message_from_tags, metadata_from_event, +}; use radroots_events_codec::message::encode::{message_build_tags, to_wire_parts}; #[test] @@ -161,3 +163,51 @@ fn message_roundtrip_from_tags() { ); assert_eq!(message.subject.as_deref(), Some("topic")); } + +#[test] +fn message_metadata_and_index_from_event_roundtrip() { + let tags = vec![ + vec!["p".to_string(), "pub1".to_string()], + vec![ + "p".to_string(), + "pub2".to_string(), + "wss://relay.example".to_string(), + ], + vec![ + "e".to_string(), + "reply".to_string(), + "wss://reply.example".to_string(), + ], + vec!["subject".to_string(), "topic".to_string()], + ]; + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_MESSAGE, + "hello".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 77); + assert_eq!(metadata.kind, KIND_MESSAGE); + assert_eq!(metadata.message.recipients.len(), 2); + assert_eq!(metadata.message.content, "hello"); + assert_eq!(metadata.message.subject.as_deref(), Some("topic")); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_MESSAGE, + "hello".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_MESSAGE); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.message.recipients.len(), 2); +} diff --git a/crates/events-codec/tests/message_file.rs b/crates/events-codec/tests/message_file.rs @@ -1,10 +1,12 @@ -use radroots_events::RadrootsNostrEventPtr; use radroots_events::kinds::{KIND_MESSAGE, KIND_MESSAGE_FILE}; use radroots_events::message::RadrootsMessageRecipient; use radroots_events::message_file::{RadrootsMessageFile, RadrootsMessageFileDimensions}; +use radroots_events::RadrootsNostrEventPtr; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::message_file::decode::message_file_from_tags; +use radroots_events_codec::message_file::decode::{ + index_from_event, message_file_from_tags, metadata_from_event, +}; use radroots_events_codec::message_file::encode::{message_file_build_tags, to_wire_parts}; fn sample_message_file() -> RadrootsMessageFile { @@ -160,3 +162,84 @@ fn message_file_from_tags_rejects_wrong_kind() { } )); } + +#[test] +fn message_file_from_tags_rejects_invalid_optional_tags() { + let message = sample_message_file(); + let mut parts = to_wire_parts(&message).unwrap(); + let size_tag = parts + .tags + .iter_mut() + .find(|tag| tag.first().map(|value| value.as_str()) == Some("size")) + .expect("size tag"); + size_tag[1] = "not-a-number".to_string(); + let err = message_file_from_tags(KIND_MESSAGE_FILE, &parts.tags, &parts.content).unwrap_err(); + assert!(matches!(err, EventParseError::InvalidNumber("size", _))); + + let err = message_file_from_tags( + KIND_MESSAGE_FILE, + &[ + vec!["p".to_string(), "pub1".to_string()], + vec!["file-type".to_string(), "image/jpeg".to_string()], + vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], + vec!["decryption-key".to_string(), "key".to_string()], + vec!["decryption-nonce".to_string(), "nonce".to_string()], + vec!["x".to_string(), "hash".to_string()], + vec!["dim".to_string(), "10".to_string()], + ], + "https://files.example/encrypted.bin", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("dim"))); + + let err = message_file_from_tags( + KIND_MESSAGE_FILE, + &[ + vec!["p".to_string(), "pub1".to_string()], + vec!["file-type".to_string(), "image/jpeg".to_string()], + vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], + vec!["decryption-key".to_string(), "key".to_string()], + vec!["decryption-nonce".to_string(), "nonce".to_string()], + vec!["x".to_string(), "hash".to_string()], + vec!["fallback".to_string()], + ], + "https://files.example/encrypted.bin", + ) + .unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("fallback"))); +} + +#[test] +fn message_file_metadata_and_index_from_event_roundtrip() { + let message = sample_message_file(); + let parts = to_wire_parts(&message).unwrap(); + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 77, + parts.kind, + parts.content.clone(), + parts.tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 77); + assert_eq!(metadata.kind, KIND_MESSAGE_FILE); + assert_eq!(metadata.message_file.file_type, "image/jpeg"); + assert_eq!(metadata.message_file.recipients.len(), 2); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 77, + parts.kind, + parts.content, + parts.tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_MESSAGE_FILE); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.message_file.file_type, "image/jpeg"); +} diff --git a/crates/events-codec/tests/post.rs b/crates/events-codec/tests/post.rs @@ -3,7 +3,9 @@ use radroots_events::{ post::RadrootsPost, }; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::post::decode::post_from_content; +use radroots_events_codec::post::decode::{ + index_from_event, metadata_from_event, post_from_content, +}; use radroots_events_codec::post::encode::to_wire_parts; #[test] @@ -45,3 +47,39 @@ fn post_from_content_requires_kind_and_content() { let err = post_from_content(KIND_POST, " ").unwrap_err(); assert!(matches!(err, EventParseError::InvalidTag("content"))); } + +#[test] +fn post_metadata_and_index_from_event_roundtrip() { + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_POST, + "hello".to_string(), + Vec::new(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 77); + assert_eq!(metadata.kind, KIND_POST); + assert_eq!(metadata.post.content, "hello"); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 77, + KIND_POST, + "hello".to_string(), + Vec::new(), + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.id, "id"); + assert_eq!(index.event.author, "author"); + assert_eq!(index.event.created_at, 77); + assert_eq!(index.event.kind, KIND_POST); + assert_eq!(index.event.content, "hello"); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.post.content, "hello"); +} diff --git a/crates/events-codec/tests/reaction.rs b/crates/events-codec/tests/reaction.rs @@ -8,7 +8,9 @@ use radroots_events::{ use radroots_events_codec::error::{EventEncodeError, EventParseError}; use radroots_events_codec::event_ref::{build_event_ref_tag, push_nip10_ref_tags}; -use radroots_events_codec::reaction::decode::reaction_from_tags; +use radroots_events_codec::reaction::decode::{ + index_from_event, metadata_from_event, reaction_from_tags, +}; use radroots_events_codec::reaction::encode::{reaction_build_tags, to_wire_parts}; #[test] @@ -80,3 +82,46 @@ fn reaction_roundtrip_from_legacy_tags() { assert_eq!(reaction.root.kind, root.kind); assert_eq!(reaction.content, "+"); } + +#[test] +fn reaction_metadata_and_index_from_event_roundtrip() { + let root = common::event_ref_with_d( + "root", + "author", + KIND_POST, + "note-1", + Some(vec!["wss://relay".to_string()]), + ); + let mut tags = Vec::new(); + push_nip10_ref_tags(&mut tags, &root, "e", "p", "k", "a"); + + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 99, + KIND_REACTION, + "+".to_string(), + tags.clone(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 99); + assert_eq!(metadata.kind, KIND_REACTION); + assert_eq!(metadata.reaction.content, "+"); + assert_eq!(metadata.reaction.root.id, root.id); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 99, + KIND_REACTION, + "+".to_string(), + tags, + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_REACTION); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.reaction.content, "+"); +} diff --git a/crates/events-codec/tests/seal.rs b/crates/events-codec/tests/seal.rs @@ -2,7 +2,7 @@ use radroots_events::kinds::{KIND_MESSAGE, KIND_SEAL}; use radroots_events::seal::RadrootsSeal; use radroots_events_codec::error::{EventEncodeError, EventParseError}; -use radroots_events_codec::seal::decode::seal_from_parts; +use radroots_events_codec::seal::decode::{index_from_event, metadata_from_event, seal_from_parts}; use radroots_events_codec::seal::encode::to_wire_parts; #[test] @@ -52,3 +52,41 @@ fn seal_from_parts_requires_empty_tags() { .unwrap_err(); assert!(matches!(err, EventParseError::InvalidTag("tags"))); } + +#[test] +fn seal_from_parts_requires_content() { + let err = seal_from_parts(KIND_SEAL, &[], " ").unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("content"))); +} + +#[test] +fn seal_metadata_and_index_from_event_roundtrip() { + let metadata = metadata_from_event( + "id".to_string(), + "author".to_string(), + 14, + KIND_SEAL, + "payload".to_string(), + Vec::new(), + ) + .unwrap(); + assert_eq!(metadata.id, "id"); + assert_eq!(metadata.author, "author"); + assert_eq!(metadata.published_at, 14); + assert_eq!(metadata.kind, KIND_SEAL); + assert_eq!(metadata.seal.content, "payload"); + + let index = index_from_event( + "id".to_string(), + "author".to_string(), + 14, + KIND_SEAL, + "payload".to_string(), + Vec::new(), + "sig".to_string(), + ) + .unwrap(); + assert_eq!(index.event.kind, KIND_SEAL); + assert_eq!(index.event.sig, "sig"); + assert_eq!(index.metadata.seal.content, "payload"); +}