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 }