pub(crate) mod state;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::marker::PhantomData;
use petgraph::prelude::DiGraphMap;
use petgraph::visit::{Bfs, DfsPostOrder, IntoNodeIdentifiers, NodeIndexable, Reversed};
#[cfg(any(test, feature = "serde"))]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::access::Access;
use crate::group::{GroupAction, GroupMember, GroupMembersState, GroupMembershipError};
use crate::traits::{Conditions, IdentityHandle, Operation, OperationId, Resolver};
const MAX_NESTED_DEPTH: u32 = 1000;
#[derive(Debug, Error)]
pub enum GroupCrdtInnerError<OP> {
#[error("states {0:?} not found")]
StatesNotFound(Vec<OP>),
}
#[derive(Debug, Error)]
pub enum GroupCrdtError<ID, OP, M, C, RS>
where
ID: IdentityHandle,
OP: OperationId + Ord,
RS: Resolver<ID, OP, M, C>,
{
#[error(transparent)]
Inner(#[from] GroupCrdtInnerError<OP>),
#[error("duplicate operation {0} processed in group {1}")]
DuplicateOperation(OP, ID),
#[error("group cycle detected adding {0} to {1} operation={2}")]
GroupCycle(ID, ID, OP),
#[error("state change error processing operation {0}: {1:?}")]
StateChangeError(OP, GroupMembershipError<GroupMember<ID>>),
#[error("attempted to add group {0} with manage access")]
ManagerGroupsNotAllowed(ID),
#[error("resolver error: {0}")]
Resolver(RS::Error),
}
pub(crate) type GroupStates<ID, C> = HashMap<ID, GroupMembersState<GroupMember<ID>, C>>;
#[derive(Debug)]
#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
pub struct GroupCrdtInnerState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
{
pub operations: HashMap<OP, M>,
pub ignore: HashSet<OP>,
pub mutual_removes: HashSet<OP>,
pub states: HashMap<OP, GroupStates<ID, C>>,
pub graph: DiGraphMap<OP, ()>,
}
impl<ID, OP, M, C> Default for GroupCrdtInnerState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
{
fn default() -> Self {
Self {
operations: Default::default(),
ignore: Default::default(),
mutual_removes: Default::default(),
states: Default::default(),
graph: Default::default(),
}
}
}
impl<ID, OP, M, C> GroupCrdtInnerState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
M: Operation<ID, OP, C>,
C: Conditions,
{
pub fn heads(&self) -> HashSet<OP> {
self.graph
.clone()
.into_graph::<usize>()
.externals(petgraph::Direction::Outgoing)
.map(|idx| self.graph.from_index(idx.index()))
.collect::<HashSet<_>>()
}
pub fn heads_filtered(&self, groups: &[ID]) -> HashSet<OP> {
let global_heads = self.heads();
global_heads
.into_iter()
.filter(|id| {
let reversed = Reversed(&self.graph);
let mut bfs = Bfs::new(&reversed, *id);
while let Some(inner_id) = bfs.next(&reversed) {
let operation = self
.operations
.get(&inner_id)
.expect("operation is present in map");
if operation.action().is_create() && groups.contains(&operation.group_id()) {
return true;
}
}
false
})
.collect()
}
pub fn current_state(&self) -> GroupStates<ID, C> {
self.merge_states(&self.heads())
.expect("states exist for processed operations")
}
pub fn state_at(
&self,
dependencies: &HashSet<OP>,
) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
self.merge_states(dependencies)
}
fn merge_states(
&self,
ids: &HashSet<OP>,
) -> Result<GroupStates<ID, C>, GroupCrdtInnerError<OP>> {
let mut current_state = HashMap::new();
for id in ids {
let group_states = match self.states.get(id) {
Some(group_states) => group_states.clone(),
None => {
return Err(GroupCrdtInnerError::StatesNotFound(
ids.iter().cloned().collect(),
));
}
};
for (id, state) in group_states.into_iter() {
current_state
.entry(id)
.and_modify(
|current_state: &mut GroupMembersState<GroupMember<ID>, C>| {
*current_state = state::merge(state.clone(), current_state.clone())
},
)
.or_insert(state);
}
}
Ok(current_state)
}
fn members_inner(
&self,
group_id: ID,
members: &mut HashMap<GroupMember<ID>, Access<C>>,
root_access: Option<Access<C>>,
mut depth: u32,
) {
if depth == MAX_NESTED_DEPTH {
return;
}
depth += 1;
let current_states = self.current_state();
let Some(group_state) = current_states.get(&group_id) else {
return;
};
for (member, access) in group_state.access_levels() {
let next_access = match root_access.clone() {
Some(root_access) => {
if access <= root_access {
access.clone()
} else {
root_access
}
}
None => access.clone(),
};
members
.entry(member)
.and_modify(|current_access| {
if *current_access < next_access {
*current_access = next_access.clone();
}
})
.or_insert_with(|| next_access.clone());
if let GroupMember::Group(id) = member {
self.members_inner(id, members, Some(next_access), depth)
}
}
}
pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
self.traverse_members(group_id, 0)
.into_iter()
.filter_map(|(member, access)| {
if member.is_individual() {
Some((member.id(), access))
} else {
None
}
})
.collect()
}
pub fn groups(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
self.traverse_members(group_id, 0)
.into_iter()
.filter_map(|(member, access)| {
if member.is_group() {
Some((member.id(), access))
} else {
None
}
})
.collect()
}
pub fn traverse_members(&self, group_id: ID, depth: u32) -> Vec<(GroupMember<ID>, Access<C>)> {
let mut members = HashMap::new();
self.members_inner(group_id, &mut members, None, depth);
members.into_iter().collect()
}
pub(crate) fn would_create_cycle(&self, operation: &M) -> bool {
let parent_group_id = operation.group_id();
if let GroupAction::Add {
member: GroupMember::Group(child_group_id),
..
} = &operation.action()
{
let states = self.current_state();
let mut stack = vec![*child_group_id];
let mut visited = HashSet::new();
while let Some(child_group_id) = stack.pop() {
if !visited.insert(child_group_id) {
continue;
}
if child_group_id == parent_group_id {
return true;
}
if let Some(group_state) = states.get(&child_group_id) {
for (member, _) in group_state.access_levels() {
if let GroupMember::Group(id) = member {
stack.push(id);
}
}
}
}
}
false
}
}
#[derive(Debug)]
#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
#[cfg_attr(
any(test, feature = "serde"),
derive(Deserialize, Serialize),
serde(bound(
deserialize = "
ID: Deserialize<'de>,
OP: Deserialize<'de>,
M: Deserialize<'de>,
C: Deserialize<'de>,
",
serialize = "
ID: Serialize,
OP: Serialize,
M: Serialize,
C: Serialize,
"
))
)]
pub struct GroupCrdtState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
{
pub inner: GroupCrdtInnerState<ID, OP, M, C>,
}
impl<ID, OP, M, C> Default for GroupCrdtState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
M: Operation<ID, OP, C>,
C: Conditions,
{
fn default() -> Self {
Self {
inner: Default::default(),
}
}
}
impl<ID, OP, M, C> GroupCrdtState<ID, OP, M, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
M: Operation<ID, OP, C>,
C: Conditions,
{
pub fn new() -> Self {
Self::default()
}
pub fn root_members(&self, group_id: ID) -> Vec<(GroupMember<ID>, Access<C>)> {
match self.inner.current_state().get(&group_id) {
Some(group_y) => group_y.access_levels(),
None => vec![],
}
}
pub fn members(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
self.inner.members(group_id)
}
pub fn groups(&self, group_id: ID) -> Vec<(ID, Access<C>)> {
self.inner.groups(group_id)
}
pub fn traverse_members(&self, group_id: ID, depth: u32) -> Vec<(GroupMember<ID>, Access<C>)> {
self.inner.traverse_members(group_id, depth)
}
pub fn has_group(&self, group_id: ID) -> bool {
self.inner.current_state().contains_key(&group_id)
}
pub fn heads(&self) -> Vec<OP> {
self.inner.heads().into_iter().collect()
}
pub fn heads_filtered(&self, groups: &[ID]) -> Vec<OP> {
self.inner.heads_filtered(groups).into_iter().collect()
}
}
#[derive(Clone, Debug, Default)]
pub struct GroupCrdt<ID, OP, M, C, RS> {
_phantom: PhantomData<(ID, OP, M, C, RS)>,
}
impl<ID, OP, M, C, RS> GroupCrdt<ID, OP, M, C, RS>
where
ID: IdentityHandle,
OP: OperationId + Ord,
M: Operation<ID, OP, C> + Clone,
C: Conditions,
RS: Resolver<ID, OP, M, C, State = GroupCrdtInnerState<ID, OP, M, C>>,
{
pub fn init() -> GroupCrdtState<ID, OP, M, C> {
GroupCrdtState {
inner: GroupCrdtInnerState::default(),
}
}
#[allow(clippy::type_complexity)]
pub fn process(
mut y: GroupCrdtState<ID, OP, M, C>,
operation: &M,
) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
let operation_id = operation.id();
let actor = operation.author();
let dependencies = HashSet::from_iter(operation.dependencies().clone());
let group_id = operation.group_id();
let rebuild_required =
RS::rebuild_required(&y.inner, operation).map_err(GroupCrdtError::Resolver)?;
y = GroupCrdt::validate(y, operation)?;
y = Self::add_operation(y, operation);
if rebuild_required {
y.inner = RS::process(y.inner).map_err(GroupCrdtError::Resolver)?;
return Ok(y);
}
let mut groups_y = y.inner.state_at(&dependencies)?;
groups_y = apply_action(
groups_y,
group_id,
operation_id,
actor,
&operation.action(),
&y.inner.ignore,
)
.state()
.to_owned();
y.inner.states.insert(operation_id, groups_y);
Ok(y)
}
#[allow(clippy::type_complexity)]
pub(crate) fn validate(
y: GroupCrdtState<ID, OP, M, C>,
operation: &M,
) -> Result<GroupCrdtState<ID, OP, M, C>, GroupCrdtError<ID, OP, M, C, RS>> {
if y.inner.operations.contains_key(&operation.id()) {
return Err(GroupCrdtError::DuplicateOperation(
operation.id(),
operation.group_id(),
));
}
match &operation.action() {
GroupAction::Add { member, access } | GroupAction::Promote { member, access }
if member.is_group() && access.is_manage() =>
{
return Err(GroupCrdtError::ManagerGroupsNotAllowed(member.id()));
}
_ => (),
};
let last_graph = y.inner.graph.clone();
let last_ignore = y.inner.ignore.clone();
let last_mutual_removes = y.inner.mutual_removes.clone();
let last_states = y.inner.states.clone();
let dependencies = HashSet::from_iter(operation.dependencies().clone());
let temp_y = if y.inner.heads() != dependencies {
let mut temp_y = y;
let mut predecessors = HashSet::new();
for dependency in operation.dependencies() {
let reversed = Reversed(&temp_y.inner.graph);
let mut dfs_rev = DfsPostOrder::new(&reversed, dependency);
while let Some(id) = dfs_rev.next(&reversed) {
predecessors.insert(id);
}
}
let to_remove: Vec<_> = temp_y
.inner
.graph
.node_identifiers()
.filter(|n| !predecessors.contains(n))
.collect();
for node in &to_remove {
temp_y.inner.graph.remove_node(*node);
}
temp_y.inner = RS::process(temp_y.inner).map_err(GroupCrdtError::Resolver)?;
temp_y
} else {
y
};
if temp_y.inner.would_create_cycle(operation) {
let parent_group = operation.group_id();
let GroupAction::Add {
member: sub_group, ..
} = operation.action()
else {
unreachable!()
};
return Err(GroupCrdtError::GroupCycle(
parent_group,
sub_group.id(),
operation.id(),
));
}
let result = apply_action(
temp_y.inner.current_state(),
operation.group_id(),
operation.id(),
operation.author(),
&operation.action(),
&temp_y.inner.ignore,
);
match result {
StateChangeResult::Ok { state } => state,
StateChangeResult::Error { error, .. } => {
return Err(GroupCrdtError::StateChangeError(operation.id(), error));
}
StateChangeResult::Filtered { .. } => {
unreachable!();
}
};
let mut y = temp_y;
y.inner.graph = last_graph;
y.inner.ignore = last_ignore;
y.inner.mutual_removes = last_mutual_removes;
y.inner.states = last_states;
Ok(y)
}
fn add_operation(
mut y: GroupCrdtState<ID, OP, M, C>,
operation: &M,
) -> GroupCrdtState<ID, OP, M, C> {
let operation_id = operation.id();
let dependencies = operation.dependencies();
y.inner.graph.add_node(operation_id);
for dependency in &dependencies {
y.inner.graph.add_edge(*dependency, operation_id, ());
}
y.inner.operations.insert(operation_id, operation.clone());
y
}
}
pub(crate) fn apply_action<ID, OP, C>(
mut groups_y: GroupStates<ID, C>,
group_id: ID,
id: OP,
actor: ID,
action: &GroupAction<ID, C>,
filter: &HashSet<OP>,
) -> StateChangeResult<ID, C>
where
ID: IdentityHandle,
OP: OperationId + Ord,
C: Conditions,
{
let members_y = if action.is_create() {
GroupMembersState::default()
} else {
groups_y
.remove(&group_id)
.expect("group already present in states map")
};
if filter.contains(&id) {
groups_y.insert(group_id, members_y);
return StateChangeResult::Filtered { state: groups_y };
}
let result = match action.clone() {
GroupAction::Add { member, access, .. } => state::add(
members_y.clone(),
GroupMember::Individual(actor),
member,
access,
),
GroupAction::Remove { member, .. } => {
state::remove(members_y.clone(), GroupMember::Individual(actor), member)
}
GroupAction::Promote { member, access } => state::promote(
members_y.clone(),
GroupMember::Individual(actor),
member,
access,
),
GroupAction::Demote { member, access } => state::demote(
members_y.clone(),
GroupMember::Individual(actor),
member,
access,
),
GroupAction::Create { initial_members } => Ok(state::create(&initial_members)),
};
match result {
Ok(members_y_i) => {
groups_y.insert(group_id, members_y_i);
StateChangeResult::Ok { state: groups_y }
}
Err(err) => {
groups_y.insert(group_id, members_y);
StateChangeResult::Error {
state: groups_y,
error: err,
}
}
}
}
pub(crate) fn apply_remove_unsafe<ID, C>(
mut groups_y: GroupStates<ID, C>,
group_id: ID,
removed: GroupMember<ID>,
) -> GroupStates<ID, C>
where
ID: IdentityHandle,
C: Conditions,
{
let mut members_y = groups_y
.remove(&group_id)
.expect("group already present in states map");
members_y.members.entry(removed).and_modify(|state| {
if state.member_counter % 2 != 0 {
state.member_counter += 1
}
});
groups_y.insert(group_id, members_y);
groups_y
}
pub enum StateChangeResult<ID, C>
where
ID: IdentityHandle,
C: Conditions,
{
Ok { state: GroupStates<ID, C> },
Error {
state: GroupStates<ID, C>,
#[allow(unused)]
error: GroupMembershipError<GroupMember<ID>>,
},
Filtered { state: GroupStates<ID, C> },
}
impl<ID, C> StateChangeResult<ID, C>
where
ID: IdentityHandle,
C: Conditions,
{
pub fn state(&self) -> &GroupStates<ID, C> {
match self {
StateChangeResult::Ok { state }
| StateChangeResult::Error { state, .. }
| StateChangeResult::Filtered { state } => state,
}
}
}
#[cfg(test)]
pub(crate) mod tests {
pub use p2panda_core::cbor::{decode_cbor, encode_cbor};
use crate::Access;
use crate::group::{GroupCrdtError, GroupMember, GroupMembershipError};
use crate::test_utils::{
TestGroup, TestGroupState, add_member, create_group, demote_member, promote_member,
remove_member,
};
use crate::traits::Operation;
const G1: char = '1';
const G2: char = '2';
const G3: char = '3';
const G4: char = '4';
const ALICE: char = 'A';
const BOB: char = 'B';
const CLAIRE: char = 'C';
const DAN: char = 'D';
const EVE: char = 'E';
#[test]
fn group_operations() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::read(),
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let mut members = y_ii.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (BOB, Access::read())]
);
let op3 = add_member(
ALICE,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::write(),
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let mut members = y_iii.members(G1);
members.sort();
assert_eq!(
members,
vec![
(ALICE, Access::manage()),
(BOB, Access::read()),
(CLAIRE, Access::write())
]
);
let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op3.id()]);
let y_iv = TestGroup::process(y_iii, &op4).unwrap();
let mut members = y_iv.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (CLAIRE, Access::write())]
);
}
#[test]
fn self_remove() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let op2 = remove_member(ALICE, 1, G1, GroupMember::Individual(ALICE), vec![op1.id()]);
let y_i = TestGroup::process(y_i, &op2).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![]);
}
#[test]
fn concurrent_removal() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let mut members = y_ii.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (BOB, Access::manage())]
);
let op3 = add_member(
BOB,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::write(),
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let mut members = y_iii.members(G1);
members.sort();
assert_eq!(
members,
vec![
(ALICE, Access::manage()),
(BOB, Access::manage()),
(CLAIRE, Access::write())
]
);
let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
let y_iv = TestGroup::process(y_iii, &op4).unwrap();
let mut members = y_iv.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
}
#[test]
fn mutual_concurrent_removal() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let mut members = y_ii.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (BOB, Access::manage())]
);
let op3 = add_member(
BOB,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::manage(),
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let mut members = y_iii.members(G1);
members.sort();
assert_eq!(
members,
vec![
(ALICE, Access::manage()),
(BOB, Access::manage()),
(CLAIRE, Access::manage())
]
);
let op4 = remove_member(BOB, 3, G1, GroupMember::Individual(CLAIRE), vec![op3.id()]);
let y_iv = TestGroup::process(y_iii, &op4).unwrap();
let mut members = y_iv.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (BOB, Access::manage())]
);
let op5 = remove_member(CLAIRE, 4, G1, GroupMember::Individual(BOB), vec![op3.id()]);
let y_v = TestGroup::process(y_iv, &op5).unwrap();
let mut members = y_v.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
}
#[test]
fn nested_groups() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let mut members = y_i.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let op2 = create_group(
BOB,
1,
G2,
vec![(GroupMember::Individual(BOB), Access::manage())],
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let mut members = y_ii.members(G2);
members.sort();
assert_eq!(members, vec![(BOB, Access::manage())]);
let op3 = add_member(
ALICE,
2,
G1,
GroupMember::Group(G2),
Access::read(),
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let mut individuals = y_iii.members(G1);
individuals.sort();
assert_eq!(
individuals,
vec![(ALICE, Access::manage()), (BOB, Access::read())]
);
let mut groups = y_iii.groups(G1);
groups.sort();
assert_eq!(groups, vec![(G2, Access::read())]);
}
#[test]
fn error_on_unauthorized_add() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::read(),
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3 = add_member(
BOB,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::read(),
vec![op2.id()],
);
assert!(TestGroup::process(y_ii, &op3).is_err());
}
#[test]
fn error_on_remove_non_member() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = remove_member(ALICE, 1, G1, GroupMember::Individual(BOB), vec![op1.id()]);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn error_on_promote_non_member() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = promote_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn error_on_add_manager_group() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Group(BOB),
Access::manage(),
vec![op1.id()],
);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn error_on_demote_non_member() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = demote_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::read(),
vec![op1.id()],
);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn error_on_add_existing_member() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(ALICE),
Access::manage(),
vec![op1.id()],
);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn error_on_remove_nonexistent_subgroup() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = remove_member(ALICE, 1, G1, GroupMember::Group(G2), vec![op1.id()]);
assert!(TestGroup::process(y_i, &op2).is_err());
}
#[test]
fn deeply_nested_groups_with_removals() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = create_group(
BOB,
1,
G2,
vec![(GroupMember::Individual(BOB), Access::manage())],
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3 = create_group(
CLAIRE,
2,
G3,
vec![(GroupMember::Individual(CLAIRE), Access::manage())],
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let op4 = create_group(
DAN,
3,
G4,
vec![(GroupMember::Individual(DAN), Access::write())],
vec![op3.id()],
);
let y_iv = TestGroup::process(y_iii, &op4).unwrap();
let op5 = add_member(
CLAIRE,
4,
G3,
GroupMember::Group(G4),
Access::read(),
vec![op4.id()],
);
let y_v = TestGroup::process(y_iv, &op5).unwrap();
let op6 = add_member(
BOB,
5,
G2,
GroupMember::Group(G3),
Access::write(),
vec![op5.id()],
);
let y_vi = TestGroup::process(y_v, &op6).unwrap();
let op7 = add_member(
ALICE,
6,
G1,
GroupMember::Group(G2),
Access::read(),
vec![op6.id()],
);
let y_vii = TestGroup::process(y_vi, &op7).unwrap();
let mut members = y_vii.members(G1);
members.sort();
assert_eq!(
members,
vec![
(ALICE, Access::manage()),
(BOB, Access::read()),
(CLAIRE, Access::read()),
(DAN, Access::read()),
]
);
let op8 = remove_member(BOB, 7, G2, GroupMember::Group(G3), vec![op7.id()]);
let y_viii = TestGroup::process(y_vii, &op8).unwrap();
let mut members_after_removal = y_viii.members(G1);
members_after_removal.sort();
assert_eq!(
members_after_removal,
vec![(ALICE, Access::manage()), (BOB, Access::read()),]
);
}
#[test]
fn nested_groups_with_concurrent_removal_and_promotion() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = create_group(
BOB,
1,
G2,
vec![(GroupMember::Individual(BOB), Access::manage())],
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3 = create_group(
CLAIRE,
2,
G3,
vec![(GroupMember::Individual(CLAIRE), Access::manage())],
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii, &op3).unwrap();
let op4 = add_member(
CLAIRE,
3,
G3,
GroupMember::Individual(DAN),
Access::write(),
vec![op3.id()],
);
let y_iv = TestGroup::process(y_iii, &op4).unwrap();
let op5 = add_member(
BOB,
4,
G2,
GroupMember::Group(G3),
Access::write(),
vec![op4.id()],
);
let y_v = TestGroup::process(y_iv, &op5).unwrap();
let op6 = add_member(
BOB,
5,
G2,
GroupMember::Individual(CLAIRE),
Access::read(),
vec![op5.id()],
);
let y_vi = TestGroup::process(y_v, &op6).unwrap();
let op7 = add_member(
ALICE,
6,
G1,
GroupMember::Group(G2),
Access::read(),
vec![op6.id()],
);
let y_vii = TestGroup::process(y_vi, &op7).unwrap();
let mut members = y_vii.members(G1);
members.sort();
assert_eq!(
members,
vec![
(ALICE, Access::manage()),
(BOB, Access::read()),
(CLAIRE, Access::read()),
(DAN, Access::read()),
]
);
let op8_remove_g2 = remove_member(ALICE, 7, G1, GroupMember::Group(G2), vec![op7.id()]);
let op9_promote_claire = promote_member(
BOB,
8,
G2,
GroupMember::Individual(CLAIRE),
Access::manage(),
vec![op7.id()],
);
let y_after_remove = TestGroup::process(y_vii.clone(), &op8_remove_g2).unwrap();
let mut members = y_after_remove.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())]);
let y_after_both = TestGroup::process(y_after_remove, &op9_promote_claire).unwrap();
let mut g1_members = y_after_both.members(G1);
g1_members.sort();
assert_eq!(g1_members, vec![(ALICE, Access::manage())]);
let mut g2_members = y_after_both.members(G2);
g2_members.sort();
assert_eq!(
g2_members,
vec![
(BOB, Access::manage()),
(CLAIRE, Access::manage()),
(DAN, Access::write()),
]
);
}
#[test]
fn concurrent_removal_ooo_processing() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3 = add_member(
BOB,
2,
G1,
GroupMember::Individual(CLAIRE),
Access::read(),
vec![op2.id()],
);
let op4 = remove_member(ALICE, 3, G1, GroupMember::Individual(BOB), vec![op2.id()]);
let y_iii_a = TestGroup::process(y_ii.clone(), &op3).unwrap();
let y_iv_a = TestGroup::process(y_iii_a, &op4).unwrap();
let y_iii_b = TestGroup::process(y_ii.clone(), &op4).unwrap();
let y_iv_b = TestGroup::process(y_iii_b, &op3).unwrap();
for (_, y) in [y_iv_a, y_iv_b].into_iter().enumerate() {
let mut members = y.members(G1);
members.sort();
assert_eq!(members, vec![(ALICE, Access::manage())],);
}
}
#[test]
fn concurrent_add_with_insufficient_access() {
let y0 = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y1 = TestGroup::process(y0, &op1).unwrap();
let op2 = add_member(
ALICE,
1,
G1,
GroupMember::Individual(BOB),
Access::manage(),
vec![op1.id()],
);
let op3 = add_member(
BOB,
2,
G1,
GroupMember::Individual(EVE),
Access::read(),
vec![op1.id()],
);
let result = TestGroup::process(y1.clone(), &op3);
std::assert_matches!(
result,
Err(GroupCrdtError::StateChangeError(
_,
GroupMembershipError::UnrecognisedActor(_)
))
);
let y1_alt = TestGroup::process(y1, &op2).unwrap();
let result = TestGroup::process(y1_alt.clone(), &op3);
std::assert_matches!(
result,
Err(GroupCrdtError::StateChangeError(
_,
GroupMembershipError::UnrecognisedActor(_)
))
);
let mut members = y1_alt.members(G1);
members.sort();
assert_eq!(
members,
vec![(ALICE, Access::manage()), (BOB, Access::manage())]
);
}
#[test]
fn add_group_with_concurrent_change() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = create_group(
BOB,
1,
G2,
vec![(GroupMember::Individual(BOB), Access::manage())],
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3a = add_member(
ALICE,
2,
G1,
GroupMember::Group(G2),
Access::read(),
vec![op2.id()],
);
let op3b = add_member(
BOB,
3,
G2,
GroupMember::Individual(CLAIRE),
Access::write(),
vec![op2.id()],
);
let y_iii = TestGroup::process(y_ii.clone(), &op3a).unwrap();
let y_iv = TestGroup::process(y_iii, &op3b).unwrap();
let mut members_1 = y_iv.members(G1);
members_1.sort();
assert_eq!(
members_1,
vec![
(ALICE, Access::manage()),
(BOB, Access::read()),
(CLAIRE, Access::read())
]
);
let y_iii_alt = TestGroup::process(y_ii.clone(), &op3b).unwrap();
let y_iv_alt = TestGroup::process(y_iii_alt, &op3a).unwrap();
let mut members_1 = y_iv_alt.members(G1);
members_1.sort();
assert_eq!(
members_1,
vec![
(ALICE, Access::manage()),
(BOB, Access::read()),
(CLAIRE, Access::read())
]
);
}
#[test]
fn nested_group_cycle_error() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let op2 = create_group(
BOB,
1,
G2,
vec![
(GroupMember::Individual(BOB), Access::manage()),
(GroupMember::Group(G1), Access::read()),
],
vec![op1.id()],
);
let y_ii = TestGroup::process(y_i, &op2).unwrap();
let op3 = add_member(
ALICE,
2,
G1,
GroupMember::Group(G2),
Access::read(),
vec![op2.id()],
);
let result = TestGroup::process(y_ii, &op3);
assert!(
result.is_err(),
"Creating a group cycle should cause an error"
);
}
#[test]
fn serde_to_from_bytes() {
let y = TestGroupState::new();
let op1 = create_group(
ALICE,
0,
G1,
vec![(GroupMember::Individual(ALICE), Access::manage())],
vec![],
);
let y_i = TestGroup::process(y, &op1).unwrap();
let members = y_i.members(G1);
assert_eq!(members, vec![(ALICE, Access::manage())]);
let bytes = encode_cbor(&y_i).unwrap();
let y_i_de: TestGroupState = decode_cbor(&bytes[..]).unwrap();
let members = y_i_de.members(G1);
assert_eq!(members, vec![(ALICE, Access::manage())]);
}
}