Skip to main content

kcode_k1_access_kmap_testkit/
lib.rs

1//! Real-stack conformance for the public K1 Access–Kmap facade.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use kcode_k1_access::{
7    AccessId, Authorizations, K1Access, ModelId, OwnerSubject, SubsystemId, Target, TxId, UserId,
8    ViewerSubject,
9};
10use kcode_k1_access_kmap::{
11    AccessProfile, ConnectionSpec, ConnectionTier, K1AccessKmap, Measurement,
12    MeasurementImportance, Node, OpenMode, Weight,
13};
14use kcode_k1_access_profiles::{
15    AuthorizationProfile, K1AccessProfiles, ProfileOwner, ProfileSelection, ProfileViewer,
16};
17use kcode_k1_groups::{ALL_MODELS, K1Groups};
18use kcode_k1_kmap::K1Kmap;
19use kcode_k1_peering::K1Peering;
20use kcode_k1_txn_ordering::K1TxnOrdering;
21
22const ORIGINAL_TITLE: &str = "shared root";
23const UPDATED_TITLE: &str = "shared root after authorized update";
24
25/// Verifies the facade's authority, projection, mutation, and restart behavior.
26pub fn verify() -> Result<(), String> {
27    let temporary =
28        tempfile::tempdir().map_err(|_| "create temporary root: local I/O error".to_string())?;
29    let root = temporary.path().to_path_buf();
30    let stack = Stack::open(&root)?;
31
32    let visible = create_leaf(
33        &stack,
34        user(1),
35        ProfileSelection::Inline(shared_profile_definition()?),
36        "visible child",
37    )?;
38    let hidden = create_leaf(&stack, user(1), ProfileSelection::BuiltIn, "hidden child")?;
39    let third_party = create_leaf(
40        &stack,
41        user(3),
42        ProfileSelection::BuiltIn,
43        "third-party child",
44    )?;
45    let root_id = create(
46        &stack,
47        user(1),
48        ProfileSelection::Inline(shared_profile_definition()?),
49        ORIGINAL_TITLE,
50        vec![edge(&visible), edge(&hidden)],
51        "create shared root",
52    )?;
53
54    verify_projection(&stack, &root_id, &visible, &hidden, ORIGINAL_TITLE)?;
55    verify_open(&stack, &root_id, &visible, &hidden)?;
56    verify_update_authority(&stack, &root_id, &visible, &hidden, &third_party)?;
57
58    let before_measurement = visible_weight(&stack, &root_id)?;
59    expect_unavailable(
60        stack
61            .facade
62            .apply_measurements(user(2), model(2), vec![measurement(&root_id, &visible)]),
63        "viewer measurement",
64    )?;
65    equal_weight(
66        visible_weight(&stack, &root_id)?,
67        before_measurement,
68        "denied measurement changed weight",
69    )?;
70    operation(
71        stack
72            .facade
73            .apply_measurements(user(1), model(1), vec![measurement(&root_id, &visible)]),
74        "apply owner measurement",
75    )?;
76    let measured_weight = visible_weight(&stack, &root_id)?;
77    require(
78        measured_weight != before_measurement,
79        "owner measurement did not change weight",
80    )?;
81
82    expect_unavailable(
83        stack.facade.create_node(
84            user(1),
85            model(1),
86            ProfileSelection::BuiltIn,
87            "rejected creation".to_string(),
88            "rejected navigation".to_string(),
89            "rejected narrative".to_string(),
90            vec![edge(&third_party)],
91        ),
92        "creation with third-party target",
93    )?;
94    verify_bad_wrappers(&stack)?;
95
96    drop(stack);
97    let reopened = Stack::open(&root)?;
98    let after_restart = get(&reopened, user(2), model(2), &root_id, "restart root")?;
99    require(
100        after_restart.title == UPDATED_TITLE,
101        "authorized title did not survive restart",
102    )?;
103    equal_weight(
104        visible_edge(
105            &after_restart,
106            &visible,
107            "restart projection did not retain its single visible edge",
108        )?,
109        measured_weight,
110        "measured weight did not survive restart",
111    )?;
112    expect_unavailable(
113        reopened.facade.get_node(user(2), model(2), hidden),
114        "restart hidden direct read",
115    )
116}
117
118struct Stack {
119    _ordering: Arc<K1TxnOrdering>,
120    _peering: Arc<K1Peering>,
121    _groups: Arc<K1Groups>,
122    _profiles: Arc<K1AccessProfiles>,
123    access: Arc<K1Access>,
124    _kmap: Arc<K1Kmap>,
125    facade: K1AccessKmap,
126}
127
128impl Stack {
129    fn open(root: &Path) -> Result<Self, String> {
130        let path = |name| root.join(name);
131        let ordering = Arc::new(operation(
132            K1TxnOrdering::open(&path("ordering")),
133            "open transaction ordering",
134        )?);
135        let peering = Arc::new(operation(
136            K1Peering::open(&path("peering"), ordering.clone()),
137            "open peering",
138        )?);
139        let groups = Arc::new(operation(
140            K1Groups::open(&path("groups"), ordering.clone(), peering.clone()),
141            "open groups",
142        )?);
143        let profiles = Arc::new(operation(
144            K1AccessProfiles::open(&path("profiles"), ordering.clone(), peering.clone()),
145            "open access profiles",
146        )?);
147        let access = Arc::new(operation(
148            K1Access::open(
149                &path("access"),
150                ordering.clone(),
151                peering.clone(),
152                groups.clone(),
153            ),
154            "open access",
155        )?);
156        let kmap = Arc::new(operation(
157            K1Kmap::open(&path("kmap"), ordering.clone(), peering.clone()),
158            "open kmap",
159        )?);
160        let facade = operation(
161            K1AccessKmap::open(access.clone(), profiles.clone(), kmap.clone()),
162            "open access-kmap facade",
163        )?;
164        Ok(Self {
165            _ordering: ordering,
166            _peering: peering,
167            _groups: groups,
168            _profiles: profiles,
169            access,
170            _kmap: kmap,
171            facade,
172        })
173    }
174}
175
176fn user(byte: u8) -> UserId {
177    UserId::from_tx_id(TxId::from_bytes([byte; 12]))
178}
179
180fn model(byte: u8) -> ModelId {
181    ModelId::from_bytes([byte; 32])
182}
183
184fn shared_profile_definition() -> Result<AuthorizationProfile, String> {
185    operation(
186        AuthorizationProfile::new(
187            vec![ProfileOwner::RequestUser],
188            vec![
189                ProfileViewer::User(user(2)),
190                ProfileViewer::Group(ALL_MODELS),
191            ],
192        ),
193        "build shared authorization profile",
194    )
195}
196
197fn edge(target: &AccessId) -> ConnectionSpec {
198    ConnectionSpec {
199        target: *target,
200        tier: ConnectionTier::Navigation,
201    }
202}
203
204fn measurement(source: &AccessId, target: &AccessId) -> Measurement {
205    Measurement {
206        source: *source,
207        target: *target,
208        useful: false,
209        importance: MeasurementImportance::NonCritical,
210    }
211}
212
213fn create_leaf(
214    stack: &Stack,
215    owner: UserId,
216    profile: AccessProfile,
217    title: &str,
218) -> Result<AccessId, String> {
219    create(
220        stack,
221        owner,
222        profile,
223        title,
224        Vec::new(),
225        "create fixture leaf",
226    )
227}
228
229fn create(
230    stack: &Stack,
231    owner: UserId,
232    profile: AccessProfile,
233    title: &str,
234    connections: Vec<ConnectionSpec>,
235    label: &str,
236) -> Result<AccessId, String> {
237    let revision = operation(
238        stack.facade.create_node(
239            owner,
240            model(1),
241            profile,
242            title.to_string(),
243            format!("navigation for {title}"),
244            format!("narrative for {title}"),
245            connections,
246        ),
247        label,
248    )?;
249    Ok(revision.access_id())
250}
251
252fn get(
253    stack: &Stack,
254    request_user: UserId,
255    request_model: ModelId,
256    id: &AccessId,
257    label: &str,
258) -> Result<Node, String> {
259    operation(
260        stack.facade.get_node(request_user, request_model, *id),
261        label,
262    )
263}
264
265fn visible_edge(node: &Node, visible: &AccessId, message: &str) -> Result<Weight, String> {
266    require(
267        node.connections.len() == 1 && node.connections[0].target == *visible,
268        message,
269    )?;
270    Ok(node.connections[0].weight)
271}
272
273fn verify_projection(
274    stack: &Stack,
275    root: &AccessId,
276    visible: &AccessId,
277    hidden: &AccessId,
278    expected_title: &str,
279) -> Result<(), String> {
280    let projected = get(stack, user(2), model(2), root, "get projected root")?;
281    require(
282        projected.access_id == *root,
283        "projection returned wrong root",
284    )?;
285    require(
286        projected.title == expected_title,
287        "projection changed title",
288    )?;
289    visible_edge(
290        &projected,
291        visible,
292        "projection did not preserve exactly the visible edge",
293    )?;
294    expect_unavailable(
295        stack.facade.get_node(user(2), model(2), *hidden),
296        "hidden direct read",
297    )
298}
299
300fn verify_open(
301    stack: &Stack,
302    root: &AccessId,
303    visible: &AccessId,
304    hidden: &AccessId,
305) -> Result<(), String> {
306    let opened = operation(
307        stack
308            .facade
309            .open_node(user(2), model(2), *root, 10.0, 0.0, OpenMode::Full),
310        "open projected root",
311    )?;
312    let contains = |id| opened.nodes.iter().any(|node| &node.access_id == id);
313    require(
314        contains(root)
315            && contains(visible)
316            && !contains(hidden)
317            && opened
318                .nodes
319                .iter()
320                .all(|node| &node.access_id == root || &node.access_id == visible),
321        "full traversal projection was incorrect",
322    )
323}
324
325fn verify_update_authority(
326    stack: &Stack,
327    root: &AccessId,
328    visible: &AccessId,
329    hidden: &AccessId,
330    third_party: &AccessId,
331) -> Result<(), String> {
332    expect_unavailable(
333        stack.facade.update_node(
334            user(2),
335            model(2),
336            *root,
337            Some("viewer edit".to_string()),
338            None,
339            None,
340            Vec::new(),
341        ),
342        "viewer update",
343    )?;
344    let unchanged = get(stack, user(1), model(1), root, "read after viewer update")?;
345    require(unchanged.title == ORIGINAL_TITLE, "viewer changed title")?;
346    let connections_before = unchanged.connections.clone();
347
348    expect_unavailable(
349        stack.facade.update_node(
350            user(1),
351            model(1),
352            *root,
353            Some("must not commit".to_string()),
354            None,
355            None,
356            vec![edge(visible), edge(hidden), edge(third_party)],
357        ),
358        "update with third-party target",
359    )?;
360    let denied = get(stack, user(1), model(1), root, "read after target denial")?;
361    require(
362        denied.title == ORIGINAL_TITLE && denied.connections == connections_before,
363        "target denial committed changes",
364    )?;
365
366    operation(
367        stack.facade.update_node(
368            user(1),
369            model(1),
370            *root,
371            Some(UPDATED_TITLE.to_string()),
372            None,
373            None,
374            vec![edge(visible), edge(hidden)],
375        ),
376        "authorized title update",
377    )?;
378    let projected = get(stack, user(2), model(2), root, "read authorized title")?;
379    require(
380        projected.title == UPDATED_TITLE,
381        "authorized title is not visible to viewer",
382    )?;
383    visible_edge(
384        &projected,
385        visible,
386        "authorized title update changed projection",
387    )?;
388    Ok(())
389}
390
391fn visible_weight(stack: &Stack, root: &AccessId) -> Result<Weight, String> {
392    let projected = get(stack, user(2), model(2), root, "read visible weight")?;
393    visible_edge(
394        &projected,
395        &projected.connections[0].target,
396        "visible edge missing while reading weight",
397    )
398}
399
400fn verify_bad_wrappers(stack: &Stack) -> Result<(), String> {
401    for (subsystem, bytes, subsystem_label, create_label, read_label) in [
402        (
403            "k1-kmap",
404            vec![91; 11],
405            "build kmap subsystem",
406            "create malformed wrapper",
407            "malformed wrapper read",
408        ),
409        (
410            "not-kmap",
411            vec![92; 12],
412            "build non-kmap subsystem",
413            "create wrong-subsystem wrapper",
414            "wrong-subsystem wrapper read",
415        ),
416    ] {
417        let target = Target::new(
418            operation(SubsystemId::from_str(subsystem), subsystem_label)?,
419            bytes,
420        );
421        let authorizations = operation(
422            Authorizations::new(
423                vec![OwnerSubject::User(user(1))],
424                vec![ViewerSubject::Group(ALL_MODELS)],
425            ),
426            "build wrapper authorizations",
427        )?;
428        let wrapper = operation(stack.access.create(target, authorizations), create_label)?;
429        expect_unavailable(
430            stack
431                .facade
432                .get_node(user(1), model(1), wrapper.access_id()),
433            read_label,
434        )?;
435    }
436    Ok(())
437}
438
439fn operation<T>(result: Result<T, String>, label: &str) -> Result<T, String> {
440    result.map_err(|error| format!("{label}: {error}"))
441}
442
443fn expect_unavailable<T>(result: Result<T, String>, label: &str) -> Result<(), String> {
444    match result {
445        Err(error) if error == "node unavailable" => Ok(()),
446        _ => Err(format!("{label}: expected exact generic unavailability")),
447    }
448}
449
450fn require(condition: bool, message: &str) -> Result<(), String> {
451    if condition {
452        Ok(())
453    } else {
454        Err(message.to_string())
455    }
456}
457
458fn equal_weight(actual: Weight, expected: Weight, message: &str) -> Result<(), String> {
459    require(actual == expected, message)
460}
461
462#[cfg(test)]
463mod tests {
464    #[test]
465    fn real_stack_conformance() {
466        super::verify().expect("K1 Access–Kmap conformance failed");
467    }
468}