@@ -2557,3 +2557,113 @@ async def test_list_namespace_branches_excludes_deactivated(
25572557 namespaces = [b ["namespace" ] for b in response .json ()]
25582558 assert active_ns in namespaces
25592559 assert deact_ns not in namespaces
2560+
2561+
2562+ @pytest .mark .asyncio
2563+ async def test_deactivate_git_default_branch_namespace_blocked (
2564+ module__client_with_all_examples : AsyncClient ,
2565+ ) -> None :
2566+ """
2567+ Deleting (soft or hard) a git-backed namespace whose git_branch matches
2568+ its default_branch must be rejected, regardless of cascade setting.
2569+ """
2570+ root = "protected.root.deact"
2571+ branch_ns = "protected.root.deact.main"
2572+
2573+ # Set up a git-root namespace with a default branch
2574+ await module__client_with_all_examples .post (f"/namespaces/{ root } /" )
2575+ await module__client_with_all_examples .patch (
2576+ f"/namespaces/{ root } /git" ,
2577+ json = {"github_repo_path" : "corp/protected-repo" , "default_branch" : "main" },
2578+ )
2579+
2580+ # Create the default branch namespace
2581+ await module__client_with_all_examples .post (f"/namespaces/{ branch_ns } /" )
2582+ await module__client_with_all_examples .patch (
2583+ f"/namespaces/{ branch_ns } /git" ,
2584+ json = {"parent_namespace" : root , "git_branch" : "main" },
2585+ )
2586+
2587+ # Soft delete should be blocked
2588+ response = await module__client_with_all_examples .delete (
2589+ f"/namespaces/{ branch_ns } /" ,
2590+ )
2591+ assert response .status_code == 422
2592+ assert "default branch" in response .json ()["message" ]
2593+ assert "corp/protected-repo" in response .json ()["message" ]
2594+
2595+ # Soft delete with cascade should also be blocked
2596+ response = await module__client_with_all_examples .delete (
2597+ f"/namespaces/{ branch_ns } /?cascade=true" ,
2598+ )
2599+ assert response .status_code == 422
2600+ assert "default branch" in response .json ()["message" ]
2601+
2602+
2603+ @pytest .mark .asyncio
2604+ async def test_hard_delete_git_default_branch_namespace_blocked (
2605+ module__client_with_all_examples : AsyncClient ,
2606+ ) -> None :
2607+ """
2608+ Hard deleting a git-backed namespace whose git_branch matches its
2609+ default_branch must be rejected.
2610+ """
2611+ root = "protected.root.hard"
2612+ branch_ns = "protected.root.hard.main"
2613+
2614+ await module__client_with_all_examples .post (f"/namespaces/{ root } /" )
2615+ await module__client_with_all_examples .patch (
2616+ f"/namespaces/{ root } /git" ,
2617+ json = {"github_repo_path" : "corp/protected-hard-repo" , "default_branch" : "main" },
2618+ )
2619+
2620+ await module__client_with_all_examples .post (f"/namespaces/{ branch_ns } /" )
2621+ await module__client_with_all_examples .patch (
2622+ f"/namespaces/{ branch_ns } /git" ,
2623+ json = {"parent_namespace" : root , "git_branch" : "main" },
2624+ )
2625+
2626+ # Hard delete should be blocked
2627+ response = await module__client_with_all_examples .delete (
2628+ f"/namespaces/{ branch_ns } /hard/" ,
2629+ )
2630+ assert response .status_code == 422
2631+ assert "default branch" in response .json ()["message" ]
2632+ assert "corp/protected-hard-repo" in response .json ()["message" ]
2633+
2634+ # Hard delete with cascade should also be blocked
2635+ response = await module__client_with_all_examples .delete (
2636+ f"/namespaces/{ branch_ns } /hard/?cascade=true" ,
2637+ )
2638+ assert response .status_code == 422
2639+ assert "default branch" in response .json ()["message" ]
2640+
2641+
2642+ @pytest .mark .asyncio
2643+ async def test_delete_non_default_git_branch_namespace_allowed (
2644+ module__client_with_all_examples : AsyncClient ,
2645+ ) -> None :
2646+ """
2647+ Deleting a non-default branch namespace should still work normally.
2648+ """
2649+ root = "protected.root.feature"
2650+ branch_ns = "protected.root.feature.my_feature"
2651+
2652+ await module__client_with_all_examples .post (f"/namespaces/{ root } /" )
2653+ await module__client_with_all_examples .patch (
2654+ f"/namespaces/{ root } /git" ,
2655+ json = {"github_repo_path" : "corp/feature-repo" , "default_branch" : "main" },
2656+ )
2657+
2658+ await module__client_with_all_examples .post (f"/namespaces/{ branch_ns } /" )
2659+ await module__client_with_all_examples .patch (
2660+ f"/namespaces/{ branch_ns } /git" ,
2661+ json = {"parent_namespace" : root , "git_branch" : "my-feature" },
2662+ )
2663+
2664+ # Soft delete of a non-default branch should succeed (no nodes → deactivates cleanly)
2665+ response = await module__client_with_all_examples .delete (
2666+ f"/namespaces/{ branch_ns } /" ,
2667+ )
2668+ assert response .status_code == 200
2669+ assert "deactivated" in response .json ()["message" ]
0 commit comments