ForcePilot/backend/test/integration/api/channels/test_audit_router.py

236 lines
7.7 KiB
Python
Raw Normal View History

"""Integration tests for channels audit_router endpoints."""
from __future__ import annotations
import httpx
import pytest
from .conftest import BASE_URL, NON_EXISTENT_LOG_ID
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
AUDIT_URL = f"{BASE_URL}/audit"
# =============================================================================
# === Auth three-tier for GET /audit/logs ===
# =============================================================================
async def test_query_audit_logs_requires_auth(test_client: httpx.AsyncClient):
# Act
response = await test_client.get(f"{AUDIT_URL}/logs")
# Assert
assert response.status_code == 401, response.text
async def test_query_audit_logs_requires_admin(
test_client: httpx.AsyncClient, standard_user
):
# Act
response = await test_client.get(
f"{AUDIT_URL}/logs", headers=standard_user["headers"]
)
# Assert
assert response.status_code == 403, response.text
async def test_admin_can_query_audit_logs(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(f"{AUDIT_URL}/logs", headers=admin_headers)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
# =============================================================================
# === Pagination validation for GET /audit/logs ===
# =============================================================================
async def test_query_audit_logs_rejects_limit_zero(
test_client: httpx.AsyncClient, admin_headers
):
# Act — limit=0 violates ge=1
response = await test_client.get(
f"{AUDIT_URL}/logs", params={"limit": 0}, headers=admin_headers
)
# Assert
assert response.status_code == 422, response.text
async def test_query_audit_logs_rejects_limit_over_max(
test_client: httpx.AsyncClient, admin_headers
):
# Act — limit=501 violates le=500
response = await test_client.get(
f"{AUDIT_URL}/logs", params={"limit": 501}, headers=admin_headers
)
# Assert
assert response.status_code == 422, response.text
async def test_query_audit_logs_rejects_negative_offset(
test_client: httpx.AsyncClient, admin_headers
):
# Act — offset=-1 violates ge=0
response = await test_client.get(
f"{AUDIT_URL}/logs", params={"offset": -1}, headers=admin_headers
)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === GET /audit/logs/stats and /audit/operations ===
# =============================================================================
async def test_admin_can_get_audit_log_stats(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(f"{AUDIT_URL}/logs/stats", headers=admin_headers)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_admin_can_list_audit_operations(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(f"{AUDIT_URL}/operations", headers=admin_headers)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
# =============================================================================
# === GET /audit/export (StreamingResponse, JSON array) ===
# =============================================================================
async def test_admin_can_export_audit_logs(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(f"{AUDIT_URL}/export", headers=admin_headers)
# Assert
assert response.status_code == 200, response.text
# /audit/export 返回 StreamingResponseJSON 数组 [entry1, ...]
# 不遵循 success/data 标准响应包装;无匹配数据时为空数组 []。
payload = response.json()
assert isinstance(payload, list)
# =============================================================================
# === GET /audit/retention-policy ===
# =============================================================================
async def test_admin_can_get_retention_policy(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(
f"{AUDIT_URL}/retention-policy", headers=admin_headers
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
# =============================================================================
# === PUT /audit/retention-policy (superadmin only) ===
# =============================================================================
async def test_update_retention_policy_requires_auth(test_client: httpx.AsyncClient):
# Arrange
body = {"default_retention_days": 90}
# Act
response = await test_client.put(f"{AUDIT_URL}/retention-policy", json=body)
# Assert
assert response.status_code == 401, response.text
async def test_update_retention_policy_rejects_standard_user(
test_client: httpx.AsyncClient, standard_user
):
# Arrange
body = {"default_retention_days": 90}
# Act
response = await test_client.put(
f"{AUDIT_URL}/retention-policy",
json=body,
headers=standard_user["headers"],
)
# Assert
assert response.status_code == 403, response.text
async def test_superadmin_can_update_retention_policy(
test_client: httpx.AsyncClient, admin_headers
):
# Arrange — admin_headers 实际为 superadmin见 conftest.py + test_auth_router
# 中 _require_superadmin 校验),通过 get_superadmin_user 守门body 合法
# 应返回 200。standard_user 被拒场景由 test_update_retention_policy_rejects_standard_user 覆盖。
body = {"default_retention_days": 90}
# Act
response = await test_client.put(
f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers
)
# Assert
assert response.status_code == 200, response.text
payload = response.json()
assert payload["success"] is True
assert "data" in payload
async def test_update_retention_policy_rejects_archive_before_ge_retention(
test_client: httpx.AsyncClient, admin_headers
):
# Arrange — auto_archive_before_days >= default_retention_days 违反
# RetentionPolicyUpdateCmd.__post_init__ 业务规则,构造时抛出
# ValidationErrorVALIDATION_ERROR -> HTTP 400。admin_headers 为
# superadmin通过 get_superadmin_user 守门后抵达 DTO 构造阶段。
# 注意:此非 Pydantic Field 校验ge=1—— 10/10 均通过 Field 约束,
# 故不是 422亦非 dispatch-stage RuleViolationError422
body = {
"default_retention_days": 10,
"auto_archive_before_days": 10,
}
# Act
response = await test_client.put(
f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers
)
# Assert — DTO __post_init__ 抛 ValidationError -> 400
assert response.status_code == 400, response.text
# =============================================================================
# === 404 paths for non-existent resources ===
# =============================================================================
async def test_get_audit_log_returns_404_for_nonexistent(
test_client: httpx.AsyncClient, admin_headers
):
# Act
response = await test_client.get(
f"{AUDIT_URL}/logs/{NON_EXISTENT_LOG_ID}", headers=admin_headers
)
# Assert
assert response.status_code == 404, response.text