"""Unit and integration tests for Phase 2 Web Console recovery controls (#644).""" from __future__ import annotations import json import os import unittest from unittest.mock import patch from starlette.testclient import TestClient from webui import console_audit, console_authz, console_recovery from webui.app import create_app class TestConsoleRecovery(unittest.TestCase): def test_diagnose_recovery_healthy(self) -> None: diag = console_recovery.diagnose_recovery() self.assertIn(diag.status, {console_recovery.STATUS_HEALTHY, console_recovery.STATUS_ACTION_REQUIRED}) self.assertIsInstance(diag.playbooks, tuple) self.assertGreaterEqual(len(diag.playbooks), 4) playbook_ids = {pb.playbook_id for pb in diag.playbooks} self.assertIn(console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, playbook_ids) self.assertIn(console_recovery.PLAYBOOK_REBIND_SESSION, playbook_ids) self.assertIn(console_recovery.PLAYBOOK_RECONCILE_CLEANUPS, playbook_ids) self.assertIn(console_recovery.PLAYBOOK_SANCTIONED_RESTART, playbook_ids) def test_confirmation_phrase_generation_and_matching(self) -> None: phrase = console_recovery.confirmation_phrase("clear_stale_binding") self.assertEqual(phrase, "confirm clear_stale_binding") self.assertTrue(console_recovery.confirmation_matches("clear_stale_binding", "confirm clear_stale_binding")) self.assertFalse(console_recovery.confirmation_matches("clear_stale_binding", "wrong phrase")) phrase_target = console_recovery.confirmation_phrase("sanctioned_restart", "gitea-author") self.assertEqual(phrase_target, "confirm sanctioned_restart gitea-author") self.assertTrue(console_recovery.confirmation_matches("sanctioned_restart", "confirm sanctioned_restart gitea-author", "gitea-author")) def test_build_recovery_preview(self) -> None: principal = console_authz.Principal("dev@example.com", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True) preview = console_recovery.build_recovery_preview( playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, target="test-worktree", principal=principal, ) self.assertEqual(preview["playbook_id"], console_recovery.PLAYBOOK_CLEAR_STALE_BINDING) self.assertEqual(preview["action_id"], console_recovery.ACTION_CLEAR_STALE_BINDING) self.assertEqual(preview["confirmation_phrase"], "confirm clear_stale_binding test-worktree") self.assertTrue(len(preview["mutation_ledger"]) >= 3) self.assertTrue(preview["authorization"]["allowed"]) def test_build_recovery_preview_unknown_playbook(self) -> None: preview = console_recovery.build_recovery_preview("unknown_playbook") self.assertFalse(preview.get("allowed")) self.assertEqual(preview.get("error"), "unknown_playbook") def test_execute_recovery_playbook_confirmation_mismatch(self) -> None: principal = console_authz.Principal("dev@example.com", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True) result = console_recovery.execute_recovery_playbook( playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, confirmation="invalid confirmation", principal=principal, ) self.assertFalse(result["success"]) self.assertFalse(result["allowed"]) self.assertEqual(result["error"], "confirmation_mismatch") def test_execute_recovery_playbook_unauthorized(self) -> None: # Anonymous principal has viewer role -> should be denied result = console_recovery.execute_recovery_playbook( playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, confirmation="confirm clear_stale_binding", principal=console_authz.ANONYMOUS, ) self.assertFalse(result["success"]) self.assertFalse(result["allowed"]) self.assertEqual(result["error"], console_authz.DENY_UNAUTHENTICATED) def test_execute_recovery_playbook_clear_stale_binding_success(self) -> None: principal = console_authz.Principal("dev@example.com", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True) phrase = console_recovery.confirmation_phrase(console_recovery.PLAYBOOK_CLEAR_STALE_BINDING) result = console_recovery.execute_recovery_playbook( playbook_id=console_recovery.PLAYBOOK_CLEAR_STALE_BINDING, confirmation=phrase, principal=principal, ) self.assertTrue(result["allowed"]) self.assertIn("applied_result", result) self.assertIn("post_recovery_verification", result) self.assertIn("audit", result) self.assertEqual(result["audit"]["event"]["action"], console_recovery.ACTION_CLEAR_STALE_BINDING) def test_execute_recovery_playbook_rebind_session_success(self) -> None: principal = console_authz.Principal("dev@example.com", console_authz.OPERATOR, console_authz.IDENTITY_LOCAL_DEV, True) phrase = console_recovery.confirmation_phrase(console_recovery.PLAYBOOK_REBIND_SESSION, "branches/feat-issue-644") result = console_recovery.execute_recovery_playbook( playbook_id=console_recovery.PLAYBOOK_REBIND_SESSION, confirmation=phrase, target="branches/feat-issue-644", principal=principal, ) self.assertTrue(result["allowed"]) self.assertTrue(result["success"]) self.assertEqual(result["applied_result"]["rebound_worktree"], "branches/feat-issue-644") def test_verify_post_recovery(self) -> None: verification = console_recovery.verify_post_recovery() self.assertIn("clean", verification) self.assertIn("status", verification) self.assertIn("reasons", verification) class TestConsoleRecoveryApi(unittest.TestCase): def setUp(self) -> None: self.app = create_app() self.client = TestClient(self.app) def test_api_recovery_diagnose(self) -> None: res = self.client.get("/api/v1/system/recovery/diagnose") self.assertEqual(res.status_code, 200) data = res.json() self.assertIn("status", data) self.assertIn("clean", data) self.assertIn("playbooks", data) self.assertTrue(len(data["playbooks"]) >= 4) def test_api_recovery_preview(self) -> None: res = self.client.post( "/api/v1/system/recovery/preview", json={"playbook_id": "clear_stale_binding", "target": "active"}, ) self.assertEqual(res.status_code, 200) data = res.json() self.assertEqual(data["playbook_id"], "clear_stale_binding") self.assertEqual(data["confirmation_phrase"], "confirm clear_stale_binding active") self.assertIn("mutation_ledger", data) def test_api_recovery_apply_denied_without_auth(self) -> None: res = self.client.post( "/api/v1/system/recovery/apply", json={"playbook_id": "clear_stale_binding", "confirmation": "confirm clear_stale_binding"}, ) self.assertEqual(res.status_code, 400) data = res.json() self.assertFalse(data["success"]) self.assertFalse(data["allowed"]) def test_api_recovery_apply_with_dev_auth(self) -> None: env = { "WEBUI_AUTH_MODE": "local_dev", "WEBUI_DEV_SUBJECT": "dev@example.com", "WEBUI_DEV_ROLE": "operator", } with patch.dict(os.environ, env): res = self.client.post( "/api/v1/system/recovery/apply", json={ "playbook_id": "rebind_session_worktree", "target": "branches/feat-issue-644", "confirmation": "confirm rebind_session_worktree branches/feat-issue-644", }, ) self.assertEqual(res.status_code, 200) data = res.json() self.assertTrue(data["success"]) self.assertTrue(data["allowed"]) self.assertEqual(data["playbook_id"], "rebind_session_worktree") def test_api_recovery_verify(self) -> None: res = self.client.get("/api/v1/system/recovery/verify") self.assertEqual(res.status_code, 200) data = res.json() self.assertIn("clean", data) self.assertIn("status", data) if __name__ == "__main__": unittest.main()