lib

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

mod.rs (23675B)


      1 pub mod decode;
      2 pub mod encode;
      3 
      4 #[cfg(test)]
      5 mod tests {
      6     use radroots_event::social::group::{
      7         GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup,
      8         GroupEditMetadata, GroupEditableMetadata, GroupJoinRequest, GroupLeaveRequest,
      9         GroupMembers, GroupMetadata, GroupPutUser, GroupRemoveUser, GroupRole, GroupRoles,
     10         GroupUserRef, KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE,
     11         KIND_GROUP_DELETE_EVENT, KIND_GROUP_DELETE_GROUP, KIND_GROUP_EDIT_METADATA,
     12         KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA,
     13         KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER, KIND_GROUP_ROLES,
     14     };
     15 
     16     use crate::error::{EventEncodeError, EventParseError};
     17     use crate::group::decode::{
     18         group_admins_from_event, group_create_group_from_event, group_create_invite_from_event,
     19         group_delete_event_from_event, group_delete_group_from_event,
     20         group_edit_metadata_from_event, group_join_request_from_event,
     21         group_leave_request_from_event, group_members_from_event, group_metadata_from_event,
     22         group_put_user_from_event, group_remove_user_from_event, group_roles_from_event,
     23     };
     24     use crate::group::encode::{
     25         group_admins_to_wire_parts, group_create_group_to_wire_parts,
     26         group_create_invite_to_wire_parts, group_delete_event_to_wire_parts,
     27         group_delete_group_to_wire_parts, group_edit_metadata_to_wire_parts,
     28         group_join_request_to_wire_parts, group_leave_request_to_wire_parts,
     29         group_members_to_wire_parts, group_metadata_to_wire_parts, group_put_user_to_wire_parts,
     30         group_remove_user_to_wire_parts, group_roles_to_wire_parts,
     31     };
     32 
     33     #[test]
     34     fn group_user_operations_use_h_group_id_routing() {
     35         let put = GroupPutUser {
     36             group_id: "field-group".to_string(),
     37             message: Some("add member".to_string()),
     38             pubkey: "member_pubkey".to_string(),
     39             roles: vec!["member".to_string()],
     40         };
     41         let remove = GroupRemoveUser {
     42             group_id: "field-group".to_string(),
     43             message: Some("remove member".to_string()),
     44             pubkey: "member_pubkey".to_string(),
     45         };
     46 
     47         let put_parts = group_put_user_to_wire_parts(&put).expect("put user");
     48         let remove_parts = group_remove_user_to_wire_parts(&remove).expect("remove user");
     49 
     50         assert_eq!(put_parts.kind, KIND_GROUP_PUT_USER);
     51         assert_eq!(remove_parts.kind, KIND_GROUP_REMOVE_USER);
     52         assert_eq!(put_parts.content, "add member");
     53         assert_eq!(remove_parts.content, "remove member");
     54         assert!(put_parts.tags.contains(&tag("h", "field-group")));
     55         assert!(
     56             !put_parts
     57                 .tags
     58                 .iter()
     59                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("d"))
     60         );
     61         assert_eq!(
     62             group_put_user_from_event(put_parts.kind, &put_parts.tags, &put_parts.content)
     63                 .expect("decode put"),
     64             put
     65         );
     66         assert_eq!(
     67             group_remove_user_from_event(
     68                 remove_parts.kind,
     69                 &remove_parts.tags,
     70                 &remove_parts.content
     71             )
     72             .expect("decode remove"),
     73             remove
     74         );
     75     }
     76 
     77     #[test]
     78     fn group_metadata_and_lists_use_d_tag_routing() {
     79         let metadata = GroupMetadata {
     80             d_tag: "field-group".to_string(),
     81             metadata: sample_metadata(),
     82         };
     83         let admins = GroupAdmins {
     84             d_tag: "field-group".to_string(),
     85             description: Some("group admins".to_string()),
     86             admins: vec![sample_user("admin_pubkey", "admin")],
     87         };
     88         let members = GroupMembers {
     89             d_tag: "field-group".to_string(),
     90             description: Some("group members".to_string()),
     91             members: vec![sample_user("member_pubkey", "member")],
     92         };
     93         let roles = GroupRoles {
     94             d_tag: "field-group".to_string(),
     95             description: Some("group roles".to_string()),
     96             roles: vec![sample_role()],
     97         };
     98 
     99         let metadata_parts = group_metadata_to_wire_parts(&metadata).expect("metadata");
    100         let admins_parts = group_admins_to_wire_parts(&admins).expect("admins");
    101         let members_parts = group_members_to_wire_parts(&members).expect("members");
    102         let roles_parts = group_roles_to_wire_parts(&roles).expect("roles");
    103 
    104         assert_eq!(metadata_parts.kind, KIND_GROUP_METADATA);
    105         assert!(metadata_parts.tags.contains(&tag("d", "field-group")));
    106         assert!(metadata_parts.tags.contains(&marker("restricted")));
    107         assert!(metadata_parts.tags.contains(&marker("closed")));
    108         assert!(metadata_parts.tags.contains(&vec![
    109             "supported_kinds".to_string(),
    110             "78".to_string(),
    111             "30078".to_string()
    112         ]));
    113         assert!(
    114             !metadata_parts
    115                 .tags
    116                 .iter()
    117                 .any(|tag| tag.first().map(|v| v.as_str()) == Some("h"))
    118         );
    119         assert_eq!(admins_parts.content, "group admins");
    120         assert_eq!(members_parts.content, "group members");
    121         assert_eq!(roles_parts.content, "group roles");
    122         assert_eq!(
    123             group_metadata_from_event(
    124                 metadata_parts.kind,
    125                 &metadata_parts.tags,
    126                 &metadata_parts.content
    127             )
    128             .expect("decode metadata"),
    129             metadata
    130         );
    131         assert_eq!(
    132             group_admins_from_event(admins_parts.kind, &admins_parts.tags, &admins_parts.content)
    133                 .expect("decode admins"),
    134             admins
    135         );
    136         assert_eq!(
    137             group_members_from_event(
    138                 members_parts.kind,
    139                 &members_parts.tags,
    140                 &members_parts.content
    141             )
    142             .expect("decode members"),
    143             members
    144         );
    145         assert_eq!(
    146             group_roles_from_event(roles_parts.kind, &roles_parts.tags, &roles_parts.content)
    147                 .expect("decode roles"),
    148             roles
    149         );
    150         assert_eq!(admins_parts.kind, KIND_GROUP_ADMINS);
    151         assert_eq!(members_parts.kind, KIND_GROUP_MEMBERS);
    152         assert_eq!(roles_parts.kind, KIND_GROUP_ROLES);
    153     }
    154 
    155     #[test]
    156     fn group_invites_and_join_requests_roundtrip_without_field_authorization() {
    157         let invite = GroupCreateInvite {
    158             group_id: "field-group".to_string(),
    159             message: Some("join the field group".to_string()),
    160             code: "invite-code".to_string(),
    161         };
    162         let join = GroupJoinRequest {
    163             group_id: "field-group".to_string(),
    164             message: Some("requesting access".to_string()),
    165             code: Some("invite-code".to_string()),
    166         };
    167 
    168         let invite_parts = group_create_invite_to_wire_parts(&invite).expect("invite");
    169         let join_parts = group_join_request_to_wire_parts(&join).expect("join");
    170 
    171         assert_eq!(invite_parts.kind, KIND_GROUP_CREATE_INVITE);
    172         assert_eq!(join_parts.kind, KIND_GROUP_JOIN_REQUEST);
    173         assert!(invite_parts.tags.contains(&tag("h", "field-group")));
    174         assert!(invite_parts.tags.contains(&tag("code", "invite-code")));
    175         assert!(join_parts.tags.contains(&tag("code", "invite-code")));
    176         assert_eq!(invite_parts.content, "join the field group");
    177         assert_eq!(join_parts.content, "requesting access");
    178         assert_eq!(
    179             group_create_invite_from_event(
    180                 invite_parts.kind,
    181                 &invite_parts.tags,
    182                 &invite_parts.content
    183             )
    184             .expect("decode invite"),
    185             invite
    186         );
    187         assert_eq!(
    188             group_join_request_from_event(join_parts.kind, &join_parts.tags, &join_parts.content)
    189                 .expect("decode join"),
    190             join
    191         );
    192     }
    193 
    194     #[test]
    195     fn group_lifecycle_and_moderation_events_roundtrip() {
    196         let metadata = GroupEditableMetadata {
    197             is_private: true,
    198             is_hidden: true,
    199             ..sample_metadata()
    200         };
    201         let create = GroupCreateGroup {
    202             group_id: "field-group".to_string(),
    203             message: Some("create group".to_string()),
    204             metadata: metadata.clone(),
    205         };
    206         let edit = GroupEditMetadata {
    207             group_id: "field-group".to_string(),
    208             message: Some("edit group".to_string()),
    209             metadata,
    210         };
    211         let delete_group = GroupDeleteGroup {
    212             group_id: "field-group".to_string(),
    213             message: Some("delete group".to_string()),
    214         };
    215         let delete_event = GroupDeleteEvent {
    216             group_id: "field-group".to_string(),
    217             message: Some("delete event".to_string()),
    218             event_id: "event_id".to_string(),
    219         };
    220         let leave = GroupLeaveRequest {
    221             group_id: "field-group".to_string(),
    222             message: None,
    223         };
    224 
    225         let create_parts = group_create_group_to_wire_parts(&create).expect("create");
    226         let edit_parts = group_edit_metadata_to_wire_parts(&edit).expect("edit");
    227         let delete_group_parts =
    228             group_delete_group_to_wire_parts(&delete_group).expect("delete group");
    229         let delete_event_parts =
    230             group_delete_event_to_wire_parts(&delete_event).expect("delete event");
    231         let leave_parts = group_leave_request_to_wire_parts(&leave).expect("leave");
    232 
    233         assert_eq!(create_parts.kind, KIND_GROUP_CREATE_GROUP);
    234         assert_eq!(edit_parts.kind, KIND_GROUP_EDIT_METADATA);
    235         assert_eq!(delete_group_parts.kind, KIND_GROUP_DELETE_GROUP);
    236         assert_eq!(delete_event_parts.kind, KIND_GROUP_DELETE_EVENT);
    237         assert_eq!(leave_parts.kind, KIND_GROUP_LEAVE_REQUEST);
    238         assert!(create_parts.tags.contains(&marker("private")));
    239         assert!(create_parts.tags.contains(&marker("hidden")));
    240         assert!(delete_event_parts.tags.contains(&tag("e", "event_id")));
    241         assert_eq!(leave_parts.content, "");
    242         assert_eq!(
    243             group_create_group_from_event(
    244                 create_parts.kind,
    245                 &create_parts.tags,
    246                 &create_parts.content
    247             )
    248             .expect("decode create"),
    249             create
    250         );
    251         assert_eq!(
    252             group_edit_metadata_from_event(edit_parts.kind, &edit_parts.tags, &edit_parts.content)
    253                 .expect("decode edit"),
    254             edit
    255         );
    256         assert_eq!(
    257             group_delete_group_from_event(
    258                 delete_group_parts.kind,
    259                 &delete_group_parts.tags,
    260                 &delete_group_parts.content
    261             )
    262             .expect("decode delete group"),
    263             delete_group
    264         );
    265         assert_eq!(
    266             group_delete_event_from_event(
    267                 delete_event_parts.kind,
    268                 &delete_event_parts.tags,
    269                 &delete_event_parts.content
    270             )
    271             .expect("decode delete event"),
    272             delete_event
    273         );
    274         assert_eq!(
    275             group_leave_request_from_event(
    276                 leave_parts.kind,
    277                 &leave_parts.tags,
    278                 &leave_parts.content
    279             )
    280             .expect("decode leave"),
    281             leave
    282         );
    283     }
    284 
    285     #[test]
    286     fn group_codecs_reject_wrong_routing_tags() {
    287         let metadata = GroupMetadata {
    288             d_tag: "field-group".to_string(),
    289             metadata: sample_metadata(),
    290         };
    291         let mut metadata_parts = group_metadata_to_wire_parts(&metadata).expect("metadata");
    292         metadata_parts
    293             .tags
    294             .retain(|tag| tag.first().map(|value| value.as_str()) != Some("d"));
    295         metadata_parts.tags.push(tag("h", "field-group"));
    296         let metadata_err = group_metadata_from_event(
    297             metadata_parts.kind,
    298             &metadata_parts.tags,
    299             &metadata_parts.content,
    300         )
    301         .unwrap_err();
    302         assert!(matches!(metadata_err, EventParseError::MissingTag("d")));
    303 
    304         let put = GroupPutUser {
    305             group_id: "field-group".to_string(),
    306             message: None,
    307             pubkey: "member_pubkey".to_string(),
    308             roles: vec!["member".to_string()],
    309         };
    310         let mut put_parts = group_put_user_to_wire_parts(&put).expect("put");
    311         put_parts
    312             .tags
    313             .retain(|tag| tag.first().map(|value| value.as_str()) != Some("h"));
    314         put_parts.tags.push(tag("d", "field-group"));
    315         let put_err =
    316             group_put_user_from_event(put_parts.kind, &put_parts.tags, &put_parts.content)
    317                 .unwrap_err();
    318         assert!(matches!(put_err, EventParseError::MissingTag("h")));
    319     }
    320 
    321     #[test]
    322     fn group_codecs_reject_nonstandard_first_pass_group_shapes() {
    323         let valued_marker_tags = vec![
    324             tag("d", "field-group"),
    325             tag("private", "true"),
    326             tag("supported_kinds", "78"),
    327         ];
    328         let metadata_err =
    329             group_metadata_from_event(KIND_GROUP_METADATA, &valued_marker_tags, "").unwrap_err();
    330         assert!(matches!(
    331             metadata_err,
    332             EventParseError::InvalidTag("private")
    333         ));
    334 
    335         let first_pass_invite_tags = vec![
    336             tag("h", "field-group"),
    337             tag("p", "member_pubkey"),
    338             tag("role", "member"),
    339             tag("claim", "claim-token"),
    340         ];
    341         let invite_err =
    342             group_create_invite_from_event(KIND_GROUP_CREATE_INVITE, &first_pass_invite_tags, "")
    343                 .unwrap_err();
    344         assert!(matches!(invite_err, EventParseError::MissingTag("code")));
    345     }
    346 
    347     #[test]
    348     fn group_encoders_reject_empty_required_fields() {
    349         assert_empty_required(
    350             group_put_user_to_wire_parts(&GroupPutUser {
    351                 group_id: "".to_string(),
    352                 message: None,
    353                 pubkey: "member_pubkey".to_string(),
    354                 roles: vec![],
    355             }),
    356             "group_id",
    357         );
    358         assert_empty_required(
    359             group_put_user_to_wire_parts(&GroupPutUser {
    360                 group_id: "field-group".to_string(),
    361                 message: None,
    362                 pubkey: "".to_string(),
    363                 roles: vec![],
    364             }),
    365             "pubkey",
    366         );
    367         assert_empty_required(
    368             group_put_user_to_wire_parts(&GroupPutUser {
    369                 group_id: "field-group".to_string(),
    370                 message: None,
    371                 pubkey: "member_pubkey".to_string(),
    372                 roles: vec!["".to_string()],
    373             }),
    374             "roles",
    375         );
    376         assert_empty_required(
    377             group_remove_user_to_wire_parts(&GroupRemoveUser {
    378                 group_id: "field-group".to_string(),
    379                 message: None,
    380                 pubkey: "".to_string(),
    381             }),
    382             "pubkey",
    383         );
    384         assert_empty_required(
    385             group_create_group_to_wire_parts(&GroupCreateGroup {
    386                 group_id: "field-group".to_string(),
    387                 message: Some("".to_string()),
    388                 metadata: sample_metadata(),
    389             }),
    390             "message",
    391         );
    392         assert_empty_required(
    393             group_edit_metadata_to_wire_parts(&GroupEditMetadata {
    394                 group_id: "field-group".to_string(),
    395                 message: None,
    396                 metadata: GroupEditableMetadata {
    397                     name: Some("".to_string()),
    398                     ..sample_metadata()
    399                 },
    400             }),
    401             "name",
    402         );
    403         assert_empty_required(
    404             group_delete_event_to_wire_parts(&GroupDeleteEvent {
    405                 group_id: "field-group".to_string(),
    406                 message: None,
    407                 event_id: "".to_string(),
    408             }),
    409             "event_id",
    410         );
    411         assert_empty_required(
    412             group_create_invite_to_wire_parts(&GroupCreateInvite {
    413                 group_id: "field-group".to_string(),
    414                 message: None,
    415                 code: "".to_string(),
    416             }),
    417             "code",
    418         );
    419         assert_empty_required(
    420             group_join_request_to_wire_parts(&GroupJoinRequest {
    421                 group_id: "field-group".to_string(),
    422                 message: None,
    423                 code: Some("".to_string()),
    424             }),
    425             "code",
    426         );
    427         assert_empty_required(
    428             group_leave_request_to_wire_parts(&GroupLeaveRequest {
    429                 group_id: "".to_string(),
    430                 message: None,
    431             }),
    432             "group_id",
    433         );
    434         assert_empty_required(
    435             group_metadata_to_wire_parts(&GroupMetadata {
    436                 d_tag: "".to_string(),
    437                 metadata: sample_metadata(),
    438             }),
    439             "d_tag",
    440         );
    441         assert_empty_required(
    442             group_admins_to_wire_parts(&GroupAdmins {
    443                 d_tag: "field-group".to_string(),
    444                 description: None,
    445                 admins: vec![GroupUserRef {
    446                     pubkey: "".to_string(),
    447                     roles: vec![],
    448                 }],
    449             }),
    450             "pubkey",
    451         );
    452         assert_empty_required(
    453             group_members_to_wire_parts(&GroupMembers {
    454                 d_tag: "field-group".to_string(),
    455                 description: Some("".to_string()),
    456                 members: vec![],
    457             }),
    458             "message",
    459         );
    460         assert_empty_required(
    461             group_members_to_wire_parts(&GroupMembers {
    462                 d_tag: "field-group".to_string(),
    463                 description: None,
    464                 members: vec![GroupUserRef {
    465                     pubkey: "member_pubkey".to_string(),
    466                     roles: vec!["".to_string()],
    467                 }],
    468             }),
    469             "roles",
    470         );
    471         assert_empty_required(
    472             group_roles_to_wire_parts(&GroupRoles {
    473                 d_tag: "field-group".to_string(),
    474                 description: None,
    475                 roles: vec![GroupRole {
    476                     name: "".to_string(),
    477                     description: None,
    478                     permissions: vec![],
    479                 }],
    480             }),
    481             "role.name",
    482         );
    483         assert_empty_required(
    484             group_roles_to_wire_parts(&GroupRoles {
    485                 d_tag: "field-group".to_string(),
    486                 description: None,
    487                 roles: vec![GroupRole {
    488                     name: "member".to_string(),
    489                     description: Some("".to_string()),
    490                     permissions: vec![],
    491                 }],
    492             }),
    493             "role.description",
    494         );
    495         assert_empty_required(
    496             group_roles_to_wire_parts(&GroupRoles {
    497                 d_tag: "field-group".to_string(),
    498                 description: None,
    499                 roles: vec![GroupRole {
    500                     name: "member".to_string(),
    501                     description: None,
    502                     permissions: vec!["".to_string()],
    503                 }],
    504             }),
    505             "role.permissions",
    506         );
    507     }
    508 
    509     #[test]
    510     fn group_decoders_reject_invalid_tag_shapes_and_kinds() {
    511         let invalid_kind = group_put_user_from_event(KIND_GROUP_REMOVE_USER, &[], "").unwrap_err();
    512         assert!(matches!(
    513             invalid_kind,
    514             EventParseError::InvalidKind {
    515                 expected: "9000",
    516                 got: KIND_GROUP_REMOVE_USER
    517             }
    518         ));
    519 
    520         let metadata_content =
    521             group_metadata_from_event(KIND_GROUP_METADATA, &[tag("d", "field-group")], "not empty")
    522                 .unwrap_err();
    523         assert!(matches!(
    524             metadata_content,
    525             EventParseError::InvalidJson("content")
    526         ));
    527 
    528         for tags in [
    529             vec![tag("d", "field-group"), marker("hidden"), marker("hidden")],
    530             vec![
    531                 tag("d", "field-group"),
    532                 tag("supported_kinds", "78"),
    533                 tag("supported_kinds", "30078"),
    534             ],
    535             vec![tag("d", "field-group"), tag("supported_kinds", "")],
    536         ] {
    537             let err = group_metadata_from_event(KIND_GROUP_METADATA, &tags, "").unwrap_err();
    538             assert!(matches!(
    539                 err,
    540                 EventParseError::InvalidTag("hidden")
    541                     | EventParseError::InvalidTag("supported_kinds")
    542             ));
    543         }
    544 
    545         let invalid_supported_kind = group_metadata_from_event(
    546             KIND_GROUP_METADATA,
    547             &[tag("d", "field-group"), tag("supported_kinds", "bad")],
    548             "",
    549         )
    550         .unwrap_err();
    551         assert!(matches!(
    552             invalid_supported_kind,
    553             EventParseError::InvalidNumber("supported_kinds", _)
    554         ));
    555 
    556         for tags in [
    557             vec![tag("h", "field-group"), marker("p")],
    558             vec![tag("h", "field-group"), tag("p", "")],
    559             vec![
    560                 tag("h", "field-group"),
    561                 vec!["p".to_string(), "member_pubkey".to_string(), "".to_string()],
    562             ],
    563         ] {
    564             let err = group_put_user_from_event(KIND_GROUP_PUT_USER, &tags, "").unwrap_err();
    565             assert!(matches!(err, EventParseError::InvalidTag("p")));
    566         }
    567 
    568         for tags in [
    569             vec![tag("d", "field-group"), marker("role")],
    570             vec![tag("d", "field-group"), tag("role", "")],
    571             vec![
    572                 tag("d", "field-group"),
    573                 vec!["role".to_string(), "member".to_string(), "".to_string()],
    574             ],
    575             vec![
    576                 tag("d", "field-group"),
    577                 vec![
    578                     "role".to_string(),
    579                     "member".to_string(),
    580                     "can read".to_string(),
    581                     "".to_string(),
    582                 ],
    583             ],
    584         ] {
    585             let err = group_roles_from_event(KIND_GROUP_ROLES, &tags, "").unwrap_err();
    586             assert!(matches!(err, EventParseError::InvalidTag("role")));
    587         }
    588     }
    589 
    590     #[cfg_attr(coverage_nightly, coverage(off))]
    591     fn assert_empty_required<T>(result: Result<T, EventEncodeError>, field: &'static str) {
    592         let err = match result {
    593             Ok(_) => panic!("expected empty required field error"),
    594             Err(err) => err,
    595         };
    596         match err {
    597             EventEncodeError::EmptyRequiredField(found) => assert_eq!(found, field),
    598             other => panic!("unexpected error: {other:?}"),
    599         }
    600     }
    601 
    602     fn sample_metadata() -> GroupEditableMetadata {
    603         GroupEditableMetadata {
    604             name: Some("Small Regen Farm".to_string()),
    605             about: Some("Field app group".to_string()),
    606             picture: Some("https://media.example.invalid/group.png".to_string()),
    607             is_private: false,
    608             is_restricted: true,
    609             is_closed: true,
    610             is_hidden: false,
    611             supported_kinds: Some(vec![78, 30078]),
    612         }
    613     }
    614 
    615     fn sample_user(pubkey: &str, role: &str) -> GroupUserRef {
    616         GroupUserRef {
    617             pubkey: pubkey.to_string(),
    618             roles: vec![role.to_string()],
    619         }
    620     }
    621 
    622     fn sample_role() -> GroupRole {
    623         GroupRole {
    624             name: "member".to_string(),
    625             description: Some("can read and write group events".to_string()),
    626             permissions: vec!["read".to_string(), "write".to_string()],
    627         }
    628     }
    629 
    630     fn tag(key: &str, value: &str) -> Vec<String> {
    631         vec![key.to_string(), value.to_string()]
    632     }
    633 
    634     fn marker(key: &str) -> Vec<String> {
    635         vec![key.to_string()]
    636     }
    637 }