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