ForcePilot/backend/test/integration/api/channels/test_audit_router.py
Kris f9f08221fc chore: 整理代码风格与优化细节
本次提交包含多类代码优化:
1. 修复多处单行代码换行格式,统一代码排版
2. 为外部系统模块新增快捷菜单配置
3. 优化前端API请求参数命名一致性
4. 补充媒体下载超限错误码与领域异常类
5. 完善仓储层更新逻辑,支持显式清空字段
6. 优化部分测试用例与工具函数代码结构
7. 为微信插件白名单缓存增加过期时间
2026-07-14 15:13:29 +08:00

250 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Integration tests for channels audit_router endpoints."""
from __future__ import annotations
import httpx
import pytest
from .conftest import BASE_URL, ISO_TIME_END, ISO_TIME_START, 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=1001 violates le=1000
response = await test_client.get(f"{AUDIT_URL}/logs", params={"limit": 1001}, 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
async def test_query_audit_logs_rejects_invalid_operation_type(test_client: httpx.AsyncClient, admin_headers):
# Act — operation_type is an enum (AuditOperationType), invalid value returns 422
response = await test_client.get(
f"{AUDIT_URL}/logs",
params={"operation_type": "invalid_op"},
headers=admin_headers,
)
# Assert
assert response.status_code == 422, response.text
async def test_admin_can_query_audit_logs_with_time_range(test_client: httpx.AsyncClient, admin_headers):
# Act — filter by start_time / end_time
response = await test_client.get(
f"{AUDIT_URL}/logs",
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END},
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_query_audit_logs_rejects_invalid_time_format(test_client: httpx.AsyncClient, admin_headers):
# Act — "not-a-date" is not ISO 8601, parse_datetime raises ValidationError (400)
response = await test_client.get(
f"{AUDIT_URL}/logs",
params={"start_time": "not-a-date"},
headers=admin_headers,
)
# Assert
assert response.status_code == 400, 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
async def test_update_retention_policy_rejects_zero_retention_days(test_client: httpx.AsyncClient, admin_headers):
# Arrange — default_retention_days=0 violates Field(ge=1)
body = {"default_retention_days": 0}
# 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_log_rejects_oversized_log_id(test_client: httpx.AsyncClient, admin_headers):
# Arrange — log_id longer than max_length=64 violates Path constraint (422)
oversized_id = "x" * 65
# Act
response = await test_client.get(f"{AUDIT_URL}/logs/{oversized_id}", headers=admin_headers)
# Assert
assert response.status_code == 422, response.text