Skip to content

Commit 106924a

Browse files
chefsaleclaude
andauthored
test: add governance preflight, cascade removal, and namespace op tests (#2158)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 592d177 commit 106924a

1 file changed

Lines changed: 376 additions & 0 deletions

File tree

crates/context/src/group_store/tests.rs

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2306,3 +2306,379 @@ fn resolve_signing_key_none_when_exceeding_max_depth() {
23062306
"key should be reachable within depth limit"
23072307
);
23082308
}
2309+
2310+
// -----------------------------------------------------------------------
2311+
// governance_preflight logic — testing the store-level checks that
2312+
// governance_preflight orchestrates (admin auth + signing key resolution)
2313+
// -----------------------------------------------------------------------
2314+
2315+
#[test]
2316+
fn preflight_rejects_non_admin_when_required() {
2317+
let store = test_store();
2318+
let gid = ContextGroupId::from([0xF0; 32]);
2319+
let admin = PublicKey::from([0x01; 32]);
2320+
let member = PublicKey::from([0x02; 32]);
2321+
2322+
save_group_meta(&store, &gid, &test_meta()).unwrap();
2323+
add_group_member(&store, &gid, &admin, GroupMemberRole::Admin).unwrap();
2324+
add_group_member(&store, &gid, &member, GroupMemberRole::Member).unwrap();
2325+
2326+
// Admin passes
2327+
assert!(require_group_admin(&store, &gid, &admin).is_ok());
2328+
// Non-admin fails
2329+
assert!(require_group_admin(&store, &gid, &member).is_err());
2330+
// Unknown identity fails
2331+
let unknown = PublicKey::from([0x03; 32]);
2332+
assert!(require_group_admin(&store, &gid, &unknown).is_err());
2333+
}
2334+
2335+
#[test]
2336+
fn preflight_signing_key_resolved_through_hierarchy() {
2337+
// Simulates what governance_preflight does: resolve signing key for a
2338+
// child group where the key only exists on the root (namespace).
2339+
let store = test_store();
2340+
let root = ContextGroupId::from([0xF0; 32]);
2341+
let child = ContextGroupId::from([0xF1; 32]);
2342+
let admin = PublicKey::from([0x01; 32]);
2343+
let sk = [0xAA; 32];
2344+
2345+
// Set up root with meta + admin + signing key
2346+
save_group_meta(&store, &root, &test_meta()).unwrap();
2347+
add_group_member(&store, &root, &admin, GroupMemberRole::Admin).unwrap();
2348+
store_group_signing_key(&store, &root, &admin, &sk).unwrap();
2349+
2350+
// Set up child nested under root, with meta + admin but NO signing key
2351+
save_group_meta(&store, &child, &test_meta()).unwrap();
2352+
add_group_member(&store, &child, &admin, GroupMemberRole::Admin).unwrap();
2353+
nest_group(&store, &root, &child).unwrap();
2354+
2355+
// Verify: group exists, admin check passes, signing key resolves via parent
2356+
assert!(load_group_meta(&store, &child).unwrap().is_some());
2357+
assert!(require_group_admin(&store, &child, &admin).is_ok());
2358+
let resolved = resolve_group_signing_key(&store, &child, &admin).unwrap();
2359+
assert_eq!(resolved, Some(sk), "signing key should resolve from root");
2360+
}
2361+
2362+
#[test]
2363+
fn preflight_fails_when_no_signing_key_in_hierarchy() {
2364+
let store = test_store();
2365+
let gid = ContextGroupId::from([0xF0; 32]);
2366+
let admin = PublicKey::from([0x01; 32]);
2367+
2368+
save_group_meta(&store, &gid, &test_meta()).unwrap();
2369+
add_group_member(&store, &gid, &admin, GroupMemberRole::Admin).unwrap();
2370+
// No signing key stored anywhere
2371+
2372+
let resolved = resolve_group_signing_key(&store, &gid, &admin).unwrap();
2373+
assert_eq!(resolved, None, "no signing key should be found");
2374+
}
2375+
2376+
#[test]
2377+
fn preflight_fails_for_nonexistent_group() {
2378+
let store = test_store();
2379+
let gid = ContextGroupId::from([0xF0; 32]);
2380+
2381+
// Group doesn't exist — load_group_meta returns None
2382+
assert!(load_group_meta(&store, &gid).unwrap().is_none());
2383+
}
2384+
2385+
// -----------------------------------------------------------------------
2386+
// recursive_remove_member — cascade removal through group hierarchy
2387+
// -----------------------------------------------------------------------
2388+
2389+
#[test]
2390+
fn recursive_remove_cascades_to_all_descendants() {
2391+
let store = test_store();
2392+
let root = ContextGroupId::from([0xE0; 32]);
2393+
let child = ContextGroupId::from([0xE1; 32]);
2394+
let grandchild = ContextGroupId::from([0xE2; 32]);
2395+
let admin = PublicKey::from([0x01; 32]);
2396+
let member = PublicKey::from([0x02; 32]);
2397+
2398+
// Build hierarchy
2399+
nest_group(&store, &root, &child).unwrap();
2400+
nest_group(&store, &child, &grandchild).unwrap();
2401+
2402+
// Add admin + member to all groups
2403+
for gid in [&root, &child, &grandchild] {
2404+
save_group_meta(&store, gid, &test_meta()).unwrap();
2405+
add_group_member(&store, gid, &admin, GroupMemberRole::Admin).unwrap();
2406+
add_group_member(&store, gid, &member, GroupMemberRole::Member).unwrap();
2407+
}
2408+
2409+
// Verify member exists everywhere
2410+
assert!(check_group_membership(&store, &root, &member).unwrap());
2411+
assert!(check_group_membership(&store, &child, &member).unwrap());
2412+
assert!(check_group_membership(&store, &grandchild, &member).unwrap());
2413+
2414+
// Remove from root — should cascade to child and grandchild
2415+
let removed_from = recursive_remove_member(&store, &root, &member).unwrap();
2416+
assert_eq!(removed_from.len(), 3, "should be removed from all 3 groups");
2417+
2418+
assert!(!check_group_membership(&store, &root, &member).unwrap());
2419+
assert!(!check_group_membership(&store, &child, &member).unwrap());
2420+
assert!(!check_group_membership(&store, &grandchild, &member).unwrap());
2421+
2422+
// Admin should be unaffected
2423+
assert!(check_group_membership(&store, &root, &admin).unwrap());
2424+
assert!(check_group_membership(&store, &child, &admin).unwrap());
2425+
assert!(check_group_membership(&store, &grandchild, &admin).unwrap());
2426+
}
2427+
2428+
#[test]
2429+
fn recursive_remove_from_child_does_not_affect_parent() {
2430+
let store = test_store();
2431+
let root = ContextGroupId::from([0xE0; 32]);
2432+
let child = ContextGroupId::from([0xE1; 32]);
2433+
let grandchild = ContextGroupId::from([0xE2; 32]);
2434+
let admin = PublicKey::from([0x01; 32]);
2435+
let member = PublicKey::from([0x02; 32]);
2436+
2437+
nest_group(&store, &root, &child).unwrap();
2438+
nest_group(&store, &child, &grandchild).unwrap();
2439+
2440+
for gid in [&root, &child, &grandchild] {
2441+
save_group_meta(&store, gid, &test_meta()).unwrap();
2442+
add_group_member(&store, gid, &admin, GroupMemberRole::Admin).unwrap();
2443+
add_group_member(&store, gid, &member, GroupMemberRole::Member).unwrap();
2444+
}
2445+
2446+
// Remove from child only — should cascade to grandchild but NOT root
2447+
let removed_from = recursive_remove_member(&store, &child, &member).unwrap();
2448+
assert_eq!(removed_from.len(), 2, "removed from child + grandchild");
2449+
2450+
// Root membership should be unaffected
2451+
assert!(
2452+
check_group_membership(&store, &root, &member).unwrap(),
2453+
"root membership must survive child removal"
2454+
);
2455+
assert!(!check_group_membership(&store, &child, &member).unwrap());
2456+
assert!(!check_group_membership(&store, &grandchild, &member).unwrap());
2457+
}
2458+
2459+
#[test]
2460+
fn recursive_remove_member_not_in_some_descendants() {
2461+
let store = test_store();
2462+
let root = ContextGroupId::from([0xE0; 32]);
2463+
let child = ContextGroupId::from([0xE1; 32]);
2464+
let admin = PublicKey::from([0x01; 32]);
2465+
let member = PublicKey::from([0x02; 32]);
2466+
2467+
nest_group(&store, &root, &child).unwrap();
2468+
2469+
for gid in [&root, &child] {
2470+
save_group_meta(&store, gid, &test_meta()).unwrap();
2471+
add_group_member(&store, gid, &admin, GroupMemberRole::Admin).unwrap();
2472+
}
2473+
// Member only in root, not in child
2474+
add_group_member(&store, &root, &member, GroupMemberRole::Member).unwrap();
2475+
2476+
let removed_from = recursive_remove_member(&store, &root, &member).unwrap();
2477+
assert_eq!(
2478+
removed_from.len(),
2479+
1,
2480+
"only removed from root where member existed"
2481+
);
2482+
assert!(!check_group_membership(&store, &root, &member).unwrap());
2483+
}
2484+
2485+
#[test]
2486+
fn recursive_remove_nonexistent_member_returns_empty() {
2487+
let store = test_store();
2488+
let root = ContextGroupId::from([0xE0; 32]);
2489+
let admin = PublicKey::from([0x01; 32]);
2490+
let stranger = PublicKey::from([0x99; 32]);
2491+
2492+
save_group_meta(&store, &root, &test_meta()).unwrap();
2493+
add_group_member(&store, &root, &admin, GroupMemberRole::Admin).unwrap();
2494+
2495+
let removed_from = recursive_remove_member(&store, &root, &stranger).unwrap();
2496+
assert!(removed_from.is_empty(), "nothing to remove");
2497+
}
2498+
2499+
// -----------------------------------------------------------------------
2500+
// NamespaceGovernance::apply_signed_op — governance state machine tests
2501+
// -----------------------------------------------------------------------
2502+
2503+
#[test]
2504+
fn governance_group_nested_and_unnested_via_signed_ops() {
2505+
use calimero_context_client::local_governance::{NamespaceOp, RootOp, SignedNamespaceOp};
2506+
use calimero_primitives::identity::PrivateKey;
2507+
use rand::rngs::OsRng;
2508+
2509+
use super::namespace_governance::NamespaceGovernance;
2510+
2511+
let store = test_store();
2512+
let mut rng = OsRng;
2513+
let admin_sk_bytes: [u8; 32] = rand::Rng::gen(&mut rng);
2514+
let admin_sk = PrivateKey::from(admin_sk_bytes);
2515+
let admin_pk = admin_sk.public_key();
2516+
2517+
let ns_id = [0xA0u8; 32];
2518+
let ns_gid = ContextGroupId::from(ns_id);
2519+
let child_id = [0xA1u8; 32];
2520+
let child_gid = ContextGroupId::from(child_id);
2521+
2522+
// Bootstrap namespace: meta + admin + namespace identity
2523+
save_group_meta(&store, &ns_gid, &sample_meta_with_admin(admin_pk)).unwrap();
2524+
add_group_member(&store, &ns_gid, &admin_pk, GroupMemberRole::Admin).unwrap();
2525+
store_namespace_identity(&store, &ns_gid, &admin_pk, &admin_sk_bytes, &[0u8; 32]).unwrap();
2526+
2527+
// Create child group meta (normally done by GroupCreated op, do manually)
2528+
save_group_meta(&store, &child_gid, &sample_meta_with_admin(admin_pk)).unwrap();
2529+
add_group_member(&store, &child_gid, &admin_pk, GroupMemberRole::Admin).unwrap();
2530+
2531+
let gov = NamespaceGovernance::new(&store, ns_id);
2532+
2533+
// Sign and apply GroupNested op
2534+
let nest_op = SignedNamespaceOp::sign(
2535+
&admin_sk,
2536+
ns_id,
2537+
vec![],
2538+
[0u8; 32],
2539+
1,
2540+
NamespaceOp::Root(RootOp::GroupNested {
2541+
parent_group_id: ns_id,
2542+
child_group_id: child_id,
2543+
}),
2544+
)
2545+
.expect("sign nest op");
2546+
2547+
gov.apply_signed_op(&nest_op).expect("apply nest op");
2548+
2549+
// Verify child is nested
2550+
let children = list_child_groups(&store, &ns_gid).unwrap();
2551+
assert_eq!(children, vec![child_gid], "child should be nested");
2552+
2553+
// Sign and apply GroupUnnested op
2554+
let unnest_op = SignedNamespaceOp::sign(
2555+
&admin_sk,
2556+
ns_id,
2557+
vec![],
2558+
[0u8; 32],
2559+
2,
2560+
NamespaceOp::Root(RootOp::GroupUnnested {
2561+
parent_group_id: ns_id,
2562+
child_group_id: child_id,
2563+
}),
2564+
)
2565+
.expect("sign unnest op");
2566+
2567+
gov.apply_signed_op(&unnest_op).expect("apply unnest op");
2568+
2569+
// Verify child is unnested
2570+
let children_after = list_child_groups(&store, &ns_gid).unwrap();
2571+
assert!(children_after.is_empty(), "child should be unnested");
2572+
}
2573+
2574+
#[test]
2575+
fn governance_rejects_non_admin_signer() {
2576+
use calimero_context_client::local_governance::{NamespaceOp, RootOp, SignedNamespaceOp};
2577+
use calimero_primitives::identity::PrivateKey;
2578+
use rand::rngs::OsRng;
2579+
2580+
use super::namespace_governance::NamespaceGovernance;
2581+
2582+
let store = test_store();
2583+
let mut rng = OsRng;
2584+
let admin_sk_bytes: [u8; 32] = rand::Rng::gen(&mut rng);
2585+
let admin_sk = PrivateKey::from(admin_sk_bytes);
2586+
let admin_pk = admin_sk.public_key();
2587+
let intruder_sk = PrivateKey::random(&mut rng);
2588+
2589+
let ns_id = [0xA0u8; 32];
2590+
let ns_gid = ContextGroupId::from(ns_id);
2591+
2592+
// Bootstrap namespace with admin
2593+
save_group_meta(&store, &ns_gid, &sample_meta_with_admin(admin_pk)).unwrap();
2594+
add_group_member(&store, &ns_gid, &admin_pk, GroupMemberRole::Admin).unwrap();
2595+
store_namespace_identity(&store, &ns_gid, &admin_pk, &admin_sk_bytes, &[0u8; 32]).unwrap();
2596+
2597+
let gov = NamespaceGovernance::new(&store, ns_id);
2598+
2599+
// Non-admin tries to create a group
2600+
let op = SignedNamespaceOp::sign(
2601+
&intruder_sk,
2602+
ns_id,
2603+
vec![],
2604+
[0u8; 32],
2605+
1,
2606+
NamespaceOp::Root(RootOp::GroupCreated {
2607+
group_id: [0xBB; 32],
2608+
}),
2609+
)
2610+
.expect("sign op");
2611+
2612+
let result = gov.apply_signed_op(&op);
2613+
assert!(result.is_err(), "non-admin signer should be rejected");
2614+
}
2615+
2616+
#[test]
2617+
fn governance_group_created_is_idempotent() {
2618+
use calimero_context_client::local_governance::{NamespaceOp, RootOp, SignedNamespaceOp};
2619+
use calimero_primitives::identity::PrivateKey;
2620+
use rand::rngs::OsRng;
2621+
2622+
use super::namespace_governance::NamespaceGovernance;
2623+
2624+
let store = test_store();
2625+
let mut rng = OsRng;
2626+
let admin_sk_bytes: [u8; 32] = rand::Rng::gen(&mut rng);
2627+
let admin_sk = PrivateKey::from(admin_sk_bytes);
2628+
let admin_pk = admin_sk.public_key();
2629+
2630+
let ns_id = [0xA0u8; 32];
2631+
let ns_gid = ContextGroupId::from(ns_id);
2632+
let new_group_id = [0xCC; 32];
2633+
2634+
save_group_meta(&store, &ns_gid, &sample_meta_with_admin(admin_pk)).unwrap();
2635+
add_group_member(&store, &ns_gid, &admin_pk, GroupMemberRole::Admin).unwrap();
2636+
store_namespace_identity(&store, &ns_gid, &admin_pk, &admin_sk_bytes, &[0u8; 32]).unwrap();
2637+
2638+
let gov = NamespaceGovernance::new(&store, ns_id);
2639+
2640+
let op1 = SignedNamespaceOp::sign(
2641+
&admin_sk,
2642+
ns_id,
2643+
vec![],
2644+
[0u8; 32],
2645+
1,
2646+
NamespaceOp::Root(RootOp::GroupCreated {
2647+
group_id: new_group_id,
2648+
}),
2649+
)
2650+
.expect("sign op1");
2651+
2652+
gov.apply_signed_op(&op1)
2653+
.expect("first apply should succeed");
2654+
2655+
// Apply same op again (different nonce but same group_id)
2656+
let op2 = SignedNamespaceOp::sign(
2657+
&admin_sk,
2658+
ns_id,
2659+
vec![],
2660+
[0u8; 32],
2661+
2,
2662+
NamespaceOp::Root(RootOp::GroupCreated {
2663+
group_id: new_group_id,
2664+
}),
2665+
)
2666+
.expect("sign op2");
2667+
2668+
// Should not error — idempotent
2669+
gov.apply_signed_op(&op2)
2670+
.expect("duplicate GroupCreated should be idempotent");
2671+
}
2672+
2673+
// Helper: create a GroupMetaValue with a specific admin
2674+
fn sample_meta_with_admin(admin: PublicKey) -> GroupMetaValue {
2675+
GroupMetaValue {
2676+
app_key: [0xBB; 32],
2677+
target_application_id: ApplicationId::from([0xCC; 32]),
2678+
upgrade_policy: UpgradePolicy::Automatic,
2679+
created_at: 1_700_000_000,
2680+
admin_identity: admin,
2681+
migration: None,
2682+
auto_join: true,
2683+
}
2684+
}

0 commit comments

Comments
 (0)