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:
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");
+}