import unittest from datetime import datetime, timezone from fastapi.testclient import TestClient from admin_app.api.dependencies import ( get_current_panel_staff_principal, get_tool_management_service, ) from admin_app.app_factory import create_app from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal from admin_app.db.models import ToolDraft, ToolVersion from admin_app.services import ToolManagementService from shared.contracts import StaffRole, ToolLifecycleStatus class _FakeToolDraftRepository: def __init__(self): self.drafts: list[ToolDraft] = [] self.next_id = 1 def list_drafts(self, *, statuses=None) -> list[ToolDraft]: drafts = sorted( self.drafts, key=lambda draft: draft.updated_at or draft.created_at or datetime.min.replace(tzinfo=timezone.utc), reverse=True, ) if statuses: allowed = set(statuses) drafts = [draft for draft in drafts if draft.status in allowed] return drafts def get_by_tool_name(self, tool_name: str) -> ToolDraft | None: normalized = str(tool_name or "").strip().lower() for draft in self.drafts: if draft.tool_name == normalized: return draft return None def create(self, **kwargs) -> ToolDraft: now = datetime(2026, 3, 31, 17, 0, tzinfo=timezone.utc) draft = ToolDraft( id=self.next_id, draft_id=f"draft_panel_{self.next_id}", created_at=now, updated_at=now, status=ToolLifecycleStatus.DRAFT, **kwargs, ) self.next_id += 1 self.drafts.append(draft) return draft def update_submission(self, draft: ToolDraft, **kwargs) -> ToolDraft: draft.display_name = kwargs["display_name"] draft.domain = kwargs["domain"] draft.description = kwargs["description"] draft.business_goal = kwargs["business_goal"] draft.summary = kwargs["summary"] draft.parameters_json = kwargs["parameters_json"] draft.required_parameter_count = kwargs["required_parameter_count"] draft.current_version_number = kwargs["current_version_number"] draft.version_count = kwargs["version_count"] draft.requires_director_approval = kwargs["requires_director_approval"] draft.owner_staff_account_id = kwargs["owner_staff_account_id"] draft.owner_display_name = kwargs["owner_display_name"] draft.updated_at = datetime(2026, 3, 31, 17, draft.current_version_number, tzinfo=timezone.utc) return draft class _FakeToolVersionRepository: def __init__(self): self.versions: list[ToolVersion] = [] self.next_id = 1 def list_versions(self, *, tool_name=None, draft_id=None, statuses=None) -> list[ToolVersion]: versions = sorted( self.versions, key=lambda version: (version.version_number, version.updated_at or version.created_at or datetime.min.replace(tzinfo=timezone.utc)), reverse=True, ) if tool_name: normalized = str(tool_name).strip().lower() versions = [version for version in versions if version.tool_name == normalized] if draft_id is not None: versions = [version for version in versions if version.draft_id == draft_id] if statuses: allowed = set(statuses) versions = [version for version in versions if version.status in allowed] return versions def get_next_version_number(self, tool_name: str) -> int: versions = self.list_versions(tool_name=tool_name) return (versions[0].version_number if versions else 0) + 1 def create(self, **kwargs) -> ToolVersion: version_number = kwargs["version_number"] now = datetime(2026, 3, 31, 18, version_number, tzinfo=timezone.utc) version = ToolVersion( id=self.next_id, version_id=self.build_version_id(kwargs["tool_name"], version_number), created_at=now, updated_at=now, **kwargs, ) self.next_id += 1 self.versions.append(version) return version @staticmethod def build_version_id(tool_name: str, version_number: int) -> str: normalized = str(tool_name or "").strip().lower() return f"tool_version::{normalized}::v{int(version_number)}" class AdminPanelToolsWebTests(unittest.TestCase): def _build_client_with_role( self, role: StaffRole, settings: AdminSettings | None = None, ) -> tuple[TestClient, object, _FakeToolDraftRepository, _FakeToolVersionRepository]: app = create_app( settings or AdminSettings( admin_auth_token_secret="test-secret", admin_api_prefix="/admin", ) ) draft_repository = _FakeToolDraftRepository() version_repository = _FakeToolVersionRepository() service = ToolManagementService( settings=app.state.admin_settings, draft_repository=draft_repository, version_repository=version_repository, ) app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal( id=21, email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com", display_name="Equipe Web", role=role, is_active=True, ) app.dependency_overrides[get_tool_management_service] = lambda: service return TestClient(app), app, draft_repository, version_repository def test_panel_tools_overview_is_available_for_colaborador_session(self): client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR) try: response = client.get("/admin/panel/tools/overview") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["mode"], "admin_tool_draft_governance") self.assertIn("persisted_versions", [item["key"] for item in payload["metrics"]]) self.assertIn("/admin/panel/tools/contracts", [item["href"] for item in payload["actions"]]) self.assertIn("/admin/panel/tools/drafts/intake", [item["href"] for item in payload["actions"]]) def test_panel_tool_intake_persists_draft_with_version_metadata_for_colaborador(self): client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR) try: response = client.post( "/admin/panel/tools/drafts/intake", json={ "domain": "vendas", "tool_name": "consultar_vendas_periodo", "display_name": "Consultar vendas por periodo", "description": "Consulta vendas consolidadas por periodo informado no painel.", "business_goal": "Ajudar o time interno a acompanhar o desempenho comercial com mais agilidade.", "parameters": [ { "name": "periodo_inicio", "parameter_type": "string", "description": "Data inicial usada no filtro.", "required": True, }, { "name": "periodo_fim", "parameter_type": "string", "description": "Data final usada no filtro.", "required": True, }, ], }, ) finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["storage_status"], "admin_database") self.assertEqual(payload["draft_preview"]["status"], "draft") self.assertEqual(payload["draft_preview"]["tool_name"], "consultar_vendas_periodo") self.assertEqual(payload["draft_preview"]["version_id"], "tool_version::consultar_vendas_periodo::v1") self.assertEqual(payload["draft_preview"]["version_number"], 1) self.assertEqual(payload["draft_preview"]["version_count"], 1) self.assertTrue(payload["draft_preview"]["requires_director_approval"]) self.assertEqual(len(payload["draft_preview"]["parameters"]), 2) self.assertEqual(len(draft_repository.drafts), 1) self.assertEqual(len(version_repository.versions), 1) def test_panel_drafts_list_returns_single_root_item_after_new_version(self): client, app, draft_repository, version_repository = self._build_client_with_role(StaffRole.COLABORADOR) try: client.post( "/admin/panel/tools/drafts/intake", json={ "domain": "revisao", "tool_name": "consultar_agenda_revisao", "display_name": "Consultar agenda de revisao", "description": "Consulta disponibilidade e contexto da agenda de revisao para o time interno.", "business_goal": "Ajudar a equipe a responder mais rapido sobre slots e horarios disponiveis.", "parameters": [], }, ) client.post( "/admin/panel/tools/drafts/intake", json={ "domain": "revisao", "tool_name": "consultar_agenda_revisao", "display_name": "Consultar agenda de revisao", "description": "Consulta disponibilidade, contexto e observacoes da agenda de revisao para o time interno.", "business_goal": "Ajudar a equipe a responder mais rapido sobre slots, horarios e observacoes relevantes.", "parameters": [], }, ) response = client.get("/admin/panel/tools/drafts") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["storage_status"], "admin_database") self.assertEqual(len(payload["drafts"]), 1) self.assertEqual(payload["drafts"][0]["tool_name"], "consultar_agenda_revisao") self.assertEqual(payload["drafts"][0]["current_version_number"], 2) self.assertEqual(payload["drafts"][0]["version_count"], 2) self.assertEqual(len(draft_repository.drafts), 1) self.assertEqual(len(version_repository.versions), 2) def test_panel_tools_review_queue_requires_director_session(self): client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR) try: response = client.get("/admin/panel/tools/review-queue") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "Permissao administrativa insuficiente: 'review_tool_generations'.", ) def test_panel_tools_review_queue_is_available_for_diretor_session(self): client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR) try: response = client.get("/admin/panel/tools/review-queue") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["queue_mode"], "bootstrap_empty_state") def test_panel_tools_publications_require_director_publication_permission(self): client, app, _, _ = self._build_client_with_role(StaffRole.COLABORADOR) try: response = client.get("/admin/panel/tools/publications") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 403) self.assertEqual( response.json()["detail"], "Permissao administrativa insuficiente: 'publish_tools'.", ) def test_panel_tools_publications_return_catalog_for_diretor_session(self): client, app, _, _ = self._build_client_with_role(StaffRole.DIRETOR) try: response = client.get("/admin/panel/tools/publications") finally: app.dependency_overrides.clear() self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload["target_service"], "product") self.assertGreaterEqual(len(payload["publications"]), 10) self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]]) if __name__ == "__main__": unittest.main()