#region TestLlmProviderDelete [C:3] [TYPE Module] [SEMANTICS test, llm, provider, delete, validation-policy, 409] # @BRIEF Tests for FR-058: LLM provider DELETE is blocked when active ValidationPolicy references exist. # @RELATION BINDS_TO -> [LlmRoutes] # @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient] # @TEST_EDGE: active_tasks_block_delete -> 409 with blocking_tasks list when provider has active policies # @TEST_EDGE: provider_not_found -> 404 when provider does not exist # @TEST_EDGE: no_active_tasks -> 204 when no policies reference the provider # @INVARIANT Provider with active ValidationPolicy.is_active=True references cannot be deleted (FR-058) import os os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:test_auth_provider_delete") os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:test_main_provider_delete") os.environ.setdefault("DEV_MODE", "true") import pytest from types import SimpleNamespace from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from sqlalchemy.orm import Session from src.app import app from src.core.database import get_db from src.dependencies import get_current_user from src.models.auth import User # ── Shared mocks ─────────────────────────────────────────────────────── mock_user = MagicMock(spec=User) mock_user.username = "admin" mock_user.roles = [] @pytest.fixture(autouse=True) def mock_auth(): """Provide authenticated admin user for all tests in this module.""" mock_user.roles = [] app.dependency_overrides[get_current_user] = lambda: mock_user yield app.dependency_overrides.clear() client = TestClient(app) def _make_mock_db(): """Build a mock DB session that controls ValidationPolicy query results. Returns (mock_db, policy_list) where policy_list can be mutated to control whether the provider has active tasks. Uses SimpleNamespace instead of MagicMock for policy objects to ensure deterministic attribute access without MagicMock interference. """ policy_list: list[SimpleNamespace] = [] mock_db = MagicMock(spec=Session) # Wire query().filter().all() to return policy_list mock_query = MagicMock() mock_db.query.return_value = mock_query mock_query.filter.return_value = mock_query mock_query.all.return_value = policy_list return mock_db, policy_list #region test_delete_provider_with_active_tasks_returns_409 [C:2] [TYPE Function] # @BRIEF T062: Delete provider referenced by active ValidationPolicy → 409 with blocking_tasks. # @TEST_INVARIANT provider_active_task_block -> VERIFIED_BY: test_delete_provider_with_active_tasks_returns_409 # @RATIONALE FR-058: providers with active task references must be protected from # accidental deletion. The route queries ValidationPolicy.is_active == True # and returns blocking_tasks names in the error detail. def test_delete_provider_with_active_tasks_returns_409(mock_auth): # noqa: ARG001 """DELETE /api/llm/providers/{id} with active validation tasks → 409 + blocking_tasks.""" mock_db, policy_list = _make_mock_db() # Seed one active validation task referencing the provider policy_list.append( SimpleNamespace(id="policy-1", name="Scheduled LLM Check") ) app.dependency_overrides[get_db] = lambda: mock_db try: response = client.delete("/api/llm/providers/provider-multi-1") assert response.status_code == 409 detail = response.json()["detail"] assert "error" in detail assert "active validation task" in detail["error"].lower() blocking_tasks = detail.get("blocking_tasks", []) assert len(blocking_tasks) == 1 assert blocking_tasks[0]["name"] == "Scheduled LLM Check" assert blocking_tasks[0]["id"] == "policy-1" finally: app.dependency_overrides.pop(get_db, None) #endregion test_delete_provider_with_active_tasks_returns_409 #region test_delete_provider_with_multiple_active_tasks [C:2] [TYPE Function] # @BRIEF T062 variant: multiple active tasks all appear in blocking_tasks list. def test_delete_provider_with_multiple_active_tasks(mock_auth): # noqa: ARG001 """DELETE with 3 active tasks returns all in blocking_tasks.""" mock_db, policy_list = _make_mock_db() policy_list.extend([ SimpleNamespace(id="p-1", name="Weekday Check"), SimpleNamespace(id="p-2", name="Weekend Check"), SimpleNamespace(id="p-3", name="Nightly Check"), ]) app.dependency_overrides[get_db] = lambda: mock_db try: response = client.delete("/api/llm/providers/provider-multi-2") assert response.status_code == 409 detail = response.json()["detail"] assert len(detail["blocking_tasks"]) == 3 names = {t["name"] for t in detail["blocking_tasks"]} assert names == {"Weekday Check", "Weekend Check", "Nightly Check"} finally: app.dependency_overrides.pop(get_db, None) #endregion test_delete_provider_with_multiple_active_tasks #region test_delete_provider_no_active_tasks_succeeds [C:2] [TYPE Function] # @BRIEF T062 variant: no active policies → 204 deletion success. def test_delete_provider_no_active_tasks_succeeds(mock_auth): # noqa: ARG001 """DELETE with no active ValidationPolicy → 204.""" mock_db, policy_list = _make_mock_db() # Empty policy_list — no active tasks policy_list.clear() # Mock the provider service to succeed mock_provider_service = MagicMock() mock_provider_service.delete_provider.return_value = True with patch("src.api.routes.llm.LLMProviderService") as mock_provider_svc: mock_provider_svc.return_value = mock_provider_service app.dependency_overrides[get_db] = lambda: mock_db try: response = client.delete("/api/llm/providers/provider-no-tasks") assert response.status_code == 204 finally: app.dependency_overrides.pop(get_db, None) #endregion test_delete_provider_no_active_tasks_succeeds #region test_delete_provider_not_found_returns_404 [C:2] [TYPE Function] # @BRIEF T062 variant: nonexistent provider → 404. def test_delete_provider_not_found_returns_404(mock_auth): # noqa: ARG001 """DELETE on nonexistent provider returns 404.""" mock_db, policy_list = _make_mock_db() policy_list.clear() mock_provider_service = MagicMock() mock_provider_service.delete_provider.return_value = False with patch("src.api.routes.llm.LLMProviderService") as mock_provider_svc: mock_provider_svc.return_value = mock_provider_service app.dependency_overrides[get_db] = lambda: mock_db try: response = client.delete("/api/llm/providers/nonexistent-provider") assert response.status_code == 404 finally: app.dependency_overrides.pop(get_db, None) #endregion test_delete_provider_not_found_returns_404 #endregion TestLlmProviderDelete