139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
# backend/tests/api/test_agent_runs.py
|
|
# #region Test.Api.AgentRuns [C:3] [TYPE Module] [SEMANTICS test,api,agent-run,rbac,ownership]
|
|
# @BRIEF API-level tests for agent runs — RBAC, ownership, permission_denied, foreign-run access.
|
|
# @RELATION BINDS_TO -> [Api.AgentRuns]
|
|
# @TEST_EDGE missing_permission -> 403.
|
|
# @TEST_EDGE foreign_run_access -> 404 (not owner).
|
|
# @TEST_EDGE terminal_run_rejects -> 409.
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from src.app import app
|
|
from src.dependencies import get_current_user
|
|
from src.models.auth import User
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_user():
|
|
u = User(id="user-1", username="qa_analyst")
|
|
return u
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_other_user():
|
|
u = User(id="user-2", username="other_user")
|
|
return u
|
|
|
|
|
|
class TestCreateRun:
|
|
def test_create_run_requires_auth(self, client):
|
|
resp = client.post("/api/agent/runs", json={"context": {}})
|
|
assert resp.status_code == 401
|
|
|
|
def test_create_run_requires_valid_context(self, client, mock_user):
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
try:
|
|
resp = client.post("/api/agent/runs", json={
|
|
"context": {
|
|
"objectType": "dashboard", "objectId": "42",
|
|
"envId": "dev", "route": "/dashboards/42",
|
|
"contextVersion": 2,
|
|
"intent": "build_dashboard_test_scenario",
|
|
}
|
|
})
|
|
# 403 because seed_permissions hasn't been run in test DB
|
|
# or 201 if permissions were seeded
|
|
assert resp.status_code in (201, 403, 422)
|
|
finally:
|
|
app.dependency_overrides.pop(get_current_user, None)
|
|
|
|
def test_create_run_rejects_v1_scenario_intent(self, client, mock_user):
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
try:
|
|
resp = client.post("/api/agent/runs", json={
|
|
"context": {
|
|
"objectType": "dashboard", "objectId": "42",
|
|
"envId": "dev", "route": "/dashboards/42",
|
|
"contextVersion": 1,
|
|
"intent": "build_dashboard_test_scenario",
|
|
}
|
|
})
|
|
# Pydantic validation catches v1+intent combo
|
|
assert resp.status_code in (422, 403)
|
|
finally:
|
|
app.dependency_overrides.pop(get_current_user, None)
|
|
|
|
|
|
class TestGetSnapshot:
|
|
def test_nonexistent_run_returns_404(self, client, mock_user):
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
try:
|
|
resp = client.get("/api/agent/runs/nonexistent-id")
|
|
assert resp.status_code == 404
|
|
finally:
|
|
app.dependency_overrides.pop(get_current_user, None)
|
|
|
|
def test_requires_auth(self, client):
|
|
resp = client.get("/api/agent/runs/some-id")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
class TestAppendEvent:
|
|
def test_append_event_requires_auth(self, client):
|
|
resp = client.post("/api/agent/runs/some-id/events", json={
|
|
"event_type": "progress", "sequence": 1,
|
|
})
|
|
assert resp.status_code == 401
|
|
|
|
def test_append_event_invalid_sequence(self, client, mock_user):
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
try:
|
|
resp = client.post("/api/agent/runs/some-id/events", json={
|
|
"event_type": "progress", "sequence": 0,
|
|
})
|
|
# 403 from permission check OR 422 from Pydantic validation — both valid
|
|
assert resp.status_code in (403, 422)
|
|
finally:
|
|
app.dependency_overrides.pop(get_current_user, None)
|
|
|
|
|
|
class TestApprovalFlow:
|
|
def test_create_gate_requires_auth(self, client):
|
|
resp = client.post("/api/agent/runs/some-id/gates", json={
|
|
"operation": "repository_write",
|
|
"request_hash": "a" * 64,
|
|
"target_paths": ["test.yaml"],
|
|
"required_permission": "dashboard:testing:WRITE",
|
|
})
|
|
assert resp.status_code == 401
|
|
|
|
def test_decide_gate_requires_auth(self, client):
|
|
resp = client.post("/api/agent/runs/some-id/gates/gate-1/decide", json={
|
|
"decision": "confirm",
|
|
})
|
|
assert resp.status_code == 401
|
|
|
|
def test_consume_gate_requires_auth(self, client):
|
|
resp = client.post("/api/agent/runs/some-id/gates/gate-1/consume")
|
|
assert resp.status_code == 401
|
|
|
|
|
|
class TestResponseSchemas:
|
|
"""Verify response models are importable and well-formed."""
|
|
def test_snapshot_schema(self):
|
|
from src.schemas.agent_run import AgentRunSnapshot
|
|
snap = AgentRunSnapshot(
|
|
id="r1", user_id="u1", intent="dashboard_scenario_build",
|
|
trigger="manual", dashboard_id="42", environment_id="dev",
|
|
context_snapshot={}, status="CREATED", last_sequence=0,
|
|
created_at="2026-01-01T00:00:00Z", updated_at="2026-01-01T00:00:00Z",
|
|
)
|
|
assert snap.id == "r1"
|
|
# #endregion Test.Api.AgentRuns
|