lib

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

mod.rs (24123B)


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