1pub mod ban;
2pub mod configuration;
3pub mod content;
4pub mod direct_messages;
5pub mod dm_body;
6pub mod identity;
7pub mod member;
8pub mod member_info;
9pub mod message;
10pub mod privacy;
11pub mod secret;
12pub mod upgrade;
13pub mod version;
14
15use crate::room_state::ban::BansV1;
16use crate::room_state::configuration::AuthorizedConfigurationV1;
17use crate::room_state::direct_messages::DirectMessagesV1;
18use crate::room_state::member::{MemberId, MembersV1};
19use crate::room_state::member_info::MemberInfoV1;
20use crate::room_state::message::MessagesV1;
21use crate::room_state::secret::RoomSecretsV1;
22use crate::room_state::upgrade::OptionalUpgradeV1;
23use crate::room_state::version::StateVersion;
24use ed25519_dalek::VerifyingKey;
25use freenet_scaffold_macro::composable;
26use serde::{Deserialize, Serialize};
27use std::collections::HashSet;
28
29#[composable(post_apply_delta = "post_apply_cleanup")]
30#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
31pub struct ChatRoomStateV1 {
32 pub configuration: AuthorizedConfigurationV1,
39
40 pub bans: BansV1,
43
44 pub members: MembersV1,
46
47 pub member_info: MemberInfoV1,
49
50 pub secrets: RoomSecretsV1,
53
54 pub recent_messages: MessagesV1,
56
57 #[serde(default)]
61 pub direct_messages: DirectMessagesV1,
62
63 pub upgrade: OptionalUpgradeV1,
66
67 #[serde(default)]
70 pub version: StateVersion,
71}
72
73impl ChatRoomStateV1 {
74 pub fn post_apply_cleanup(&mut self, parameters: &ChatRoomParametersV1) -> Result<(), String> {
102 let owner_id = MemberId::from(¶meters.owner);
103
104 let max_bans = self.configuration.configuration.max_user_bans;
153 if self.bans.0.len() > max_bans {
154 let members_by_id = self.members.members_by_member_id();
155 self.bans.0.sort_by_cached_key(|ban| {
160 (
161 BansV1::ban_is_enforcing(
162 ban,
163 &members_by_id,
164 &self.member_info,
165 owner_id,
166 ¶meters.owner,
167 ),
168 ban.ban.banned_at,
169 ban.id(),
170 )
171 });
172 let to_remove = self.bans.0.len() - max_bans;
173 self.bans.0.drain(0..to_remove);
174 self.bans.0.sort_by(|a, b| {
176 a.ban
177 .banned_at
178 .cmp(&b.ban.banned_at)
179 .then_with(|| a.id().cmp(&b.id()))
180 });
181 }
182
183 let enforced_banned_ids =
199 self.members
200 .banned_member_ids(&self.bans, &self.member_info, parameters);
201 self.members
202 .members
203 .retain(|m| !enforced_banned_ids.contains(&m.member.id()));
204
205 let message_authors: HashSet<MemberId> = self
228 .recent_messages
229 .messages
230 .iter()
231 .map(|m| m.message.author)
232 .collect();
233 let dm_participants: HashSet<MemberId> = self.direct_messages.active_participants();
234 let current_secret_version = self.secrets.current_version;
235 let secret_recipients: HashSet<MemberId> = self
236 .secrets
237 .encrypted_secrets
238 .iter()
239 .filter(|s| s.secret.secret_version == current_secret_version)
240 .map(|s| s.secret.member_id)
241 .collect();
242
243 let required_ids = {
246 let members_by_id = self.members.members_by_member_id();
247 let mut required_ids: HashSet<MemberId> = HashSet::new();
248
249 for author_id in &message_authors {
250 if *author_id != owner_id && members_by_id.contains_key(author_id) {
251 required_ids.insert(*author_id);
252 }
253 }
254
255 for participant_id in &dm_participants {
256 if *participant_id != owner_id && members_by_id.contains_key(participant_id) {
257 required_ids.insert(*participant_id);
258 }
259 }
260
261 for recipient_id in &secret_recipients {
262 if *recipient_id != owner_id && members_by_id.contains_key(recipient_id) {
263 required_ids.insert(*recipient_id);
264 }
265 }
266
267 for ban in &self.bans.0 {
297 let banner = ban.banned_by;
298 if banner != owner_id
299 && BansV1::ban_signature_matches_current_key(
300 ban,
301 &members_by_id,
302 owner_id,
303 ¶meters.owner,
304 )
305 {
306 required_ids.insert(banner);
307 }
308 }
309
310 let mut to_process: Vec<MemberId> = required_ids.iter().cloned().collect();
312 while let Some(member_id) = to_process.pop() {
313 if let Some(member) = members_by_id.get(&member_id) {
314 let inviter_id = member.member.invited_by;
315 if inviter_id != owner_id && !required_ids.contains(&inviter_id) {
316 required_ids.insert(inviter_id);
317 to_process.push(inviter_id);
318 }
319 }
320 }
321
322 required_ids
323 };
324
325 self.members
327 .members
328 .retain(|m| required_ids.contains(&m.member.id()));
329
330 self.member_info.member_info.retain(|info| {
332 info.member_info.member_id == owner_id
333 || required_ids.contains(&info.member_info.member_id)
334 });
335
336 self.member_info.dedup_to_canonical();
348
349 let current_member_ids: HashSet<MemberId> =
360 self.members.members.iter().map(|m| m.member.id()).collect();
361 self.recent_messages.messages.retain(|m| {
362 m.message.author == owner_id || current_member_ids.contains(&m.message.author)
363 });
364
365 self.recent_messages.rebuild_actions_state();
374
375 let members_by_id_for_ban_sweep = self.members.members_by_member_id();
395 self.bans.0.retain(|ban| {
396 BansV1::ban_signature_matches_current_key(
397 ban,
398 &members_by_id_for_ban_sweep,
399 owner_id,
400 ¶meters.owner,
401 )
402 });
403
404 let active_member_ids_for_sweep: HashSet<MemberId> =
421 self.members.members.iter().map(|m| m.member.id()).collect();
422 self.direct_messages.sweep_after_membership_change(
423 owner_id,
424 &active_member_ids_for_sweep,
425 &enforced_banned_ids,
426 );
427
428 self.members.members.sort_by_key(|m| m.member.id());
430 self.member_info
431 .member_info
432 .sort_by_key(|info| info.member_info.member_id);
433
434 Ok(())
435 }
436}
437
438#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
439pub struct ChatRoomParametersV1 {
440 pub owner: VerifyingKey,
441}
442
443impl ChatRoomParametersV1 {
444 pub fn owner_id(&self) -> MemberId {
445 self.owner.into()
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
453 use crate::room_state::configuration::Configuration;
454 use crate::room_state::member::{AuthorizedMember, Member};
455 use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
456 use crate::room_state::message::{AuthorizedMessageV1, MessageV1, RoomMessageBody};
457 use ed25519_dalek::SigningKey;
458 use std::fmt::Debug;
459 use std::time::SystemTime;
460
461 #[test]
462 fn test_state() {
463 let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
464
465 assert!(
466 state.verify(&state, ¶meters).is_ok(),
467 "Empty state should verify"
468 );
469
470 let mut new_cfg = state.configuration.configuration.clone();
472 new_cfg.configuration_version += 1;
473 new_cfg.max_recent_messages = 10; let new_cfg = AuthorizedConfigurationV1::new(new_cfg, &owner_signing_key);
475
476 let mut cfg_modified_state = state.clone();
477 cfg_modified_state.configuration = new_cfg;
478 test_apply_delta(state.clone(), cfg_modified_state, ¶meters);
479 }
480
481 fn test_apply_delta<CS>(orig_state: CS, modified_state: CS, parameters: &CS::Parameters)
482 where
483 CS: ComposableState<ParentState = CS> + Clone + PartialEq + Debug,
484 {
485 let orig_verify_result = orig_state.verify(&orig_state, parameters);
486 assert!(
487 orig_verify_result.is_ok(),
488 "Original state verification failed: {:?}",
489 orig_verify_result.err()
490 );
491
492 let modified_verify_result = modified_state.verify(&modified_state, parameters);
493 assert!(
494 modified_verify_result.is_ok(),
495 "Modified state verification failed: {:?}",
496 modified_verify_result.err()
497 );
498
499 let delta = modified_state.delta(
500 &orig_state,
501 parameters,
502 &orig_state.summarize(&orig_state, parameters),
503 );
504
505 println!("Delta: {:?}", delta);
506
507 let mut new_state = orig_state.clone();
508 let apply_delta_result = new_state.apply_delta(&orig_state, parameters, &delta);
509 assert!(
510 apply_delta_result.is_ok(),
511 "Applying delta failed: {:?}",
512 apply_delta_result.err()
513 );
514
515 assert_eq!(new_state, modified_state);
516 }
517 fn create_empty_chat_room_state() -> (ChatRoomStateV1, ChatRoomParametersV1, SigningKey) {
518 let rng = &mut rand::thread_rng();
521 let owner_signing_key = SigningKey::generate(rng);
522 let owner_verifying_key = owner_signing_key.verifying_key();
523
524 let config = AuthorizedConfigurationV1::new(Configuration::default(), &owner_signing_key);
525
526 (
527 ChatRoomStateV1 {
528 configuration: config,
529 bans: BansV1::default(),
530 members: MembersV1::default(),
531 member_info: MemberInfoV1::default(),
532 secrets: RoomSecretsV1::default(),
533 recent_messages: MessagesV1::default(),
534 upgrade: OptionalUpgradeV1(None),
535 ..Default::default()
536 },
537 ChatRoomParametersV1 {
538 owner: owner_verifying_key,
539 },
540 owner_signing_key,
541 )
542 }
543
544 #[test]
549 fn test_orphaned_ban_cleanup_after_cascade_removal() {
550 let rng = &mut rand::thread_rng();
551
552 let owner_sk = SigningKey::generate(rng);
554 let owner_vk = owner_sk.verifying_key();
555 let owner_id = MemberId::from(&owner_vk);
556 let params = ChatRoomParametersV1 { owner: owner_vk };
557
558 let config = Configuration {
560 max_user_bans: 10,
561 max_members: 10,
562 ..Default::default()
563 };
564 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
565
566 let a_sk = SigningKey::generate(rng);
568 let a_vk = a_sk.verifying_key();
569 let a_id = MemberId::from(&a_vk);
570
571 let b_sk = SigningKey::generate(rng);
572 let b_vk = b_sk.verifying_key();
573 let b_id = MemberId::from(&b_vk);
574
575 let member_a = AuthorizedMember::new(
576 Member {
577 owner_member_id: owner_id,
578 invited_by: owner_id,
579 member_vk: a_vk,
580 },
581 &owner_sk,
582 );
583
584 let ban_b_by_a = AuthorizedUserBan::new(
586 UserBan {
587 owner_member_id: owner_id,
588 banned_at: std::time::SystemTime::now(),
589 banned_user: b_id,
590 },
591 a_id,
592 &a_sk,
593 );
594
595 let initial_state = ChatRoomStateV1 {
597 configuration: auth_config.clone(),
598 bans: BansV1(vec![ban_b_by_a.clone()]),
599 members: MembersV1 {
600 members: vec![member_a.clone()],
601 },
602 ..Default::default()
603 };
604
605 assert!(
606 initial_state.verify(&initial_state, ¶ms).is_ok(),
607 "Initial state should verify: {:?}",
608 initial_state.verify(&initial_state, ¶ms)
609 );
610
611 let ban_a_by_owner = AuthorizedUserBan::new(
614 UserBan {
615 owner_member_id: owner_id,
616 banned_at: std::time::SystemTime::now() + std::time::Duration::from_secs(1),
617 banned_user: a_id,
618 },
619 owner_id,
620 &owner_sk,
621 );
622
623 let modified_for_delta = ChatRoomStateV1 {
625 configuration: auth_config,
626 bans: BansV1(vec![ban_b_by_a.clone(), ban_a_by_owner.clone()]),
627 members: MembersV1 {
628 members: vec![member_a.clone()],
629 },
630 ..Default::default()
631 };
632
633 let summary = initial_state.summarize(&initial_state, ¶ms);
635 let delta = modified_for_delta.delta(&initial_state, ¶ms, &summary);
636
637 let mut result_state = initial_state.clone();
638 let apply_result = result_state.apply_delta(&initial_state, ¶ms, &delta);
639 assert!(
640 apply_result.is_ok(),
641 "apply_delta should succeed: {:?}",
642 apply_result
643 );
644
645 assert!(
647 result_state.members.members.is_empty(),
648 "A should be removed from members: {:?}",
649 result_state.members.members
650 );
651
652 assert_eq!(
654 result_state.bans.0.len(),
655 1,
656 "Only owner's ban should remain, orphaned ban cleaned: {:?}",
657 result_state.bans.0
658 );
659 assert_eq!(
660 result_state.bans.0[0].banned_by, owner_id,
661 "Remaining ban should be by owner"
662 );
663
664 assert!(
666 result_state.verify(&result_state, ¶ms).is_ok(),
667 "Result state should verify after orphaned ban cleanup: {:?}",
668 result_state.verify(&result_state, ¶ms)
669 );
670 }
671
672 #[test]
673 fn test_member_pruned_when_no_messages() {
674 let rng = &mut rand::thread_rng();
675 let owner_sk = SigningKey::generate(rng);
676 let owner_vk = owner_sk.verifying_key();
677 let owner_id = MemberId::from(&owner_vk);
678 let params = ChatRoomParametersV1 { owner: owner_vk };
679
680 let a_sk = SigningKey::generate(rng);
681 let a_vk = a_sk.verifying_key();
682 let a_id = MemberId::from(&a_vk);
683
684 let b_sk = SigningKey::generate(rng);
685 let b_vk = b_sk.verifying_key();
686
687 let member_a = AuthorizedMember::new(
688 Member {
689 owner_member_id: owner_id,
690 invited_by: owner_id,
691 member_vk: a_vk,
692 },
693 &owner_sk,
694 );
695 let member_b = AuthorizedMember::new(
696 Member {
697 owner_member_id: owner_id,
698 invited_by: owner_id,
699 member_vk: b_vk,
700 },
701 &owner_sk,
702 );
703
704 let msg_a = AuthorizedMessageV1::new(
706 MessageV1 {
707 room_owner: owner_id,
708 author: a_id,
709 time: SystemTime::now(),
710 content: RoomMessageBody::public("Hello from A".to_string()),
711 },
712 &a_sk,
713 );
714
715 let config = Configuration {
716 max_members: 10,
717 max_recent_messages: 100,
718 ..Default::default()
719 };
720 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
721
722 let mut state = ChatRoomStateV1 {
723 configuration: auth_config,
724 members: MembersV1 {
725 members: vec![member_a, member_b],
726 },
727 recent_messages: MessagesV1 {
728 messages: vec![msg_a],
729 ..Default::default()
730 },
731 ..Default::default()
732 };
733
734 state.post_apply_cleanup(¶ms).unwrap();
735
736 assert_eq!(state.members.members.len(), 1, "Only A should remain");
737 assert_eq!(state.members.members[0].member.id(), a_id);
738 }
739
740 #[test]
741 fn test_member_with_join_event_not_pruned() {
742 let rng = &mut rand::thread_rng();
743 let owner_sk = SigningKey::generate(rng);
744 let owner_vk = owner_sk.verifying_key();
745 let owner_id = MemberId::from(&owner_vk);
746 let params = ChatRoomParametersV1 { owner: owner_vk };
747
748 let a_sk = SigningKey::generate(rng);
749 let a_vk = a_sk.verifying_key();
750 let a_id = MemberId::from(&a_vk);
751
752 let member_a = AuthorizedMember::new(
753 Member {
754 owner_member_id: owner_id,
755 invited_by: owner_id,
756 member_vk: a_vk,
757 },
758 &owner_sk,
759 );
760
761 let join_msg = AuthorizedMessageV1::new(
763 MessageV1 {
764 room_owner: owner_id,
765 author: a_id,
766 time: SystemTime::now(),
767 content: RoomMessageBody::join_event(),
768 },
769 &a_sk,
770 );
771
772 let config = Configuration {
773 max_members: 10,
774 max_recent_messages: 100,
775 ..Default::default()
776 };
777 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
778
779 let mut state = ChatRoomStateV1 {
780 configuration: auth_config,
781 members: MembersV1 {
782 members: vec![member_a],
783 },
784 recent_messages: MessagesV1 {
785 messages: vec![join_msg],
786 ..Default::default()
787 },
788 ..Default::default()
789 };
790
791 state.post_apply_cleanup(¶ms).unwrap();
792
793 assert_eq!(
794 state.members.members.len(),
795 1,
796 "Member with join event should not be pruned"
797 );
798 assert_eq!(state.members.members[0].member.id(), a_id);
799 }
800
801 #[test]
804 fn test_atomic_join_delta_applies_and_verifies() {
805 use crate::room_state::member::MembersDelta;
806 use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
807 use crate::room_state::privacy::SealedBytes;
808
809 let rng = &mut rand::thread_rng();
810 let owner_sk = SigningKey::generate(rng);
811 let owner_vk = owner_sk.verifying_key();
812 let owner_id = MemberId::from(&owner_vk);
813 let params = ChatRoomParametersV1 { owner: owner_vk };
814
815 let config = Configuration {
817 owner_member_id: owner_id,
818 max_members: 10,
819 max_recent_messages: 100,
820 ..Default::default()
821 };
822 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
823 let mut state = ChatRoomStateV1 {
824 configuration: auth_config,
825 ..Default::default()
826 };
827
828 let joiner_sk = SigningKey::generate(rng);
830 let joiner_vk = joiner_sk.verifying_key();
831 let joiner_id = MemberId::from(&joiner_vk);
832
833 let authorized_member = AuthorizedMember::new(
834 Member {
835 owner_member_id: owner_id,
836 invited_by: owner_id,
837 member_vk: joiner_vk,
838 },
839 &owner_sk,
840 );
841
842 let member_info = MemberInfo {
843 member_id: joiner_id,
844 version: 0,
845 preferred_nickname: SealedBytes::public("NewUser".to_string().into_bytes()),
846 deputies: Vec::new(),
847 };
848 let authorized_info = AuthorizedMemberInfo::new_with_member_key(member_info, &joiner_sk);
849
850 let join_message = AuthorizedMessageV1::new(
851 MessageV1 {
852 room_owner: owner_id,
853 author: joiner_id,
854 content: RoomMessageBody::join_event(),
855 time: SystemTime::now(),
856 },
857 &joiner_sk,
858 );
859
860 let delta = ChatRoomStateV1Delta {
862 recent_messages: Some(vec![join_message]),
863 members: Some(MembersDelta::new(vec![authorized_member])),
864 member_info: Some(vec![authorized_info]),
865 ..Default::default()
866 };
867
868 let old_state = state.clone();
870 state
871 .apply_delta(&old_state, ¶ms, &Some(delta))
872 .expect("atomic join delta should apply cleanly");
873
874 state
876 .verify(&state, ¶ms)
877 .expect("state should verify after join delta");
878
879 assert!(
881 state
882 .members
883 .members
884 .iter()
885 .any(|m| m.member.id() == joiner_id),
886 "Joiner should be in members list"
887 );
888
889 assert!(
891 state
892 .member_info
893 .member_info
894 .iter()
895 .any(|i| i.member_info.member_id == joiner_id),
896 "Joiner should have member_info"
897 );
898
899 assert_eq!(state.recent_messages.messages.len(), 1);
901 assert!(state.recent_messages.messages[0].message.content.is_event());
902
903 state.post_apply_cleanup(¶ms).unwrap();
905 assert!(
906 state
907 .members
908 .members
909 .iter()
910 .any(|m| m.member.id() == joiner_id),
911 "Joiner should survive cleanup"
912 );
913 }
914
915 #[test]
916 fn test_invite_chain_preserved_for_active_member() {
917 let rng = &mut rand::thread_rng();
918 let owner_sk = SigningKey::generate(rng);
919 let owner_vk = owner_sk.verifying_key();
920 let owner_id = MemberId::from(&owner_vk);
921 let params = ChatRoomParametersV1 { owner: owner_vk };
922
923 let a_sk = SigningKey::generate(rng);
924 let a_vk = a_sk.verifying_key();
925 let a_id = MemberId::from(&a_vk);
926
927 let b_sk = SigningKey::generate(rng);
928 let b_vk = b_sk.verifying_key();
929 let b_id = MemberId::from(&b_vk);
930
931 let member_a = AuthorizedMember::new(
933 Member {
934 owner_member_id: owner_id,
935 invited_by: owner_id,
936 member_vk: a_vk,
937 },
938 &owner_sk,
939 );
940 let member_b = AuthorizedMember::new(
941 Member {
942 owner_member_id: owner_id,
943 invited_by: a_id,
944 member_vk: b_vk,
945 },
946 &a_sk,
947 );
948
949 let msg_b = AuthorizedMessageV1::new(
951 MessageV1 {
952 room_owner: owner_id,
953 author: b_id,
954 time: SystemTime::now(),
955 content: RoomMessageBody::public("Hello from B".to_string()),
956 },
957 &b_sk,
958 );
959
960 let config = Configuration {
961 max_members: 10,
962 max_recent_messages: 100,
963 ..Default::default()
964 };
965 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
966
967 let mut state = ChatRoomStateV1 {
968 configuration: auth_config,
969 members: MembersV1 {
970 members: vec![member_a, member_b],
971 },
972 recent_messages: MessagesV1 {
973 messages: vec![msg_b],
974 ..Default::default()
975 },
976 ..Default::default()
977 };
978
979 state.post_apply_cleanup(¶ms).unwrap();
980
981 assert_eq!(state.members.members.len(), 2);
983 let member_ids: HashSet<MemberId> = state
984 .members
985 .members
986 .iter()
987 .map(|m| m.member.id())
988 .collect();
989 assert!(
990 member_ids.contains(&a_id),
991 "A should be kept (in B's invite chain)"
992 );
993 assert!(
994 member_ids.contains(&b_id),
995 "B should be kept (has messages)"
996 );
997 }
998
999 #[test]
1005 fn test_banner_exempt_from_inactivity_prune_so_ban_persists() {
1006 let rng = &mut rand::thread_rng();
1007 let owner_sk = SigningKey::generate(rng);
1008 let owner_vk = owner_sk.verifying_key();
1009 let owner_id = MemberId::from(&owner_vk);
1010 let params = ChatRoomParametersV1 { owner: owner_vk };
1011
1012 let a_sk = SigningKey::generate(rng);
1013 let a_vk = a_sk.verifying_key();
1014 let a_id = MemberId::from(&a_vk);
1015
1016 let c_sk = SigningKey::generate(rng);
1017 let c_vk = c_sk.verifying_key();
1018 let c_id = MemberId::from(&c_vk);
1019
1020 let member_a = AuthorizedMember::new(
1022 Member {
1023 owner_member_id: owner_id,
1024 invited_by: owner_id,
1025 member_vk: a_vk,
1026 },
1027 &owner_sk,
1028 );
1029
1030 let ban_c_by_a = AuthorizedUserBan::new(
1032 UserBan {
1033 owner_member_id: owner_id,
1034 banned_at: SystemTime::now(),
1035 banned_user: c_id,
1036 },
1037 a_id,
1038 &a_sk,
1039 );
1040
1041 let config = Configuration {
1042 max_members: 10,
1043 max_user_bans: 10,
1044 ..Default::default()
1045 };
1046 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1047
1048 let mut state = ChatRoomStateV1 {
1051 configuration: auth_config,
1052 members: MembersV1 {
1053 members: vec![member_a],
1054 },
1055 bans: BansV1(vec![ban_c_by_a]),
1056 ..Default::default()
1057 };
1058
1059 state.post_apply_cleanup(¶ms).unwrap();
1060
1061 assert_eq!(
1063 state.members.members.len(),
1064 1,
1065 "A should be exempt from prune"
1066 );
1067 assert_eq!(state.members.members[0].member.id(), a_id);
1068
1069 assert_eq!(state.bans.0.len(), 1, "Ban should persist");
1071 assert_eq!(state.bans.0[0].ban.banned_user, c_id);
1072 assert_eq!(state.bans.0[0].banned_by, a_id);
1073 }
1074
1075 #[test]
1081 fn over_cap_ban_does_not_one_shot_remove() {
1082 let rng = &mut rand::thread_rng();
1083 let owner_sk = SigningKey::generate(rng);
1084 let owner_vk = owner_sk.verifying_key();
1085 let owner_id = MemberId::from(&owner_vk);
1086 let params = ChatRoomParametersV1 { owner: owner_vk };
1087
1088 let config = Configuration {
1090 max_members: 10,
1091 max_user_bans: 1,
1092 max_recent_messages: 100,
1093 ..Default::default()
1094 };
1095 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1096
1097 let t_sk = SigningKey::generate(rng);
1099 let t_vk = t_sk.verifying_key();
1100 let t_id = MemberId::from(&t_vk);
1101 let member_t = AuthorizedMember::new(
1102 Member {
1103 owner_member_id: owner_id,
1104 invited_by: owner_id,
1105 member_vk: t_vk,
1106 },
1107 &owner_sk,
1108 );
1109 let msg_t = AuthorizedMessageV1::new(
1110 MessageV1 {
1111 room_owner: owner_id,
1112 author: t_id,
1113 time: SystemTime::now(),
1114 content: RoomMessageBody::public("hi".to_string()),
1115 },
1116 &t_sk,
1117 );
1118
1119 let base = SystemTime::now();
1120 let ban_t = AuthorizedUserBan::new(
1122 UserBan {
1123 owner_member_id: owner_id,
1124 banned_at: base,
1125 banned_user: t_id,
1126 },
1127 owner_id,
1128 &owner_sk,
1129 );
1130 let absent = MemberId::from(&SigningKey::generate(rng).verifying_key());
1132 let ban_absent = AuthorizedUserBan::new(
1133 UserBan {
1134 owner_member_id: owner_id,
1135 banned_at: base + std::time::Duration::from_secs(10),
1136 banned_user: absent,
1137 },
1138 owner_id,
1139 &owner_sk,
1140 );
1141
1142 let mut state = ChatRoomStateV1 {
1143 configuration: auth_config,
1144 members: MembersV1 {
1145 members: vec![member_t],
1146 },
1147 recent_messages: MessagesV1 {
1148 messages: vec![msg_t],
1149 ..Default::default()
1150 },
1151 bans: BansV1(vec![ban_t, ban_absent]),
1152 ..Default::default()
1153 };
1154
1155 state.post_apply_cleanup(¶ms).unwrap();
1156
1157 assert!(
1158 state.members.members.iter().any(|m| m.member.id() == t_id),
1159 "an over-cap ban that gets evicted must NOT one-shot-remove its target"
1160 );
1161 assert_eq!(state.bans.0.len(), 1, "capped to max_user_bans");
1162 assert!(
1163 state.bans.0.iter().any(|b| b.ban.banned_user == absent),
1164 "the surviving ban is the newer absent-target one"
1165 );
1166 }
1167
1168 #[test]
1174 fn cleanup_is_idempotent_over_cap_evicted_ban() {
1175 let rng = &mut rand::thread_rng();
1176 let owner_sk = SigningKey::generate(rng);
1177 let owner_vk = owner_sk.verifying_key();
1178 let owner_id = MemberId::from(&owner_vk);
1179 let params = ChatRoomParametersV1 { owner: owner_vk };
1180
1181 let config = Configuration {
1182 max_members: 10,
1183 max_user_bans: 2,
1184 max_recent_messages: 100,
1185 ..Default::default()
1186 };
1187 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1188
1189 let b_sk = SigningKey::generate(rng);
1192 let b_vk = b_sk.verifying_key();
1193 let b_id = MemberId::from(&b_vk);
1194 let member_b = AuthorizedMember::new(
1195 Member {
1196 owner_member_id: owner_id,
1197 invited_by: owner_id,
1198 member_vk: b_vk,
1199 },
1200 &owner_sk,
1201 );
1202
1203 let base = SystemTime::now();
1204 let absent1 = MemberId::from(&SigningKey::generate(rng).verifying_key());
1206 let absent2 = MemberId::from(&SigningKey::generate(rng).verifying_key());
1207 let owner_ban1 = AuthorizedUserBan::new(
1208 UserBan {
1209 owner_member_id: owner_id,
1210 banned_at: base + std::time::Duration::from_secs(10),
1211 banned_user: absent1,
1212 },
1213 owner_id,
1214 &owner_sk,
1215 );
1216 let owner_ban2 = AuthorizedUserBan::new(
1217 UserBan {
1218 owner_member_id: owner_id,
1219 banned_at: base + std::time::Duration::from_secs(11),
1220 banned_user: absent2,
1221 },
1222 owner_id,
1223 &owner_sk,
1224 );
1225 let c_id = MemberId::from(&SigningKey::generate(rng).verifying_key());
1228 let ban_c_by_b = AuthorizedUserBan::new(
1229 UserBan {
1230 owner_member_id: owner_id,
1231 banned_at: base,
1232 banned_user: c_id,
1233 },
1234 b_id,
1235 &b_sk,
1236 );
1237
1238 let state = ChatRoomStateV1 {
1239 configuration: auth_config,
1240 members: MembersV1 {
1241 members: vec![member_b],
1242 },
1243 bans: BansV1(vec![ban_c_by_b, owner_ban1, owner_ban2]),
1244 ..Default::default()
1245 };
1246
1247 let mut once = state.clone();
1248 once.post_apply_cleanup(¶ms).unwrap();
1249
1250 assert!(
1252 !once.members.members.iter().any(|m| m.member.id() == b_id),
1253 "over-cap-evicted banner B must be pruned on the FIRST cleanup pass"
1254 );
1255 assert_eq!(once.bans.0.len(), 2, "capped to max_user_bans");
1256
1257 let mut twice = once.clone();
1259 twice.post_apply_cleanup(¶ms).unwrap();
1260 assert_eq!(once, twice, "post_apply_cleanup must be idempotent");
1261 }
1262
1263 #[test]
1268 fn banned_member_reaction_removed_from_actions_state_cache() {
1269 let rng = &mut rand::thread_rng();
1270 let owner_sk = SigningKey::generate(rng);
1271 let owner_vk = owner_sk.verifying_key();
1272 let owner_id = MemberId::from(&owner_vk);
1273 let params = ChatRoomParametersV1 { owner: owner_vk };
1274
1275 let config = Configuration {
1276 max_members: 10,
1277 max_user_bans: 10,
1278 max_recent_messages: 100,
1279 ..Default::default()
1280 };
1281 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1282
1283 let m_sk = SigningKey::generate(rng);
1285 let m_vk = m_sk.verifying_key();
1286 let m_id = MemberId::from(&m_vk);
1287 let r_sk = SigningKey::generate(rng);
1288 let r_vk = r_sk.verifying_key();
1289 let r_id = MemberId::from(&r_vk);
1290 let member_m = AuthorizedMember::new(
1291 Member {
1292 owner_member_id: owner_id,
1293 invited_by: owner_id,
1294 member_vk: m_vk,
1295 },
1296 &owner_sk,
1297 );
1298 let member_r = AuthorizedMember::new(
1299 Member {
1300 owner_member_id: owner_id,
1301 invited_by: owner_id,
1302 member_vk: r_vk,
1303 },
1304 &owner_sk,
1305 );
1306
1307 let msg1 = AuthorizedMessageV1::new(
1308 MessageV1 {
1309 room_owner: owner_id,
1310 author: m_id,
1311 time: SystemTime::now(),
1312 content: RoomMessageBody::public("hello".to_string()),
1313 },
1314 &m_sk,
1315 );
1316 let msg1_id = msg1.id();
1317 let reaction = AuthorizedMessageV1::new(
1318 MessageV1 {
1319 room_owner: owner_id,
1320 author: r_id,
1321 time: SystemTime::now() + std::time::Duration::from_secs(1),
1322 content: RoomMessageBody::reaction(msg1_id.clone(), "👍".to_string()),
1323 },
1324 &r_sk,
1325 );
1326
1327 let mut state = ChatRoomStateV1 {
1328 configuration: auth_config,
1329 members: MembersV1 {
1330 members: vec![member_m, member_r],
1331 },
1332 recent_messages: MessagesV1 {
1333 messages: vec![msg1.clone(), reaction],
1334 ..Default::default()
1335 },
1336 ..Default::default()
1337 };
1338
1339 state.recent_messages.rebuild_actions_state();
1341 assert!(
1342 state
1343 .recent_messages
1344 .reactions(&msg1_id)
1345 .and_then(|r| r.get("👍"))
1346 .is_some_and(|v| v.contains(&r_id)),
1347 "sanity: R's reaction is present in the cache before the ban"
1348 );
1349
1350 state.bans.0.push(AuthorizedUserBan::new(
1352 UserBan {
1353 owner_member_id: owner_id,
1354 banned_at: SystemTime::now(),
1355 banned_user: r_id,
1356 },
1357 owner_id,
1358 &owner_sk,
1359 ));
1360
1361 state.post_apply_cleanup(¶ms).unwrap();
1362
1363 assert!(
1364 !state.members.members.iter().any(|m| m.member.id() == r_id),
1365 "R is banned and removed"
1366 );
1367 let lingers = state
1368 .recent_messages
1369 .reactions(&msg1_id)
1370 .and_then(|r| r.get("👍"))
1371 .is_some_and(|v| v.contains(&r_id));
1372 assert!(
1373 !lingers,
1374 "the banned member's reaction must be gone from the rebuilt public \
1375 actions_state cache"
1376 );
1377 }
1378
1379 #[test]
1383 fn post_apply_cleanup_is_idempotent_on_adversarial_states() {
1384 let rng = &mut rand::thread_rng();
1385 let owner_sk = SigningKey::generate(rng);
1386 let owner_vk = owner_sk.verifying_key();
1387 let owner_id = MemberId::from(&owner_vk);
1388 let params = ChatRoomParametersV1 { owner: owner_vk };
1389
1390 let cfg = |max_bans: usize| {
1391 AuthorizedConfigurationV1::new(
1392 Configuration {
1393 max_members: 50,
1394 max_user_bans: max_bans,
1395 max_recent_messages: 100,
1396 ..Default::default()
1397 },
1398 &owner_sk,
1399 )
1400 };
1401 let owner_member = |vk| {
1402 AuthorizedMember::new(
1403 Member {
1404 owner_member_id: owner_id,
1405 invited_by: owner_id,
1406 member_vk: vk,
1407 },
1408 &owner_sk,
1409 )
1410 };
1411 let assert_idem = |state: &ChatRoomStateV1, label: &str| {
1412 let mut once = state.clone();
1413 once.post_apply_cleanup(¶ms).unwrap();
1414 let mut twice = once.clone();
1415 twice.post_apply_cleanup(¶ms).unwrap();
1416 assert_eq!(once, twice, "post_apply_cleanup not idempotent: {}", label);
1417 };
1418
1419 {
1422 let banners: Vec<SigningKey> = (0..5).map(|_| SigningKey::generate(rng)).collect();
1423 let members: Vec<AuthorizedMember> = banners
1424 .iter()
1425 .map(|sk| owner_member(sk.verifying_key()))
1426 .collect();
1427 let base = SystemTime::now();
1428 let bans: Vec<AuthorizedUserBan> = banners
1429 .iter()
1430 .enumerate()
1431 .map(|(i, sk)| {
1432 let absent = MemberId::from(&SigningKey::generate(rng).verifying_key());
1433 AuthorizedUserBan::new(
1434 UserBan {
1435 owner_member_id: owner_id,
1436 banned_at: base + std::time::Duration::from_secs(i as u64),
1437 banned_user: absent,
1438 },
1439 MemberId::from(&sk.verifying_key()),
1440 sk,
1441 )
1442 })
1443 .collect();
1444 let state = ChatRoomStateV1 {
1445 configuration: cfg(2),
1446 members: MembersV1 { members },
1447 bans: BansV1(bans),
1448 ..Default::default()
1449 };
1450 assert_idem(&state, "member-banners over cap");
1451 }
1452
1453 {
1456 let a_sk = SigningKey::generate(rng);
1457 let a_id = MemberId::from(&a_sk.verifying_key());
1458 let b_id = MemberId::from(&SigningKey::generate(rng).verifying_key());
1459 let x_sk = SigningKey::generate(rng);
1460 let x_id = MemberId::from(&x_sk.verifying_key());
1461 let flood_banners: Vec<SigningKey> =
1462 (0..4).map(|_| SigningKey::generate(rng)).collect();
1463
1464 let mut members = vec![
1465 owner_member(a_sk.verifying_key()),
1466 owner_member(x_sk.verifying_key()),
1467 ];
1468 for sk in &flood_banners {
1469 members.push(owner_member(sk.verifying_key()));
1470 }
1471
1472 let base = SystemTime::now();
1473 let mut bans = vec![
1474 AuthorizedUserBan::new(
1476 UserBan {
1477 owner_member_id: owner_id,
1478 banned_at: base,
1479 banned_user: b_id,
1480 },
1481 a_id,
1482 &a_sk,
1483 ),
1484 AuthorizedUserBan::new(
1486 UserBan {
1487 owner_member_id: owner_id,
1488 banned_at: base + std::time::Duration::from_secs(1),
1489 banned_user: a_id,
1490 },
1491 owner_id,
1492 &owner_sk,
1493 ),
1494 ];
1495 for (i, sk) in flood_banners.iter().enumerate() {
1497 bans.push(AuthorizedUserBan::new(
1498 UserBan {
1499 owner_member_id: owner_id,
1500 banned_at: base + std::time::Duration::from_secs(100 + i as u64),
1501 banned_user: x_id,
1502 },
1503 MemberId::from(&sk.verifying_key()),
1504 sk,
1505 ));
1506 }
1507
1508 let state = ChatRoomStateV1 {
1509 configuration: cfg(3),
1510 members: MembersV1 { members },
1511 bans: BansV1(bans),
1512 ..Default::default()
1513 };
1514 assert_idem(&state, "owner cascade + inert flood over cap");
1515 }
1516 }
1517
1518 #[test]
1519 fn test_member_re_added_with_message() {
1520 let rng = &mut rand::thread_rng();
1521 let owner_sk = SigningKey::generate(rng);
1522 let owner_vk = owner_sk.verifying_key();
1523 let owner_id = MemberId::from(&owner_vk);
1524 let params = ChatRoomParametersV1 { owner: owner_vk };
1525
1526 let a_sk = SigningKey::generate(rng);
1527 let a_vk = a_sk.verifying_key();
1528 let a_id = MemberId::from(&a_vk);
1529
1530 let member_a = AuthorizedMember::new(
1531 Member {
1532 owner_member_id: owner_id,
1533 invited_by: owner_id,
1534 member_vk: a_vk,
1535 },
1536 &owner_sk,
1537 );
1538
1539 let config = Configuration {
1540 max_members: 10,
1541 max_recent_messages: 100,
1542 ..Default::default()
1543 };
1544 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1545
1546 let mut state = ChatRoomStateV1 {
1548 configuration: auth_config,
1549 members: MembersV1 {
1550 members: vec![member_a.clone()],
1551 },
1552 ..Default::default()
1553 };
1554
1555 state.post_apply_cleanup(¶ms).unwrap();
1557 assert!(state.members.members.is_empty(), "A should be pruned");
1558
1559 state.members.members.push(member_a);
1561 let msg = AuthorizedMessageV1::new(
1562 MessageV1 {
1563 room_owner: owner_id,
1564 author: a_id,
1565 time: SystemTime::now(),
1566 content: RoomMessageBody::public("Hello again!".to_string()),
1567 },
1568 &a_sk,
1569 );
1570 state.recent_messages.messages.push(msg);
1571
1572 state.post_apply_cleanup(¶ms).unwrap();
1574 assert_eq!(state.members.members.len(), 1, "A should be kept");
1575 assert_eq!(state.members.members[0].member.id(), a_id);
1576 }
1577
1578 #[test]
1579 fn test_member_info_cleaned_after_pruning() {
1580 let rng = &mut rand::thread_rng();
1581 let owner_sk = SigningKey::generate(rng);
1582 let owner_vk = owner_sk.verifying_key();
1583 let owner_id = MemberId::from(&owner_vk);
1584 let params = ChatRoomParametersV1 { owner: owner_vk };
1585
1586 let a_sk = SigningKey::generate(rng);
1587 let a_vk = a_sk.verifying_key();
1588 let a_id = MemberId::from(&a_vk);
1589
1590 let member_a = AuthorizedMember::new(
1591 Member {
1592 owner_member_id: owner_id,
1593 invited_by: owner_id,
1594 member_vk: a_vk,
1595 },
1596 &owner_sk,
1597 );
1598
1599 let a_info = AuthorizedMemberInfo::new_with_member_key(
1601 MemberInfo::new_public(a_id, 1, "Alice".to_string()),
1602 &a_sk,
1603 );
1604 let owner_info = AuthorizedMemberInfo::new(
1605 MemberInfo::new_public(owner_id, 1, "Owner".to_string()),
1606 &owner_sk,
1607 );
1608
1609 let config = Configuration {
1610 max_members: 10,
1611 ..Default::default()
1612 };
1613 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1614
1615 let mut state = ChatRoomStateV1 {
1616 configuration: auth_config,
1617 members: MembersV1 {
1618 members: vec![member_a],
1619 },
1620 member_info: MemberInfoV1 {
1621 member_info: vec![owner_info, a_info],
1622 },
1623 ..Default::default()
1624 };
1625
1626 state.post_apply_cleanup(¶ms).unwrap();
1628
1629 assert!(state.members.members.is_empty(), "A should be pruned");
1630 assert_eq!(
1631 state.member_info.member_info.len(),
1632 1,
1633 "Only owner's info should remain"
1634 );
1635 assert_eq!(
1636 state.member_info.member_info[0].member_info.member_id, owner_id,
1637 "Remaining info should be owner's"
1638 );
1639 }
1640
1641 #[test]
1652 fn test_member_with_encrypted_secret_survives_cleanup() {
1653 let rng = &mut rand::thread_rng();
1654 let owner_sk = SigningKey::generate(rng);
1655 let owner_vk = owner_sk.verifying_key();
1656 let owner_id = MemberId::from(&owner_vk);
1657 let params = ChatRoomParametersV1 { owner: owner_vk };
1658
1659 let a_sk = SigningKey::generate(rng);
1660 let a_vk = a_sk.verifying_key();
1661 let a_id = MemberId::from(&a_vk);
1662
1663 let member_a = AuthorizedMember::new(
1664 Member {
1665 owner_member_id: owner_id,
1666 invited_by: owner_id,
1667 member_vk: a_vk,
1668 },
1669 &owner_sk,
1670 );
1671
1672 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1676 member_id: a_id,
1677 secret_version: 0,
1678 ciphertext: vec![0u8; 16], nonce: [0u8; 12],
1680 sender_ephemeral_public_key: [0u8; 32],
1681 provider: owner_id,
1682 };
1683 let authorized_secret = crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1684 secret_for_a,
1685 &owner_sk,
1686 );
1687
1688 let config = Configuration {
1689 max_members: 10,
1690 max_recent_messages: 100,
1691 ..Default::default()
1692 };
1693 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1694
1695 let mut state = ChatRoomStateV1 {
1696 configuration: auth_config,
1697 members: MembersV1 {
1698 members: vec![member_a],
1699 },
1700 secrets: crate::room_state::secret::RoomSecretsV1 {
1701 current_version: 0,
1702 versions: vec![],
1703 encrypted_secrets: vec![authorized_secret],
1704 },
1705 ..Default::default()
1706 };
1707
1708 state.post_apply_cleanup(¶ms).unwrap();
1709
1710 assert_eq!(
1711 state.members.members.len(),
1712 1,
1713 "A should survive cleanup because they have an encrypted_secrets entry"
1714 );
1715 assert_eq!(state.members.members[0].member.id(), a_id);
1716 }
1717
1718 #[test]
1734 fn test_banned_member_with_encrypted_secret_is_still_pruned() {
1735 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
1736 use std::time::SystemTime;
1737
1738 let rng = &mut rand::thread_rng();
1739 let owner_sk = SigningKey::generate(rng);
1740 let owner_vk = owner_sk.verifying_key();
1741 let owner_id = MemberId::from(&owner_vk);
1742 let params = ChatRoomParametersV1 { owner: owner_vk };
1743
1744 let a_sk = SigningKey::generate(rng);
1745 let a_vk = a_sk.verifying_key();
1746 let a_id = MemberId::from(&a_vk);
1747
1748 let member_a = AuthorizedMember::new(
1749 Member {
1750 owner_member_id: owner_id,
1751 invited_by: owner_id,
1752 member_vk: a_vk,
1753 },
1754 &owner_sk,
1755 );
1756
1757 let ban = AuthorizedUserBan::new(
1758 UserBan {
1759 owner_member_id: owner_id,
1760 banned_at: SystemTime::now(),
1761 banned_user: a_id,
1762 },
1763 owner_id,
1764 &owner_sk,
1765 );
1766
1767 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1772 member_id: a_id,
1773 secret_version: 0,
1774 ciphertext: vec![0u8; 16],
1775 nonce: [0u8; 12],
1776 sender_ephemeral_public_key: [0u8; 32],
1777 provider: owner_id,
1778 };
1779 let authorized_secret = crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1780 secret_for_a,
1781 &owner_sk,
1782 );
1783
1784 let config = Configuration {
1785 max_members: 10,
1786 max_recent_messages: 100,
1787 ..Default::default()
1788 };
1789 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1790
1791 let mut state = ChatRoomStateV1 {
1792 configuration: auth_config,
1793 members: MembersV1 {
1794 members: vec![member_a],
1795 },
1796 bans: crate::room_state::ban::BansV1(vec![ban]),
1797 secrets: crate::room_state::secret::RoomSecretsV1 {
1798 current_version: 0,
1799 versions: vec![],
1800 encrypted_secrets: vec![authorized_secret],
1801 },
1802 ..Default::default()
1803 };
1804
1805 state.members.members.retain(|m| m.member.id() != a_id);
1811
1812 state.post_apply_cleanup(¶ms).unwrap();
1813
1814 assert!(
1815 state.members.members.is_empty(),
1816 "banned member A must NOT be resurrected by post_apply_cleanup's \
1817 encrypted_secrets exemption — see IMPORTANT #4 on PR #272 review round 2"
1818 );
1819 assert_eq!(state.bans.0.len(), 1);
1821 assert_eq!(state.bans.0[0].ban.banned_user, a_id);
1822 }
1823
1824 #[test]
1835 fn test_stale_secret_recipient_is_pruned_after_rotation() {
1836 use crate::room_state::privacy::RoomCipherSpec;
1837 use crate::room_state::secret::{AuthorizedSecretVersionRecord, SecretVersionRecordV1};
1838 use std::time::SystemTime;
1839
1840 let rng = &mut rand::thread_rng();
1841 let owner_sk = SigningKey::generate(rng);
1842 let owner_vk = owner_sk.verifying_key();
1843 let owner_id = MemberId::from(&owner_vk);
1844 let params = ChatRoomParametersV1 { owner: owner_vk };
1845
1846 let a_sk = SigningKey::generate(rng);
1847 let a_vk = a_sk.verifying_key();
1848 let a_id = MemberId::from(&a_vk);
1849
1850 let member_a = AuthorizedMember::new(
1851 Member {
1852 owner_member_id: owner_id,
1853 invited_by: owner_id,
1854 member_vk: a_vk,
1855 },
1856 &owner_sk,
1857 );
1858
1859 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1863 member_id: a_id,
1864 secret_version: 0,
1865 ciphertext: vec![0u8; 16],
1866 nonce: [0u8; 12],
1867 sender_ephemeral_public_key: [0u8; 32],
1868 provider: owner_id,
1869 };
1870 let authorized_secret_v0 =
1871 crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1872 secret_for_a,
1873 &owner_sk,
1874 );
1875
1876 let v1_record = AuthorizedSecretVersionRecord::new(
1877 SecretVersionRecordV1 {
1878 version: 1,
1879 cipher_spec: RoomCipherSpec::Aes256Gcm,
1880 created_at: SystemTime::now(),
1881 },
1882 &owner_sk,
1883 );
1884
1885 let config = Configuration {
1886 max_members: 10,
1887 max_recent_messages: 100,
1888 ..Default::default()
1889 };
1890 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1891
1892 let mut state = ChatRoomStateV1 {
1893 configuration: auth_config,
1894 members: MembersV1 {
1895 members: vec![member_a],
1896 },
1897 secrets: crate::room_state::secret::RoomSecretsV1 {
1898 current_version: 1,
1899 versions: vec![v1_record],
1900 encrypted_secrets: vec![authorized_secret_v0],
1901 },
1902 ..Default::default()
1903 };
1904
1905 state.post_apply_cleanup(¶ms).unwrap();
1906
1907 assert!(
1908 state.members.members.is_empty(),
1909 "member A with ONLY a stale v0 blob (no v1 re-issue, no messages, no \
1910 DMs) must be pruned — see IMPORTANT #5 on PR #272 review round 2"
1911 );
1912 }
1913
1914 #[test]
1928 fn test_ban_race_with_encrypted_secret_converges_to_pruned() {
1929 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
1930 use std::time::SystemTime;
1931
1932 let rng = &mut rand::thread_rng();
1933 let owner_sk = SigningKey::generate(rng);
1934 let owner_vk = owner_sk.verifying_key();
1935 let owner_id = MemberId::from(&owner_vk);
1936 let params = ChatRoomParametersV1 { owner: owner_vk };
1937
1938 let x_sk = SigningKey::generate(rng);
1939 let x_vk = x_sk.verifying_key();
1940 let x_id = MemberId::from(&x_vk);
1941
1942 let member_x = AuthorizedMember::new(
1943 Member {
1944 owner_member_id: owner_id,
1945 invited_by: owner_id,
1946 member_vk: x_vk,
1947 },
1948 &owner_sk,
1949 );
1950
1951 let ban_x = AuthorizedUserBan::new(
1952 UserBan {
1953 owner_member_id: owner_id,
1954 banned_at: SystemTime::now(),
1955 banned_user: x_id,
1956 },
1957 owner_id,
1958 &owner_sk,
1959 );
1960
1961 let secret_for_x = crate::room_state::secret::EncryptedSecretForMemberV1 {
1962 member_id: x_id,
1963 secret_version: 0,
1964 ciphertext: vec![0u8; 16],
1965 nonce: [0u8; 12],
1966 sender_ephemeral_public_key: [0u8; 32],
1967 provider: owner_id,
1968 };
1969 let authorized_secret_x =
1970 crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1971 secret_for_x,
1972 &owner_sk,
1973 );
1974
1975 let config = Configuration {
1976 max_members: 10,
1977 max_recent_messages: 100,
1978 ..Default::default()
1979 };
1980 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1981
1982 let mut peer_a_state = ChatRoomStateV1 {
1995 configuration: auth_config.clone(),
1996 members: MembersV1 { members: vec![] },
1997 bans: crate::room_state::ban::BansV1(vec![ban_x.clone()]),
1998 secrets: crate::room_state::secret::RoomSecretsV1 {
1999 current_version: 0,
2000 versions: vec![],
2001 encrypted_secrets: vec![authorized_secret_x.clone()],
2002 },
2003 ..Default::default()
2004 };
2005 peer_a_state.post_apply_cleanup(¶ms).unwrap();
2006 assert!(
2007 peer_a_state.members.members.is_empty(),
2008 "peer A: X must remain pruned despite the encrypted_secret being present"
2009 );
2010
2011 let mut peer_b_state = ChatRoomStateV1 {
2017 configuration: auth_config,
2018 members: MembersV1 { members: vec![] },
2019 bans: crate::room_state::ban::BansV1(vec![ban_x]),
2020 secrets: crate::room_state::secret::RoomSecretsV1 {
2021 current_version: 0,
2022 versions: vec![],
2023 encrypted_secrets: vec![authorized_secret_x],
2024 },
2025 ..Default::default()
2026 };
2027 peer_b_state.post_apply_cleanup(¶ms).unwrap();
2028 assert!(
2029 peer_b_state.members.members.is_empty(),
2030 "peer B: X must remain pruned despite the encrypted_secret being present"
2031 );
2032
2033 assert_eq!(peer_a_state.members, peer_b_state.members);
2036 assert_eq!(peer_a_state.bans, peer_b_state.bans);
2037 assert_eq!(peer_a_state.secrets, peer_b_state.secrets);
2038
2039 let _ = member_x;
2044 }
2045
2046 #[test]
2047 fn test_state_with_none_deltas() {
2048 let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
2049
2050 let modified_state = state.clone();
2052
2053 let summary = state.summarize(&state, ¶meters);
2055 let delta = modified_state.delta(&state, ¶meters, &summary);
2056
2057 assert!(
2058 delta.is_none(),
2059 "Delta should be None when no changes are made"
2060 );
2061
2062 let mut partially_modified_state = state.clone();
2064 let new_config = Configuration {
2065 configuration_version: 2,
2066 ..partially_modified_state.configuration.configuration.clone()
2067 };
2068 partially_modified_state.configuration =
2069 AuthorizedConfigurationV1::new(new_config, &owner_signing_key);
2070
2071 let summary = state.summarize(&state, ¶meters);
2072 let delta = partially_modified_state
2073 .delta(&state, ¶meters, &summary)
2074 .unwrap();
2075
2076 assert!(
2078 delta.configuration.is_some(),
2079 "Configuration delta should be Some"
2080 );
2081 assert!(delta.bans.is_none(), "Bans delta should be None");
2082 assert!(delta.members.is_none(), "Members delta should be None");
2083 assert!(
2084 delta.member_info.is_none(),
2085 "Member info delta should be None"
2086 );
2087 assert!(
2088 delta.recent_messages.is_none(),
2089 "Recent messages delta should be None"
2090 );
2091 assert!(delta.upgrade.is_none(), "Upgrade delta should be None");
2092
2093 let mut new_state = state.clone();
2095 new_state
2096 .apply_delta(&state, ¶meters, &Some(delta))
2097 .unwrap();
2098
2099 assert_eq!(
2100 new_state, partially_modified_state,
2101 "State should be partially modified"
2102 );
2103 }
2104
2105 #[test]
2117 fn test_upgrade_pointer_applies_as_delta() {
2118 use crate::room_state::upgrade::{AuthorizedUpgradeV1, UpgradeV1};
2119
2120 let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
2121 assert!(
2123 state.verify(&state, ¶meters).is_ok(),
2124 "baseline room state should verify"
2125 );
2126 assert!(
2127 state.upgrade.0.is_none(),
2128 "baseline room state should have no upgrade pointer"
2129 );
2130
2131 let upgrade = UpgradeV1 {
2132 owner_member_id: MemberId::from(¶meters.owner),
2133 version: 1,
2134 new_chatroom_address: blake3::Hash::from([7u8; 32]),
2135 };
2136 let authorized = AuthorizedUpgradeV1::new(upgrade, &owner_signing_key);
2137
2138 let delta = ChatRoomStateV1Delta {
2142 upgrade: Some(authorized.clone()),
2143 ..Default::default()
2144 };
2145 let mut updated = state.clone();
2146 updated
2147 .apply_delta(&state, ¶meters, &Some(delta))
2148 .expect("applying the upgrade-pointer delta must succeed");
2149 assert_eq!(
2150 updated.upgrade.0.as_ref(),
2151 Some(&authorized),
2152 "the upgrade pointer must land on the old contract's state"
2153 );
2154 assert!(
2155 updated.verify(&updated, ¶meters).is_ok(),
2156 "the state with the upgrade pointer applied must still verify"
2157 );
2158
2159 let buggy_full_state = ChatRoomStateV1 {
2163 upgrade: OptionalUpgradeV1(Some(authorized)),
2164 ..Default::default()
2165 };
2166 assert!(
2167 buggy_full_state
2168 .verify(&buggy_full_state, ¶meters)
2169 .is_err(),
2170 "a `..Default::default()` full-state upgrade must fail verification (the #127 bug)"
2171 );
2172 }
2173
2174 #[test]
2179 fn post_apply_cleanup_dedups_member_info_to_canonical() {
2180 use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
2181
2182 let rng = &mut rand::thread_rng();
2183 let owner_sk = SigningKey::generate(rng);
2184 let owner_vk = owner_sk.verifying_key();
2185 let owner_id = MemberId::from(&owner_vk);
2186 let params = ChatRoomParametersV1 { owner: owner_vk };
2187
2188 let config = Configuration {
2189 max_members: 10,
2190 max_recent_messages: 100,
2191 ..Default::default()
2192 };
2193 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
2194
2195 let m_sk = SigningKey::generate(rng);
2198 let m_vk = m_sk.verifying_key();
2199 let m_id = MemberId::from(&m_vk);
2200 let member_m = AuthorizedMember::new(
2201 Member {
2202 owner_member_id: owner_id,
2203 invited_by: owner_id,
2204 member_vk: m_vk,
2205 },
2206 &owner_sk,
2207 );
2208 let msg_m = AuthorizedMessageV1::new(
2209 MessageV1 {
2210 room_owner: owner_id,
2211 author: m_id,
2212 time: SystemTime::now(),
2213 content: RoomMessageBody::public("hi".to_string()),
2214 },
2215 &m_sk,
2216 );
2217
2218 let dep = MemberId::from(&SigningKey::generate(rng).verifying_key());
2219 let mut g = MemberInfo::new_public(m_id, 1, "nick".to_string());
2220 g.deputies = vec![dep];
2221 let grant = AuthorizedMemberInfo::new_with_member_key(g, &m_sk);
2222 let mut r = MemberInfo::new_public(m_id, 2, "nick".to_string());
2223 r.deputies = vec![];
2224 let revoke = AuthorizedMemberInfo::new_with_member_key(r, &m_sk);
2225
2226 let mut state = ChatRoomStateV1 {
2227 configuration: auth_config,
2228 members: MembersV1 {
2229 members: vec![member_m],
2230 },
2231 member_info: MemberInfoV1 {
2232 member_info: vec![grant, revoke],
2234 },
2235 recent_messages: MessagesV1 {
2236 messages: vec![msg_m],
2237 ..Default::default()
2238 },
2239 ..Default::default()
2240 };
2241
2242 state.post_apply_cleanup(¶ms).unwrap();
2243
2244 let recs: Vec<_> = state
2245 .member_info
2246 .member_info
2247 .iter()
2248 .filter(|i| i.member_info.member_id == m_id)
2249 .collect();
2250 assert_eq!(
2251 recs.len(),
2252 1,
2253 "exactly one member_info record for M after cleanup"
2254 );
2255 assert_eq!(
2256 recs[0].member_info.version, 2,
2257 "the surviving record is the v2 revoke"
2258 );
2259 assert!(
2260 recs[0].member_info.deputies.is_empty(),
2261 "the canonical (revoke) record has no deputies — revoked authority is not resurrected"
2262 );
2263 assert_eq!(
2264 state.member_info.deputies_of(m_id),
2265 &[] as &[MemberId],
2266 "deputies_of agrees with the deduped canonical record"
2267 );
2268 }
2269}