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:
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()),