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

416 lines
16 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 config_router endpoints.
覆盖配置管理域CFG-02 / CFG-READ-01 / CFG-READ-MANY / CFG-UPDATE-01 /
CFG-ROLL-01 / CFG-HIST-01 / CFG-EXPORT / CFG-IMPORT / CFG-BATCH-UPDATE
鉴权矩阵、请求体校验与关键错误路径。所有动态 ``/{key}`` 路径用
``NON_EXISTENT_CONFIG_KEY`` 触发 400VALIDATION_ERROR静态路径
schema / values / 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)
async def test_import_config_rejects_empty_config_data(test_client: httpx.AsyncClient, admin_headers):
# Arrange — config_data min_length=1空字典违反约束
body = {"config_data": {}, "dry_run": True}
# Act
response = await test_client.post(f"{CONFIG_URL}/import", json=body, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_import_config_rejects_missing_config_data(test_client: httpx.AsyncClient, admin_headers):
# Arrange — config_data 必填字段缺失
body = {"dry_run": True}
# Act
response = await test_client.post(f"{CONFIG_URL}/import", json=body, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === 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/values — batch read (CFG-READ-MANY) ===
# =============================================================================
async def test_get_config_values_requires_auth(test_client: httpx.AsyncClient):
# Act
response = await test_client.get(f"{CONFIG_URL}/values", params={"keys": ["k"]})
# Assert
assert response.status_code == 401
async def test_get_config_values_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Act
response = await test_client.get(f"{CONFIG_URL}/values", params={"keys": ["k"]}, headers=standard_user["headers"])
# Assert
assert response.status_code == 403
async def test_admin_can_get_config_values(test_client: httpx.AsyncClient, admin_headers):
# Act — 使用 CONFIG_SCHEMA 中已声明 key整体返回 200
response = await test_client.get(f"{CONFIG_URL}/values", params={"keys": ["dm_policy"]}, 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))
async def test_get_config_values_rejects_empty_keys(test_client: httpx.AsyncClient, admin_headers):
# Act — keys min_length=1空列表违反约束
response = await test_client.get(f"{CONFIG_URL}/values", params={"keys": []}, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_get_config_values_rejects_too_many_keys(test_client: httpx.AsyncClient, admin_headers):
# Arrange — 51 个 key 超过 max_length=50BATCH_UPDATE_LIMIT
params = [("keys", f"k{i}") for i in range(51)]
# Act
response = await test_client.get(f"{CONFIG_URL}/values", params=params, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_get_config_values_rejects_missing_keys_param(test_client: httpx.AsyncClient, admin_headers):
# Act — keys 为必填 Query 参数,缺失触发 422
response = await test_client.get(f"{CONFIG_URL}/values", headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === GET /config/{key} — non-existent key ===
# =============================================================================
async def test_get_config_requires_auth(test_client: httpx.AsyncClient):
# Act
response = await test_client.get(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}")
# Assert
assert response.status_code == 401
async def test_get_config_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Act
response = await test_client.get(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", headers=standard_user["headers"])
# Assert
assert response.status_code == 403
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_requires_auth(test_client: httpx.AsyncClient):
# Arrange
body = {"value": "new_value"}
# Act
response = await test_client.put(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body)
# Assert
assert response.status_code == 401
async def test_update_config_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Arrange
body = {"value": "new_value"}
# Act
response = await test_client.put(
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body, headers=standard_user["headers"]
)
# Assert
assert response.status_code == 403
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"
async def test_update_config_rejects_missing_value(test_client: httpx.AsyncClient, admin_headers):
# Arrange — value 必填字段缺失
body = {"scope": "global"}
# Act
response = await test_client.put(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
async def test_update_config_rejects_zero_expected_version(test_client: httpx.AsyncClient, admin_headers):
# Arrange — expected_version ge=10 违反约束
body = {"value": "new_value", "expected_version": 0}
# Act
response = await test_client.put(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body, headers=admin_headers)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === POST /config/{key}/rollback — non-existent key + validation ===
# =============================================================================
async def test_rollback_config_requires_auth(test_client: httpx.AsyncClient):
# Arrange
body = {"target_version": 1}
# Act
response = await test_client.post(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/rollback", json=body)
# Assert
assert response.status_code == 401
async def test_rollback_config_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Arrange
body = {"target_version": 1}
# Act
response = await test_client.post(
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/rollback", json=body, headers=standard_user["headers"]
)
# Assert
assert response.status_code == 403
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
async def test_rollback_config_rejects_missing_target_version(test_client: httpx.AsyncClient, admin_headers):
# Act — target_version 必填字段缺失
response = await test_client.post(
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/rollback",
json={},
headers=admin_headers,
)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === GET /config/{key}/history — non-existent key ===
# =============================================================================
async def test_get_config_history_requires_auth(test_client: httpx.AsyncClient):
# Act
response = await test_client.get(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/history")
# Assert
assert response.status_code == 401
async def test_get_config_history_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Act
response = await test_client.get(
f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/history", headers=standard_user["headers"]
)
# Assert
assert response.status_code == 403
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