Skip to main content

river_core/room_state/
ban.rs

1use crate::room_state::member::{AuthorizedMember, MemberId};
2use crate::room_state::ChatRoomParametersV1;
3use crate::util::{sign_struct, verify_struct};
4use crate::ChatRoomStateV1;
5use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
6use freenet_scaffold::util::{fast_hash, FastHash};
7use freenet_scaffold::ComposableState;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11use std::hash::{Hash, Hasher};
12use std::time::SystemTime;
13
14/// Represents a collection of user bans in a chat room
15///
16/// This structure maintains a list of authorized bans and provides methods
17/// to verify, summarize, and apply changes to the ban list while ensuring
18/// all bans are valid according to room rules.
19#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
20pub struct BansV1(pub Vec<AuthorizedUserBan>);
21
22/// Represents different types of validation errors that can occur with bans
23#[derive(Debug, Clone, PartialEq)]
24pub enum BanValidationError {
25    /// The banned member was not found in the member list
26    MemberNotFound(MemberId),
27
28    /// The banning member was not found in the member list
29    BannerNotFound(MemberId),
30
31    /// The banning member is not in the invite chain of the banned member
32    NotInInviteChain(MemberId, MemberId),
33
34    /// A circular invite chain was detected
35    SelfInvitationDetected(MemberId),
36
37    /// The inviting member was not found for a member in the chain
38    InviterNotFound(MemberId),
39
40    /// The number of bans exceeds the maximum allowed
41    ExceededMaximumBans,
42}
43
44impl fmt::Display for BanValidationError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            BanValidationError::MemberNotFound(id) => {
48                write!(f, "Banned member not found in member list: {:?}", id)
49            }
50            BanValidationError::BannerNotFound(id) => {
51                write!(f, "Banning member not found in member list: {:?}", id)
52            }
53            BanValidationError::NotInInviteChain(banner_id, banned_id) => write!(
54                f,
55                "Banner {:?} is not in the invite chain of banned member {:?}",
56                banner_id, banned_id
57            ),
58            BanValidationError::SelfInvitationDetected(id) => {
59                write!(f, "Self-invitation detected for member {:?}", id)
60            }
61            BanValidationError::InviterNotFound(id) => {
62                write!(f, "Inviting member not found for {:?}", id)
63            }
64            BanValidationError::ExceededMaximumBans => {
65                write!(f, "Exceeded maximum number of user bans")
66            }
67        }
68    }
69}
70
71impl BansV1 {
72    /// Validates all bans in the collection and returns a map of invalid bans with errors
73    ///
74    /// This method checks:
75    /// - If the banned member still exists, verifies the banning member is in their invite chain
76    ///   (unless banner is owner). If the banned member was already removed, the ban is valid.
77    /// - If the banning member exists (for non-owner bans where banned member still exists)
78    /// - If the number of bans exceeds the maximum allowed
79    fn get_invalid_bans(
80        &self,
81        parent_state: &ChatRoomStateV1,
82        parameters: &ChatRoomParametersV1,
83    ) -> HashMap<BanId, BanValidationError> {
84        let member_map = parent_state.members.members_by_member_id();
85        let mut invalid_bans = HashMap::new();
86        let banned_user_ids: HashSet<MemberId> = self.0.iter().map(|b| b.ban.banned_user).collect();
87
88        // Validate each ban
89        for ban in &self.0 {
90            self.validate_single_ban(
91                ban,
92                &member_map,
93                parameters,
94                &mut invalid_bans,
95                &banned_user_ids,
96            );
97        }
98
99        // Check for excess bans
100        self.identify_excess_bans(parent_state, &mut invalid_bans);
101
102        invalid_bans
103    }
104
105    /// Validates a single ban and adds any validation errors to the invalid_bans map
106    fn validate_single_ban(
107        &self,
108        ban: &AuthorizedUserBan,
109        member_map: &HashMap<MemberId, &AuthorizedMember>,
110        parameters: &ChatRoomParametersV1,
111        invalid_bans: &mut HashMap<BanId, BanValidationError>,
112        banned_user_ids: &HashSet<MemberId>,
113    ) {
114        // Check if banned member exists - if not, that's OK, they've been removed due to the ban.
115        // We can skip the invite chain verification in that case since:
116        // 1. The ban signature verification (done separately) proves authenticity
117        // 2. The ban has already taken effect (member was removed)
118        // 3. The invite chain was valid when the ban was first created and applied
119        let banned_member = match member_map.get(&ban.ban.banned_user) {
120            Some(member) => member,
121            None => {
122                // Banned member already removed - ban is valid, skip further checks
123                return;
124            }
125        };
126
127        // Skip banning member verification if banner is room owner
128        if ban.banned_by != parameters.owner_id() {
129            // Check if banning member exists
130            let banning_member = match member_map.get(&ban.banned_by) {
131                Some(member) => member,
132                None => {
133                    // Banner not in members list. Check if they were banned
134                    // (orphaned ban) or just pruned for inactivity (valid ban).
135                    if banned_user_ids.contains(&ban.banned_by) {
136                        // Banner was banned — this ban is orphaned
137                        invalid_bans
138                            .insert(ban.id(), BanValidationError::BannerNotFound(ban.banned_by));
139                    }
140                    // Otherwise banner was pruned for inactivity — ban is still valid
141                    return;
142                }
143            };
144
145            // Verify banning member is in the invite chain of banned member
146            if let Err(error) = self.validate_invite_chain(
147                banned_member,
148                banning_member,
149                member_map,
150                parameters.owner_id(),
151                ban.id(),
152            ) {
153                invalid_bans.insert(ban.id(), error);
154            }
155        }
156    }
157
158    /// Validates that the banning member is in the invite chain of the banned member
159    fn validate_invite_chain(
160        &self,
161        banned_member: &AuthorizedMember,
162        banning_member: &AuthorizedMember,
163        member_map: &HashMap<MemberId, &AuthorizedMember>,
164        owner_id: MemberId,
165        _ban_id: BanId,
166    ) -> Result<(), BanValidationError> {
167        let mut current_member = banned_member;
168        let mut chain = Vec::new();
169
170        while current_member.member.id() != owner_id {
171            chain.push(current_member);
172
173            // If we found the banning member in the chain, the ban is valid
174            if current_member.member.id() == banning_member.member.id() {
175                return Ok(());
176            }
177
178            // Move up the invite chain
179            current_member = match member_map.get(&current_member.member.invited_by) {
180                Some(m) => m,
181                None => {
182                    return Err(BanValidationError::InviterNotFound(
183                        current_member.member.id(),
184                    ));
185                }
186            };
187
188            // Check for circular invite chains
189            if chain.contains(&current_member) {
190                return Err(BanValidationError::SelfInvitationDetected(
191                    current_member.member.id(),
192                ));
193            }
194        }
195
196        // If we reached the owner without finding the banning member, the ban is invalid
197        Err(BanValidationError::NotInInviteChain(
198            banning_member.member.id(),
199            banned_member.member.id(),
200        ))
201    }
202
203    /// Identifies bans that exceed the maximum allowed limit.
204    /// When timestamps are equal, uses BanId as a secondary sort key
205    /// for deterministic ordering (CRDT convergence requirement).
206    fn identify_excess_bans(
207        &self,
208        parent_state: &ChatRoomStateV1,
209        invalid_bans: &mut HashMap<BanId, BanValidationError>,
210    ) {
211        let max_bans = parent_state.configuration.configuration.max_user_bans;
212        let extra_bans = self.0.len() as isize - max_bans as isize;
213
214        if extra_bans > 0 {
215            // Add oldest extra bans to invalid bans
216            // Sort by timestamp (newest first), with BanId as tie-breaker
217            let mut extra_bans_vec = self.0.clone();
218            extra_bans_vec.sort_by(|a, b| {
219                // Primary: sort by timestamp (will be reversed, so older = later in list)
220                // Secondary: sort by BanId for deterministic tie-breaking
221                a.ban
222                    .banned_at
223                    .cmp(&b.ban.banned_at)
224                    .then_with(|| a.id().cmp(&b.id()))
225            });
226            extra_bans_vec.reverse();
227
228            for ban in extra_bans_vec.iter().take(extra_bans as usize) {
229                invalid_bans.insert(ban.id(), BanValidationError::ExceededMaximumBans);
230            }
231        }
232    }
233}
234
235impl ComposableState for BansV1 {
236    type ParentState = ChatRoomStateV1;
237    type Summary = HashSet<BanId>;
238    type Delta = Vec<AuthorizedUserBan>;
239    type Parameters = ChatRoomParametersV1;
240
241    /// Verifies that all bans in the collection are valid
242    ///
243    /// Checks that:
244    /// - All bans have valid signatures
245    /// - Banning members are authorized to ban (in invite chain)
246    /// - The number of bans doesn't exceed the maximum allowed
247    fn verify(
248        &self,
249        parent_state: &Self::ParentState,
250        parameters: &Self::Parameters,
251    ) -> Result<(), String> {
252        let invalid_bans = self.get_invalid_bans(parent_state, parameters);
253        if !invalid_bans.is_empty() {
254            let error_messages: Vec<String> = invalid_bans
255                .iter()
256                .map(|(id, error)| format!("{:?}: {}", id, error))
257                .collect();
258            return Err(format!("Invalid bans: {}", error_messages.join(", ")));
259        }
260
261        // Check if the number of bans exceeds the maximum allowed
262        if self.0.len() > parent_state.configuration.configuration.max_user_bans {
263            return Err(format!(
264                "Number of bans ({}) exceeds the maximum allowed ({})",
265                self.0.len(),
266                parent_state.configuration.configuration.max_user_bans
267            ));
268        }
269
270        let members_by_id = parent_state.members.members_by_member_id();
271
272        let owner_vk = parameters.owner;
273        let owner_id = parameters.owner_id();
274
275        // Verify signatures for all bans
276        for ban in &self.0 {
277            if ban.banned_by == owner_id {
278                ban.verify_signature(&owner_vk)
279                    .map_err(|e| format!("Invalid ban signature: {}", e))?;
280            } else if let Some(banning_member) = members_by_id.get(&ban.banned_by) {
281                ban.verify_signature(&banning_member.member.member_vk)
282                    .map_err(|e| format!("Invalid ban signature: {}", e))?;
283            } else {
284                // Banning member not in current members list. This can happen when:
285                // 1. During merge when bans are applied before the members delta
286                // 2. The banner was pruned for inactivity (no recent messages)
287                // In both cases, skip signature verification — we can't verify
288                // without the banner's key, and the signature was verified when
289                // the ban was first created. post_apply_cleanup will remove
290                // truly orphaned bans (where the banner was banned, not pruned).
291            }
292        }
293
294        Ok(())
295    }
296
297    /// Creates a summary of the current ban state
298    ///
299    /// Returns a set of all ban IDs currently in the collection
300    fn summarize(
301        &self,
302        _parent_state: &Self::ParentState,
303        _parameters: &Self::Parameters,
304    ) -> Self::Summary {
305        self.0.iter().map(|ban| ban.id()).collect()
306    }
307
308    /// Computes the difference between current ban state and old state
309    ///
310    /// Returns a vector of bans that exist in the current state but not in the old state,
311    /// or None if there are no differences
312    fn delta(
313        &self,
314        _parent_state: &Self::ParentState,
315        _parameters: &Self::Parameters,
316        old_state_summary: &Self::Summary,
317    ) -> Option<Self::Delta> {
318        // Identify bans in self.0 that are not in old_state_summary
319        let delta = self
320            .0
321            .iter()
322            .filter(|ban| !old_state_summary.contains(&ban.id()))
323            .cloned()
324            .collect::<Vec<_>>();
325        if delta.is_empty() {
326            None
327        } else {
328            Some(delta)
329        }
330    }
331
332    /// Applies changes from a delta to the current ban state
333    ///
334    /// This method:
335    /// - Checks for duplicate bans
336    /// - Verifies all new bans are valid
337    /// - Adds the new bans to the collection
338    /// - Removes oldest bans if the total exceeds max_user_bans
339    ///
340    /// Returns an error if any ban in the delta is invalid or already exists
341    fn apply_delta(
342        &mut self,
343        parent_state: &Self::ParentState,
344        parameters: &Self::Parameters,
345        delta: &Option<Self::Delta>,
346    ) -> Result<(), String> {
347        if let Some(delta) = delta {
348            // Check for duplicate bans
349            let existing_ban_ids: std::collections::HashSet<_> =
350                self.0.iter().map(|ban| ban.id()).collect();
351            for new_ban in delta {
352                if existing_ban_ids.contains(&new_ban.id()) {
353                    return Err(format!("Duplicate ban detected: {:?}", new_ban.id()));
354                }
355            }
356
357            // Create a temporary BansV1 with the new bans
358            let mut temp_bans = self.clone();
359            temp_bans.0.extend(delta.iter().cloned());
360
361            // Remove oldest bans if we exceed the limit
362            let max_bans = parent_state.configuration.configuration.max_user_bans;
363            if temp_bans.0.len() > max_bans {
364                // Sort by banned_at time (oldest first), with BanId tie-breaking
365                // for deterministic ordering (CRDT convergence requirement)
366                temp_bans.0.sort_by(|a, b| {
367                    a.ban
368                        .banned_at
369                        .cmp(&b.ban.banned_at)
370                        .then_with(|| a.id().cmp(&b.id()))
371                });
372                // Remove oldest bans to get back to the limit
373                let to_remove = temp_bans.0.len() - max_bans;
374                temp_bans.0.drain(0..to_remove);
375            }
376
377            // Verify the temporary room_state (excluding the max_bans check since we just enforced it)
378            if let Err(e) = temp_bans.verify(parent_state, parameters) {
379                return Err(format!("Invalid delta: {}", e));
380            }
381
382            // If verification passes, update the actual room_state
383            self.0 = temp_bans.0;
384        }
385
386        // Sort for deterministic ordering (CRDT convergence requirement)
387        self.0.sort_by(|a, b| {
388            a.ban
389                .banned_at
390                .cmp(&b.ban.banned_at)
391                .then_with(|| a.id().cmp(&b.id()))
392        });
393
394        Ok(())
395    }
396}
397
398/// A user ban with authorization proof
399///
400/// Contains the ban details, the ID of the member who created the ban,
401/// and a cryptographic signature proving the ban's authenticity
402#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
403pub struct AuthorizedUserBan {
404    pub ban: UserBan,
405    pub banned_by: MemberId,
406    pub signature: Signature,
407}
408
409impl Eq for AuthorizedUserBan {}
410
411impl Hash for AuthorizedUserBan {
412    fn hash<H: Hasher>(&self, state: &mut H) {
413        self.signature.to_bytes().hash(state);
414    }
415}
416
417impl AuthorizedUserBan {
418    /// Creates a new authorized ban
419    ///
420    /// Signs the ban with the provided signing key and verifies that the
421    /// banned_by ID matches the public key derived from the signing key
422    pub fn new(ban: UserBan, banned_by: MemberId, banner_signing_key: &SigningKey) -> Self {
423        assert_eq!(
424            MemberId::from(banner_signing_key.verifying_key()),
425            banned_by
426        );
427
428        let signature = sign_struct(&ban, banner_signing_key);
429
430        Self {
431            ban,
432            banned_by,
433            signature,
434        }
435    }
436
437    /// Create an AuthorizedUserBan with a pre-computed signature.
438    /// Use this when signing is done externally (e.g., via delegate).
439    pub fn with_signature(ban: UserBan, banned_by: MemberId, signature: Signature) -> Self {
440        Self {
441            ban,
442            banned_by,
443            signature,
444        }
445    }
446
447    /// Verifies that the ban's signature is valid
448    ///
449    /// Checks that the signature was created by the key corresponding to the provided verifying key
450    pub fn verify_signature(&self, banner_verifying_key: &VerifyingKey) -> Result<(), String> {
451        verify_struct(&self.ban, &self.signature, banner_verifying_key)
452            .map_err(|e| format!("Invalid ban signature: {}", e))
453    }
454
455    /// Generates a unique identifier for this ban based on its signature
456    pub fn id(&self) -> BanId {
457        BanId(fast_hash(&self.signature.to_bytes()))
458    }
459}
460
461/// Contains the core information about a user ban
462///
463/// Includes the room owner's ID, the time of the ban, and the ID of the banned user
464#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
465pub struct UserBan {
466    pub owner_member_id: MemberId,
467    pub banned_at: SystemTime,
468    pub banned_user: MemberId,
469}
470
471/// A unique identifier for a ban
472///
473/// Created from a hash of the ban's signature to ensure uniqueness
474#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Hash, Debug, Ord, PartialOrd)]
475pub struct BanId(pub FastHash);
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480    use crate::room_state::configuration::AuthorizedConfigurationV1;
481    use crate::room_state::member::{AuthorizedMember, Member, MembersV1};
482    use ed25519_dalek::SigningKey;
483    use std::time::Duration;
484
485    fn create_test_chat_room_state() -> ChatRoomStateV1 {
486        // Create a minimal ChatRoomStateV1 for testing
487        ChatRoomStateV1 {
488            configuration: AuthorizedConfigurationV1::default(),
489            bans: Default::default(),
490            members: MembersV1::default(),
491            member_info: Default::default(),
492            secrets: Default::default(),
493            recent_messages: Default::default(),
494            upgrade: Default::default(),
495            ..Default::default()
496        }
497    }
498
499    fn create_test_parameters() -> ChatRoomParametersV1 {
500        // Create minimal ChatRoomParametersV1 for testing
501        let owner_key = SigningKey::generate(&mut rand::thread_rng());
502        ChatRoomParametersV1 {
503            owner: owner_key.verifying_key(),
504        }
505    }
506
507    #[test]
508    fn test_bans_verify() {
509        let mut state = create_test_chat_room_state();
510        let params = create_test_parameters();
511
512        // Create some test members
513        let owner_key = SigningKey::generate(&mut rand::thread_rng());
514        let owner_id: MemberId = owner_key.verifying_key().into();
515        let member1_key = SigningKey::generate(&mut rand::thread_rng());
516        let member1_id: MemberId = member1_key.verifying_key().into();
517        let member2_key = SigningKey::generate(&mut rand::thread_rng());
518        let member2_id: MemberId = member2_key.verifying_key().into();
519
520        // Add members to the room_state
521        state.members.members.push(AuthorizedMember::new(
522            Member {
523                owner_member_id: owner_id,
524                invited_by: owner_id,
525                member_vk: owner_key.verifying_key(),
526            },
527            &owner_key,
528        ));
529        state.members.members.push(AuthorizedMember::new(
530            Member {
531                owner_member_id: owner_id,
532                invited_by: owner_id,
533                member_vk: member1_key.verifying_key(),
534            },
535            &owner_key,
536        ));
537        state.members.members.push(AuthorizedMember::new(
538            Member {
539                owner_member_id: owner_id,
540                invited_by: member1_id,
541                member_vk: member2_key.verifying_key(),
542            },
543            &member1_key,
544        ));
545
546        // Update the configuration to allow bans
547        state.configuration.configuration.max_user_bans = 5;
548
549        // Test 1: Valid ban by owner
550        let ban1 = AuthorizedUserBan::new(
551            UserBan {
552                owner_member_id: owner_id,
553                banned_at: SystemTime::now(),
554                banned_user: member1_id,
555            },
556            owner_id,
557            &owner_key,
558        );
559
560        let bans = BansV1(vec![ban1]);
561        assert!(
562            bans.verify(&state, &params).is_ok(),
563            "Valid ban should be verified successfully: {:?}",
564            bans.verify(&state, &params).err()
565        );
566
567        // Test 2: Exceeding max_user_bans
568        let mut many_bans = Vec::new();
569        for _ in 0..6 {
570            many_bans.push(AuthorizedUserBan::new(
571                UserBan {
572                    owner_member_id: owner_id,
573                    banned_at: SystemTime::now(),
574                    banned_user: member1_id,
575                },
576                owner_id,
577                &owner_key,
578            ));
579        }
580        let too_many_bans = BansV1(many_bans);
581        assert!(
582            too_many_bans.verify(&state, &params).is_err(),
583            "Exceeding max_user_bans should fail verification"
584        );
585
586        // Test 3: Ban by pruned member (not in member list, not banned) — valid
587        // With message-based lifecycle, banners not in the members list who
588        // are not themselves banned are considered pruned for inactivity.
589        let pruned_key = SigningKey::generate(&mut rand::thread_rng());
590        let pruned_id: MemberId = pruned_key.verifying_key().into();
591        let pruned_ban = AuthorizedUserBan::new(
592            UserBan {
593                owner_member_id: owner_id,
594                banned_at: SystemTime::now(),
595                banned_user: member2_id,
596            },
597            pruned_id,
598            &pruned_key,
599        );
600
601        let pruned_bans = BansV1(vec![pruned_ban]);
602        assert!(
603            pruned_bans.verify(&state, &params).is_ok(),
604            "Ban by pruned (non-banned) member should pass verification: {:?}",
605            pruned_bans.verify(&state, &params).err()
606        );
607
608        // Test 3b: Orphaned ban (banner was banned) — invalid
609        let orphaned_key = SigningKey::generate(&mut rand::thread_rng());
610        let orphaned_id: MemberId = orphaned_key.verifying_key().into();
611        let orphaned_ban = AuthorizedUserBan::new(
612            UserBan {
613                owner_member_id: owner_id,
614                banned_at: SystemTime::now(),
615                banned_user: member2_id,
616            },
617            orphaned_id,
618            &orphaned_key,
619        );
620        // A ban targeting the orphaned member makes them "banned" (not just pruned)
621        let ban_of_orphaned = AuthorizedUserBan::new(
622            UserBan {
623                owner_member_id: owner_id,
624                banned_at: SystemTime::now(),
625                banned_user: orphaned_id,
626            },
627            owner_id,
628            &owner_key,
629        );
630        let orphaned_bans = BansV1(vec![orphaned_ban, ban_of_orphaned]);
631        assert!(
632            orphaned_bans.verify(&state, &params).is_err(),
633            "Orphaned ban (banner was banned) should fail verification"
634        );
635
636        // Test 4: Valid ban by non-owner member
637        let ban_by_member = AuthorizedUserBan::new(
638            UserBan {
639                owner_member_id: owner_id,
640                banned_at: SystemTime::now(),
641                banned_user: member2_id,
642            },
643            member1_id,
644            &member1_key,
645        );
646
647        let member_bans = BansV1(vec![ban_by_member]);
648        assert!(
649            member_bans.verify(&state, &params).is_ok(),
650            "Valid ban by non-owner member should pass verification"
651        );
652    }
653
654    #[test]
655    fn test_bans_summarize() {
656        let state = create_test_chat_room_state();
657        let params = create_test_parameters();
658
659        let key = SigningKey::generate(&mut rand::thread_rng());
660        let id: MemberId = key.verifying_key().into();
661
662        let ban1 = AuthorizedUserBan::new(
663            UserBan {
664                owner_member_id: id,
665                banned_at: SystemTime::now(),
666                banned_user: id,
667            },
668            id,
669            &key,
670        );
671
672        let ban2 = AuthorizedUserBan::new(
673            UserBan {
674                owner_member_id: id,
675                banned_at: SystemTime::now() + Duration::from_secs(1),
676                banned_user: id,
677            },
678            id,
679            &key,
680        );
681
682        let bans = BansV1(vec![ban1.clone(), ban2.clone()]);
683        let summary = bans.summarize(&state, &params);
684
685        assert_eq!(summary.len(), 2);
686        assert!(summary.contains(&ban1.id()));
687        assert!(summary.contains(&ban2.id()));
688    }
689
690    #[test]
691    fn test_bans_delta() {
692        let state = create_test_chat_room_state();
693        let params = create_test_parameters();
694
695        let key = SigningKey::generate(&mut rand::thread_rng());
696        let id: MemberId = key.verifying_key().into();
697
698        let ban1 = AuthorizedUserBan::new(
699            UserBan {
700                owner_member_id: id,
701                banned_at: SystemTime::now(),
702                banned_user: id,
703            },
704            id,
705            &key,
706        );
707
708        let ban2 = AuthorizedUserBan::new(
709            UserBan {
710                owner_member_id: id,
711                banned_at: SystemTime::now() + Duration::from_secs(1),
712                banned_user: id,
713            },
714            id,
715            &key,
716        );
717
718        let bans = BansV1(vec![ban1.clone(), ban2.clone()]);
719
720        // Test 1: Empty old summary
721        let empty_summary = HashSet::new();
722        let delta = bans.delta(&state, &params, &empty_summary);
723        assert_eq!(delta, Some(vec![ban1.clone(), ban2.clone()]));
724
725        // Test 2: Partial old summary
726        let partial_summary: HashSet<BanId> = vec![ban1.id()].into_iter().collect();
727        let delta = bans.delta(&state, &params, &partial_summary);
728        assert_eq!(delta, Some(vec![ban2.clone()]));
729
730        // Test 3: Full old summary
731        let full_summary: HashSet<BanId> = vec![ban1.id(), ban2.id()].into_iter().collect();
732        let delta = bans.delta(&state, &params, &full_summary);
733        assert_eq!(delta, None);
734    }
735
736    #[test]
737    fn test_bans_apply_delta() {
738        let mut state = create_test_chat_room_state();
739        let params = create_test_parameters();
740
741        let owner_key = SigningKey::generate(&mut rand::thread_rng());
742        let owner_id: MemberId = owner_key.verifying_key().into();
743        let member_key = SigningKey::generate(&mut rand::thread_rng());
744        let member_id: MemberId = member_key.verifying_key().into();
745
746        // Add members to the room_state
747        state.members.members.push(AuthorizedMember::new(
748            Member {
749                owner_member_id: owner_id,
750                invited_by: owner_id,
751                member_vk: owner_key.verifying_key(),
752            },
753            &owner_key,
754        ));
755        state.members.members.push(AuthorizedMember::new(
756            Member {
757                owner_member_id: owner_id,
758                invited_by: owner_id,
759                member_vk: member_key.verifying_key(),
760            },
761            &owner_key,
762        ));
763
764        // Update the configuration to allow bans
765        state.configuration.configuration.max_user_bans = 5;
766
767        let mut bans = BansV1::default();
768
769        let new_ban = AuthorizedUserBan::new(
770            UserBan {
771                owner_member_id: owner_id,
772                banned_at: SystemTime::now(),
773                banned_user: member_id,
774            },
775            owner_id,
776            &owner_key,
777        );
778
779        // Test 1: Apply valid delta
780        let delta = vec![new_ban.clone()];
781        assert!(
782            bans.apply_delta(&state, &params, &Some(delta.clone()))
783                .is_ok(),
784            "Valid delta should be applied successfully: {:?}",
785            bans.apply_delta(&state, &params, &Some(delta)).err()
786        );
787        assert_eq!(
788            bans.0.len(),
789            1,
790            "Bans should contain one ban after applying delta"
791        );
792        assert_eq!(bans.0[0], new_ban, "Applied ban should match the new ban");
793
794        // Test 2: Apply delta exceeding max_user_bans - should succeed by removing oldest bans
795        let mut many_bans = Vec::new();
796        for i in 0..5 {
797            many_bans.push(AuthorizedUserBan::new(
798                UserBan {
799                    owner_member_id: owner_id,
800                    // Give each ban a different timestamp so we can verify oldest are removed
801                    banned_at: SystemTime::now() + Duration::from_secs(i as u64 + 10),
802                    banned_user: member_id,
803                },
804                owner_id,
805                &owner_key,
806            ));
807        }
808        let delta_exceeding_max = Some(many_bans.clone());
809        assert!(
810            bans.apply_delta(&state, &params, &delta_exceeding_max)
811                .is_ok(),
812            "Delta exceeding max_user_bans should succeed by removing oldest: {:?}",
813            bans.apply_delta(&state, &params, &delta_exceeding_max)
814                .err()
815        );
816        assert_eq!(
817            bans.0.len(),
818            5,
819            "Bans should be at max_user_bans limit after removing oldest"
820        );
821        // The original new_ban (the oldest) should have been removed
822        assert!(
823            !bans.0.contains(&new_ban),
824            "Oldest ban should have been removed"
825        );
826
827        // Test 3: Apply invalid delta (duplicate ban) - use one of the bans still in the list
828        let existing_ban = many_bans.last().unwrap().clone();
829        let invalid_delta = Some(vec![existing_ban]);
830        assert!(
831            bans.apply_delta(&state, &params, &invalid_delta).is_err(),
832            "Applying duplicate ban should fail: {:?}",
833            bans.apply_delta(&state, &params, &invalid_delta).ok()
834        );
835        assert_eq!(
836            bans.0.len(),
837            5,
838            "State should not change after applying duplicate ban"
839        );
840
841        // Test 4: Adding more bans should evict oldest ones but keep max_user_bans
842        let mut additional_bans = Vec::new();
843        for i in 0..2 {
844            additional_bans.push(AuthorizedUserBan::new(
845                UserBan {
846                    owner_member_id: owner_id,
847                    banned_at: SystemTime::now() + Duration::from_secs(i as u64 + 100),
848                    banned_user: member_id,
849                },
850                owner_id,
851                &owner_key,
852            ));
853        }
854        assert!(
855            bans.apply_delta(&state, &params, &Some(additional_bans))
856                .is_ok(),
857            "Applying more bans should succeed by evicting oldest: {:?}",
858            bans.apply_delta(&state, &params, &Some(Vec::new())).err()
859        );
860        assert_eq!(
861            bans.0.len(),
862            5,
863            "State should still have max number of bans after evicting oldest"
864        );
865    }
866
867    #[test]
868    fn test_authorized_user_ban() {
869        let owner_key = SigningKey::generate(&mut rand::thread_rng());
870        let owner_id: MemberId = owner_key.verifying_key().into();
871        let member_key = SigningKey::generate(&mut rand::thread_rng());
872        let member_id: MemberId = member_key.verifying_key().into();
873
874        let ban = UserBan {
875            owner_member_id: owner_id,
876            banned_at: SystemTime::now(),
877            banned_user: member_id,
878        };
879
880        let authorized_ban = AuthorizedUserBan::new(ban.clone(), owner_id, &owner_key);
881
882        // Test 1: Verify signature
883        assert!(authorized_ban
884            .verify_signature(&owner_key.verifying_key())
885            .is_ok());
886
887        // Test 2: Verify signature with wrong key
888        let wrong_key = SigningKey::generate(&mut rand::thread_rng());
889        assert!(authorized_ban
890            .verify_signature(&wrong_key.verifying_key())
891            .is_err());
892
893        // Test 3: Check ban ID
894        let id1 = authorized_ban.id();
895        let id2 = authorized_ban.id();
896        assert_eq!(id1, id2);
897
898        // Test 4: Different bans should have different IDs
899        let another_ban = AuthorizedUserBan::new(
900            UserBan {
901                owner_member_id: owner_id,
902                banned_at: SystemTime::now() + Duration::from_secs(1),
903                banned_user: member_id,
904            },
905            owner_id,
906            &owner_key,
907        );
908        assert_ne!(authorized_ban.id(), another_ban.id());
909    }
910}