Files
ss-tools/backend/tests/api/test_llm_provider_delete.py
busya d2b53c2a79 feat(validation): v2 LLM dashboard validation — plugin, routes, services
Core implementation of the v2 LLM dashboard validation pipeline:
- LLM plugin with Path A (screenshots) and Path B (logs-only) execution
- Validation task management (CRUD, schedule, run)
- WebSocket task progress with Python 3.13 asyncio fix
- Cross-task runs listing (GET /validation-tasks/runs/all)
- RecordResponse schema for validation records
- JSON prompt helper, per-dashboard status aggregation
- Prompt templates with docs/git-commit/validation presets
- Migration: v2 validation models + description column
- Tests: plugin persistence, prompt templates, batch, payload, url
2026-05-31 22:32:20 +03:00

169 lines
7.0 KiB
Python

#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