p2panda_auth/group/crdt/
state.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Core group membership state represented as a Causal Length CRDT (CL-CRDT).
4//!
5//! The approach used here was first described by Weihai Yu and Sigbjørn Rostad and in their paper
6//! titled 'A low-cost set CRDT based on causal lengths'.
7//!
8//! Yu, W. and Rostad, S. A Low-Cost Set CRDT Based on Causal Lengths. In Proceedings of the 7th
9//! Workshop on Principles and Practice of Consistency for Distributed Data (2020), Article no. 5,
10//! pp. 1-6.
11
12use std::collections::{HashMap, HashSet};
13use std::fmt::Debug;
14use std::hash::Hash;
15
16#[cfg(any(test, feature = "serde"))]
17use serde::{Deserialize, Serialize};
18use thiserror::Error;
19
20use crate::Access;
21use crate::traits::Conditions;
22
23/// Invalid group state modification attempts due to group membership state and member access
24/// levels.
25#[derive(Debug, Error, PartialEq)]
26pub enum GroupMembershipError<ID> {
27    #[error("attempted to add a member who is already active in the group: {0}")]
28    AlreadyAdded(ID),
29
30    #[error("attempted to remove a member who is already inactive in the group: {0}")]
31    AlreadyRemoved(ID),
32
33    #[error("actor lacks sufficient access to update the group: {0}")]
34    InsufficientAccess(ID),
35
36    #[error("actor is not an active member of the group: {0}")]
37    InactiveActor(ID),
38
39    #[error("member is not an active member of the group: {0}")]
40    InactiveMember(ID),
41
42    #[error("actor is not known to the group: {0}")]
43    UnrecognisedActor(ID),
44
45    #[error("member is not known to the group: {0}")]
46    UnrecognisedMember(ID),
47}
48
49/// The access state of an individual group member.
50///
51/// Counters are used to allow conflict-free merging of states.
52#[derive(Clone, Debug)]
53#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
54pub struct MemberState<C> {
55    pub(crate) member_counter: usize,
56    pub(crate) access: Access<C>,
57    pub(crate) access_counter: usize,
58}
59
60impl<C> MemberState<C>
61where
62    C: Clone + Debug + PartialEq,
63{
64    /// Return the access level of the member.
65    pub fn access(&self) -> Access<C> {
66        self.access.clone()
67    }
68
69    /// Return `true` if the member is an active member of the group.
70    pub fn is_member(&self) -> bool {
71        !self.member_counter.is_multiple_of(2)
72    }
73
74    /// Return `true` if the member has `Pull` access.
75    pub fn is_puller(&self) -> bool {
76        self.access.is_pull()
77    }
78
79    /// Return `true` if the member has `Read` access.
80    pub fn is_reader(&self) -> bool {
81        self.access.is_read()
82    }
83
84    /// Return `true` if the member has `Write` access.
85    pub fn is_writer(&self) -> bool {
86        self.access.is_write()
87    }
88
89    /// Return `true` if the member has `Manage` access.
90    pub fn is_manager(&self) -> bool {
91        self.access.is_manage()
92    }
93}
94
95/// The membership state of all known groups.
96#[derive(Clone, Debug)]
97#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
98pub struct GroupMembersState<ID, C>
99where
100    ID: Hash + Eq,
101{
102    pub(crate) members: HashMap<ID, MemberState<C>>,
103}
104
105impl<ID, C> Default for GroupMembersState<ID, C>
106where
107    ID: Hash + Eq,
108{
109    fn default() -> Self {
110        Self {
111            members: Default::default(),
112        }
113    }
114}
115
116impl<ID, C> GroupMembersState<ID, C>
117where
118    ID: Clone + Hash + Eq,
119    C: Conditions,
120{
121    /// Return all active group members.
122    pub fn members(&self) -> HashSet<ID> {
123        self.members
124            .iter()
125            .filter_map(|(id, state)| {
126                if state.is_member() {
127                    Some(id.to_owned())
128                } else {
129                    None
130                }
131            })
132            .collect::<HashSet<ID>>()
133    }
134
135    /// Return all active group members and their access levels.
136    pub fn access_levels(&self) -> Vec<(ID, Access<C>)> {
137        self.members
138            .iter()
139            .filter_map(|(id, state)| {
140                if state.is_member() {
141                    Some((id.to_owned(), state.access()))
142                } else {
143                    None
144                }
145            })
146            .collect::<Vec<(ID, Access<C>)>>()
147    }
148
149    /// Return all active group members with `Manage` access.
150    pub fn managers(&self) -> HashSet<ID> {
151        self.members
152            .iter()
153            .filter_map(|(id, state)| {
154                if state.is_member() && state.is_manager() {
155                    Some(id.to_owned())
156                } else {
157                    None
158                }
159            })
160            .collect::<HashSet<_>>()
161    }
162}
163
164/// Create a new group and add the given set of initial members.
165pub fn create<ID: Clone + Eq + Hash, C: Conditions>(
166    initial_members: &[(ID, Access<C>)],
167) -> GroupMembersState<ID, C> {
168    let mut members = HashMap::new();
169    for (id, access) in initial_members {
170        let member = MemberState {
171            member_counter: 1,
172            access: access.clone(),
173            access_counter: 0,
174        };
175        members.insert(id.clone(), member);
176    }
177
178    GroupMembersState { members }
179}
180
181/// Add a member to the group with the given access level.
182///
183/// The `adder` must be an active member of the group with `Manage` access and the `added` identity
184/// must not be a current member of the group; failure to meet these conditions will result in an
185/// error.
186///
187/// Re-adding a previously removed member is supported.
188pub fn add<ID: Clone + Eq + Hash, C: Conditions>(
189    state: GroupMembersState<ID, C>,
190    adder: ID,
191    added: ID,
192    access: Access<C>,
193) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
194    // Ensure that "adder" is known to the group.
195    let Some(adder_state) = state.members.get(&adder) else {
196        return Err(GroupMembershipError::UnrecognisedActor(adder));
197    };
198
199    // Ensure that "adder" is a member of the group with manage access level.
200    if !adder_state.is_member() {
201        return Err(GroupMembershipError::InactiveActor(adder));
202    } else if !adder_state.is_manager() {
203        return Err(GroupMembershipError::InsufficientAccess(adder));
204    }
205
206    // Ensure that "added" is not already an active member of the group.
207    if let Some(added_state) = state.members.get(&added)
208        && added_state.is_member()
209    {
210        return Err(GroupMembershipError::AlreadyAdded(added));
211    }
212
213    // Add "added" to the group or increment their counters if they are already known but were
214    // previously removed.
215    let mut state = state;
216    state
217        .members
218        .entry(added.clone())
219        .and_modify(|added| {
220            if !added.is_member() {
221                added.member_counter += 1;
222                added.access = access.clone();
223                added.access_counter = 0;
224            }
225        })
226        .or_insert(MemberState {
227            member_counter: 1,
228            access,
229            access_counter: 0,
230        });
231
232    Ok(state)
233}
234
235/// Remove a member from the group.
236///
237/// The `remover` must be an active member of the group with `Manage` access and the `removed`
238/// identity must also be an active member of the group; failure to meet these conditions will
239/// result in an error.
240pub fn remove<ID: Eq + Hash, C: Conditions>(
241    state: GroupMembersState<ID, C>,
242    remover: ID,
243    removed: ID,
244) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
245    // Ensure that "remover" is known to the group.
246    let Some(remover_state) = state.members.get(&remover) else {
247        return Err(GroupMembershipError::UnrecognisedActor(remover));
248    };
249
250    // Ensure that "remover" is a member of the group with manage access level.
251    if !remover_state.is_member() {
252        return Err(GroupMembershipError::InactiveActor(remover));
253    } else if !remover_state.is_manager() {
254        return Err(GroupMembershipError::InsufficientAccess(remover));
255    }
256
257    // Ensure that "removed" is known to the group.
258    if !state.members.contains_key(&removed) {
259        return Err(GroupMembershipError::UnrecognisedMember(removed));
260    };
261
262    // Ensure that "removed" is not already an inactive member of the group.
263    if let Some(removed_state) = state.members.get(&removed)
264        && !removed_state.is_member()
265    {
266        return Err(GroupMembershipError::AlreadyRemoved(removed));
267    }
268
269    // Increment "removed" counters unless they are already removed.
270    let mut state = state;
271    state.members.entry(removed).and_modify(|removed| {
272        if removed.is_member() {
273            removed.member_counter += 1;
274            removed.access_counter = 0;
275        }
276    });
277
278    Ok(state)
279}
280
281/// Modify the access level of a group member.
282///
283/// Both the `modifier` and `modified` identity must be active group members; failure to meet these
284/// conditions will result in an error.
285///
286/// This is a helper method to reduce code duplication in `promote()` and `demote()`.
287fn modify<ID: Eq + Hash, C: Conditions>(
288    state: GroupMembersState<ID, C>,
289    modifier: ID,
290    modified: ID,
291    access: Access<C>,
292) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
293    // Ensure that "modifier" is known to the group.
294    let Some(modifier_state) = state.members.get(&modifier) else {
295        return Err(GroupMembershipError::UnrecognisedActor(modifier));
296    };
297
298    // Ensure that "modifier" is a member of the group with manage access level.
299    if !modifier_state.is_member() {
300        return Err(GroupMembershipError::InactiveActor(modifier));
301    } else if !modifier_state.is_manager() {
302        return Err(GroupMembershipError::InsufficientAccess(modifier));
303    }
304
305    // Ensure that "modified" is an active member of the group.
306    if let Some(modified_state) = state.members.get(&modified) {
307        if !modified_state.is_member() {
308            return Err(GroupMembershipError::InactiveMember(modified));
309        }
310    } else {
311        return Err(GroupMembershipError::UnrecognisedMember(modified));
312    }
313
314    // Update access level.
315    let mut state = state;
316    state.members.entry(modified).and_modify(|modified| {
317        // Only perform the modification if the access levels differ.
318        if modified.access != access {
319            modified.access = access;
320            modified.access_counter += 1;
321        }
322    });
323
324    Ok(state)
325}
326
327/// Promote a group member to the given access level.
328///
329/// No modification will occur if the promoted member already has `Manage` access. In that case, the
330/// given state is returned unchanged.
331///
332/// The `promoter` must be an active member of the group with `Manage` access and the `promoted`
333/// identity must also be an active member of the group; failure to meet these conditions will
334/// result in an error.
335pub fn promote<ID: Eq + Hash, C: Conditions>(
336    state: GroupMembersState<ID, C>,
337    promoter: ID,
338    promoted: ID,
339    access: Access<C>,
340) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
341    if let Some(member) = state.members.get(&promoted) {
342        // No action is required if the member is already set to the highest access level.
343        let new_state = if member.is_manager() {
344            state
345        } else {
346            modify(state, promoter, promoted, access)?
347        };
348
349        Ok(new_state)
350    } else {
351        Err(GroupMembershipError::UnrecognisedMember(promoted))
352    }
353}
354
355/// Demote a group member to the given access level.
356///
357/// No modification will occur if the demoted member already has `Pull` access. In that case, the
358/// given state is returned unchanged.
359///
360/// The `demoter` must be an active member of the group with `Manage` access and the `demoted`
361/// identity must also be an active member of the group; failure to meet these conditions will
362/// result in an error.
363pub fn demote<ID: Eq + Hash, C: Conditions>(
364    state: GroupMembersState<ID, C>,
365    demoter: ID,
366    demoted: ID,
367    access: Access<C>,
368) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
369    if let Some(member) = state.members.get(&demoted) {
370        // No action is required if the member is already set to the lowest access level.
371        let new_state = if member.is_puller() {
372            state
373        } else {
374            modify(state, demoter, demoted, access)?
375        };
376
377        Ok(new_state)
378    } else {
379        Err(GroupMembershipError::UnrecognisedMember(demoted))
380    }
381}
382
383/// Merge two group states into one using a deterministic, conflict-free approach.
384///
385/// Grow-only counters are used internally to track state changes; one counter for add / remove
386/// actions and one for access modification actions. These values are used to determine which
387/// membership and access states should be included in the merged group state. A state with a higher
388/// counter indicates that it has undergone more actions; this state will be included in the merge.
389///
390/// If a member exists with different access levels in each state but the same number of access
391/// modifications, the lower of the two access levels will be chosen.
392pub fn merge<ID: Clone + Eq + Hash, C: Conditions>(
393    state_1: GroupMembersState<ID, C>,
394    state_2: GroupMembersState<ID, C>,
395) -> GroupMembersState<ID, C> {
396    // Start from state_2 state.
397    let mut next_state = state_2.clone();
398
399    // Iterate over entries in state_1.
400    for (id, member_state_1) in state_1.members {
401        if let Some(member_state) = next_state.members.get_mut(&id) {
402            // If the member is present in both states, take the higher counter.
403            if member_state_1.member_counter > member_state.member_counter {
404                member_state.member_counter = member_state_1.member_counter;
405                member_state.access = member_state_1.access.clone();
406                member_state.access_counter = member_state_1.access_counter;
407            }
408
409            // If the member counters are equal, take the access level for the state with a higher
410            // access counter. If the access counters are equal, do nothing.
411            if member_state_1.member_counter == member_state.member_counter {
412                if member_state_1.access_counter > member_state.access_counter {
413                    member_state.access = member_state_1.access.clone();
414                    member_state.access_counter = member_state_1.access_counter;
415                }
416
417                // If the access counters are the same, take the lower of the two access levels.
418                if member_state_1.access_counter == member_state.access_counter
419                    && member_state_1.access < member_state.access
420                {
421                    member_state.access = member_state_1.access;
422                }
423            }
424        } else {
425            // Otherwise insert the member into the next state.
426            next_state.members.insert(id, member_state_1);
427        }
428    }
429
430    next_state
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn create_add_remove() {
439        // "Happy path" test for create, add and remove functions.
440
441        let alice = 0;
442        let bob = 1;
443        let charlie = 2;
444
445        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
446
447        // Alice creates a group with Alice and Bob as members.
448        let group_y = create(&initial_members);
449
450        assert!(group_y.members().contains(&alice));
451        assert!(group_y.members().contains(&bob));
452
453        assert!(group_y.managers().contains(&alice));
454        assert!(!group_y.managers().contains(&bob));
455
456        // Alice adds Charlie.
457        let group_y = add(group_y, alice, charlie, Access::write()).unwrap();
458
459        assert!(group_y.members().contains(&charlie));
460
461        // Alice removes Bob.
462        let group_y = remove(group_y, alice, bob).unwrap();
463
464        assert!(!group_y.members().contains(&bob));
465    }
466
467    #[test]
468    fn promote_demote_modify() {
469        let alice = 0;
470        let bob = 1;
471
472        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
473
474        // Alice creates a group with Alice and Bob as members.
475        let group_y = create(&initial_members);
476
477        // Alice promotes Bob to Write access.
478        let group_y = promote(group_y, alice, bob, Access::write()).unwrap();
479
480        let group_y_clone = group_y.clone();
481
482        let bob_state = group_y_clone.members.get(&bob).unwrap();
483        assert!(bob_state.is_writer());
484
485        // Alice demotes Bob to Read access.
486        let group_y = demote(group_y.clone(), alice, bob, Access::read()).unwrap();
487
488        // Alice promotes Bob to Manage access.
489        let group_y = modify(group_y, alice, bob, Access::manage()).unwrap();
490
491        let bob_state = group_y.members.get(&bob).unwrap();
492        assert!(bob_state.is_manager());
493    }
494
495    #[test]
496    fn add_errors() {
497        // "Unhappy path" test for add functions.
498
499        let alice = 0;
500        let bob = 1;
501        let charlie = 2;
502        let daphne = 3;
503
504        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
505
506        // Alice creates a group with Alice and Bob as members.
507        let group_y = create(&initial_members);
508
509        // Charlie adds Daphne...
510        let result = add(group_y.clone(), charlie, daphne, Access::read());
511
512        // ...but Charlie isn't known to the group (has never been a member).
513        assert!(matches!(
514            result,
515            Err(GroupMembershipError::UnrecognisedActor(_bob))
516        ));
517
518        // Bob adds Daphne...
519        let result = add(group_y.clone(), bob, daphne, Access::read());
520
521        // ...but Bob isn't a manager.
522        assert!(matches!(
523            result,
524            Err(GroupMembershipError::InsufficientAccess(_bob))
525        ));
526
527        // Alice adds Bob...
528        let result = add(group_y.clone(), alice, bob, Access::read());
529
530        // ...but Bob is already an active member.
531        assert!(matches!(
532            result,
533            Err(GroupMembershipError::AlreadyAdded(_bob))
534        ));
535
536        // Alice removes Bob.
537        let group_y = remove(group_y, alice, bob).unwrap();
538
539        // Bob adds Daphne...
540        let result = add(group_y, bob, daphne, Access::read());
541
542        // ...but Bob isn't an active member.
543        assert!(matches!(
544            result,
545            Err(GroupMembershipError::InactiveActor(_bob))
546        ));
547
548        // TODO.
549        // The `assert!(matches!())` tests don't test the value in the variant tuple.
550        // We should consider rather using `if let` to match fully.
551        /*
552        if let Err(GroupMembershipError::InactiveActor(actor)) = result {
553            assert_eq!(actor, bob)
554        } else {
555            panic!("description goes here...")
556        }
557        */
558    }
559
560    #[test]
561    fn remove_errors() {
562        // "Unhappy path" test for remove functions.
563
564        let alice = 0;
565        let bob = 1;
566        let charlie = 2;
567        let daphne = 3;
568
569        let initial_members = [
570            (alice, <Access>::manage()),
571            (bob, Access::read()),
572            (charlie, Access::read()),
573        ];
574
575        // Alice creates a group with Alice, Bob and Charlie as members.
576        let group_y = create(&initial_members);
577
578        // Daphne removes Charlie...
579        let result = remove(group_y.clone(), daphne, charlie);
580
581        // ...but Daphne isn't known to the group (has never been a member).
582        assert!(matches!(
583            result,
584            Err(GroupMembershipError::UnrecognisedActor(_daphne))
585        ));
586
587        // Bob removes Charlie...
588        let result = remove(group_y.clone(), bob, charlie);
589
590        // ...but Bob isn't a manager.
591        assert!(matches!(
592            result,
593            Err(GroupMembershipError::InsufficientAccess(_bob))
594        ));
595
596        // Alice removes Daphne...
597        let result = remove(group_y.clone(), alice, daphne);
598
599        // ...but Daphne isn't a member.
600        assert!(matches!(
601            result,
602            Err(GroupMembershipError::UnrecognisedMember(_daphne))
603        ));
604
605        // Alice removes Charlie.
606        let group_y = remove(group_y, alice, charlie).unwrap();
607
608        // Alice removes Charlie...
609        let result = remove(group_y, alice, charlie);
610
611        // ...but Charlie has already been removed.
612        assert!(matches!(
613            result,
614            Err(GroupMembershipError::AlreadyRemoved(_charlie))
615        ));
616    }
617
618    #[test]
619    fn promote_errors() {
620        // "Unhappy path" test for promote functions.
621
622        let alice = 0;
623        let bob = 1;
624        let charlie = 2;
625        let daphne = 3;
626
627        let initial_members = [
628            (alice, <Access>::manage()),
629            (bob, Access::read()),
630            (charlie, Access::read()),
631        ];
632
633        // Alice creates a group with Alice, Bob and Charlie as members.
634        let group_y = create(&initial_members);
635
636        // Daphne promotes Charlie...
637        let result = promote(group_y.clone(), daphne, charlie, Access::manage());
638
639        // ...but Daphne isn't known to the group (has never been a member).
640        assert!(matches!(
641            result,
642            Err(GroupMembershipError::UnrecognisedActor(_daphne))
643        ));
644
645        // Bob promotes Charlie...
646        let result = promote(group_y.clone(), bob, charlie, Access::write());
647
648        // ...but Bob isn't a manager.
649        assert!(matches!(
650            result,
651            Err(GroupMembershipError::InsufficientAccess(_bob))
652        ));
653
654        // Alice promotes Daphne...
655        let result = promote(group_y.clone(), alice, daphne, Access::read());
656
657        // ...but Daphne isn't a member.
658        assert!(matches!(
659            result,
660            Err(GroupMembershipError::UnrecognisedMember(_daphne))
661        ));
662
663        // Alice removes Charlie.
664        let group_y = remove(group_y, alice, charlie).unwrap();
665
666        // Alice promotes Charlie...
667        let result = promote(group_y.clone(), alice, charlie, Access::pull());
668
669        // ...but Charlie isn't a member.
670        assert!(matches!(
671            result,
672            Err(GroupMembershipError::InactiveMember(_charlie))
673        ));
674
675        // Charlie promotes Bob...
676        let result = promote(group_y, charlie, bob, Access::manage());
677
678        // ...but Charlie isn't a member.
679        assert!(matches!(
680            result,
681            Err(GroupMembershipError::InactiveActor(_charlie))
682        ));
683    }
684
685    #[test]
686    fn demote_errors() {
687        // "Unhappy path" test for demote functions.
688
689        let alice = 0;
690        let bob = 1;
691        let charlie = 2;
692        let daphne = 3;
693
694        let initial_members = [
695            (alice, <Access>::manage()),
696            (bob, Access::read()),
697            (charlie, Access::read()),
698        ];
699
700        // Alice creates a group with Alice, Bob and Charlie as members.
701        let group_y = create(&initial_members);
702
703        // Daphne demotes Charlie...
704        let result = demote(group_y.clone(), daphne, charlie, Access::pull());
705
706        // ...but Daphne isn't known to the group (has never been a member).
707        assert!(matches!(
708            result,
709            Err(GroupMembershipError::UnrecognisedActor(_daphne))
710        ));
711
712        // Bob demotes Charlie...
713        let result = demote(group_y.clone(), bob, charlie, Access::pull());
714
715        // ...but Bob isn't a manager.
716        assert!(matches!(
717            result,
718            Err(GroupMembershipError::InsufficientAccess(_bob))
719        ));
720
721        // Alice demotes Daphne...
722        let result = demote(group_y.clone(), alice, daphne, Access::read());
723
724        // ...but Daphne isn't a member.
725        assert!(matches!(
726            result,
727            Err(GroupMembershipError::UnrecognisedMember(_daphne))
728        ));
729
730        // Alice removes Charlie.
731        let group_y = remove(group_y, alice, charlie).unwrap();
732
733        // Alice demotes Charlie...
734        let result = demote(group_y.clone(), alice, charlie, Access::pull());
735
736        // ...but Charlie isn't a member.
737        assert!(matches!(
738            result,
739            Err(GroupMembershipError::InactiveMember(_charlie))
740        ));
741
742        // Charlie demotes Bob...
743        let result = demote(group_y, charlie, bob, Access::pull());
744
745        // ...but Charlie isn't a member.
746        assert!(matches!(
747            result,
748            Err(GroupMembershipError::InactiveActor(_charlie))
749        ));
750    }
751
752    #[test]
753    fn merge_state_member() {
754        // A member is added in one group state but not the other.
755        // We expect the post-merge state to include the member.
756
757        let alice = 0;
758        let bob = 1;
759        let charlie = 2;
760        let daphne = 3;
761
762        let initial_members = [
763            (alice, <Access>::manage()),
764            (bob, Access::read()),
765            (charlie, Access::pull()),
766        ];
767
768        // Alice creates a group with Alice, Bob and Charlie as members.
769        let group_y_i = create(&initial_members);
770
771        // Alice adds Daphne.
772        let group_y_ii = add(group_y_i.clone(), alice, daphne, Access::read()).unwrap();
773
774        // Merge the states.
775        let group_y = merge(group_y_i, group_y_ii);
776
777        assert!(group_y.members().contains(&daphne));
778    }
779
780    #[test]
781    fn merge_state_counter() {
782        // A member exists in both group states but with different counters.
783        // We expect the post-merge state to contain the higher of the two counters.
784
785        let alice = 0;
786        let bob = 1;
787        let charlie = 2;
788
789        let initial_members = [
790            (alice, <Access>::manage()),
791            (bob, Access::read()),
792            (charlie, Access::pull()),
793        ];
794
795        // Alice creates a group with Alice, Bob and Charlie as members.
796        let group_y_i = create(&initial_members);
797
798        // Alice removes Bob.
799        let group_y_ii = remove(group_y_i.clone(), alice, bob).unwrap();
800
801        // Alice adds Bob.
802        let group_y_ii = add(group_y_ii, alice, bob, Access::read()).unwrap();
803
804        // Merge the states.
805        let group_y = merge(group_y_i, group_y_ii);
806
807        assert!(group_y.members().contains(&alice));
808        assert!(group_y.members().contains(&bob));
809        assert!(group_y.members().contains(&charlie));
810
811        let bob_state = group_y.members.get(&bob).unwrap();
812
813        // We expect the merge to choose the higher counter value for Bob.
814        assert!(bob_state.member_counter == 3);
815    }
816
817    #[test]
818    fn merge_state_access_counter() {
819        // A member exists in both group states with equal counters but different access counters.
820        // We expect the post-merge state to contain the higher of the two access counters.
821
822        let alice = 0;
823        let bob = 1;
824        let charlie = 2;
825
826        let initial_members = [
827            (alice, <Access>::manage()),
828            (bob, Access::read()),
829            (charlie, Access::pull()),
830        ];
831
832        // Alice creates a group with Alice, Bob and Charlie as members.
833        let group_y_i = create(&initial_members);
834
835        // Alice promotes Charlie.
836        let group_y_ii = promote(group_y_i.clone(), alice, charlie, Access::read()).unwrap();
837
838        // Alice demotes Charlie.
839        let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::pull()).unwrap();
840
841        // Merge the states.
842        let group_y = merge(group_y_i, group_y_ii);
843
844        let charlie_state = group_y.members.get(&charlie).unwrap();
845
846        // We expect the merge to choose the higher access counter value for Charlie.
847        assert!(charlie_state.access_counter == 2);
848
849        // We expect the access level to be Pull for Charlie.
850        assert!(charlie_state.is_puller());
851    }
852
853    #[test]
854    fn merge_state_access() {
855        // A member exists in both group states with equal counters and equal access counters
856        // but different access levels.
857        // We expect the post-merge state to contain the lower of the two access levels.
858
859        let alice = 0;
860        let bob = 1;
861        let charlie = 2;
862
863        let initial_members = [
864            (alice, <Access>::manage()),
865            (bob, Access::read()),
866            (charlie, Access::pull()),
867        ];
868
869        // Alice creates a group with Alice, Bob and Charlie as members.
870        let group_y = create(&initial_members);
871
872        // Alice promotes Charlie.
873        let group_y_i = promote(group_y.clone(), alice, charlie, Access::read()).unwrap();
874
875        // Alice demotes Charlie.
876        let group_y_i = demote(group_y_i.clone(), alice, charlie, Access::pull()).unwrap();
877
878        // Alice promotes Charlie.
879        let group_y_ii = modify(group_y.clone(), alice, charlie, Access::manage()).unwrap();
880
881        // Alice demotes Charlie.
882        let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::read()).unwrap();
883
884        // Merge the states.
885        let group_y = merge(group_y_i.clone(), group_y_ii.clone());
886
887        let charlie_state = group_y.members.get(&charlie).unwrap();
888
889        // We expect the access level to be Pull for Charlie.
890        assert!(charlie_state.is_puller());
891    }
892}