本次提交修复了多个测试文件中的问题: 1. 为各管道测试类添加tracer_port属性初始化 2. 修复QQBot客户端关闭异常的错误类型匹配 3. 修正配对接口查询的状态值小写格式 4. 移除ChannelType枚举值的显式.value调用 5. 新增QQ常量的导出项与Instagram测试桩函数 6. 修复API接口返回值解析,正确访问data字段 7. 调整企业微信富文本消息的断言结构 8. 修正微信iLink的消息类型测试用例 9. 替换废弃的datetime.utc相关导入为UTC常量 10. 修复QQBot白名单适配器的返回值结构 11. 调整outbox工具的channel_msg_id处理逻辑 12. 新增多个适配器与服务的测试用例,覆盖异常降级、缓存处理等场景 13. 重构部分QQBot适配器的辅助函数测试,清理冗余代码 14. 修复微信iLink客户端的上传接口调用参数与缓存清理逻辑 15. 修正配置导出接口的敏感字段过滤与返回值结构
245 lines
9.5 KiB
Python
245 lines
9.5 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_400_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 == 400, response.text
|
||
payload = response.json()
|
||
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
||
|
||
|
||
# =============================================================================
|
||
# === PUT /config/{key} — non-existent key ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_update_config_returns_400_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 == 400, response.text
|
||
payload = response.json()
|
||
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /config/{key}/rollback — non-existent key + validation ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_rollback_config_returns_400_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 == 400, response.text
|
||
payload = response.json()
|
||
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
||
|
||
|
||
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_400_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 == 400, response.text
|
||
payload = response.json()
|
||
assert payload["error"]["code"] == "VALIDATION_ERROR"
|
||
|
||
|
||
# =============================================================================
|
||
# === 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
|