本次提交修复了多个测试文件中的问题: 1. 将 ChannelType 枚举调用改为字符串实例化方式 2. 修正了日志断言、异步mock使用、配置参数等多处测试细节 3. 新增了会话聚合根、跨渠道关联策略等单元测试用例 4. 修复了路由测试中的路径方法错误与断言逻辑 5. 调整了依赖导入与测试夹具的兼容性 6. 统一了重试回退调度的列表/元组使用规范
277 lines
9.3 KiB
Python
277 lines
9.3 KiB
Python
"""Integration tests for channels config_router endpoints.
|
||
|
||
覆盖配置管理域(CFG-02 / CFG-READ-01 / CFG-UPDATE-01 / CFG-ROLL-01 /
|
||
CFG-HIST-01 / CFG-EXPORT / CFG-IMPORT / CFG-BATCH-UPDATE)的鉴权矩阵、
|
||
请求体校验与关键错误路径。所有动态 ``/{key}`` 路径用 ``NON_EXISTENT_CONFIG_KEY``
|
||
触发 404,静态路径(schema / export / import / batch)通过提前声明规避动态捕获。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from .conftest import BASE_URL, NON_EXISTENT_CONFIG_KEY
|
||
|
||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||
|
||
CONFIG_URL = f"{BASE_URL}/config"
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth three-tier for GET /config/schema ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_config_schema_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{CONFIG_URL}/schema")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_get_config_schema_requires_admin(
|
||
test_client: httpx.AsyncClient, standard_user
|
||
):
|
||
# Act
|
||
response = await test_client.get(f"{CONFIG_URL}/schema", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_admin_can_get_config_schema(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(f"{CONFIG_URL}/schema", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], (dict, list))
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /config/export — superadmin only ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_export_config_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{CONFIG_URL}/export")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_export_config_requires_superadmin_for_standard_user(
|
||
test_client: httpx.AsyncClient, standard_user
|
||
):
|
||
# Act
|
||
response = await test_client.get(f"{CONFIG_URL}/export", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_superadmin_can_export_config(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act — admin_headers 实际为 superadmin(见 conftest.py + test_auth_router
|
||
# 中 _require_superadmin 校验),通过 get_superadmin_user 守门后导出成功。
|
||
# standard_user 被拒场景由 test_export_config_requires_superadmin_for_standard_user 覆盖。
|
||
response = await test_client.get(f"{CONFIG_URL}/export", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], dict)
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /config/import — superadmin only ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_import_config_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"config_data": {"key": "value"}, "dry_run": True}
|
||
# Act
|
||
response = await test_client.post(f"{CONFIG_URL}/import", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_import_config_rejects_standard_user(
|
||
test_client: httpx.AsyncClient, standard_user
|
||
):
|
||
# Arrange
|
||
body = {"config_data": {"key": "value"}, "dry_run": True}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{CONFIG_URL}/import", json=body, headers=standard_user["headers"]
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_superadmin_can_import_config_dry_run(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Arrange — admin_headers 为 superadmin,通过 get_superadmin_user 守门;
|
||
# dry_run=True 仅校验统计不写入,非法 key 记入 failed_count,整体返回 200。
|
||
body = {"config_data": {"key": "value"}, "dry_run": True}
|
||
# Act
|
||
response = await test_client.post(f"{CONFIG_URL}/import", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert isinstance(payload["data"], dict)
|
||
|
||
|
||
# =============================================================================
|
||
# === PUT /config/batch — validation ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_batch_update_config_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"updates": [{"key": "k", "value": "v"}]}
|
||
# Act
|
||
response = await test_client.put(f"{CONFIG_URL}/batch", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_batch_update_config_requires_admin(
|
||
test_client: httpx.AsyncClient, standard_user
|
||
):
|
||
# Arrange
|
||
body = {"updates": [{"key": "k", "value": "v"}]}
|
||
# Act
|
||
response = await test_client.put(
|
||
f"{CONFIG_URL}/batch", json=body, headers=standard_user["headers"]
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_batch_update_config_rejects_empty_updates(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act — updates 为空违反 min_length=1
|
||
response = await test_client.put(
|
||
f"{CONFIG_URL}/batch", json={"updates": []}, headers=admin_headers
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_batch_update_config_rejects_too_many_updates(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Arrange — 51 条超过 max_length=50
|
||
body = {"updates": [{"key": f"k{i}", "value": "v"} for i in range(51)]}
|
||
# Act
|
||
response = await test_client.put(f"{CONFIG_URL}/batch", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /config/{key} — non-existent key ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_config_returns_404_for_non_existent_key(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", headers=admin_headers
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === PUT /config/{key} — non-existent key ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_update_config_returns_404_for_non_existent_key(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Arrange
|
||
body = {"value": "new_value"}
|
||
# Act
|
||
response = await test_client.put(
|
||
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body, headers=admin_headers
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /config/{key}/rollback — non-existent key + validation ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_rollback_config_returns_404_for_non_existent_key(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Arrange
|
||
body = {"target_version": 1}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/rollback", json=body, headers=admin_headers
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_rollback_config_rejects_zero_target_version(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act — target_version=0 违反 ge=1
|
||
response = await test_client.post(
|
||
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/rollback",
|
||
json={"target_version": 0},
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /config/{key}/history — non-existent key ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_config_history_returns_404_for_non_existent_key(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/history", headers=admin_headers
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Static path vs dynamic — /config/schema not captured as key ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_schema_static_path_not_captured_as_key(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Act — 静态路径 /config/schema 应被 get_config_schema 捕获,而非 get_config
|
||
response = await test_client.get(f"{CONFIG_URL}/schema", headers=admin_headers)
|
||
# Assert — schema 端点返回 200,而非 404(若被当作 key="schema" 则 404)
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|