@@ -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