lib

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

commit fdfa2a225286b82ba6a3edd48dcd524b88a937c6
parent e54c7594882c7536f04e51dc28d643dcdd5c05d9
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 19:45:12 +0000

events-codec: expand post coverage

- Cover post farm, address, quote, topic, and media encoding error paths.

- Exercise generated imeta fields and richer post media decoding branches.

- Validate post target and refreshed codec coverage run.

- Leave radroots_events_codec gate failing for follow-on codec family slices.

Diffstat:
Mcrates/events_codec/tests/post.rs | 294++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 278 insertions(+), 16 deletions(-)

diff --git a/crates/events_codec/tests/post.rs b/crates/events_codec/tests/post.rs @@ -3,8 +3,8 @@ use radroots_events::{ kinds::{KIND_ARTICLE, KIND_COMMENT, KIND_POST}, post::RadrootsPost, social::{ - RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaMetadata, - RadrootsSocialTarget, + RadrootsSocialFarmAnchor, RadrootsSocialLocation, RadrootsSocialMediaDimensions, + RadrootsSocialMediaMetadata, RadrootsSocialMediaThumbnail, RadrootsSocialTarget, }, tags::{TAG_A, TAG_G, TAG_IMETA, TAG_LOCATION, TAG_Q, TAG_T}, }; @@ -20,6 +20,18 @@ const QUOTE_ID: &str = "0123456789abcdef0123456789abcdef0123456789abcdef01234567 const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; const ARTICLE_D_TAG: &str = "BBBBBBBBBBBBBBBBBBBBBA"; +fn content_post() -> RadrootsPost { + RadrootsPost { + content: "field update".to_string(), + farm: None, + address_refs: None, + location: None, + topics: None, + quote_refs: None, + media: None, + } +} + #[test] fn post_to_wire_parts_requires_content() { let post = RadrootsPost { @@ -191,26 +203,100 @@ fn post_to_wire_parts_roundtrips_optional_social_tags() { #[test] fn post_social_tags_reject_malformed_supported_structures() { - let mut post = RadrootsPost { - content: "field update".to_string(), - farm: None, - address_refs: Some(vec![RadrootsSocialTarget::Event { - id: QUOTE_ID.to_string(), - author: None, - event_kind: None, - relays: None, - }]), - location: None, - topics: None, - quote_refs: None, - media: None, - }; + let mut post = content_post(); + post.address_refs = Some(vec![RadrootsSocialTarget::Event { + id: QUOTE_ID.to_string(), + author: None, + event_kind: None, + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("address_refs")) + )); + + post.address_refs = Some(vec![RadrootsSocialTarget::Address { + address: "not-an-address".to_string(), + author: None, + event_kind: None, + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("address_refs")) + )); + + post.address_refs = Some(vec![RadrootsSocialTarget::Address { + address: format!("30340:farm_pubkey:{FARM_D_TAG}"), + author: Some("farm_pubkey".to_string()), + event_kind: Some(30340), + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("address_refs")) + )); + + post.address_refs = Some(vec![RadrootsSocialTarget::Address { + address: format!("30023:article_author:{ARTICLE_D_TAG}"), + author: Some("other_author".to_string()), + event_kind: Some(30023), + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("address_refs")) + )); + + post.address_refs = Some(vec![RadrootsSocialTarget::Address { + address: format!("30023:article_author:{ARTICLE_D_TAG}"), + author: Some("article_author".to_string()), + event_kind: Some(30024), + relays: None, + }]); assert!(matches!( post_build_tags(&post), Err(EventEncodeError::InvalidField("address_refs")) )); post.address_refs = None; + post.farm = Some(RadrootsSocialFarmAnchor { + farm: RadrootsFarmRef { + pubkey: String::new(), + d_tag: FARM_D_TAG.to_string(), + }, + relays: None, + }); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::EmptyRequiredField("farm.pubkey")) + )); + + post.farm = Some(RadrootsSocialFarmAnchor { + farm: RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: String::new(), + }, + relays: None, + }); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::EmptyRequiredField("farm.d_tag")) + )); + + post.farm = Some(RadrootsSocialFarmAnchor { + farm: RadrootsFarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "bad d".to_string(), + }, + relays: None, + }); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("farm")) + )); + + post.farm = None; post.quote_refs = Some(vec![RadrootsSocialTarget::Event { id: "not-hex".to_string(), author: None, @@ -222,6 +308,69 @@ fn post_social_tags_reject_malformed_supported_structures() { Err(EventEncodeError::InvalidField("quote_refs")) )); + post.quote_refs = Some(vec![RadrootsSocialTarget::Address { + address: "not-an-address".to_string(), + author: None, + event_kind: None, + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("quote_refs")) + )); + + post.quote_refs = Some(vec![RadrootsSocialTarget::Address { + address: format!("30023:quote_author:{ARTICLE_D_TAG}"), + author: None, + event_kind: Some(30024), + relays: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("quote_refs")) + )); + + post.quote_refs = Some(vec![RadrootsSocialTarget::External { + id: "https://example.test/object".to_string(), + external_kind: "web".to_string(), + hint: None, + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("quote_refs")) + )); + + post.quote_refs = None; + post.media = Some(vec![RadrootsSocialMediaMetadata { + imeta: Some(vec![Vec::new()]), + ..RadrootsSocialMediaMetadata::default() + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("imeta")) + )); + + post.media = Some(vec![RadrootsSocialMediaMetadata { + imeta: Some(vec![vec![" ".to_string()]]), + ..RadrootsSocialMediaMetadata::default() + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("imeta")) + )); + + post.media = Some(vec![RadrootsSocialMediaMetadata { + thumbnails: Some(vec![RadrootsSocialMediaThumbnail { + url: " ".to_string(), + dimensions: None, + }]), + ..RadrootsSocialMediaMetadata::default() + }]); + assert!(matches!( + post_build_tags(&post), + Err(EventEncodeError::InvalidField("imeta")) + )); + let err = post_from_event( KIND_POST, &[vec![TAG_IMETA.to_string(), "bad-imeta-entry".to_string()]], @@ -232,6 +381,119 @@ fn post_social_tags_reject_malformed_supported_structures() { } #[test] +fn post_media_structured_fields_encode_and_decode_imeta() { + let mut post = content_post(); + post.topics = Some(vec![ + "soil".to_string(), + " ".to_string(), + "market".to_string(), + ]); + post.media = Some(vec![ + RadrootsSocialMediaMetadata::default(), + RadrootsSocialMediaMetadata { + url: Some("https://media.example.test/field.jpg".to_string()), + mime_type: Some("image/jpeg".to_string()), + sha256: Some(QUOTE_ID.to_string()), + original_sha256: Some(QUOTE_ID.to_string()), + size: Some(42), + dimensions: Some(RadrootsSocialMediaDimensions { + width: 1200, + height: 800, + }), + blurhash: Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string()), + thumbnails: Some(vec![RadrootsSocialMediaThumbnail { + url: "https://media.example.test/thumb.jpg".to_string(), + dimensions: Some(RadrootsSocialMediaDimensions { + width: 120, + height: 80, + }), + }]), + image: Some("https://media.example.test/poster.jpg".to_string()), + summary: Some("Field row image".to_string()), + alt: Some("rows in field".to_string()), + fallback: Some("https://media.example.test/fallback.jpg".to_string()), + magnet: Some("magnet:?xt=urn:btih:fixture".to_string()), + content_hashes: Some(vec!["hash-a".to_string(), "hash-b".to_string()]), + services: Some(vec!["https://media.example.test".to_string()]), + imeta: None, + }, + ]); + + let parts = to_wire_parts(&post).unwrap(); + let topic_tags = parts + .tags + .iter() + .filter(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_T)) + .count(); + assert_eq!(topic_tags, 2); + + let imeta = parts + .tags + .iter() + .find(|tag| tag.first().map(|value| value.as_str()) == Some(TAG_IMETA)) + .expect("imeta tag"); + for expected in [ + "url https://media.example.test/field.jpg", + "m image/jpeg", + "size 42", + "dim 1200x800", + "blurhash LEHV6nWB2yk8pyo0adR*.7kCMdnj", + "thumb https://media.example.test/thumb.jpg", + "dim 120x80", + "image https://media.example.test/poster.jpg", + "summary Field row image", + "alt rows in field", + "fallback https://media.example.test/fallback.jpg", + "magnet magnet:?xt=urn:btih:fixture", + "i hash-a", + "i hash-b", + "service https://media.example.test", + ] { + assert!(imeta.iter().any(|value| value == expected), "{expected}"); + } + + let decoded = post_from_event(parts.kind, &parts.tags, &parts.content).unwrap(); + let media = decoded.media.expect("media"); + assert_eq!(media.len(), 1); + assert_eq!(media[0].original_sha256.as_deref(), Some(QUOTE_ID)); + assert_eq!(media[0].size, Some(42)); + assert_eq!( + media[0].blurhash.as_deref(), + Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj") + ); + assert_eq!( + media[0].image.as_deref(), + Some("https://media.example.test/poster.jpg") + ); + assert_eq!(media[0].summary.as_deref(), Some("Field row image")); + assert_eq!( + media[0].fallback.as_deref(), + Some("https://media.example.test/fallback.jpg") + ); + assert_eq!( + media[0].magnet.as_deref(), + Some("magnet:?xt=urn:btih:fixture") + ); + assert_eq!(media[0].content_hashes.as_ref().map(Vec::len), Some(2)); +} + +#[test] +fn post_decode_rejects_more_invalid_imeta_shapes() { + for entry in ["url ", "size not-a-number", "dim bad", "dim 0x10"] { + let err = post_from_event( + KIND_POST, + &[vec![TAG_IMETA.to_string(), entry.to_string()]], + "hello", + ) + .unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidTag(TAG_IMETA) | EventParseError::InvalidNumber(TAG_IMETA, _) + )); + } +} + +#[test] fn post_from_content_requires_kind_and_content() { let err = post_from_content(KIND_COMMENT, "hello").unwrap_err(); assert!(matches!(