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 }