Files
ss-tools/backend/tests/api/test_agent_status_routes.py
busya f0d7b79523 fix(rbac): single admin authority, gate privileged surfaces, enforce session on dual-auth
- is_admin flag is the sole admin authority; remove role-name fallbacks (dependencies, reports, tasks, agent lifecycle, security badge, frontend)
- normalize (resource, ACTION) across REST/MCP/catalog; strict admin role/permission parsing (400 on unknown)
- gate LLM provider CRUD/status/test on admin:settings; require auth on agent llm-status; quarantine listing requires scenario:automation MANAGE
- enforce logical-session revocation/idle in require_api_key_or_jwt
- last-admin lockout guards for user/role CRUD; block non-admin from granting is_admin
- frontend: flag-only admin, route permission map from the nav registry, write-control gating, 403 dedupe
- tests: orthogonal edge coverage (flag-only admin, lockout boundaries, session states, normalization)
2026-09-22 21:14:35 +03:00

144 lines
5.9 KiB
Python

# #region Test.Api.AgentStatusRoutes [C:2] [TYPE Module] [SEMANTICS test,agent,llm,status,health]
# @BRIEF Unit tests for Agent LLM provider health status endpoint.
# @RELATION BINDS_TO -> [Api.Agent.Status]
import os
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
os.environ.setdefault("DEV_MODE", "true")
import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
# Ensure backend/src is importable for src.api / src.core modules
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
def _make_client(authenticated: bool = True) -> TestClient:
"""Build a TestClient with the agent status router.
The endpoint requires an authenticated user (RBAC-02); tests override
get_current_user unless the test is specifically asserting the 401 path.
"""
from src.api.routes.agent_status import router
app = FastAPI()
app.include_router(router)
if authenticated:
from src.dependencies import get_current_user
app.dependency_overrides[get_current_user] = lambda: MagicMock(
username="tester", id="user-1"
)
return TestClient(app)
class TestGetLlmStatus:
"""GET /api/agent/llm-status"""
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
@patch("src.core.utils.llm_health._llm_status", {"status": "ok", "last_error": ""})
def test_status_ok(self):
"""Returns ok when LLM provider is healthy."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "ok"
assert data["last_error"] == ""
assert data["retry_after_s"] == 0
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="unavailable"))
@patch("src.core.utils.llm_health._llm_status", {"status": "unavailable", "last_error": "Provider unreachable"})
def test_status_unavailable(self):
"""Returns unavailable with retry_after_s > 0."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "unavailable"
assert data["last_error"] == "Provider unreachable"
assert data["retry_after_s"] == 30
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="timeout"))
@patch("src.core.utils.llm_health._llm_status", {"status": "timeout", "last_error": "Request timed out"})
def test_status_timeout(self):
"""Returns timeout status."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
assert resp.json()["status"] == "timeout"
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="auth_error"))
@patch("src.core.utils.llm_health._llm_status", {"status": "auth_error", "last_error": "Invalid API key"})
def test_status_auth_error(self):
"""Returns auth_error status."""
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
assert resp.json()["status"] == "auth_error"
assert resp.json()["last_error"] == "Invalid API key"
@patch("src.core.utils.llm_health._check_llm_provider_health", AsyncMock(return_value="ok"))
@patch("src.core.utils.llm_health._llm_status", {"status": "ok", "last_error": ""})
def test_auth_required(self):
"""Status endpoint requires authentication (RBAC-02: no anonymous provider probe)."""
client = _make_client(authenticated=False)
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 401
def test_check_health_called(self):
"""_check_llm_provider_health is called on each request."""
mock_check = AsyncMock(return_value="ok")
with (
patch("src.core.utils.llm_health._check_llm_provider_health", mock_check),
patch("src.core.utils.llm_health._llm_status", {"status": "ok", "last_error": ""}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
mock_check.assert_called_once()
def test_force_param_bypasses_cache(self):
"""?force=true must reach _check_llm_provider_health(force=True) so the
"Retry now" button gets a fresh probe instead of the 30s cached status."""
mock_check = AsyncMock(return_value="unavailable")
with (
patch("src.core.utils.llm_health._check_llm_provider_health", mock_check),
patch("src.core.utils.llm_health._llm_status", {"status": "unavailable", "last_error": "Provider unreachable"}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status?force=true")
assert resp.status_code == 200
assert resp.json()["status"] == "unavailable"
mock_check.assert_called_once_with(force=True)
def test_force_false_default_passes_no_force(self):
"""Without the force param the health check is called with force=False (cached)."""
mock_check = AsyncMock(return_value="ok")
with (
patch("src.core.utils.llm_health._check_llm_provider_health", mock_check),
patch("src.core.utils.llm_health._llm_status", {"status": "ok", "last_error": ""}),
):
client = _make_client()
resp = client.get("/api/agent/llm-status")
assert resp.status_code == 200
mock_check.assert_called_once_with(force=False)
# #endregion Test.Api.AgentStatusRoutes