use kcode_k1_groups_domain::{
ALL_MODELS, ALL_USERS, Group, GroupId, GroupMemberships, GroupName, GroupRevision, GroupRole,
LOCAL_MODELS, ModelId, TxId, UserId,
};
use kcode_k1_groups_projection_test_contract as projection_contract;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConcurrencyObservation {
pub both_submissions_completed: bool,
pub revoked_actor_rejected: bool,
pub final_role: Option<GroupRole>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MalformedObservation {
pub canonical_callback_failed: bool,
pub facade_unavailable: bool,
pub fresh_replay_failed: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReorgObservation {
pub old_facade_unavailable: bool,
pub projection_cleared: bool,
pub fresh_open_empty: bool,
}
pub trait DriverAdapter {
fn create(&self, owner: UserId, name: GroupName) -> Result<GroupRevision, String>;
fn rename(
&self,
actor: UserId,
group: GroupId,
name: GroupName,
) -> Result<GroupRevision, String>;
fn set_user_role(
&self,
actor: UserId,
group: GroupId,
user: UserId,
role: Option<GroupRole>,
) -> Result<GroupRevision, String>;
fn set_model_membership(
&self,
actor: UserId,
group: GroupId,
model: ModelId,
present: bool,
) -> Result<GroupRevision, String>;
fn get(&self, group: GroupId) -> Result<Option<Group>, String>;
fn groups_for_user(&self, user: UserId) -> Result<Vec<GroupId>, String>;
fn groups_for_model(&self, model: ModelId) -> Result<Vec<GroupId>, String>;
fn memberships(&self, user: UserId, model: ModelId) -> Result<GroupMemberships, String>;
fn restart(&mut self) -> Result<(), String>;
fn concurrency_observation(&mut self) -> Result<ConcurrencyObservation, String>;
fn malformed_observation(&mut self) -> Result<MalformedObservation, String>;
fn reorg_observation(&mut self) -> Result<ReorgObservation, String>;
}
pub fn user(index: usize) -> UserId {
projection_contract::user(index)
}
pub fn model(index: usize) -> ModelId {
projection_contract::model(index)
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct GroupView {
id: GroupId,
name: GroupName,
revision: GroupRevision,
users: Vec<(UserId, GroupRole)>,
models: Vec<ModelId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MembershipView {
revision: Option<TxId>,
user_groups: Vec<GroupId>,
model_groups: Vec<GroupId>,
shared_groups: Vec<GroupId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Snapshot {
group: GroupView,
user_groups: Vec<GroupId>,
model_groups: Vec<GroupId>,
memberships: MembershipView,
}
fn fixture_name(value: &str) -> Result<GroupName, String> {
GroupName::new(value.to_owned()).map_err(|error| format!("fixture name {value:?}: {error}"))
}
fn adapter_result<T>(result: Result<T, String>, context: &str) -> Result<T, String> {
result.map_err(|error| format!("{context}: adapter error: {error}"))
}
fn changed(
result: Result<GroupRevision, String>,
group: GroupId,
previous: Option<&GroupRevision>,
context: &str,
) -> Result<GroupRevision, String> {
let revision = adapter_result(result, context)?;
if revision.group_id() != group {
return Err(format!("{context}: revision belonged to another group"));
}
if previous == Some(&revision) {
return Err(format!("{context}: expected a new revision"));
}
Ok(revision)
}
fn unchanged(
result: Result<GroupRevision, String>,
expected: &GroupRevision,
context: &str,
) -> Result<(), String> {
let actual = adapter_result(result, context)?;
exact(&actual, expected, context)
}
fn rejected(
result: Result<GroupRevision, String>,
expected: &str,
context: &str,
) -> Result<(), String> {
match result {
Err(reason) if reason == expected => Ok(()),
Err(reason) => Err(format!("{context}: expected {expected:?}, got {reason:?}")),
Ok(revision) => Err(format!("{context}: expected rejection, got {revision:?}")),
}
}
fn exact<T: Eq + std::fmt::Debug>(actual: &T, expected: &T, context: &str) -> Result<(), String> {
if actual == expected {
Ok(())
} else {
Err(format!("{context}: expected {expected:?}, got {actual:?}"))
}
}
fn ordered<T: Ord>(mut values: Vec<T>) -> Vec<T> {
values.sort();
values
}
fn group_view(group: &Group) -> GroupView {
let mut users: Vec<_> = group
.users()
.iter()
.map(|entry| (entry.user_id(), entry.role()))
.collect();
users.sort_by_key(|(id, role)| (*id.as_tx_id().as_bytes(), *role));
GroupView {
id: group.id(),
name: group.name().clone(),
revision: group.revision().clone(),
users,
models: ordered(group.models().to_vec()),
}
}
fn membership_view(value: &GroupMemberships) -> MembershipView {
MembershipView {
revision: value.revision(),
user_groups: ordered(value.user_groups().to_vec()),
model_groups: ordered(value.model_groups().to_vec()),
shared_groups: ordered(value.shared_groups().to_vec()),
}
}
fn snapshot<A: DriverAdapter>(
adapter: &A,
group: GroupId,
user: UserId,
model: ModelId,
context: &str,
) -> Result<Snapshot, String> {
let group = match adapter.get(group) {
Ok(Some(value)) => value,
Ok(None) => return Err(format!("{context}: group was absent")),
Err(error) => return Err(format!("{context}: adapter error: {error}")),
};
Ok(Snapshot {
group: group_view(&group),
user_groups: ordered(adapter_result(adapter.groups_for_user(user), context)?),
model_groups: ordered(adapter_result(adapter.groups_for_model(model), context)?),
memberships: membership_view(&adapter_result(adapter.memberships(user, model), context)?),
})
}
fn same_group_queries(before: &Snapshot, after: &Snapshot, context: &str) -> Result<(), String> {
exact(&after.group, &before.group, context)?;
exact(&after.user_groups, &before.user_groups, context)?;
exact(&after.model_groups, &before.model_groups, context)?;
exact(
&after.memberships.user_groups,
&before.memberships.user_groups,
context,
)?;
exact(
&after.memberships.model_groups,
&before.memberships.model_groups,
context,
)?;
exact(
&after.memberships.shared_groups,
&before.memberships.shared_groups,
context,
)
}
pub fn verify<A: DriverAdapter>(adapter: &mut A) -> Result<(), String> {
use GroupRole::{Admin, Owner, User};
let owner = user(1);
let admin = user(2);
let member = user(3);
let temporary = user(4);
let outsider = user(5);
let removed_model = model(1);
let shared_model = model(2);
let original_name = fixture_name("named group")?;
let renamed_name = fixture_name("renamed group")?;
let created = adapter_result(adapter.create(owner, original_name.clone()), "create")?;
let group = created.group_id();
if group.txid() != created.txid() {
return Err("create: group identity did not equal its revision transaction".to_owned());
}
let created_snapshot = snapshot(adapter, group, owner, removed_model, "create queries")?;
exact(&created_snapshot.group.revision, &created, "create queries")?;
exact(
&created_snapshot.group,
&GroupView {
id: group,
name: original_name.clone(),
revision: created.clone(),
users: vec![(owner, Owner)],
models: vec![],
},
"create queries",
)?;
let renamed = changed(
adapter.rename(owner, group, renamed_name.clone()),
group,
Some(&created),
"rename",
)?;
unchanged(
adapter.rename(owner, group, renamed_name.clone()),
&renamed,
"equal rename",
)?;
let after_equal_rename =
snapshot(adapter, group, owner, removed_model, "equal rename queries")?;
exact(
&after_equal_rename.group.revision,
&renamed,
"equal rename queries",
)?;
rejected(
adapter.rename(outsider, group, original_name.clone()),
"actor is not authorized",
"non-owner rename",
)?;
let after_rejected_rename = snapshot(
adapter,
group,
owner,
removed_model,
"non-owner rename queries",
)?;
same_group_queries(
&after_equal_rename,
&after_rejected_rename,
"non-owner rename queries",
)?;
let admin_revision = changed(
adapter.set_user_role(owner, group, admin, Some(Admin)),
group,
Some(&renamed),
"add admin",
)?;
let member_revision = changed(
adapter.set_user_role(owner, group, member, Some(User)),
group,
Some(&admin_revision),
"add user",
)?;
let temporary_revision = changed(
adapter.set_user_role(admin, group, temporary, Some(User)),
group,
Some(&member_revision),
"admin add",
)?;
let removed_revision = changed(
adapter.set_user_role(admin, group, temporary, None),
group,
Some(&temporary_revision),
"admin remove",
)?;
rejected(
adapter.set_user_role(admin, group, member, Some(Admin)),
"administrator transition is not permitted",
"admin elevate",
)?;
rejected(
adapter.set_user_role(owner, group, owner, None),
"final owner cannot be removed or demoted",
"final owner",
)?;
let model_revision = changed(
adapter.set_model_membership(owner, group, removed_model, true),
group,
Some(&removed_revision),
"model add",
)?;
unchanged(
adapter.set_model_membership(owner, group, removed_model, true),
&model_revision,
"model equal",
)?;
let after_equal_model = snapshot(adapter, group, member, removed_model, "model equal queries")?;
exact(
&after_equal_model.group.revision,
&model_revision,
"model equal queries",
)?;
let removed_model_revision = changed(
adapter.set_model_membership(owner, group, removed_model, false),
group,
Some(&model_revision),
"model remove",
)?;
rejected(
adapter.set_model_membership(admin, group, removed_model, true),
"actor is not authorized",
"model non-owner",
)?;
let final_revision = changed(
adapter.set_model_membership(owner, group, shared_model, true),
group,
Some(&removed_model_revision),
"shared model add",
)?;
let normal = snapshot(adapter, group, member, shared_model, "normal queries")?;
let expected_normal = Snapshot {
group: GroupView {
id: group,
name: renamed_name,
revision: final_revision.clone(),
users: vec![(owner, Owner), (admin, Admin), (member, User)],
models: vec![shared_model],
},
user_groups: ordered(vec![ALL_USERS, group]),
model_groups: ordered(vec![ALL_MODELS, group]),
memberships: MembershipView {
revision: Some(final_revision.txid()),
user_groups: ordered(vec![ALL_USERS, group]),
model_groups: ordered(vec![ALL_MODELS, group]),
shared_groups: vec![group],
},
};
exact(&normal, &expected_normal, "normal queries")?;
for (context, sentinel) in [
("all-users rename", ALL_USERS),
("all-models role", ALL_MODELS),
("local-models model", LOCAL_MODELS),
] {
let result = if sentinel == ALL_USERS {
adapter.rename(owner, sentinel, original_name.clone())
} else if sentinel == ALL_MODELS {
adapter.set_user_role(owner, sentinel, member, None)
} else {
adapter.set_model_membership(owner, sentinel, shared_model, false)
};
rejected(result, "sentinel groups are immutable", context)?;
let after = snapshot(adapter, group, member, shared_model, context)?;
same_group_queries(&normal, &after, context)?;
}
let before_restart = snapshot(adapter, group, member, shared_model, "before restart")?;
if before_restart.memberships.revision.is_none() {
return Err("before restart: missing membership revision".to_owned());
}
same_group_queries(&normal, &before_restart, "before restart")?;
adapter_result(adapter.restart(), "restart")?;
let after_restart = snapshot(adapter, group, member, shared_model, "after restart")?;
exact(&after_restart, &before_restart, "after restart")?;
exact(
&adapter_result(adapter.concurrency_observation(), "concurrency observation")?,
&ConcurrencyObservation {
both_submissions_completed: true,
revoked_actor_rejected: true,
final_role: None,
},
"concurrency observation",
)?;
exact(
&adapter_result(adapter.reorg_observation(), "reorg observation")?,
&ReorgObservation {
old_facade_unavailable: true,
projection_cleared: true,
fresh_open_empty: true,
},
"reorg observation",
)?;
exact(
&adapter_result(adapter.malformed_observation(), "malformed observation")?,
&MalformedObservation {
canonical_callback_failed: true,
facade_unavailable: true,
fresh_replay_failed: true,
},
"malformed observation",
)
}
#[cfg(test)]
mod tests {
use super::{model, user};
use std::hint::black_box;
use std::time::{Duration, Instant};
#[test]
fn helpers_match_contract_and_budget() {
let index = 0x0102_0304usize;
let mut user_bytes = [0; 12];
user_bytes[4..].copy_from_slice(&(index as u64).to_be_bytes());
assert_eq!(user(index).as_tx_id().as_bytes(), &user_bytes);
let mut model_bytes = [0; 32];
model_bytes[24..].copy_from_slice(&(index as u64).to_be_bytes());
assert_eq!(model(index).as_bytes(), &model_bytes);
let started = Instant::now();
for index in 0..100_000 {
black_box((user(index), model(index)));
}
assert!(started.elapsed() < Duration::from_secs(5));
}
}