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> {
94 let owner_id = MemberId::from(¶meters.owner);
95
96 let message_authors: HashSet<MemberId> = self
119 .recent_messages
120 .messages
121 .iter()
122 .map(|m| m.message.author)
123 .collect();
124 let dm_participants: HashSet<MemberId> = self.direct_messages.active_participants();
125 let current_secret_version = self.secrets.current_version;
126 let secret_recipients: HashSet<MemberId> = self
127 .secrets
128 .encrypted_secrets
129 .iter()
130 .filter(|s| s.secret.secret_version == current_secret_version)
131 .map(|s| s.secret.member_id)
132 .collect();
133
134 let required_ids = {
137 let members_by_id = self.members.members_by_member_id();
138 let mut required_ids: HashSet<MemberId> = HashSet::new();
139
140 for author_id in &message_authors {
141 if *author_id != owner_id && members_by_id.contains_key(author_id) {
142 required_ids.insert(*author_id);
143 }
144 }
145
146 for participant_id in &dm_participants {
147 if *participant_id != owner_id && members_by_id.contains_key(participant_id) {
148 required_ids.insert(*participant_id);
149 }
150 }
151
152 for recipient_id in &secret_recipients {
153 if *recipient_id != owner_id && members_by_id.contains_key(recipient_id) {
154 required_ids.insert(*recipient_id);
155 }
156 }
157
158 let mut to_process: Vec<MemberId> = required_ids.iter().cloned().collect();
160 while let Some(member_id) = to_process.pop() {
161 if let Some(member) = members_by_id.get(&member_id) {
162 let inviter_id = member.member.invited_by;
163 if inviter_id != owner_id && !required_ids.contains(&inviter_id) {
164 required_ids.insert(inviter_id);
165 to_process.push(inviter_id);
166 }
167 }
168 }
169
170 required_ids
171 };
172
173 self.members
175 .members
176 .retain(|m| required_ids.contains(&m.member.id()));
177
178 self.member_info.member_info.retain(|info| {
180 info.member_info.member_id == owner_id
181 || required_ids.contains(&info.member_info.member_id)
182 });
183
184 let banned_user_ids: HashSet<MemberId> =
190 self.bans.0.iter().map(|b| b.ban.banned_user).collect();
191 let current_member_ids: HashSet<MemberId> =
192 self.members.members.iter().map(|m| m.member.id()).collect();
193
194 self.bans.0.retain(|ban| {
195 ban.banned_by == owner_id
198 || current_member_ids.contains(&ban.banned_by)
199 || !banned_user_ids.contains(&ban.banned_by)
200 });
201
202 let banned_user_ids_for_sweep: HashSet<MemberId> =
207 self.bans.0.iter().map(|b| b.ban.banned_user).collect();
208 let active_member_ids_for_sweep: HashSet<MemberId> =
209 self.members.members.iter().map(|m| m.member.id()).collect();
210 self.direct_messages.sweep_after_membership_change(
211 owner_id,
212 &active_member_ids_for_sweep,
213 &banned_user_ids_for_sweep,
214 );
215
216 self.members.members.sort_by_key(|m| m.member.id());
218 self.member_info
219 .member_info
220 .sort_by_key(|info| info.member_info.member_id);
221
222 Ok(())
223 }
224}
225
226#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
227pub struct ChatRoomParametersV1 {
228 pub owner: VerifyingKey,
229}
230
231impl ChatRoomParametersV1 {
232 pub fn owner_id(&self) -> MemberId {
233 self.owner.into()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
241 use crate::room_state::configuration::Configuration;
242 use crate::room_state::member::{AuthorizedMember, Member};
243 use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
244 use crate::room_state::message::{AuthorizedMessageV1, MessageV1, RoomMessageBody};
245 use ed25519_dalek::SigningKey;
246 use std::fmt::Debug;
247 use std::time::SystemTime;
248
249 #[test]
250 fn test_state() {
251 let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
252
253 assert!(
254 state.verify(&state, ¶meters).is_ok(),
255 "Empty state should verify"
256 );
257
258 let mut new_cfg = state.configuration.configuration.clone();
260 new_cfg.configuration_version += 1;
261 new_cfg.max_recent_messages = 10; let new_cfg = AuthorizedConfigurationV1::new(new_cfg, &owner_signing_key);
263
264 let mut cfg_modified_state = state.clone();
265 cfg_modified_state.configuration = new_cfg;
266 test_apply_delta(state.clone(), cfg_modified_state, ¶meters);
267 }
268
269 fn test_apply_delta<CS>(orig_state: CS, modified_state: CS, parameters: &CS::Parameters)
270 where
271 CS: ComposableState<ParentState = CS> + Clone + PartialEq + Debug,
272 {
273 let orig_verify_result = orig_state.verify(&orig_state, parameters);
274 assert!(
275 orig_verify_result.is_ok(),
276 "Original state verification failed: {:?}",
277 orig_verify_result.err()
278 );
279
280 let modified_verify_result = modified_state.verify(&modified_state, parameters);
281 assert!(
282 modified_verify_result.is_ok(),
283 "Modified state verification failed: {:?}",
284 modified_verify_result.err()
285 );
286
287 let delta = modified_state.delta(
288 &orig_state,
289 parameters,
290 &orig_state.summarize(&orig_state, parameters),
291 );
292
293 println!("Delta: {:?}", delta);
294
295 let mut new_state = orig_state.clone();
296 let apply_delta_result = new_state.apply_delta(&orig_state, parameters, &delta);
297 assert!(
298 apply_delta_result.is_ok(),
299 "Applying delta failed: {:?}",
300 apply_delta_result.err()
301 );
302
303 assert_eq!(new_state, modified_state);
304 }
305 fn create_empty_chat_room_state() -> (ChatRoomStateV1, ChatRoomParametersV1, SigningKey) {
306 let rng = &mut rand::thread_rng();
309 let owner_signing_key = SigningKey::generate(rng);
310 let owner_verifying_key = owner_signing_key.verifying_key();
311
312 let config = AuthorizedConfigurationV1::new(Configuration::default(), &owner_signing_key);
313
314 (
315 ChatRoomStateV1 {
316 configuration: config,
317 bans: BansV1::default(),
318 members: MembersV1::default(),
319 member_info: MemberInfoV1::default(),
320 secrets: RoomSecretsV1::default(),
321 recent_messages: MessagesV1::default(),
322 upgrade: OptionalUpgradeV1(None),
323 ..Default::default()
324 },
325 ChatRoomParametersV1 {
326 owner: owner_verifying_key,
327 },
328 owner_signing_key,
329 )
330 }
331
332 #[test]
337 fn test_orphaned_ban_cleanup_after_cascade_removal() {
338 let rng = &mut rand::thread_rng();
339
340 let owner_sk = SigningKey::generate(rng);
342 let owner_vk = owner_sk.verifying_key();
343 let owner_id = MemberId::from(&owner_vk);
344 let params = ChatRoomParametersV1 { owner: owner_vk };
345
346 let config = Configuration {
348 max_user_bans: 10,
349 max_members: 10,
350 ..Default::default()
351 };
352 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
353
354 let a_sk = SigningKey::generate(rng);
356 let a_vk = a_sk.verifying_key();
357 let a_id = MemberId::from(&a_vk);
358
359 let b_sk = SigningKey::generate(rng);
360 let b_vk = b_sk.verifying_key();
361 let b_id = MemberId::from(&b_vk);
362
363 let member_a = AuthorizedMember::new(
364 Member {
365 owner_member_id: owner_id,
366 invited_by: owner_id,
367 member_vk: a_vk,
368 },
369 &owner_sk,
370 );
371
372 let ban_b_by_a = AuthorizedUserBan::new(
374 UserBan {
375 owner_member_id: owner_id,
376 banned_at: std::time::SystemTime::now(),
377 banned_user: b_id,
378 },
379 a_id,
380 &a_sk,
381 );
382
383 let initial_state = ChatRoomStateV1 {
385 configuration: auth_config.clone(),
386 bans: BansV1(vec![ban_b_by_a.clone()]),
387 members: MembersV1 {
388 members: vec![member_a.clone()],
389 },
390 ..Default::default()
391 };
392
393 assert!(
394 initial_state.verify(&initial_state, ¶ms).is_ok(),
395 "Initial state should verify: {:?}",
396 initial_state.verify(&initial_state, ¶ms)
397 );
398
399 let ban_a_by_owner = AuthorizedUserBan::new(
402 UserBan {
403 owner_member_id: owner_id,
404 banned_at: std::time::SystemTime::now() + std::time::Duration::from_secs(1),
405 banned_user: a_id,
406 },
407 owner_id,
408 &owner_sk,
409 );
410
411 let modified_for_delta = ChatRoomStateV1 {
413 configuration: auth_config,
414 bans: BansV1(vec![ban_b_by_a.clone(), ban_a_by_owner.clone()]),
415 members: MembersV1 {
416 members: vec![member_a.clone()],
417 },
418 ..Default::default()
419 };
420
421 let summary = initial_state.summarize(&initial_state, ¶ms);
423 let delta = modified_for_delta.delta(&initial_state, ¶ms, &summary);
424
425 let mut result_state = initial_state.clone();
426 let apply_result = result_state.apply_delta(&initial_state, ¶ms, &delta);
427 assert!(
428 apply_result.is_ok(),
429 "apply_delta should succeed: {:?}",
430 apply_result
431 );
432
433 assert!(
435 result_state.members.members.is_empty(),
436 "A should be removed from members: {:?}",
437 result_state.members.members
438 );
439
440 assert_eq!(
442 result_state.bans.0.len(),
443 1,
444 "Only owner's ban should remain, orphaned ban cleaned: {:?}",
445 result_state.bans.0
446 );
447 assert_eq!(
448 result_state.bans.0[0].banned_by, owner_id,
449 "Remaining ban should be by owner"
450 );
451
452 assert!(
454 result_state.verify(&result_state, ¶ms).is_ok(),
455 "Result state should verify after orphaned ban cleanup: {:?}",
456 result_state.verify(&result_state, ¶ms)
457 );
458 }
459
460 #[test]
461 fn test_member_pruned_when_no_messages() {
462 let rng = &mut rand::thread_rng();
463 let owner_sk = SigningKey::generate(rng);
464 let owner_vk = owner_sk.verifying_key();
465 let owner_id = MemberId::from(&owner_vk);
466 let params = ChatRoomParametersV1 { owner: owner_vk };
467
468 let a_sk = SigningKey::generate(rng);
469 let a_vk = a_sk.verifying_key();
470 let a_id = MemberId::from(&a_vk);
471
472 let b_sk = SigningKey::generate(rng);
473 let b_vk = b_sk.verifying_key();
474
475 let member_a = AuthorizedMember::new(
476 Member {
477 owner_member_id: owner_id,
478 invited_by: owner_id,
479 member_vk: a_vk,
480 },
481 &owner_sk,
482 );
483 let member_b = AuthorizedMember::new(
484 Member {
485 owner_member_id: owner_id,
486 invited_by: owner_id,
487 member_vk: b_vk,
488 },
489 &owner_sk,
490 );
491
492 let msg_a = AuthorizedMessageV1::new(
494 MessageV1 {
495 room_owner: owner_id,
496 author: a_id,
497 time: SystemTime::now(),
498 content: RoomMessageBody::public("Hello from A".to_string()),
499 },
500 &a_sk,
501 );
502
503 let config = Configuration {
504 max_members: 10,
505 max_recent_messages: 100,
506 ..Default::default()
507 };
508 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
509
510 let mut state = ChatRoomStateV1 {
511 configuration: auth_config,
512 members: MembersV1 {
513 members: vec![member_a, member_b],
514 },
515 recent_messages: MessagesV1 {
516 messages: vec![msg_a],
517 ..Default::default()
518 },
519 ..Default::default()
520 };
521
522 state.post_apply_cleanup(¶ms).unwrap();
523
524 assert_eq!(state.members.members.len(), 1, "Only A should remain");
525 assert_eq!(state.members.members[0].member.id(), a_id);
526 }
527
528 #[test]
529 fn test_member_with_join_event_not_pruned() {
530 let rng = &mut rand::thread_rng();
531 let owner_sk = SigningKey::generate(rng);
532 let owner_vk = owner_sk.verifying_key();
533 let owner_id = MemberId::from(&owner_vk);
534 let params = ChatRoomParametersV1 { owner: owner_vk };
535
536 let a_sk = SigningKey::generate(rng);
537 let a_vk = a_sk.verifying_key();
538 let a_id = MemberId::from(&a_vk);
539
540 let member_a = AuthorizedMember::new(
541 Member {
542 owner_member_id: owner_id,
543 invited_by: owner_id,
544 member_vk: a_vk,
545 },
546 &owner_sk,
547 );
548
549 let join_msg = AuthorizedMessageV1::new(
551 MessageV1 {
552 room_owner: owner_id,
553 author: a_id,
554 time: SystemTime::now(),
555 content: RoomMessageBody::join_event(),
556 },
557 &a_sk,
558 );
559
560 let config = Configuration {
561 max_members: 10,
562 max_recent_messages: 100,
563 ..Default::default()
564 };
565 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
566
567 let mut state = ChatRoomStateV1 {
568 configuration: auth_config,
569 members: MembersV1 {
570 members: vec![member_a],
571 },
572 recent_messages: MessagesV1 {
573 messages: vec![join_msg],
574 ..Default::default()
575 },
576 ..Default::default()
577 };
578
579 state.post_apply_cleanup(¶ms).unwrap();
580
581 assert_eq!(
582 state.members.members.len(),
583 1,
584 "Member with join event should not be pruned"
585 );
586 assert_eq!(state.members.members[0].member.id(), a_id);
587 }
588
589 #[test]
592 fn test_atomic_join_delta_applies_and_verifies() {
593 use crate::room_state::member::MembersDelta;
594 use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
595 use crate::room_state::privacy::SealedBytes;
596
597 let rng = &mut rand::thread_rng();
598 let owner_sk = SigningKey::generate(rng);
599 let owner_vk = owner_sk.verifying_key();
600 let owner_id = MemberId::from(&owner_vk);
601 let params = ChatRoomParametersV1 { owner: owner_vk };
602
603 let config = Configuration {
605 owner_member_id: owner_id,
606 max_members: 10,
607 max_recent_messages: 100,
608 ..Default::default()
609 };
610 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
611 let mut state = ChatRoomStateV1 {
612 configuration: auth_config,
613 ..Default::default()
614 };
615
616 let joiner_sk = SigningKey::generate(rng);
618 let joiner_vk = joiner_sk.verifying_key();
619 let joiner_id = MemberId::from(&joiner_vk);
620
621 let authorized_member = AuthorizedMember::new(
622 Member {
623 owner_member_id: owner_id,
624 invited_by: owner_id,
625 member_vk: joiner_vk,
626 },
627 &owner_sk,
628 );
629
630 let member_info = MemberInfo {
631 member_id: joiner_id,
632 version: 0,
633 preferred_nickname: SealedBytes::public("NewUser".to_string().into_bytes()),
634 };
635 let authorized_info = AuthorizedMemberInfo::new_with_member_key(member_info, &joiner_sk);
636
637 let join_message = AuthorizedMessageV1::new(
638 MessageV1 {
639 room_owner: owner_id,
640 author: joiner_id,
641 content: RoomMessageBody::join_event(),
642 time: SystemTime::now(),
643 },
644 &joiner_sk,
645 );
646
647 let delta = ChatRoomStateV1Delta {
649 recent_messages: Some(vec![join_message]),
650 members: Some(MembersDelta::new(vec![authorized_member])),
651 member_info: Some(vec![authorized_info]),
652 ..Default::default()
653 };
654
655 let old_state = state.clone();
657 state
658 .apply_delta(&old_state, ¶ms, &Some(delta))
659 .expect("atomic join delta should apply cleanly");
660
661 state
663 .verify(&state, ¶ms)
664 .expect("state should verify after join delta");
665
666 assert!(
668 state
669 .members
670 .members
671 .iter()
672 .any(|m| m.member.id() == joiner_id),
673 "Joiner should be in members list"
674 );
675
676 assert!(
678 state
679 .member_info
680 .member_info
681 .iter()
682 .any(|i| i.member_info.member_id == joiner_id),
683 "Joiner should have member_info"
684 );
685
686 assert_eq!(state.recent_messages.messages.len(), 1);
688 assert!(state.recent_messages.messages[0].message.content.is_event());
689
690 state.post_apply_cleanup(¶ms).unwrap();
692 assert!(
693 state
694 .members
695 .members
696 .iter()
697 .any(|m| m.member.id() == joiner_id),
698 "Joiner should survive cleanup"
699 );
700 }
701
702 #[test]
703 fn test_invite_chain_preserved_for_active_member() {
704 let rng = &mut rand::thread_rng();
705 let owner_sk = SigningKey::generate(rng);
706 let owner_vk = owner_sk.verifying_key();
707 let owner_id = MemberId::from(&owner_vk);
708 let params = ChatRoomParametersV1 { owner: owner_vk };
709
710 let a_sk = SigningKey::generate(rng);
711 let a_vk = a_sk.verifying_key();
712 let a_id = MemberId::from(&a_vk);
713
714 let b_sk = SigningKey::generate(rng);
715 let b_vk = b_sk.verifying_key();
716 let b_id = MemberId::from(&b_vk);
717
718 let member_a = AuthorizedMember::new(
720 Member {
721 owner_member_id: owner_id,
722 invited_by: owner_id,
723 member_vk: a_vk,
724 },
725 &owner_sk,
726 );
727 let member_b = AuthorizedMember::new(
728 Member {
729 owner_member_id: owner_id,
730 invited_by: a_id,
731 member_vk: b_vk,
732 },
733 &a_sk,
734 );
735
736 let msg_b = AuthorizedMessageV1::new(
738 MessageV1 {
739 room_owner: owner_id,
740 author: b_id,
741 time: SystemTime::now(),
742 content: RoomMessageBody::public("Hello from B".to_string()),
743 },
744 &b_sk,
745 );
746
747 let config = Configuration {
748 max_members: 10,
749 max_recent_messages: 100,
750 ..Default::default()
751 };
752 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
753
754 let mut state = ChatRoomStateV1 {
755 configuration: auth_config,
756 members: MembersV1 {
757 members: vec![member_a, member_b],
758 },
759 recent_messages: MessagesV1 {
760 messages: vec![msg_b],
761 ..Default::default()
762 },
763 ..Default::default()
764 };
765
766 state.post_apply_cleanup(¶ms).unwrap();
767
768 assert_eq!(state.members.members.len(), 2);
770 let member_ids: HashSet<MemberId> = state
771 .members
772 .members
773 .iter()
774 .map(|m| m.member.id())
775 .collect();
776 assert!(
777 member_ids.contains(&a_id),
778 "A should be kept (in B's invite chain)"
779 );
780 assert!(
781 member_ids.contains(&b_id),
782 "B should be kept (has messages)"
783 );
784 }
785
786 #[test]
787 fn test_ban_persists_after_banner_pruned() {
788 let rng = &mut rand::thread_rng();
789 let owner_sk = SigningKey::generate(rng);
790 let owner_vk = owner_sk.verifying_key();
791 let owner_id = MemberId::from(&owner_vk);
792 let params = ChatRoomParametersV1 { owner: owner_vk };
793
794 let a_sk = SigningKey::generate(rng);
795 let a_vk = a_sk.verifying_key();
796 let a_id = MemberId::from(&a_vk);
797
798 let c_sk = SigningKey::generate(rng);
799 let c_vk = c_sk.verifying_key();
800 let c_id = MemberId::from(&c_vk);
801
802 let member_a = AuthorizedMember::new(
804 Member {
805 owner_member_id: owner_id,
806 invited_by: owner_id,
807 member_vk: a_vk,
808 },
809 &owner_sk,
810 );
811
812 let ban_c_by_a = AuthorizedUserBan::new(
814 UserBan {
815 owner_member_id: owner_id,
816 banned_at: SystemTime::now(),
817 banned_user: c_id,
818 },
819 a_id,
820 &a_sk,
821 );
822
823 let config = Configuration {
824 max_members: 10,
825 max_user_bans: 10,
826 ..Default::default()
827 };
828 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
829
830 let mut state = ChatRoomStateV1 {
832 configuration: auth_config,
833 members: MembersV1 {
834 members: vec![member_a],
835 },
836 bans: BansV1(vec![ban_c_by_a]),
837 ..Default::default()
838 };
839
840 state.post_apply_cleanup(¶ms).unwrap();
841
842 assert!(state.members.members.is_empty(), "A should be pruned");
844
845 assert_eq!(state.bans.0.len(), 1, "Ban should persist");
847 assert_eq!(state.bans.0[0].ban.banned_user, c_id);
848 assert_eq!(state.bans.0[0].banned_by, a_id);
849 }
850
851 #[test]
852 fn test_member_re_added_with_message() {
853 let rng = &mut rand::thread_rng();
854 let owner_sk = SigningKey::generate(rng);
855 let owner_vk = owner_sk.verifying_key();
856 let owner_id = MemberId::from(&owner_vk);
857 let params = ChatRoomParametersV1 { owner: owner_vk };
858
859 let a_sk = SigningKey::generate(rng);
860 let a_vk = a_sk.verifying_key();
861 let a_id = MemberId::from(&a_vk);
862
863 let member_a = AuthorizedMember::new(
864 Member {
865 owner_member_id: owner_id,
866 invited_by: owner_id,
867 member_vk: a_vk,
868 },
869 &owner_sk,
870 );
871
872 let config = Configuration {
873 max_members: 10,
874 max_recent_messages: 100,
875 ..Default::default()
876 };
877 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
878
879 let mut state = ChatRoomStateV1 {
881 configuration: auth_config,
882 members: MembersV1 {
883 members: vec![member_a.clone()],
884 },
885 ..Default::default()
886 };
887
888 state.post_apply_cleanup(¶ms).unwrap();
890 assert!(state.members.members.is_empty(), "A should be pruned");
891
892 state.members.members.push(member_a);
894 let msg = AuthorizedMessageV1::new(
895 MessageV1 {
896 room_owner: owner_id,
897 author: a_id,
898 time: SystemTime::now(),
899 content: RoomMessageBody::public("Hello again!".to_string()),
900 },
901 &a_sk,
902 );
903 state.recent_messages.messages.push(msg);
904
905 state.post_apply_cleanup(¶ms).unwrap();
907 assert_eq!(state.members.members.len(), 1, "A should be kept");
908 assert_eq!(state.members.members[0].member.id(), a_id);
909 }
910
911 #[test]
912 fn test_member_info_cleaned_after_pruning() {
913 let rng = &mut rand::thread_rng();
914 let owner_sk = SigningKey::generate(rng);
915 let owner_vk = owner_sk.verifying_key();
916 let owner_id = MemberId::from(&owner_vk);
917 let params = ChatRoomParametersV1 { owner: owner_vk };
918
919 let a_sk = SigningKey::generate(rng);
920 let a_vk = a_sk.verifying_key();
921 let a_id = MemberId::from(&a_vk);
922
923 let member_a = AuthorizedMember::new(
924 Member {
925 owner_member_id: owner_id,
926 invited_by: owner_id,
927 member_vk: a_vk,
928 },
929 &owner_sk,
930 );
931
932 let a_info = AuthorizedMemberInfo::new_with_member_key(
934 MemberInfo::new_public(a_id, 1, "Alice".to_string()),
935 &a_sk,
936 );
937 let owner_info = AuthorizedMemberInfo::new(
938 MemberInfo::new_public(owner_id, 1, "Owner".to_string()),
939 &owner_sk,
940 );
941
942 let config = Configuration {
943 max_members: 10,
944 ..Default::default()
945 };
946 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
947
948 let mut state = ChatRoomStateV1 {
949 configuration: auth_config,
950 members: MembersV1 {
951 members: vec![member_a],
952 },
953 member_info: MemberInfoV1 {
954 member_info: vec![owner_info, a_info],
955 },
956 ..Default::default()
957 };
958
959 state.post_apply_cleanup(¶ms).unwrap();
961
962 assert!(state.members.members.is_empty(), "A should be pruned");
963 assert_eq!(
964 state.member_info.member_info.len(),
965 1,
966 "Only owner's info should remain"
967 );
968 assert_eq!(
969 state.member_info.member_info[0].member_info.member_id, owner_id,
970 "Remaining info should be owner's"
971 );
972 }
973
974 #[test]
985 fn test_member_with_encrypted_secret_survives_cleanup() {
986 let rng = &mut rand::thread_rng();
987 let owner_sk = SigningKey::generate(rng);
988 let owner_vk = owner_sk.verifying_key();
989 let owner_id = MemberId::from(&owner_vk);
990 let params = ChatRoomParametersV1 { owner: owner_vk };
991
992 let a_sk = SigningKey::generate(rng);
993 let a_vk = a_sk.verifying_key();
994 let a_id = MemberId::from(&a_vk);
995
996 let member_a = AuthorizedMember::new(
997 Member {
998 owner_member_id: owner_id,
999 invited_by: owner_id,
1000 member_vk: a_vk,
1001 },
1002 &owner_sk,
1003 );
1004
1005 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1009 member_id: a_id,
1010 secret_version: 0,
1011 ciphertext: vec![0u8; 16], nonce: [0u8; 12],
1013 sender_ephemeral_public_key: [0u8; 32],
1014 provider: owner_id,
1015 };
1016 let authorized_secret = crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1017 secret_for_a,
1018 &owner_sk,
1019 );
1020
1021 let config = Configuration {
1022 max_members: 10,
1023 max_recent_messages: 100,
1024 ..Default::default()
1025 };
1026 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1027
1028 let mut state = ChatRoomStateV1 {
1029 configuration: auth_config,
1030 members: MembersV1 {
1031 members: vec![member_a],
1032 },
1033 secrets: crate::room_state::secret::RoomSecretsV1 {
1034 current_version: 0,
1035 versions: vec![],
1036 encrypted_secrets: vec![authorized_secret],
1037 },
1038 ..Default::default()
1039 };
1040
1041 state.post_apply_cleanup(¶ms).unwrap();
1042
1043 assert_eq!(
1044 state.members.members.len(),
1045 1,
1046 "A should survive cleanup because they have an encrypted_secrets entry"
1047 );
1048 assert_eq!(state.members.members[0].member.id(), a_id);
1049 }
1050
1051 #[test]
1067 fn test_banned_member_with_encrypted_secret_is_still_pruned() {
1068 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
1069 use std::time::SystemTime;
1070
1071 let rng = &mut rand::thread_rng();
1072 let owner_sk = SigningKey::generate(rng);
1073 let owner_vk = owner_sk.verifying_key();
1074 let owner_id = MemberId::from(&owner_vk);
1075 let params = ChatRoomParametersV1 { owner: owner_vk };
1076
1077 let a_sk = SigningKey::generate(rng);
1078 let a_vk = a_sk.verifying_key();
1079 let a_id = MemberId::from(&a_vk);
1080
1081 let member_a = AuthorizedMember::new(
1082 Member {
1083 owner_member_id: owner_id,
1084 invited_by: owner_id,
1085 member_vk: a_vk,
1086 },
1087 &owner_sk,
1088 );
1089
1090 let ban = AuthorizedUserBan::new(
1091 UserBan {
1092 owner_member_id: owner_id,
1093 banned_at: SystemTime::now(),
1094 banned_user: a_id,
1095 },
1096 owner_id,
1097 &owner_sk,
1098 );
1099
1100 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1105 member_id: a_id,
1106 secret_version: 0,
1107 ciphertext: vec![0u8; 16],
1108 nonce: [0u8; 12],
1109 sender_ephemeral_public_key: [0u8; 32],
1110 provider: owner_id,
1111 };
1112 let authorized_secret = crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1113 secret_for_a,
1114 &owner_sk,
1115 );
1116
1117 let config = Configuration {
1118 max_members: 10,
1119 max_recent_messages: 100,
1120 ..Default::default()
1121 };
1122 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1123
1124 let mut state = ChatRoomStateV1 {
1125 configuration: auth_config,
1126 members: MembersV1 {
1127 members: vec![member_a],
1128 },
1129 bans: crate::room_state::ban::BansV1(vec![ban]),
1130 secrets: crate::room_state::secret::RoomSecretsV1 {
1131 current_version: 0,
1132 versions: vec![],
1133 encrypted_secrets: vec![authorized_secret],
1134 },
1135 ..Default::default()
1136 };
1137
1138 state.members.members.retain(|m| m.member.id() != a_id);
1144
1145 state.post_apply_cleanup(¶ms).unwrap();
1146
1147 assert!(
1148 state.members.members.is_empty(),
1149 "banned member A must NOT be resurrected by post_apply_cleanup's \
1150 encrypted_secrets exemption — see IMPORTANT #4 on PR #272 review round 2"
1151 );
1152 assert_eq!(state.bans.0.len(), 1);
1154 assert_eq!(state.bans.0[0].ban.banned_user, a_id);
1155 }
1156
1157 #[test]
1168 fn test_stale_secret_recipient_is_pruned_after_rotation() {
1169 use crate::room_state::privacy::RoomCipherSpec;
1170 use crate::room_state::secret::{AuthorizedSecretVersionRecord, SecretVersionRecordV1};
1171 use std::time::SystemTime;
1172
1173 let rng = &mut rand::thread_rng();
1174 let owner_sk = SigningKey::generate(rng);
1175 let owner_vk = owner_sk.verifying_key();
1176 let owner_id = MemberId::from(&owner_vk);
1177 let params = ChatRoomParametersV1 { owner: owner_vk };
1178
1179 let a_sk = SigningKey::generate(rng);
1180 let a_vk = a_sk.verifying_key();
1181 let a_id = MemberId::from(&a_vk);
1182
1183 let member_a = AuthorizedMember::new(
1184 Member {
1185 owner_member_id: owner_id,
1186 invited_by: owner_id,
1187 member_vk: a_vk,
1188 },
1189 &owner_sk,
1190 );
1191
1192 let secret_for_a = crate::room_state::secret::EncryptedSecretForMemberV1 {
1196 member_id: a_id,
1197 secret_version: 0,
1198 ciphertext: vec![0u8; 16],
1199 nonce: [0u8; 12],
1200 sender_ephemeral_public_key: [0u8; 32],
1201 provider: owner_id,
1202 };
1203 let authorized_secret_v0 =
1204 crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1205 secret_for_a,
1206 &owner_sk,
1207 );
1208
1209 let v1_record = AuthorizedSecretVersionRecord::new(
1210 SecretVersionRecordV1 {
1211 version: 1,
1212 cipher_spec: RoomCipherSpec::Aes256Gcm,
1213 created_at: SystemTime::now(),
1214 },
1215 &owner_sk,
1216 );
1217
1218 let config = Configuration {
1219 max_members: 10,
1220 max_recent_messages: 100,
1221 ..Default::default()
1222 };
1223 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1224
1225 let mut state = ChatRoomStateV1 {
1226 configuration: auth_config,
1227 members: MembersV1 {
1228 members: vec![member_a],
1229 },
1230 secrets: crate::room_state::secret::RoomSecretsV1 {
1231 current_version: 1,
1232 versions: vec![v1_record],
1233 encrypted_secrets: vec![authorized_secret_v0],
1234 },
1235 ..Default::default()
1236 };
1237
1238 state.post_apply_cleanup(¶ms).unwrap();
1239
1240 assert!(
1241 state.members.members.is_empty(),
1242 "member A with ONLY a stale v0 blob (no v1 re-issue, no messages, no \
1243 DMs) must be pruned — see IMPORTANT #5 on PR #272 review round 2"
1244 );
1245 }
1246
1247 #[test]
1261 fn test_ban_race_with_encrypted_secret_converges_to_pruned() {
1262 use crate::room_state::ban::{AuthorizedUserBan, UserBan};
1263 use std::time::SystemTime;
1264
1265 let rng = &mut rand::thread_rng();
1266 let owner_sk = SigningKey::generate(rng);
1267 let owner_vk = owner_sk.verifying_key();
1268 let owner_id = MemberId::from(&owner_vk);
1269 let params = ChatRoomParametersV1 { owner: owner_vk };
1270
1271 let x_sk = SigningKey::generate(rng);
1272 let x_vk = x_sk.verifying_key();
1273 let x_id = MemberId::from(&x_vk);
1274
1275 let member_x = AuthorizedMember::new(
1276 Member {
1277 owner_member_id: owner_id,
1278 invited_by: owner_id,
1279 member_vk: x_vk,
1280 },
1281 &owner_sk,
1282 );
1283
1284 let ban_x = AuthorizedUserBan::new(
1285 UserBan {
1286 owner_member_id: owner_id,
1287 banned_at: SystemTime::now(),
1288 banned_user: x_id,
1289 },
1290 owner_id,
1291 &owner_sk,
1292 );
1293
1294 let secret_for_x = crate::room_state::secret::EncryptedSecretForMemberV1 {
1295 member_id: x_id,
1296 secret_version: 0,
1297 ciphertext: vec![0u8; 16],
1298 nonce: [0u8; 12],
1299 sender_ephemeral_public_key: [0u8; 32],
1300 provider: owner_id,
1301 };
1302 let authorized_secret_x =
1303 crate::room_state::secret::AuthorizedEncryptedSecretForMember::new(
1304 secret_for_x,
1305 &owner_sk,
1306 );
1307
1308 let config = Configuration {
1309 max_members: 10,
1310 max_recent_messages: 100,
1311 ..Default::default()
1312 };
1313 let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
1314
1315 let mut peer_a_state = ChatRoomStateV1 {
1328 configuration: auth_config.clone(),
1329 members: MembersV1 { members: vec![] },
1330 bans: crate::room_state::ban::BansV1(vec![ban_x.clone()]),
1331 secrets: crate::room_state::secret::RoomSecretsV1 {
1332 current_version: 0,
1333 versions: vec![],
1334 encrypted_secrets: vec![authorized_secret_x.clone()],
1335 },
1336 ..Default::default()
1337 };
1338 peer_a_state.post_apply_cleanup(¶ms).unwrap();
1339 assert!(
1340 peer_a_state.members.members.is_empty(),
1341 "peer A: X must remain pruned despite the encrypted_secret being present"
1342 );
1343
1344 let mut peer_b_state = ChatRoomStateV1 {
1350 configuration: auth_config,
1351 members: MembersV1 { members: vec![] },
1352 bans: crate::room_state::ban::BansV1(vec![ban_x]),
1353 secrets: crate::room_state::secret::RoomSecretsV1 {
1354 current_version: 0,
1355 versions: vec![],
1356 encrypted_secrets: vec![authorized_secret_x],
1357 },
1358 ..Default::default()
1359 };
1360 peer_b_state.post_apply_cleanup(¶ms).unwrap();
1361 assert!(
1362 peer_b_state.members.members.is_empty(),
1363 "peer B: X must remain pruned despite the encrypted_secret being present"
1364 );
1365
1366 assert_eq!(peer_a_state.members, peer_b_state.members);
1369 assert_eq!(peer_a_state.bans, peer_b_state.bans);
1370 assert_eq!(peer_a_state.secrets, peer_b_state.secrets);
1371
1372 let _ = member_x;
1377 }
1378
1379 #[test]
1380 fn test_state_with_none_deltas() {
1381 let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
1382
1383 let modified_state = state.clone();
1385
1386 let summary = state.summarize(&state, ¶meters);
1388 let delta = modified_state.delta(&state, ¶meters, &summary);
1389
1390 assert!(
1391 delta.is_none(),
1392 "Delta should be None when no changes are made"
1393 );
1394
1395 let mut partially_modified_state = state.clone();
1397 let new_config = Configuration {
1398 configuration_version: 2,
1399 ..partially_modified_state.configuration.configuration.clone()
1400 };
1401 partially_modified_state.configuration =
1402 AuthorizedConfigurationV1::new(new_config, &owner_signing_key);
1403
1404 let summary = state.summarize(&state, ¶meters);
1405 let delta = partially_modified_state
1406 .delta(&state, ¶meters, &summary)
1407 .unwrap();
1408
1409 assert!(
1411 delta.configuration.is_some(),
1412 "Configuration delta should be Some"
1413 );
1414 assert!(delta.bans.is_none(), "Bans delta should be None");
1415 assert!(delta.members.is_none(), "Members delta should be None");
1416 assert!(
1417 delta.member_info.is_none(),
1418 "Member info delta should be None"
1419 );
1420 assert!(
1421 delta.recent_messages.is_none(),
1422 "Recent messages delta should be None"
1423 );
1424 assert!(delta.upgrade.is_none(), "Upgrade delta should be None");
1425
1426 let mut new_state = state.clone();
1428 new_state
1429 .apply_delta(&state, ¶meters, &Some(delta))
1430 .unwrap();
1431
1432 assert_eq!(
1433 new_state, partially_modified_state,
1434 "State should be partially modified"
1435 );
1436 }
1437}