Skip to main content

kcode_k1_groups_driver_testkit/
lib.rs

1use kcode_k1_groups_domain::{
2    ALL_MODELS, ALL_USERS, Group, GroupId, GroupMemberships, GroupName, GroupRevision, GroupRole,
3    LOCAL_MODELS, ModelId, TxId, UserId,
4};
5use kcode_k1_groups_projection_test_contract as projection_contract;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct ConcurrencyObservation {
9    pub both_submissions_completed: bool,
10    pub revoked_actor_rejected: bool,
11    pub final_role: Option<GroupRole>,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct MalformedObservation {
16    pub canonical_callback_failed: bool,
17    pub facade_unavailable: bool,
18    pub fresh_replay_failed: bool,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub struct ReorgObservation {
23    pub old_facade_unavailable: bool,
24    pub projection_cleared: bool,
25    pub fresh_open_empty: bool,
26}
27
28pub trait DriverAdapter {
29    fn create(&self, owner: UserId, name: GroupName) -> Result<GroupRevision, String>;
30    fn rename(
31        &self,
32        actor: UserId,
33        group: GroupId,
34        name: GroupName,
35    ) -> Result<GroupRevision, String>;
36    fn set_user_role(
37        &self,
38        actor: UserId,
39        group: GroupId,
40        user: UserId,
41        role: Option<GroupRole>,
42    ) -> Result<GroupRevision, String>;
43    fn set_model_membership(
44        &self,
45        actor: UserId,
46        group: GroupId,
47        model: ModelId,
48        present: bool,
49    ) -> Result<GroupRevision, String>;
50    fn get(&self, group: GroupId) -> Result<Option<Group>, String>;
51    fn groups_for_user(&self, user: UserId) -> Result<Vec<GroupId>, String>;
52    fn groups_for_model(&self, model: ModelId) -> Result<Vec<GroupId>, String>;
53    fn memberships(&self, user: UserId, model: ModelId) -> Result<GroupMemberships, String>;
54    fn restart(&mut self) -> Result<(), String>;
55    fn concurrency_observation(&mut self) -> Result<ConcurrencyObservation, String>;
56    fn malformed_observation(&mut self) -> Result<MalformedObservation, String>;
57    fn reorg_observation(&mut self) -> Result<ReorgObservation, String>;
58}
59
60pub fn user(index: usize) -> UserId {
61    projection_contract::user(index)
62}
63
64pub fn model(index: usize) -> ModelId {
65    projection_contract::model(index)
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69struct GroupView {
70    id: GroupId,
71    name: GroupName,
72    revision: GroupRevision,
73    users: Vec<(UserId, GroupRole)>,
74    models: Vec<ModelId>,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
78struct MembershipView {
79    revision: Option<TxId>,
80    user_groups: Vec<GroupId>,
81    model_groups: Vec<GroupId>,
82    shared_groups: Vec<GroupId>,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86struct Snapshot {
87    group: GroupView,
88    user_groups: Vec<GroupId>,
89    model_groups: Vec<GroupId>,
90    memberships: MembershipView,
91}
92
93fn fixture_name(value: &str) -> Result<GroupName, String> {
94    GroupName::new(value.to_owned()).map_err(|error| format!("fixture name {value:?}: {error}"))
95}
96
97fn adapter_result<T>(result: Result<T, String>, context: &str) -> Result<T, String> {
98    result.map_err(|error| format!("{context}: adapter error: {error}"))
99}
100
101fn changed(
102    result: Result<GroupRevision, String>,
103    group: GroupId,
104    previous: Option<&GroupRevision>,
105    context: &str,
106) -> Result<GroupRevision, String> {
107    let revision = adapter_result(result, context)?;
108    if revision.group_id() != group {
109        return Err(format!("{context}: revision belonged to another group"));
110    }
111    if previous == Some(&revision) {
112        return Err(format!("{context}: expected a new revision"));
113    }
114    Ok(revision)
115}
116
117fn unchanged(
118    result: Result<GroupRevision, String>,
119    expected: &GroupRevision,
120    context: &str,
121) -> Result<(), String> {
122    let actual = adapter_result(result, context)?;
123    exact(&actual, expected, context)
124}
125
126fn rejected(
127    result: Result<GroupRevision, String>,
128    expected: &str,
129    context: &str,
130) -> Result<(), String> {
131    match result {
132        Err(reason) if reason == expected => Ok(()),
133        Err(reason) => Err(format!("{context}: expected {expected:?}, got {reason:?}")),
134        Ok(revision) => Err(format!("{context}: expected rejection, got {revision:?}")),
135    }
136}
137
138fn exact<T: Eq + std::fmt::Debug>(actual: &T, expected: &T, context: &str) -> Result<(), String> {
139    if actual == expected {
140        Ok(())
141    } else {
142        Err(format!("{context}: expected {expected:?}, got {actual:?}"))
143    }
144}
145
146fn ordered<T: Ord>(mut values: Vec<T>) -> Vec<T> {
147    values.sort();
148    values
149}
150
151fn group_view(group: &Group) -> GroupView {
152    let mut users: Vec<_> = group
153        .users()
154        .iter()
155        .map(|entry| (entry.user_id(), entry.role()))
156        .collect();
157    users.sort_by_key(|(id, role)| (*id.as_tx_id().as_bytes(), *role));
158    GroupView {
159        id: group.id(),
160        name: group.name().clone(),
161        revision: group.revision().clone(),
162        users,
163        models: ordered(group.models().to_vec()),
164    }
165}
166
167fn membership_view(value: &GroupMemberships) -> MembershipView {
168    MembershipView {
169        revision: value.revision(),
170        user_groups: ordered(value.user_groups().to_vec()),
171        model_groups: ordered(value.model_groups().to_vec()),
172        shared_groups: ordered(value.shared_groups().to_vec()),
173    }
174}
175
176fn snapshot<A: DriverAdapter>(
177    adapter: &A,
178    group: GroupId,
179    user: UserId,
180    model: ModelId,
181    context: &str,
182) -> Result<Snapshot, String> {
183    let group = match adapter.get(group) {
184        Ok(Some(value)) => value,
185        Ok(None) => return Err(format!("{context}: group was absent")),
186        Err(error) => return Err(format!("{context}: adapter error: {error}")),
187    };
188    Ok(Snapshot {
189        group: group_view(&group),
190        user_groups: ordered(adapter_result(adapter.groups_for_user(user), context)?),
191        model_groups: ordered(adapter_result(adapter.groups_for_model(model), context)?),
192        memberships: membership_view(&adapter_result(adapter.memberships(user, model), context)?),
193    })
194}
195
196fn same_group_queries(before: &Snapshot, after: &Snapshot, context: &str) -> Result<(), String> {
197    exact(&after.group, &before.group, context)?;
198    exact(&after.user_groups, &before.user_groups, context)?;
199    exact(&after.model_groups, &before.model_groups, context)?;
200    exact(
201        &after.memberships.user_groups,
202        &before.memberships.user_groups,
203        context,
204    )?;
205    exact(
206        &after.memberships.model_groups,
207        &before.memberships.model_groups,
208        context,
209    )?;
210    exact(
211        &after.memberships.shared_groups,
212        &before.memberships.shared_groups,
213        context,
214    )
215}
216
217pub fn verify<A: DriverAdapter>(adapter: &mut A) -> Result<(), String> {
218    use GroupRole::{Admin, Owner, User};
219
220    let owner = user(1);
221    let admin = user(2);
222    let member = user(3);
223    let temporary = user(4);
224    let outsider = user(5);
225    let removed_model = model(1);
226    let shared_model = model(2);
227    let original_name = fixture_name("named group")?;
228    let renamed_name = fixture_name("renamed group")?;
229
230    let created = adapter_result(adapter.create(owner, original_name.clone()), "create")?;
231    let group = created.group_id();
232    if group.txid() != created.txid() {
233        return Err("create: group identity did not equal its revision transaction".to_owned());
234    }
235    let created_snapshot = snapshot(adapter, group, owner, removed_model, "create queries")?;
236    exact(&created_snapshot.group.revision, &created, "create queries")?;
237    exact(
238        &created_snapshot.group,
239        &GroupView {
240            id: group,
241            name: original_name.clone(),
242            revision: created.clone(),
243            users: vec![(owner, Owner)],
244            models: vec![],
245        },
246        "create queries",
247    )?;
248
249    let renamed = changed(
250        adapter.rename(owner, group, renamed_name.clone()),
251        group,
252        Some(&created),
253        "rename",
254    )?;
255    unchanged(
256        adapter.rename(owner, group, renamed_name.clone()),
257        &renamed,
258        "equal rename",
259    )?;
260    let after_equal_rename =
261        snapshot(adapter, group, owner, removed_model, "equal rename queries")?;
262    exact(
263        &after_equal_rename.group.revision,
264        &renamed,
265        "equal rename queries",
266    )?;
267    rejected(
268        adapter.rename(outsider, group, original_name.clone()),
269        "actor is not authorized",
270        "non-owner rename",
271    )?;
272    let after_rejected_rename = snapshot(
273        adapter,
274        group,
275        owner,
276        removed_model,
277        "non-owner rename queries",
278    )?;
279    same_group_queries(
280        &after_equal_rename,
281        &after_rejected_rename,
282        "non-owner rename queries",
283    )?;
284
285    let admin_revision = changed(
286        adapter.set_user_role(owner, group, admin, Some(Admin)),
287        group,
288        Some(&renamed),
289        "add admin",
290    )?;
291    let member_revision = changed(
292        adapter.set_user_role(owner, group, member, Some(User)),
293        group,
294        Some(&admin_revision),
295        "add user",
296    )?;
297    let temporary_revision = changed(
298        adapter.set_user_role(admin, group, temporary, Some(User)),
299        group,
300        Some(&member_revision),
301        "admin add",
302    )?;
303    let removed_revision = changed(
304        adapter.set_user_role(admin, group, temporary, None),
305        group,
306        Some(&temporary_revision),
307        "admin remove",
308    )?;
309    rejected(
310        adapter.set_user_role(admin, group, member, Some(Admin)),
311        "administrator transition is not permitted",
312        "admin elevate",
313    )?;
314    rejected(
315        adapter.set_user_role(owner, group, owner, None),
316        "final owner cannot be removed or demoted",
317        "final owner",
318    )?;
319
320    let model_revision = changed(
321        adapter.set_model_membership(owner, group, removed_model, true),
322        group,
323        Some(&removed_revision),
324        "model add",
325    )?;
326    unchanged(
327        adapter.set_model_membership(owner, group, removed_model, true),
328        &model_revision,
329        "model equal",
330    )?;
331    let after_equal_model = snapshot(adapter, group, member, removed_model, "model equal queries")?;
332    exact(
333        &after_equal_model.group.revision,
334        &model_revision,
335        "model equal queries",
336    )?;
337    let removed_model_revision = changed(
338        adapter.set_model_membership(owner, group, removed_model, false),
339        group,
340        Some(&model_revision),
341        "model remove",
342    )?;
343    rejected(
344        adapter.set_model_membership(admin, group, removed_model, true),
345        "actor is not authorized",
346        "model non-owner",
347    )?;
348    let final_revision = changed(
349        adapter.set_model_membership(owner, group, shared_model, true),
350        group,
351        Some(&removed_model_revision),
352        "shared model add",
353    )?;
354
355    let normal = snapshot(adapter, group, member, shared_model, "normal queries")?;
356    let expected_normal = Snapshot {
357        group: GroupView {
358            id: group,
359            name: renamed_name,
360            revision: final_revision.clone(),
361            users: vec![(owner, Owner), (admin, Admin), (member, User)],
362            models: vec![shared_model],
363        },
364        user_groups: ordered(vec![ALL_USERS, group]),
365        model_groups: ordered(vec![ALL_MODELS, group]),
366        memberships: MembershipView {
367            revision: Some(final_revision.txid()),
368            user_groups: ordered(vec![ALL_USERS, group]),
369            model_groups: ordered(vec![ALL_MODELS, group]),
370            shared_groups: vec![group],
371        },
372    };
373    exact(&normal, &expected_normal, "normal queries")?;
374
375    for (context, sentinel) in [
376        ("all-users rename", ALL_USERS),
377        ("all-models role", ALL_MODELS),
378        ("local-models model", LOCAL_MODELS),
379    ] {
380        let result = if sentinel == ALL_USERS {
381            adapter.rename(owner, sentinel, original_name.clone())
382        } else if sentinel == ALL_MODELS {
383            adapter.set_user_role(owner, sentinel, member, None)
384        } else {
385            adapter.set_model_membership(owner, sentinel, shared_model, false)
386        };
387        rejected(result, "sentinel groups are immutable", context)?;
388        let after = snapshot(adapter, group, member, shared_model, context)?;
389        same_group_queries(&normal, &after, context)?;
390    }
391
392    let before_restart = snapshot(adapter, group, member, shared_model, "before restart")?;
393    if before_restart.memberships.revision.is_none() {
394        return Err("before restart: missing membership revision".to_owned());
395    }
396    same_group_queries(&normal, &before_restart, "before restart")?;
397    adapter_result(adapter.restart(), "restart")?;
398    let after_restart = snapshot(adapter, group, member, shared_model, "after restart")?;
399    exact(&after_restart, &before_restart, "after restart")?;
400
401    exact(
402        &adapter_result(adapter.concurrency_observation(), "concurrency observation")?,
403        &ConcurrencyObservation {
404            both_submissions_completed: true,
405            revoked_actor_rejected: true,
406            final_role: None,
407        },
408        "concurrency observation",
409    )?;
410    exact(
411        &adapter_result(adapter.reorg_observation(), "reorg observation")?,
412        &ReorgObservation {
413            old_facade_unavailable: true,
414            projection_cleared: true,
415            fresh_open_empty: true,
416        },
417        "reorg observation",
418    )?;
419    exact(
420        &adapter_result(adapter.malformed_observation(), "malformed observation")?,
421        &MalformedObservation {
422            canonical_callback_failed: true,
423            facade_unavailable: true,
424            fresh_replay_failed: true,
425        },
426        "malformed observation",
427    )
428}
429
430#[cfg(test)]
431mod tests {
432    use super::{model, user};
433    use std::hint::black_box;
434    use std::time::{Duration, Instant};
435
436    #[test]
437    fn helpers_match_contract_and_budget() {
438        let index = 0x0102_0304usize;
439        let mut user_bytes = [0; 12];
440        user_bytes[4..].copy_from_slice(&(index as u64).to_be_bytes());
441        assert_eq!(user(index).as_tx_id().as_bytes(), &user_bytes);
442        let mut model_bytes = [0; 32];
443        model_bytes[24..].copy_from_slice(&(index as u64).to_be_bytes());
444        assert_eq!(model(index).as_bytes(), &model_bytes);
445        let started = Instant::now();
446        for index in 0..100_000 {
447            black_box((user(index), model(index)));
448        }
449        assert!(started.elapsed() < Duration::from_secs(5));
450    }
451}