本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
250 lines
9.6 KiB
Python
250 lines
9.6 KiB
Python
"""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 返回 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
|
||
|
||
|
||
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
|