lib

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

commit 73cb8d0fc3690b250210e34ba637a1baf9faafb3
parent fdfa2a225286b82ba6a3edd48dcd524b88a937c6
Author: triesap <tyson@radroots.org>
Date:   Sun, 21 Jun 2026 19:50:10 +0000

events-codec: expand group coverage

- Cover group lifecycle, moderation, and leave-request roundtrips.

- Exercise group encoder validation for empty routing, metadata, user, and role fields.

- Add decoder rejection coverage for invalid kinds, marker tags, supported kinds, users, and roles.

- Validate full radroots_events_codec tests, check, diff check, and refreshed coverage run.

Diffstat:
Mcrates/events_codec/src/group/mod.rs | 378++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 365 insertions(+), 13 deletions(-)

diff --git a/crates/events_codec/src/group/mod.rs b/crates/events_codec/src/group/mod.rs @@ -4,24 +4,31 @@ pub mod encode; #[cfg(test)] mod tests { use radroots_events::group::{ - KIND_GROUP_ADMINS, KIND_GROUP_CREATE_INVITE, KIND_GROUP_JOIN_REQUEST, KIND_GROUP_MEMBERS, - KIND_GROUP_METADATA, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, KIND_GROUP_ROLES, - RadrootsGroupAdmins, RadrootsGroupCreateInvite, RadrootsGroupEditableMetadata, - RadrootsGroupJoinRequest, RadrootsGroupMembers, RadrootsGroupMetadata, - RadrootsGroupPutUser, RadrootsGroupRemoveUser, RadrootsGroupRole, RadrootsGroupRoles, - RadrootsGroupUserRef, + KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, + KIND_GROUP_DELETE_EVENT, KIND_GROUP_DELETE_GROUP, KIND_GROUP_EDIT_METADATA, + KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, + KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, KIND_GROUP_ROLES, RadrootsGroupAdmins, + RadrootsGroupCreateGroup, RadrootsGroupCreateInvite, RadrootsGroupDeleteEvent, + RadrootsGroupDeleteGroup, RadrootsGroupEditMetadata, RadrootsGroupEditableMetadata, + RadrootsGroupJoinRequest, RadrootsGroupLeaveRequest, RadrootsGroupMembers, + RadrootsGroupMetadata, RadrootsGroupPutUser, RadrootsGroupRemoveUser, RadrootsGroupRole, + RadrootsGroupRoles, RadrootsGroupUserRef, }; - use crate::error::EventParseError; + use crate::error::{EventEncodeError, EventParseError}; use crate::group::decode::{ - group_admins_from_event, group_create_invite_from_event, group_join_request_from_event, - group_members_from_event, group_metadata_from_event, group_put_user_from_event, - group_remove_user_from_event, group_roles_from_event, + group_admins_from_event, group_create_group_from_event, group_create_invite_from_event, + group_delete_event_from_event, group_delete_group_from_event, + group_edit_metadata_from_event, group_join_request_from_event, + group_leave_request_from_event, group_members_from_event, group_metadata_from_event, + group_put_user_from_event, group_remove_user_from_event, group_roles_from_event, }; use crate::group::encode::{ - group_admins_to_wire_parts, group_create_invite_to_wire_parts, - group_join_request_to_wire_parts, group_members_to_wire_parts, - group_metadata_to_wire_parts, group_put_user_to_wire_parts, + group_admins_to_wire_parts, group_create_group_to_wire_parts, + group_create_invite_to_wire_parts, group_delete_event_to_wire_parts, + group_delete_group_to_wire_parts, group_edit_metadata_to_wire_parts, + group_join_request_to_wire_parts, group_leave_request_to_wire_parts, + group_members_to_wire_parts, group_metadata_to_wire_parts, group_put_user_to_wire_parts, group_remove_user_to_wire_parts, group_roles_to_wire_parts, }; @@ -187,6 +194,97 @@ mod tests { } #[test] + fn group_lifecycle_and_moderation_events_roundtrip() { + let metadata = RadrootsGroupEditableMetadata { + is_private: true, + is_hidden: true, + ..sample_metadata() + }; + let create = RadrootsGroupCreateGroup { + group_id: "field-group".to_string(), + message: Some("create group".to_string()), + metadata: metadata.clone(), + }; + let edit = RadrootsGroupEditMetadata { + group_id: "field-group".to_string(), + message: Some("edit group".to_string()), + metadata, + }; + let delete_group = RadrootsGroupDeleteGroup { + group_id: "field-group".to_string(), + message: Some("delete group".to_string()), + }; + let delete_event = RadrootsGroupDeleteEvent { + group_id: "field-group".to_string(), + message: Some("delete event".to_string()), + event_id: "event_id".to_string(), + }; + let leave = RadrootsGroupLeaveRequest { + group_id: "field-group".to_string(), + message: None, + }; + + let create_parts = group_create_group_to_wire_parts(&create).expect("create"); + let edit_parts = group_edit_metadata_to_wire_parts(&edit).expect("edit"); + let delete_group_parts = + group_delete_group_to_wire_parts(&delete_group).expect("delete group"); + let delete_event_parts = + group_delete_event_to_wire_parts(&delete_event).expect("delete event"); + let leave_parts = group_leave_request_to_wire_parts(&leave).expect("leave"); + + assert_eq!(create_parts.kind, KIND_GROUP_CREATE_GROUP); + assert_eq!(edit_parts.kind, KIND_GROUP_EDIT_METADATA); + assert_eq!(delete_group_parts.kind, KIND_GROUP_DELETE_GROUP); + assert_eq!(delete_event_parts.kind, KIND_GROUP_DELETE_EVENT); + assert_eq!(leave_parts.kind, KIND_GROUP_LEAVE_REQUEST); + assert!(create_parts.tags.contains(&marker("private"))); + assert!(create_parts.tags.contains(&marker("hidden"))); + assert!(delete_event_parts.tags.contains(&tag("e", "event_id"))); + assert_eq!(leave_parts.content, ""); + assert_eq!( + group_create_group_from_event( + create_parts.kind, + &create_parts.tags, + &create_parts.content + ) + .expect("decode create"), + create + ); + assert_eq!( + group_edit_metadata_from_event(edit_parts.kind, &edit_parts.tags, &edit_parts.content) + .expect("decode edit"), + edit + ); + assert_eq!( + group_delete_group_from_event( + delete_group_parts.kind, + &delete_group_parts.tags, + &delete_group_parts.content + ) + .expect("decode delete group"), + delete_group + ); + assert_eq!( + group_delete_event_from_event( + delete_event_parts.kind, + &delete_event_parts.tags, + &delete_event_parts.content + ) + .expect("decode delete event"), + delete_event + ); + assert_eq!( + group_leave_request_from_event( + leave_parts.kind, + &leave_parts.tags, + &leave_parts.content + ) + .expect("decode leave"), + leave + ); + } + + #[test] fn group_codecs_reject_wrong_routing_tags() { let metadata = RadrootsGroupMetadata { d_tag: "field-group".to_string(), @@ -248,6 +346,260 @@ mod tests { assert!(matches!(invite_err, EventParseError::MissingTag("code"))); } + #[test] + fn group_encoders_reject_empty_required_fields() { + assert_empty_required( + group_put_user_to_wire_parts(&RadrootsGroupPutUser { + group_id: "".to_string(), + message: None, + pubkey: "member_pubkey".to_string(), + roles: vec![], + }), + "group_id", + ); + assert_empty_required( + group_put_user_to_wire_parts(&RadrootsGroupPutUser { + group_id: "field-group".to_string(), + message: None, + pubkey: "".to_string(), + roles: vec![], + }), + "pubkey", + ); + assert_empty_required( + group_put_user_to_wire_parts(&RadrootsGroupPutUser { + group_id: "field-group".to_string(), + message: None, + pubkey: "member_pubkey".to_string(), + roles: vec!["".to_string()], + }), + "roles", + ); + assert_empty_required( + group_remove_user_to_wire_parts(&RadrootsGroupRemoveUser { + group_id: "field-group".to_string(), + message: None, + pubkey: "".to_string(), + }), + "pubkey", + ); + assert_empty_required( + group_create_group_to_wire_parts(&RadrootsGroupCreateGroup { + group_id: "field-group".to_string(), + message: Some("".to_string()), + metadata: sample_metadata(), + }), + "message", + ); + assert_empty_required( + group_edit_metadata_to_wire_parts(&RadrootsGroupEditMetadata { + group_id: "field-group".to_string(), + message: None, + metadata: RadrootsGroupEditableMetadata { + name: Some("".to_string()), + ..sample_metadata() + }, + }), + "name", + ); + assert_empty_required( + group_delete_event_to_wire_parts(&RadrootsGroupDeleteEvent { + group_id: "field-group".to_string(), + message: None, + event_id: "".to_string(), + }), + "event_id", + ); + assert_empty_required( + group_create_invite_to_wire_parts(&RadrootsGroupCreateInvite { + group_id: "field-group".to_string(), + message: None, + code: "".to_string(), + }), + "code", + ); + assert_empty_required( + group_join_request_to_wire_parts(&RadrootsGroupJoinRequest { + group_id: "field-group".to_string(), + message: None, + code: Some("".to_string()), + }), + "code", + ); + assert_empty_required( + group_leave_request_to_wire_parts(&RadrootsGroupLeaveRequest { + group_id: "".to_string(), + message: None, + }), + "group_id", + ); + assert_empty_required( + group_metadata_to_wire_parts(&RadrootsGroupMetadata { + d_tag: "".to_string(), + metadata: sample_metadata(), + }), + "d_tag", + ); + assert_empty_required( + group_admins_to_wire_parts(&RadrootsGroupAdmins { + d_tag: "field-group".to_string(), + description: None, + admins: vec![RadrootsGroupUserRef { + pubkey: "".to_string(), + roles: vec![], + }], + }), + "pubkey", + ); + assert_empty_required( + group_members_to_wire_parts(&RadrootsGroupMembers { + d_tag: "field-group".to_string(), + description: Some("".to_string()), + members: vec![], + }), + "message", + ); + assert_empty_required( + group_members_to_wire_parts(&RadrootsGroupMembers { + d_tag: "field-group".to_string(), + description: None, + members: vec![RadrootsGroupUserRef { + pubkey: "member_pubkey".to_string(), + roles: vec!["".to_string()], + }], + }), + "roles", + ); + assert_empty_required( + group_roles_to_wire_parts(&RadrootsGroupRoles { + d_tag: "field-group".to_string(), + description: None, + roles: vec![RadrootsGroupRole { + name: "".to_string(), + description: None, + permissions: vec![], + }], + }), + "role.name", + ); + assert_empty_required( + group_roles_to_wire_parts(&RadrootsGroupRoles { + d_tag: "field-group".to_string(), + description: None, + roles: vec![RadrootsGroupRole { + name: "member".to_string(), + description: Some("".to_string()), + permissions: vec![], + }], + }), + "role.description", + ); + assert_empty_required( + group_roles_to_wire_parts(&RadrootsGroupRoles { + d_tag: "field-group".to_string(), + description: None, + roles: vec![RadrootsGroupRole { + name: "member".to_string(), + description: None, + permissions: vec!["".to_string()], + }], + }), + "role.permissions", + ); + } + + #[test] + fn group_decoders_reject_invalid_tag_shapes_and_kinds() { + let invalid_kind = group_put_user_from_event(KIND_GROUP_REMOVE_USER, &[], "").unwrap_err(); + assert!(matches!( + invalid_kind, + EventParseError::InvalidKind { + expected: "9000", + got: KIND_GROUP_REMOVE_USER + } + )); + + let metadata_content = + group_metadata_from_event(KIND_GROUP_METADATA, &[tag("d", "field-group")], "not empty") + .unwrap_err(); + assert!(matches!( + metadata_content, + EventParseError::InvalidJson("content") + )); + + for tags in [ + vec![tag("d", "field-group"), marker("hidden"), marker("hidden")], + vec![ + tag("d", "field-group"), + tag("supported_kinds", "78"), + tag("supported_kinds", "30078"), + ], + vec![tag("d", "field-group"), tag("supported_kinds", "")], + ] { + let err = group_metadata_from_event(KIND_GROUP_METADATA, &tags, "").unwrap_err(); + assert!(matches!( + err, + EventParseError::InvalidTag("hidden") + | EventParseError::InvalidTag("supported_kinds") + )); + } + + let invalid_supported_kind = group_metadata_from_event( + KIND_GROUP_METADATA, + &[tag("d", "field-group"), tag("supported_kinds", "bad")], + "", + ) + .unwrap_err(); + assert!(matches!( + invalid_supported_kind, + EventParseError::InvalidNumber("supported_kinds", _) + )); + + for tags in [ + vec![tag("h", "field-group"), marker("p")], + vec![tag("h", "field-group"), tag("p", "")], + vec![ + tag("h", "field-group"), + vec!["p".to_string(), "member_pubkey".to_string(), "".to_string()], + ], + ] { + let err = group_put_user_from_event(KIND_GROUP_PUT_USER, &tags, "").unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("p"))); + } + + for tags in [ + vec![tag("d", "field-group"), marker("role")], + vec![tag("d", "field-group"), tag("role", "")], + vec![ + tag("d", "field-group"), + vec!["role".to_string(), "member".to_string(), "".to_string()], + ], + vec![ + tag("d", "field-group"), + vec![ + "role".to_string(), + "member".to_string(), + "can read".to_string(), + "".to_string(), + ], + ], + ] { + let err = group_roles_from_event(KIND_GROUP_ROLES, &tags, "").unwrap_err(); + assert!(matches!(err, EventParseError::InvalidTag("role"))); + } + } + + fn assert_empty_required<T>(result: Result<T, EventEncodeError>, field: &'static str) { + let err = match result { + Ok(_) => panic!("expected empty required field error"), + Err(err) => err, + }; + assert!(matches!( + err, + EventEncodeError::EmptyRequiredField(found) if found == field + )); + } + fn sample_metadata() -> RadrootsGroupEditableMetadata { RadrootsGroupEditableMetadata { name: Some("Small Regen Farm".to_string()),