Skip to main content

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 or they are are
251    // removing themselves.
252    if !remover_state.is_member() {
253        return Err(GroupMembershipError::InactiveActor(remover));
254    } else if !remover_state.is_manager() && remover != removed {
255        return Err(GroupMembershipError::InsufficientAccess(remover));
256    }
257
258    // Ensure that "removed" is known to the group.
259    if !state.members.contains_key(&removed) {
260        return Err(GroupMembershipError::UnrecognisedMember(removed));
261    };
262
263    // Ensure that "removed" is not already an inactive member of the group.
264    if let Some(removed_state) = state.members.get(&removed)
265        && !removed_state.is_member()
266    {
267        return Err(GroupMembershipError::AlreadyRemoved(removed));
268    }
269
270    // Increment "removed" counters unless they are already removed.
271    let mut state = state;
272    state.members.entry(removed).and_modify(|removed| {
273        if removed.is_member() {
274            removed.member_counter += 1;
275            removed.access_counter = 0;
276        }
277    });
278
279    Ok(state)
280}
281
282/// Modify the access level of a group member.
283///
284/// Both the `modifier` and `modified` identity must be active group members; failure to meet these
285/// conditions will result in an error.
286///
287/// This is a helper method to reduce code duplication in `promote()` and `demote()`.
288fn modify<ID: Eq + Hash, C: Conditions>(
289    state: GroupMembersState<ID, C>,
290    modifier: ID,
291    modified: ID,
292    access: Access<C>,
293) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
294    // Ensure that "modifier" is known to the group.
295    let Some(modifier_state) = state.members.get(&modifier) else {
296        return Err(GroupMembershipError::UnrecognisedActor(modifier));
297    };
298
299    // Ensure that "modifier" is a member of the group with manage access level.
300    if !modifier_state.is_member() {
301        return Err(GroupMembershipError::InactiveActor(modifier));
302    } else if !modifier_state.is_manager() {
303        return Err(GroupMembershipError::InsufficientAccess(modifier));
304    }
305
306    // Ensure that "modified" is an active member of the group.
307    if let Some(modified_state) = state.members.get(&modified) {
308        if !modified_state.is_member() {
309            return Err(GroupMembershipError::InactiveMember(modified));
310        }
311    } else {
312        return Err(GroupMembershipError::UnrecognisedMember(modified));
313    }
314
315    // Update access level.
316    let mut state = state;
317    state.members.entry(modified).and_modify(|modified| {
318        // Only perform the modification if the access levels differ.
319        if modified.access != access {
320            modified.access = access;
321            modified.access_counter += 1;
322        }
323    });
324
325    Ok(state)
326}
327
328/// Promote a group member to the given access level.
329///
330/// No modification will occur if the promoted member already has `Manage` access. In that case, the
331/// given state is returned unchanged.
332///
333/// The `promoter` must be an active member of the group with `Manage` access and the `promoted`
334/// identity must also be an active member of the group; failure to meet these conditions will
335/// result in an error.
336pub fn promote<ID: Eq + Hash, C: Conditions>(
337    state: GroupMembersState<ID, C>,
338    promoter: ID,
339    promoted: ID,
340    access: Access<C>,
341) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
342    if let Some(member) = state.members.get(&promoted) {
343        // No action is required if the member is already set to the highest access level.
344        let new_state = if member.is_manager() {
345            state
346        } else {
347            modify(state, promoter, promoted, access)?
348        };
349
350        Ok(new_state)
351    } else {
352        Err(GroupMembershipError::UnrecognisedMember(promoted))
353    }
354}
355
356/// Demote a group member to the given access level.
357///
358/// No modification will occur if the demoted member already has `Pull` access. In that case, the
359/// given state is returned unchanged.
360///
361/// The `demoter` must be an active member of the group with `Manage` access and the `demoted`
362/// identity must also be an active member of the group; failure to meet these conditions will
363/// result in an error.
364pub fn demote<ID: Eq + Hash, C: Conditions>(
365    state: GroupMembersState<ID, C>,
366    demoter: ID,
367    demoted: ID,
368    access: Access<C>,
369) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
370    if let Some(member) = state.members.get(&demoted) {
371        // No action is required if the member is already set to the lowest access level.
372        let new_state = if member.is_puller() {
373            state
374        } else {
375            modify(state, demoter, demoted, access)?
376        };
377
378        Ok(new_state)
379    } else {
380        Err(GroupMembershipError::UnrecognisedMember(demoted))
381    }
382}
383
384/// Merge two group states into one using a deterministic, conflict-free approach.
385///
386/// Grow-only counters are used internally to track state changes; one counter for add / remove
387/// actions and one for access modification actions. These values are used to determine which
388/// membership and access states should be included in the merged group state. A state with a higher
389/// counter indicates that it has undergone more actions; this state will be included in the merge.
390///
391/// If a member exists with different access levels in each state but the same number of access
392/// modifications, the lower of the two access levels will be chosen.
393pub fn merge<ID: Clone + Eq + Hash, C: Conditions>(
394    state_1: GroupMembersState<ID, C>,
395    state_2: GroupMembersState<ID, C>,
396) -> GroupMembersState<ID, C> {
397    // Start from state_2 state.
398    let mut next_state = state_2.clone();
399
400    // Iterate over entries in state_1.
401    for (id, member_state_1) in state_1.members {
402        if let Some(member_state) = next_state.members.get_mut(&id) {
403            // If the member is present in both states, take the higher counter.
404            if member_state_1.member_counter > member_state.member_counter {
405                member_state.member_counter = member_state_1.member_counter;
406                member_state.access = member_state_1.access.clone();
407                member_state.access_counter = member_state_1.access_counter;
408            }
409
410            // If the member counters are equal, take the access level for the state with a higher
411            // access counter. If the access counters are equal, do nothing.
412            if member_state_1.member_counter == member_state.member_counter {
413                if member_state_1.access_counter > member_state.access_counter {
414                    member_state.access = member_state_1.access.clone();
415                    member_state.access_counter = member_state_1.access_counter;
416                }
417
418                // If the access counters are the same, take the lower of the two access levels.
419                if member_state_1.access_counter == member_state.access_counter
420                    && member_state_1.access < member_state.access
421                {
422                    member_state.access = member_state_1.access;
423                }
424            }
425        } else {
426            // Otherwise insert the member into the next state.
427            next_state.members.insert(id, member_state_1);
428        }
429    }
430
431    next_state
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn create_add_remove() {
440        // "Happy path" test for create, add and remove functions.
441
442        let alice = 0;
443        let bob = 1;
444        let charlie = 2;
445
446        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
447
448        // Alice creates a group with Alice and Bob as members.
449        let group_y = create(&initial_members);
450
451        assert!(group_y.members().contains(&alice));
452        assert!(group_y.members().contains(&bob));
453
454        assert!(group_y.managers().contains(&alice));
455        assert!(!group_y.managers().contains(&bob));
456
457        // Alice adds Charlie.
458        let group_y = add(group_y, alice, charlie, Access::write()).unwrap();
459
460        assert!(group_y.members().contains(&charlie));
461
462        // Alice removes Bob.
463        let group_y = remove(group_y, alice, bob).unwrap();
464
465        assert!(!group_y.members().contains(&bob));
466    }
467
468    #[test]
469    fn self_remove() {
470        let alice = 0;
471        let bob = 1;
472
473        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
474
475        // Alice creates a group with Alice and Bob as members.
476        let group_y = create(&initial_members);
477
478        assert!(group_y.members().contains(&alice));
479        assert!(group_y.members().contains(&bob));
480
481        assert!(group_y.managers().contains(&alice));
482        assert!(!group_y.managers().contains(&bob));
483
484        // Alice removes themselves.
485        let group_y = remove(group_y, alice, alice).unwrap();
486
487        assert!(!group_y.members().contains(&alice));
488        assert!(group_y.members().contains(&bob));
489    }
490
491    #[test]
492    fn promote_demote_modify() {
493        let alice = 0;
494        let bob = 1;
495
496        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
497
498        // Alice creates a group with Alice and Bob as members.
499        let group_y = create(&initial_members);
500
501        // Alice promotes Bob to Write access.
502        let group_y = promote(group_y, alice, bob, Access::write()).unwrap();
503
504        let group_y_clone = group_y.clone();
505
506        let bob_state = group_y_clone.members.get(&bob).unwrap();
507        assert!(bob_state.is_writer());
508
509        // Alice demotes Bob to Read access.
510        let group_y = demote(group_y.clone(), alice, bob, Access::read()).unwrap();
511
512        // Alice promotes Bob to Manage access.
513        let group_y = modify(group_y, alice, bob, Access::manage()).unwrap();
514
515        let bob_state = group_y.members.get(&bob).unwrap();
516        assert!(bob_state.is_manager());
517    }
518
519    #[test]
520    fn add_errors() {
521        // "Unhappy path" test for add functions.
522
523        let alice = 0;
524        let bob = 1;
525        let charlie = 2;
526        let daphne = 3;
527
528        let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
529
530        // Alice creates a group with Alice and Bob as members.
531        let group_y = create(&initial_members);
532
533        // Charlie adds Daphne...
534        let result = add(group_y.clone(), charlie, daphne, Access::read());
535
536        // ...but Charlie isn't known to the group (has never been a member).
537        std::assert_matches!(result, Err(GroupMembershipError::UnrecognisedActor(_bob)));
538
539        // Bob adds Daphne...
540        let result = add(group_y.clone(), bob, daphne, Access::read());
541
542        // ...but Bob isn't a manager.
543        std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
544
545        // Alice adds Bob...
546        let result = add(group_y.clone(), alice, bob, Access::read());
547
548        // ...but Bob is already an active member.
549        std::assert_matches!(result, Err(GroupMembershipError::AlreadyAdded(_bob)));
550
551        // Alice removes Bob.
552        let group_y = remove(group_y, alice, bob).unwrap();
553
554        // Bob adds Daphne...
555        let result = add(group_y, bob, daphne, Access::read());
556
557        // ...but Bob isn't an active member.
558        std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_bob)));
559
560        // TODO.
561        // The `std::assert_matches!())` tests don't test the value in the variant tuple.
562        // We should consider rather using `if let` to match fully.
563        /*
564        if let Err(GroupMembershipError::InactiveActor(actor)) = result {
565            assert_eq!(actor, bob)
566        } else {
567            panic!("description goes here...")
568        }
569        */
570    }
571
572    #[test]
573    fn remove_errors() {
574        // "Unhappy path" test for remove functions.
575
576        let alice = 0;
577        let bob = 1;
578        let charlie = 2;
579        let daphne = 3;
580
581        let initial_members = [
582            (alice, <Access>::manage()),
583            (bob, Access::read()),
584            (charlie, Access::read()),
585        ];
586
587        // Alice creates a group with Alice, Bob and Charlie as members.
588        let group_y = create(&initial_members);
589
590        // Daphne removes Charlie...
591        let result = remove(group_y.clone(), daphne, charlie);
592
593        // ...but Daphne isn't known to the group (has never been a member).
594        std::assert_matches!(
595            result,
596            Err(GroupMembershipError::UnrecognisedActor(_daphne))
597        );
598
599        // Bob removes Charlie...
600        let result = remove(group_y.clone(), bob, charlie);
601
602        // ...but Bob isn't a manager.
603        std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
604
605        // Alice removes Daphne...
606        let result = remove(group_y.clone(), alice, daphne);
607
608        // ...but Daphne isn't a member.
609        std::assert_matches!(
610            result,
611            Err(GroupMembershipError::UnrecognisedMember(_daphne))
612        );
613
614        // Alice removes Charlie.
615        let group_y = remove(group_y, alice, charlie).unwrap();
616
617        // Alice removes Charlie...
618        let result = remove(group_y, alice, charlie);
619
620        // ...but Charlie has already been removed.
621        std::assert_matches!(result, Err(GroupMembershipError::AlreadyRemoved(_charlie)));
622    }
623
624    #[test]
625    fn promote_errors() {
626        // "Unhappy path" test for promote functions.
627
628        let alice = 0;
629        let bob = 1;
630        let charlie = 2;
631        let daphne = 3;
632
633        let initial_members = [
634            (alice, <Access>::manage()),
635            (bob, Access::read()),
636            (charlie, Access::read()),
637        ];
638
639        // Alice creates a group with Alice, Bob and Charlie as members.
640        let group_y = create(&initial_members);
641
642        // Daphne promotes Charlie...
643        let result = promote(group_y.clone(), daphne, charlie, Access::manage());
644
645        // ...but Daphne isn't known to the group (has never been a member).
646        std::assert_matches!(
647            result,
648            Err(GroupMembershipError::UnrecognisedActor(_daphne))
649        );
650
651        // Bob promotes Charlie...
652        let result = promote(group_y.clone(), bob, charlie, Access::write());
653
654        // ...but Bob isn't a manager.
655        std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
656
657        // Alice promotes Daphne...
658        let result = promote(group_y.clone(), alice, daphne, Access::read());
659
660        // ...but Daphne isn't a member.
661        std::assert_matches!(
662            result,
663            Err(GroupMembershipError::UnrecognisedMember(_daphne))
664        );
665
666        // Alice removes Charlie.
667        let group_y = remove(group_y, alice, charlie).unwrap();
668
669        // Alice promotes Charlie...
670        let result = promote(group_y.clone(), alice, charlie, Access::pull());
671
672        // ...but Charlie isn't a member.
673        std::assert_matches!(result, Err(GroupMembershipError::InactiveMember(_charlie)));
674
675        // Charlie promotes Bob...
676        let result = promote(group_y, charlie, bob, Access::manage());
677
678        // ...but Charlie isn't a member.
679        std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_charlie)));
680    }
681
682    #[test]
683    fn demote_errors() {
684        // "Unhappy path" test for demote functions.
685
686        let alice = 0;
687        let bob = 1;
688        let charlie = 2;
689        let daphne = 3;
690
691        let initial_members = [
692            (alice, <Access>::manage()),
693            (bob, Access::read()),
694            (charlie, Access::read()),
695        ];
696
697        // Alice creates a group with Alice, Bob and Charlie as members.
698        let group_y = create(&initial_members);
699
700        // Daphne demotes Charlie...
701        let result = demote(group_y.clone(), daphne, charlie, Access::pull());
702
703        // ...but Daphne isn't known to the group (has never been a member).
704        std::assert_matches!(
705            result,
706            Err(GroupMembershipError::UnrecognisedActor(_daphne))
707        );
708
709        // Bob demotes Charlie...
710        let result = demote(group_y.clone(), bob, charlie, Access::pull());
711
712        // ...but Bob isn't a manager.
713        std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
714
715        // Alice demotes Daphne...
716        let result = demote(group_y.clone(), alice, daphne, Access::read());
717
718        // ...but Daphne isn't a member.
719        std::assert_matches!(
720            result,
721            Err(GroupMembershipError::UnrecognisedMember(_daphne))
722        );
723
724        // Alice removes Charlie.
725        let group_y = remove(group_y, alice, charlie).unwrap();
726
727        // Alice demotes Charlie...
728        let result = demote(group_y.clone(), alice, charlie, Access::pull());
729
730        // ...but Charlie isn't a member.
731        std::assert_matches!(result, Err(GroupMembershipError::InactiveMember(_charlie)));
732
733        // Charlie demotes Bob...
734        let result = demote(group_y, charlie, bob, Access::pull());
735
736        // ...but Charlie isn't a member.
737        std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_charlie)));
738    }
739
740    #[test]
741    fn merge_state_member() {
742        // A member is added in one group state but not the other.
743        // We expect the post-merge state to include the member.
744
745        let alice = 0;
746        let bob = 1;
747        let charlie = 2;
748        let daphne = 3;
749
750        let initial_members = [
751            (alice, <Access>::manage()),
752            (bob, Access::read()),
753            (charlie, Access::pull()),
754        ];
755
756        // Alice creates a group with Alice, Bob and Charlie as members.
757        let group_y_i = create(&initial_members);
758
759        // Alice adds Daphne.
760        let group_y_ii = add(group_y_i.clone(), alice, daphne, Access::read()).unwrap();
761
762        // Merge the states.
763        let group_y = merge(group_y_i, group_y_ii);
764
765        assert!(group_y.members().contains(&daphne));
766    }
767
768    #[test]
769    fn merge_state_counter() {
770        // A member exists in both group states but with different counters.
771        // We expect the post-merge state to contain the higher of the two counters.
772
773        let alice = 0;
774        let bob = 1;
775        let charlie = 2;
776
777        let initial_members = [
778            (alice, <Access>::manage()),
779            (bob, Access::read()),
780            (charlie, Access::pull()),
781        ];
782
783        // Alice creates a group with Alice, Bob and Charlie as members.
784        let group_y_i = create(&initial_members);
785
786        // Alice removes Bob.
787        let group_y_ii = remove(group_y_i.clone(), alice, bob).unwrap();
788
789        // Alice adds Bob.
790        let group_y_ii = add(group_y_ii, alice, bob, Access::read()).unwrap();
791
792        // Merge the states.
793        let group_y = merge(group_y_i, group_y_ii);
794
795        assert!(group_y.members().contains(&alice));
796        assert!(group_y.members().contains(&bob));
797        assert!(group_y.members().contains(&charlie));
798
799        let bob_state = group_y.members.get(&bob).unwrap();
800
801        // We expect the merge to choose the higher counter value for Bob.
802        assert!(bob_state.member_counter == 3);
803    }
804
805    #[test]
806    fn merge_state_access_counter() {
807        // A member exists in both group states with equal counters but different access counters.
808        // We expect the post-merge state to contain the higher of the two access counters.
809
810        let alice = 0;
811        let bob = 1;
812        let charlie = 2;
813
814        let initial_members = [
815            (alice, <Access>::manage()),
816            (bob, Access::read()),
817            (charlie, Access::pull()),
818        ];
819
820        // Alice creates a group with Alice, Bob and Charlie as members.
821        let group_y_i = create(&initial_members);
822
823        // Alice promotes Charlie.
824        let group_y_ii = promote(group_y_i.clone(), alice, charlie, Access::read()).unwrap();
825
826        // Alice demotes Charlie.
827        let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::pull()).unwrap();
828
829        // Merge the states.
830        let group_y = merge(group_y_i, group_y_ii);
831
832        let charlie_state = group_y.members.get(&charlie).unwrap();
833
834        // We expect the merge to choose the higher access counter value for Charlie.
835        assert!(charlie_state.access_counter == 2);
836
837        // We expect the access level to be Pull for Charlie.
838        assert!(charlie_state.is_puller());
839    }
840
841    #[test]
842    fn merge_state_access() {
843        // A member exists in both group states with equal counters and equal access counters
844        // but different access levels.
845        // We expect the post-merge state to contain the lower of the two access levels.
846
847        let alice = 0;
848        let bob = 1;
849        let charlie = 2;
850
851        let initial_members = [
852            (alice, <Access>::manage()),
853            (bob, Access::read()),
854            (charlie, Access::pull()),
855        ];
856
857        // Alice creates a group with Alice, Bob and Charlie as members.
858        let group_y = create(&initial_members);
859
860        // Alice promotes Charlie.
861        let group_y_i = promote(group_y.clone(), alice, charlie, Access::read()).unwrap();
862
863        // Alice demotes Charlie.
864        let group_y_i = demote(group_y_i.clone(), alice, charlie, Access::pull()).unwrap();
865
866        // Alice promotes Charlie.
867        let group_y_ii = modify(group_y.clone(), alice, charlie, Access::manage()).unwrap();
868
869        // Alice demotes Charlie.
870        let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::read()).unwrap();
871
872        // Merge the states.
873        let group_y = merge(group_y_i.clone(), group_y_ii.clone());
874
875        let charlie_state = group_y.members.get(&charlie).unwrap();
876
877        // We expect the access level to be Pull for Charlie.
878        assert!(charlie_state.is_puller());
879    }
880}