Skip to main content

river_core/
room_state.rs

1pub mod ban;
2pub mod configuration;
3pub mod content;
4pub mod direct_messages;
5pub mod identity;
6pub mod member;
7pub mod member_info;
8pub mod message;
9pub mod privacy;
10pub mod secret;
11pub mod upgrade;
12pub mod version;
13
14use crate::room_state::ban::BansV1;
15use crate::room_state::configuration::AuthorizedConfigurationV1;
16use crate::room_state::direct_messages::DirectMessagesV1;
17use crate::room_state::member::{MemberId, MembersV1};
18use crate::room_state::member_info::MemberInfoV1;
19use crate::room_state::message::MessagesV1;
20use crate::room_state::secret::RoomSecretsV1;
21use crate::room_state::upgrade::OptionalUpgradeV1;
22use crate::room_state::version::StateVersion;
23use ed25519_dalek::VerifyingKey;
24use freenet_scaffold_macro::composable;
25use serde::{Deserialize, Serialize};
26use std::collections::HashSet;
27
28#[composable(post_apply_delta = "post_apply_cleanup")]
29#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
30pub struct ChatRoomStateV1 {
31    // WARNING: The order of these fields is important for the purposes of the #[composable] macro.
32    // `configuration` must be first, followed by `bans`, `members`, `member_info`, `secrets`,
33    // and then `recent_messages`.
34    // This is due to interdependencies between the fields and the order in which they must be applied in
35    // the `apply_delta` function. DO NOT reorder fields without fully understanding the implications.
36    /// Configures things like maximum message length, can be updated by the owner.
37    pub configuration: AuthorizedConfigurationV1,
38
39    /// A list of recently banned members, a banned member can't be present in the
40    /// members list and will be removed from it ifc necessary.
41    pub bans: BansV1,
42
43    /// The members in the chat room along with who invited them
44    pub members: MembersV1,
45
46    /// Metadata about members like their nickname, can be updated by members themselves.
47    pub member_info: MemberInfoV1,
48
49    /// Secret distribution for private rooms. Must come before recent_messages so message
50    /// validation can check secret version consistency.
51    pub secrets: RoomSecretsV1,
52
53    /// The most recent messages in the chat room, the number is limited by the room configuration.
54    pub recent_messages: MessagesV1,
55
56    /// In-room encrypted direct messages between members (#230 Phase 1).
57    /// `#[serde(default)]` keeps states written before this field was added
58    /// backwards-compatible.
59    #[serde(default)]
60    pub direct_messages: DirectMessagesV1,
61
62    /// If this contract has been replaced by a new contract this will contain the new contract address.
63    /// This can only be set by the owner.
64    pub upgrade: OptionalUpgradeV1,
65
66    /// State format version for migration compatibility.
67    /// Defaults to 0 for backward compatibility with states created before versioning.
68    #[serde(default)]
69    pub version: StateVersion,
70}
71
72impl ChatRoomStateV1 {
73    /// Post-apply cleanup: prune members who have no recent messages, clean up
74    /// member_info for pruned members, remove orphaned bans, and sweep
75    /// direct messages whose participants are no longer in the room.
76    ///
77    /// Members are kept if they have at least one message in recent_messages,
78    /// are a sender/recipient of a currently-held direct message (see
79    /// [`crate::room_state::direct_messages::DirectMessagesV1::active_participants`]),
80    /// or are in the invite chain of someone who qualifies. The owner is
81    /// never in the members list (they're implicit via parameters).
82    ///
83    /// Bans are only removed if the banner was themselves BANNED (orphaned ban).
84    /// If the banner was merely pruned for inactivity, their bans persist.
85    ///
86    /// Direct-message sweep: after pruning, any DM whose sender or
87    /// recipient is now non-member or banned is dropped. Without this,
88    /// adding a ban for a DM participant would silently make every
89    /// peer's verify fail, and members referenced only by a DM would be
90    /// pruned (orphaning their DMs). See
91    /// `direct_messages.rs` module docs, "Interaction with bans".
92    pub fn post_apply_cleanup(&mut self, parameters: &ChatRoomParametersV1) -> Result<(), String> {
93        let owner_id = MemberId::from(&parameters.owner);
94
95        // 1. Collect message author IDs + DM participants
96        let message_authors: HashSet<MemberId> = self
97            .recent_messages
98            .messages
99            .iter()
100            .map(|m| m.message.author)
101            .collect();
102        let dm_participants: HashSet<MemberId> = self.direct_messages.active_participants();
103
104        // 2. Compute required members: authors + DM participants + their invite chains
105        let required_ids = {
106            let members_by_id = self.members.members_by_member_id();
107            let mut required_ids: HashSet<MemberId> = HashSet::new();
108
109            for author_id in &message_authors {
110                if *author_id != owner_id && members_by_id.contains_key(author_id) {
111                    required_ids.insert(*author_id);
112                }
113            }
114
115            for participant_id in &dm_participants {
116                if *participant_id != owner_id && members_by_id.contains_key(participant_id) {
117                    required_ids.insert(*participant_id);
118                }
119            }
120
121            // Walk invite chains upward, adding all ancestors (stop at owner)
122            let mut to_process: Vec<MemberId> = required_ids.iter().cloned().collect();
123            while let Some(member_id) = to_process.pop() {
124                if let Some(member) = members_by_id.get(&member_id) {
125                    let inviter_id = member.member.invited_by;
126                    if inviter_id != owner_id && !required_ids.contains(&inviter_id) {
127                        required_ids.insert(inviter_id);
128                        to_process.push(inviter_id);
129                    }
130                }
131            }
132
133            required_ids
134        };
135
136        // 3. Prune members not in required set
137        self.members
138            .members
139            .retain(|m| required_ids.contains(&m.member.id()));
140
141        // 4. Clean member_info for pruned members
142        self.member_info.member_info.retain(|info| {
143            info.member_info.member_id == owner_id
144                || required_ids.contains(&info.member_info.member_id)
145        });
146
147        // 5. Clean orphaned bans: only remove if banner was BANNED (not just pruned)
148        // A ban is orphaned when:
149        // - The banner is not the owner AND
150        // - The banner is not in the current members list AND
151        // - The banner IS in the banned users set (i.e., they were banned, not pruned)
152        let banned_user_ids: HashSet<MemberId> =
153            self.bans.0.iter().map(|b| b.ban.banned_user).collect();
154        let current_member_ids: HashSet<MemberId> =
155            self.members.members.iter().map(|m| m.member.id()).collect();
156
157        self.bans.0.retain(|ban| {
158            // Keep if: banner is owner, OR banner is still a member, OR banner is NOT banned
159            // (not banned = pruned for inactivity, their bans should persist)
160            ban.banned_by == owner_id
161                || current_member_ids.contains(&ban.banned_by)
162                || !banned_user_ids.contains(&ban.banned_by)
163        });
164
165        // 6. Sweep DMs whose participants are no longer current members
166        //    or are banned. Without this, a fresh ban (or member-prune)
167        //    would leave the DMs in state but break `verify` because the
168        //    sender/recipient can no longer be resolved.
169        let banned_user_ids_for_sweep: HashSet<MemberId> =
170            self.bans.0.iter().map(|b| b.ban.banned_user).collect();
171        let active_member_ids_for_sweep: HashSet<MemberId> =
172            self.members.members.iter().map(|m| m.member.id()).collect();
173        self.direct_messages.sweep_after_membership_change(
174            owner_id,
175            &active_member_ids_for_sweep,
176            &banned_user_ids_for_sweep,
177        );
178
179        // 7. Re-sort for deterministic ordering
180        self.members.members.sort_by_key(|m| m.member.id());
181        self.member_info
182            .member_info
183            .sort_by_key(|info| info.member_info.member_id);
184
185        Ok(())
186    }
187}
188
189#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
190pub struct ChatRoomParametersV1 {
191    pub owner: VerifyingKey,
192}
193
194impl ChatRoomParametersV1 {
195    pub fn owner_id(&self) -> MemberId {
196        self.owner.into()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::room_state::ban::{AuthorizedUserBan, UserBan};
204    use crate::room_state::configuration::Configuration;
205    use crate::room_state::member::{AuthorizedMember, Member};
206    use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
207    use crate::room_state::message::{AuthorizedMessageV1, MessageV1, RoomMessageBody};
208    use ed25519_dalek::SigningKey;
209    use std::fmt::Debug;
210    use std::time::SystemTime;
211
212    #[test]
213    fn test_state() {
214        let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
215
216        assert!(
217            state.verify(&state, &parameters).is_ok(),
218            "Empty state should verify"
219        );
220
221        // Test that the configuration can be updated
222        let mut new_cfg = state.configuration.configuration.clone();
223        new_cfg.configuration_version += 1;
224        new_cfg.max_recent_messages = 10; // Change from default of 100 to 10
225        let new_cfg = AuthorizedConfigurationV1::new(new_cfg, &owner_signing_key);
226
227        let mut cfg_modified_state = state.clone();
228        cfg_modified_state.configuration = new_cfg;
229        test_apply_delta(state.clone(), cfg_modified_state, &parameters);
230    }
231
232    fn test_apply_delta<CS>(orig_state: CS, modified_state: CS, parameters: &CS::Parameters)
233    where
234        CS: ComposableState<ParentState = CS> + Clone + PartialEq + Debug,
235    {
236        let orig_verify_result = orig_state.verify(&orig_state, parameters);
237        assert!(
238            orig_verify_result.is_ok(),
239            "Original state verification failed: {:?}",
240            orig_verify_result.err()
241        );
242
243        let modified_verify_result = modified_state.verify(&modified_state, parameters);
244        assert!(
245            modified_verify_result.is_ok(),
246            "Modified state verification failed: {:?}",
247            modified_verify_result.err()
248        );
249
250        let delta = modified_state.delta(
251            &orig_state,
252            parameters,
253            &orig_state.summarize(&orig_state, parameters),
254        );
255
256        println!("Delta: {:?}", delta);
257
258        let mut new_state = orig_state.clone();
259        let apply_delta_result = new_state.apply_delta(&orig_state, parameters, &delta);
260        assert!(
261            apply_delta_result.is_ok(),
262            "Applying delta failed: {:?}",
263            apply_delta_result.err()
264        );
265
266        assert_eq!(new_state, modified_state);
267    }
268    fn create_empty_chat_room_state() -> (ChatRoomStateV1, ChatRoomParametersV1, SigningKey) {
269        // Create a test room_state with a single member and two messages, one written by
270        // the owner and one by the member - the member must be invited by the owner
271        let rng = &mut rand::thread_rng();
272        let owner_signing_key = SigningKey::generate(rng);
273        let owner_verifying_key = owner_signing_key.verifying_key();
274
275        let config = AuthorizedConfigurationV1::new(Configuration::default(), &owner_signing_key);
276
277        (
278            ChatRoomStateV1 {
279                configuration: config,
280                bans: BansV1::default(),
281                members: MembersV1::default(),
282                member_info: MemberInfoV1::default(),
283                secrets: RoomSecretsV1::default(),
284                recent_messages: MessagesV1::default(),
285                upgrade: OptionalUpgradeV1(None),
286                ..Default::default()
287            },
288            ChatRoomParametersV1 {
289                owner: owner_verifying_key,
290            },
291            owner_signing_key,
292        )
293    }
294
295    /// Regression test: when a member who issued bans is subsequently banned themselves,
296    /// their bans become orphaned (banning member no longer in members list and not owner).
297    /// The post_apply_delta hook post_apply_cleanup must remove these to prevent verify() failure.
298    /// See: technic corrupted state incident (Feb 2026)
299    #[test]
300    fn test_orphaned_ban_cleanup_after_cascade_removal() {
301        let rng = &mut rand::thread_rng();
302
303        // Create owner
304        let owner_sk = SigningKey::generate(rng);
305        let owner_vk = owner_sk.verifying_key();
306        let owner_id = MemberId::from(&owner_vk);
307        let params = ChatRoomParametersV1 { owner: owner_vk };
308
309        // Configuration allowing bans and members
310        let config = Configuration {
311            max_user_bans: 10,
312            max_members: 10,
313            ..Default::default()
314        };
315        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
316
317        // Create member A (invited by owner) and member B (invited by A)
318        let a_sk = SigningKey::generate(rng);
319        let a_vk = a_sk.verifying_key();
320        let a_id = MemberId::from(&a_vk);
321
322        let b_sk = SigningKey::generate(rng);
323        let b_vk = b_sk.verifying_key();
324        let b_id = MemberId::from(&b_vk);
325
326        let member_a = AuthorizedMember::new(
327            Member {
328                owner_member_id: owner_id,
329                invited_by: owner_id,
330                member_vk: a_vk,
331            },
332            &owner_sk,
333        );
334
335        // A bans B (authorized because A is in B's invite chain)
336        let ban_b_by_a = AuthorizedUserBan::new(
337            UserBan {
338                owner_member_id: owner_id,
339                banned_at: std::time::SystemTime::now(),
340                banned_user: b_id,
341            },
342            a_id,
343            &a_sk,
344        );
345
346        // Initial state: A is a member, B already removed (ban took effect)
347        let initial_state = ChatRoomStateV1 {
348            configuration: auth_config.clone(),
349            bans: BansV1(vec![ban_b_by_a.clone()]),
350            members: MembersV1 {
351                members: vec![member_a.clone()],
352            },
353            ..Default::default()
354        };
355
356        assert!(
357            initial_state.verify(&initial_state, &params).is_ok(),
358            "Initial state should verify: {:?}",
359            initial_state.verify(&initial_state, &params)
360        );
361
362        // Now owner bans A — this will cascade-remove A from members,
363        // making A's ban of B orphaned (A is no longer in members and not owner)
364        let ban_a_by_owner = AuthorizedUserBan::new(
365            UserBan {
366                owner_member_id: owner_id,
367                banned_at: std::time::SystemTime::now() + std::time::Duration::from_secs(1),
368                banned_user: a_id,
369            },
370            owner_id,
371            &owner_sk,
372        );
373
374        // Modified state for delta computation: add owner's ban of A
375        let modified_for_delta = ChatRoomStateV1 {
376            configuration: auth_config,
377            bans: BansV1(vec![ban_b_by_a.clone(), ban_a_by_owner.clone()]),
378            members: MembersV1 {
379                members: vec![member_a.clone()],
380            },
381            ..Default::default()
382        };
383
384        // Compute and apply delta
385        let summary = initial_state.summarize(&initial_state, &params);
386        let delta = modified_for_delta.delta(&initial_state, &params, &summary);
387
388        let mut result_state = initial_state.clone();
389        let apply_result = result_state.apply_delta(&initial_state, &params, &delta);
390        assert!(
391            apply_result.is_ok(),
392            "apply_delta should succeed: {:?}",
393            apply_result
394        );
395
396        // A should be removed (banned by owner)
397        assert!(
398            result_state.members.members.is_empty(),
399            "A should be removed from members: {:?}",
400            result_state.members.members
401        );
402
403        // Only owner's ban should remain — A's ban of B is orphaned and cleaned
404        assert_eq!(
405            result_state.bans.0.len(),
406            1,
407            "Only owner's ban should remain, orphaned ban cleaned: {:?}",
408            result_state.bans.0
409        );
410        assert_eq!(
411            result_state.bans.0[0].banned_by, owner_id,
412            "Remaining ban should be by owner"
413        );
414
415        // Result state should pass verification
416        assert!(
417            result_state.verify(&result_state, &params).is_ok(),
418            "Result state should verify after orphaned ban cleanup: {:?}",
419            result_state.verify(&result_state, &params)
420        );
421    }
422
423    #[test]
424    fn test_member_pruned_when_no_messages() {
425        let rng = &mut rand::thread_rng();
426        let owner_sk = SigningKey::generate(rng);
427        let owner_vk = owner_sk.verifying_key();
428        let owner_id = MemberId::from(&owner_vk);
429        let params = ChatRoomParametersV1 { owner: owner_vk };
430
431        let a_sk = SigningKey::generate(rng);
432        let a_vk = a_sk.verifying_key();
433        let a_id = MemberId::from(&a_vk);
434
435        let b_sk = SigningKey::generate(rng);
436        let b_vk = b_sk.verifying_key();
437
438        let member_a = AuthorizedMember::new(
439            Member {
440                owner_member_id: owner_id,
441                invited_by: owner_id,
442                member_vk: a_vk,
443            },
444            &owner_sk,
445        );
446        let member_b = AuthorizedMember::new(
447            Member {
448                owner_member_id: owner_id,
449                invited_by: owner_id,
450                member_vk: b_vk,
451            },
452            &owner_sk,
453        );
454
455        // Only A has a message
456        let msg_a = AuthorizedMessageV1::new(
457            MessageV1 {
458                room_owner: owner_id,
459                author: a_id,
460                time: SystemTime::now(),
461                content: RoomMessageBody::public("Hello from A".to_string()),
462            },
463            &a_sk,
464        );
465
466        let config = Configuration {
467            max_members: 10,
468            max_recent_messages: 100,
469            ..Default::default()
470        };
471        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
472
473        let mut state = ChatRoomStateV1 {
474            configuration: auth_config,
475            members: MembersV1 {
476                members: vec![member_a, member_b],
477            },
478            recent_messages: MessagesV1 {
479                messages: vec![msg_a],
480                ..Default::default()
481            },
482            ..Default::default()
483        };
484
485        state.post_apply_cleanup(&params).unwrap();
486
487        assert_eq!(state.members.members.len(), 1, "Only A should remain");
488        assert_eq!(state.members.members[0].member.id(), a_id);
489    }
490
491    #[test]
492    fn test_member_with_join_event_not_pruned() {
493        let rng = &mut rand::thread_rng();
494        let owner_sk = SigningKey::generate(rng);
495        let owner_vk = owner_sk.verifying_key();
496        let owner_id = MemberId::from(&owner_vk);
497        let params = ChatRoomParametersV1 { owner: owner_vk };
498
499        let a_sk = SigningKey::generate(rng);
500        let a_vk = a_sk.verifying_key();
501        let a_id = MemberId::from(&a_vk);
502
503        let member_a = AuthorizedMember::new(
504            Member {
505                owner_member_id: owner_id,
506                invited_by: owner_id,
507                member_vk: a_vk,
508            },
509            &owner_sk,
510        );
511
512        // A has only a join event (no regular messages)
513        let join_msg = AuthorizedMessageV1::new(
514            MessageV1 {
515                room_owner: owner_id,
516                author: a_id,
517                time: SystemTime::now(),
518                content: RoomMessageBody::join_event(),
519            },
520            &a_sk,
521        );
522
523        let config = Configuration {
524            max_members: 10,
525            max_recent_messages: 100,
526            ..Default::default()
527        };
528        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
529
530        let mut state = ChatRoomStateV1 {
531            configuration: auth_config,
532            members: MembersV1 {
533                members: vec![member_a],
534            },
535            recent_messages: MessagesV1 {
536                messages: vec![join_msg],
537                ..Default::default()
538            },
539            ..Default::default()
540        };
541
542        state.post_apply_cleanup(&params).unwrap();
543
544        assert_eq!(
545            state.members.members.len(),
546            1,
547            "Member with join event should not be pruned"
548        );
549        assert_eq!(state.members.members[0].member.id(), a_id);
550    }
551
552    /// Test that the atomic join delta (members + member_info + join event)
553    /// as produced by accept_invitation applies correctly and passes verify().
554    #[test]
555    fn test_atomic_join_delta_applies_and_verifies() {
556        use crate::room_state::member::MembersDelta;
557        use crate::room_state::member_info::{AuthorizedMemberInfo, MemberInfo};
558        use crate::room_state::privacy::SealedBytes;
559
560        let rng = &mut rand::thread_rng();
561        let owner_sk = SigningKey::generate(rng);
562        let owner_vk = owner_sk.verifying_key();
563        let owner_id = MemberId::from(&owner_vk);
564        let params = ChatRoomParametersV1 { owner: owner_vk };
565
566        // Create a room with owner config
567        let config = Configuration {
568            owner_member_id: owner_id,
569            max_members: 10,
570            max_recent_messages: 100,
571            ..Default::default()
572        };
573        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
574        let mut state = ChatRoomStateV1 {
575            configuration: auth_config,
576            ..Default::default()
577        };
578
579        // New member accepts an invitation
580        let joiner_sk = SigningKey::generate(rng);
581        let joiner_vk = joiner_sk.verifying_key();
582        let joiner_id = MemberId::from(&joiner_vk);
583
584        let authorized_member = AuthorizedMember::new(
585            Member {
586                owner_member_id: owner_id,
587                invited_by: owner_id,
588                member_vk: joiner_vk,
589            },
590            &owner_sk,
591        );
592
593        let member_info = MemberInfo {
594            member_id: joiner_id,
595            version: 0,
596            preferred_nickname: SealedBytes::public("NewUser".to_string().into_bytes()),
597        };
598        let authorized_info = AuthorizedMemberInfo::new_with_member_key(member_info, &joiner_sk);
599
600        let join_message = AuthorizedMessageV1::new(
601            MessageV1 {
602                room_owner: owner_id,
603                author: joiner_id,
604                content: RoomMessageBody::join_event(),
605                time: SystemTime::now(),
606            },
607            &joiner_sk,
608        );
609
610        // Build the atomic delta (same as accept_invitation produces)
611        let delta = ChatRoomStateV1Delta {
612            recent_messages: Some(vec![join_message]),
613            members: Some(MembersDelta::new(vec![authorized_member])),
614            member_info: Some(vec![authorized_info]),
615            ..Default::default()
616        };
617
618        // Apply delta
619        let old_state = state.clone();
620        state
621            .apply_delta(&old_state, &params, &Some(delta))
622            .expect("atomic join delta should apply cleanly");
623
624        // Verify state is valid
625        state
626            .verify(&state, &params)
627            .expect("state should verify after join delta");
628
629        // Member should be present
630        assert!(
631            state
632                .members
633                .members
634                .iter()
635                .any(|m| m.member.id() == joiner_id),
636            "Joiner should be in members list"
637        );
638
639        // Member info should be present
640        assert!(
641            state
642                .member_info
643                .member_info
644                .iter()
645                .any(|i| i.member_info.member_id == joiner_id),
646            "Joiner should have member_info"
647        );
648
649        // Join event message should be present
650        assert_eq!(state.recent_messages.messages.len(), 1);
651        assert!(state.recent_messages.messages[0].message.content.is_event());
652
653        // Should survive post_apply_cleanup
654        state.post_apply_cleanup(&params).unwrap();
655        assert!(
656            state
657                .members
658                .members
659                .iter()
660                .any(|m| m.member.id() == joiner_id),
661            "Joiner should survive cleanup"
662        );
663    }
664
665    #[test]
666    fn test_invite_chain_preserved_for_active_member() {
667        let rng = &mut rand::thread_rng();
668        let owner_sk = SigningKey::generate(rng);
669        let owner_vk = owner_sk.verifying_key();
670        let owner_id = MemberId::from(&owner_vk);
671        let params = ChatRoomParametersV1 { owner: owner_vk };
672
673        let a_sk = SigningKey::generate(rng);
674        let a_vk = a_sk.verifying_key();
675        let a_id = MemberId::from(&a_vk);
676
677        let b_sk = SigningKey::generate(rng);
678        let b_vk = b_sk.verifying_key();
679        let b_id = MemberId::from(&b_vk);
680
681        // Owner → A → B
682        let member_a = AuthorizedMember::new(
683            Member {
684                owner_member_id: owner_id,
685                invited_by: owner_id,
686                member_vk: a_vk,
687            },
688            &owner_sk,
689        );
690        let member_b = AuthorizedMember::new(
691            Member {
692                owner_member_id: owner_id,
693                invited_by: a_id,
694                member_vk: b_vk,
695            },
696            &a_sk,
697        );
698
699        // Only B has a message
700        let msg_b = AuthorizedMessageV1::new(
701            MessageV1 {
702                room_owner: owner_id,
703                author: b_id,
704                time: SystemTime::now(),
705                content: RoomMessageBody::public("Hello from B".to_string()),
706            },
707            &b_sk,
708        );
709
710        let config = Configuration {
711            max_members: 10,
712            max_recent_messages: 100,
713            ..Default::default()
714        };
715        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
716
717        let mut state = ChatRoomStateV1 {
718            configuration: auth_config,
719            members: MembersV1 {
720                members: vec![member_a, member_b],
721            },
722            recent_messages: MessagesV1 {
723                messages: vec![msg_b],
724                ..Default::default()
725            },
726            ..Default::default()
727        };
728
729        state.post_apply_cleanup(&params).unwrap();
730
731        // Both A and B should remain (A is in B's invite chain)
732        assert_eq!(state.members.members.len(), 2);
733        let member_ids: HashSet<MemberId> = state
734            .members
735            .members
736            .iter()
737            .map(|m| m.member.id())
738            .collect();
739        assert!(
740            member_ids.contains(&a_id),
741            "A should be kept (in B's invite chain)"
742        );
743        assert!(
744            member_ids.contains(&b_id),
745            "B should be kept (has messages)"
746        );
747    }
748
749    #[test]
750    fn test_ban_persists_after_banner_pruned() {
751        let rng = &mut rand::thread_rng();
752        let owner_sk = SigningKey::generate(rng);
753        let owner_vk = owner_sk.verifying_key();
754        let owner_id = MemberId::from(&owner_vk);
755        let params = ChatRoomParametersV1 { owner: owner_vk };
756
757        let a_sk = SigningKey::generate(rng);
758        let a_vk = a_sk.verifying_key();
759        let a_id = MemberId::from(&a_vk);
760
761        let c_sk = SigningKey::generate(rng);
762        let c_vk = c_sk.verifying_key();
763        let c_id = MemberId::from(&c_vk);
764
765        // A is a member (invited by owner)
766        let member_a = AuthorizedMember::new(
767            Member {
768                owner_member_id: owner_id,
769                invited_by: owner_id,
770                member_vk: a_vk,
771            },
772            &owner_sk,
773        );
774
775        // A bans C
776        let ban_c_by_a = AuthorizedUserBan::new(
777            UserBan {
778                owner_member_id: owner_id,
779                banned_at: SystemTime::now(),
780                banned_user: c_id,
781            },
782            a_id,
783            &a_sk,
784        );
785
786        let config = Configuration {
787            max_members: 10,
788            max_user_bans: 10,
789            ..Default::default()
790        };
791        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
792
793        // A has no messages → will be pruned
794        let mut state = ChatRoomStateV1 {
795            configuration: auth_config,
796            members: MembersV1 {
797                members: vec![member_a],
798            },
799            bans: BansV1(vec![ban_c_by_a]),
800            ..Default::default()
801        };
802
803        state.post_apply_cleanup(&params).unwrap();
804
805        // A should be pruned (no messages)
806        assert!(state.members.members.is_empty(), "A should be pruned");
807
808        // A's ban of C should persist (A was pruned, not banned)
809        assert_eq!(state.bans.0.len(), 1, "Ban should persist");
810        assert_eq!(state.bans.0[0].ban.banned_user, c_id);
811        assert_eq!(state.bans.0[0].banned_by, a_id);
812    }
813
814    #[test]
815    fn test_member_re_added_with_message() {
816        let rng = &mut rand::thread_rng();
817        let owner_sk = SigningKey::generate(rng);
818        let owner_vk = owner_sk.verifying_key();
819        let owner_id = MemberId::from(&owner_vk);
820        let params = ChatRoomParametersV1 { owner: owner_vk };
821
822        let a_sk = SigningKey::generate(rng);
823        let a_vk = a_sk.verifying_key();
824        let a_id = MemberId::from(&a_vk);
825
826        let member_a = AuthorizedMember::new(
827            Member {
828                owner_member_id: owner_id,
829                invited_by: owner_id,
830                member_vk: a_vk,
831            },
832            &owner_sk,
833        );
834
835        let config = Configuration {
836            max_members: 10,
837            max_recent_messages: 100,
838            ..Default::default()
839        };
840        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
841
842        // State with A but no messages
843        let mut state = ChatRoomStateV1 {
844            configuration: auth_config,
845            members: MembersV1 {
846                members: vec![member_a.clone()],
847            },
848            ..Default::default()
849        };
850
851        // Cleanup prunes A
852        state.post_apply_cleanup(&params).unwrap();
853        assert!(state.members.members.is_empty(), "A should be pruned");
854
855        // Re-add A with a message
856        state.members.members.push(member_a);
857        let msg = AuthorizedMessageV1::new(
858            MessageV1 {
859                room_owner: owner_id,
860                author: a_id,
861                time: SystemTime::now(),
862                content: RoomMessageBody::public("Hello again!".to_string()),
863            },
864            &a_sk,
865        );
866        state.recent_messages.messages.push(msg);
867
868        // Cleanup should keep A now
869        state.post_apply_cleanup(&params).unwrap();
870        assert_eq!(state.members.members.len(), 1, "A should be kept");
871        assert_eq!(state.members.members[0].member.id(), a_id);
872    }
873
874    #[test]
875    fn test_member_info_cleaned_after_pruning() {
876        let rng = &mut rand::thread_rng();
877        let owner_sk = SigningKey::generate(rng);
878        let owner_vk = owner_sk.verifying_key();
879        let owner_id = MemberId::from(&owner_vk);
880        let params = ChatRoomParametersV1 { owner: owner_vk };
881
882        let a_sk = SigningKey::generate(rng);
883        let a_vk = a_sk.verifying_key();
884        let a_id = MemberId::from(&a_vk);
885
886        let member_a = AuthorizedMember::new(
887            Member {
888                owner_member_id: owner_id,
889                invited_by: owner_id,
890                member_vk: a_vk,
891            },
892            &owner_sk,
893        );
894
895        // Create member_info for A and owner
896        let a_info = AuthorizedMemberInfo::new_with_member_key(
897            MemberInfo::new_public(a_id, 1, "Alice".to_string()),
898            &a_sk,
899        );
900        let owner_info = AuthorizedMemberInfo::new(
901            MemberInfo::new_public(owner_id, 1, "Owner".to_string()),
902            &owner_sk,
903        );
904
905        let config = Configuration {
906            max_members: 10,
907            ..Default::default()
908        };
909        let auth_config = AuthorizedConfigurationV1::new(config, &owner_sk);
910
911        let mut state = ChatRoomStateV1 {
912            configuration: auth_config,
913            members: MembersV1 {
914                members: vec![member_a],
915            },
916            member_info: MemberInfoV1 {
917                member_info: vec![owner_info, a_info],
918            },
919            ..Default::default()
920        };
921
922        // A has no messages → gets pruned along with their member_info
923        state.post_apply_cleanup(&params).unwrap();
924
925        assert!(state.members.members.is_empty(), "A should be pruned");
926        assert_eq!(
927            state.member_info.member_info.len(),
928            1,
929            "Only owner's info should remain"
930        );
931        assert_eq!(
932            state.member_info.member_info[0].member_info.member_id, owner_id,
933            "Remaining info should be owner's"
934        );
935    }
936
937    #[test]
938    fn test_state_with_none_deltas() {
939        let (state, parameters, owner_signing_key) = create_empty_chat_room_state();
940
941        // Create a modified room_state with no changes (all deltas should be None)
942        let modified_state = state.clone();
943
944        // Apply the delta
945        let summary = state.summarize(&state, &parameters);
946        let delta = modified_state.delta(&state, &parameters, &summary);
947
948        assert!(
949            delta.is_none(),
950            "Delta should be None when no changes are made"
951        );
952
953        // Now, let's modify only one field and check if other deltas are None
954        let mut partially_modified_state = state.clone();
955        let new_config = Configuration {
956            configuration_version: 2,
957            ..partially_modified_state.configuration.configuration.clone()
958        };
959        partially_modified_state.configuration =
960            AuthorizedConfigurationV1::new(new_config, &owner_signing_key);
961
962        let summary = state.summarize(&state, &parameters);
963        let delta = partially_modified_state
964            .delta(&state, &parameters, &summary)
965            .unwrap();
966
967        // Check that only the configuration delta is Some, and others are None
968        assert!(
969            delta.configuration.is_some(),
970            "Configuration delta should be Some"
971        );
972        assert!(delta.bans.is_none(), "Bans delta should be None");
973        assert!(delta.members.is_none(), "Members delta should be None");
974        assert!(
975            delta.member_info.is_none(),
976            "Member info delta should be None"
977        );
978        assert!(
979            delta.recent_messages.is_none(),
980            "Recent messages delta should be None"
981        );
982        assert!(delta.upgrade.is_none(), "Upgrade delta should be None");
983
984        // Apply the partial delta
985        let mut new_state = state.clone();
986        new_state
987            .apply_delta(&state, &parameters, &Some(delta))
988            .unwrap();
989
990        assert_eq!(
991            new_state, partially_modified_state,
992            "State should be partially modified"
993        );
994    }
995}