196 lines
7.5 KiB
Python
196 lines
7.5 KiB
Python
"""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 返回 StreamingResponse(JSON 数组 [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__ 业务规则,构造时抛出
|
||
# ValidationError(VALIDATION_ERROR -> HTTP 400)。admin_headers 为
|
||
# superadmin,通过 get_superadmin_user 守门后抵达 DTO 构造阶段。
|
||
# 注意:此非 Pydantic Field 校验(ge=1)—— 10/10 均通过 Field 约束,
|
||
# 故不是 422;亦非 dispatch-stage RuleViolationError(422)。
|
||
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
|