Systematic rename of all semantic anchors (#region, [DEF], @RELATION) across 1400+ files — backend Python, frontend Svelte/TS, specs, docs: - Flat anchors become Namespace.Module.Entity - @RELATION references updated to match new anchor paths - Zero business logic changes
169 lines
7.2 KiB
Python
169 lines
7.2 KiB
Python
#region Test.LlmProviderDelete.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 -> [Api.Llm.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.LlmProviderDelete.TestDeleteProviderWithActiveTasksReturns409 [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.LlmProviderDelete.TestDeleteProviderWithActiveTasksReturns409
|
|
|
|
|
|
#region Test.LlmProviderDelete.TestDeleteProviderWithMultipleActiveTasks [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.LlmProviderDelete.TestDeleteProviderWithMultipleActiveTasks
|
|
|
|
|
|
#region Test.LlmProviderDelete.TestDeleteProviderNoActiveTasksSucceeds [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.LlmProviderDelete.TestDeleteProviderNoActiveTasksSucceeds
|
|
|
|
|
|
#region Test.LlmProviderDelete.TestDeleteProviderNotFoundReturns404 [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.LlmProviderDelete.TestDeleteProviderNotFoundReturns404
|
|
|
|
#endregion Test.LlmProviderDelete.TestLlmProviderDelete
|