1. 新增channels模块集成测试目录与基础fixture 2. 为capability、doctor、reports、dashboard、directory、webhook、wizard、health、analytics、allowlist、config等路由编写完整的鉴权、参数校验与异常场景测试 3. 修复基础集成测试setup,新增external/scheduler/channel数据库schema初始化步骤
281 lines
8.5 KiB
Python
281 lines
8.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, NON_EXISTENT_TASK_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
|
|
|
|
|
|
# =============================================================================
|
|
# === POST /audit/export (create export task) ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_create_audit_export_with_empty_body(
|
|
test_client: httpx.AsyncClient, admin_headers
|
|
):
|
|
# Arrange — all fields optional, empty dict is valid
|
|
body: dict = {}
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{AUDIT_URL}/export", 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
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /audit/export (list export tasks) ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_list_export_tasks(
|
|
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
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert "data" in payload
|
|
|
|
|
|
# =============================================================================
|
|
# === 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_update_retention_policy_requires_superadmin(
|
|
test_client: httpx.AsyncClient, admin_headers
|
|
):
|
|
# Arrange — admin role (not superadmin) should be rejected
|
|
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 == 403, response.text
|
|
|
|
|
|
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 violates
|
|
# dispatch-stage rule (RULE_VIOLATION -> 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
|
|
assert response.status_code == 422, 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
|
|
|
|
|
|
async def test_get_audit_export_task_returns_404_for_nonexistent(
|
|
test_client: httpx.AsyncClient, admin_headers
|
|
):
|
|
# Act
|
|
response = await test_client.get(
|
|
f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}", headers=admin_headers
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|
|
|
|
|
|
async def test_cancel_audit_export_task_returns_404_for_nonexistent(
|
|
test_client: httpx.AsyncClient, admin_headers
|
|
):
|
|
# Act
|
|
response = await test_client.delete(
|
|
f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}", headers=admin_headers
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|
|
|
|
|
|
async def test_download_audit_export_returns_404_for_nonexistent(
|
|
test_client: httpx.AsyncClient, admin_headers
|
|
):
|
|
# Act
|
|
response = await test_client.get(
|
|
f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}/download",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|