本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
445 lines
18 KiB
Python
445 lines
18 KiB
Python
"""Integration tests for channels plugin_router endpoints.
|
||
|
||
覆盖插件管理域(PLG-QUERY-01/02 + PLG-LIFE-01~07 + PLG-INSTALL +
|
||
PLG-UNINSTALL-FILE + PLG-CONFIG + PLG-BATCH-LIFE)的鉴权矩阵与
|
||
非存在插件 404 错误路径。install / uninstall 需超级管理员权限,
|
||
其余端点要求管理员。生命周期操作以 ``NON_EXISTENT_PLUGIN_ID`` 触发
|
||
``PLUGIN_NOT_FOUND`` → 404。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from .conftest import BASE_URL, NON_EXISTENT_PLUGIN_ID
|
||
|
||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||
|
||
PLUGINS_URL = f"{BASE_URL}/plugins"
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth three-tier for GET /plugins ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_plugins_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(PLUGINS_URL)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_list_plugins_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(PLUGINS_URL, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_admin_can_list_plugins(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(PLUGINS_URL, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert "plugins" in payload["data"]
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /plugins/catalog — auth check ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_plugin_catalog_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/catalog")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_list_plugin_catalog_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/catalog", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_admin_can_list_plugin_catalog(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/catalog", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 200, response.text
|
||
|
||
payload = response.json()
|
||
assert payload["success"] is True
|
||
assert "plugins" in payload["data"]
|
||
|
||
|
||
async def test_catalog_static_path_not_captured_as_plugin_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — GET /plugins/catalog 应路由到 list_plugin_catalog(返回 200),
|
||
# 而非被 /{plugin_id} 捕获为 plugin_id="catalog"(返回 404)。
|
||
response = await test_client.get(f"{PLUGINS_URL}/catalog", headers=admin_headers)
|
||
# Assert — 200 表示命中 catalog 端点(而非被 /{plugin_id} 捕获返回 404)
|
||
assert response.status_code == 200, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /plugins/install — superadmin only ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_install_plugin_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"source_type": "path", "source": "/tmp/nonexistent_plugin"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_install_plugin_rejects_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"source_type": "path", "source": "/tmp/nonexistent_plugin"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_superadmin_install_nonexistent_source_fails(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — admin_headers 为 superadmin,通过 get_superadmin_user 守门后
|
||
# 抵达 dispatch;/tmp/nonexistent_plugin 路径不存在,install 在 adapter 层
|
||
# 失败,映射为 PLUGIN_FAILED(500) 或 VALIDATION_ERROR(400)。
|
||
# standard_user 被拒场景由 test_install_plugin_rejects_standard_user 覆盖。
|
||
body = {"source_type": "path", "source": "/tmp/nonexistent_plugin"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=admin_headers)
|
||
# Assert — 非 403(已通过守门),具体失败码取决于 adapter 实现
|
||
assert response.status_code in (400, 500), response.text
|
||
|
||
|
||
async def test_install_plugin_rejects_invalid_source_type(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — source_type 为 Literal["path","url","registry"],非法值触发 422
|
||
body = {"source_type": "ftp", "source": "/tmp/nonexistent_plugin"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_install_plugin_rejects_missing_source(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — source 必填字段缺失
|
||
body = {"source_type": "path"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_install_plugin_rejects_missing_source_type(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — source_type 必填字段缺失
|
||
body = {"source": "/tmp/nonexistent_plugin"}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === DELETE /plugins/{plugin_id} — superadmin only ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_uninstall_plugin_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.delete(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_uninstall_plugin_rejects_standard_user(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.delete(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_superadmin_uninstall_nonexistent_plugin_returns_404(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — admin_headers 为 superadmin,通过 get_superadmin_user 守门后
|
||
# dispatch 查找 NON_EXISTENT_PLUGIN_ID 失败 → PLUGIN_NOT_FOUND → 404。
|
||
response = await test_client.delete(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === GET /plugins/{plugin_id} — non-existent (404) ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_plugin_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_get_plugin_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_get_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === GET/PUT /plugins/{plugin_id}/config — non-existent (404) ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_get_plugin_config_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_get_plugin_config_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_get_plugin_config_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_update_plugin_config_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"config": {"key": "value"}, "apply_mode": "hot"}
|
||
# Act
|
||
response = await test_client.put(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_update_plugin_config_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"config": {"key": "value"}, "apply_mode": "hot"}
|
||
# Act
|
||
response = await test_client.put(
|
||
f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body, headers=standard_user["headers"]
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_update_plugin_config_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — 合法请求体,确保非 422 而走到 use case 触发 404
|
||
body = {"config": {"key": "value"}, "apply_mode": "hot"}
|
||
# Act
|
||
response = await test_client.put(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_update_plugin_config_rejects_missing_config(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — config 必填字段缺失
|
||
body = {"apply_mode": "hot"}
|
||
# Act
|
||
response = await test_client.put(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_update_plugin_config_rejects_missing_apply_mode(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — apply_mode 必填字段缺失
|
||
body = {"config": {"key": "value"}}
|
||
# Act
|
||
response = await test_client.put(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_update_plugin_config_rejects_invalid_apply_mode(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — apply_mode 为 Literal["hot","restart_required"],非法值触发 422
|
||
body = {"config": {"key": "value"}, "apply_mode": "invalid_mode"}
|
||
# Act
|
||
response = await test_client.put(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/config", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Lifecycle endpoints — non-existent plugin (404) ===
|
||
# =============================================================================
|
||
|
||
|
||
# 生命周期端点(load/start/pause/resume/stop/unload/reload)共用 get_admin_user
|
||
# 依赖,鉴权矩阵一致。以 load 为代表验证 401/403,其余端点复用同一鉴权策略。
|
||
|
||
|
||
async def test_load_plugin_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/load")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_load_plugin_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/load", headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_load_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/load", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_start_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/start", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_pause_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/pause", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_resume_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/resume", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_stop_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/stop", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_unload_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/unload", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
async def test_reload_plugin_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/reload", headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /plugins/batch/start — empty plugin_ids ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_batch_start_plugins_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"plugin_ids": ["some_plugin"]}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/start", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_batch_start_plugins_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"plugin_ids": ["some_plugin"]}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/start", json=body, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_batch_start_plugins_with_empty_plugin_ids(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — 空 plugin_ids 且无 filter,BatchPluginLifecycleCmd.__post_init__ 抛 ValidationError → 400
|
||
body = {"plugin_ids": []}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/start", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 400, response.text
|
||
|
||
|
||
async def test_batch_start_plugins_with_empty_body(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — 空请求体,字段均取默认值(plugin_ids=[] + filter=None),__post_init__ 抛 ValidationError → 400
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/start", json={}, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 400, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /plugins/batch/stop — auth check ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_batch_stop_plugins_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"plugin_ids": ["some_plugin"]}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/stop", json=body)
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_batch_stop_plugins_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"plugin_ids": ["some_plugin"]}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/stop", json=body, headers=standard_user["headers"])
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_batch_stop_plugins_with_empty_plugin_ids(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — 空 plugin_ids 且无 filter,__post_init__ 抛 ValidationError → 400
|
||
body = {"plugin_ids": []}
|
||
# Act
|
||
response = await test_client.post(f"{PLUGINS_URL}/batch/stop", json=body, headers=admin_headers)
|
||
# Assert
|
||
assert response.status_code == 400, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Static path /install not captured as plugin_id ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_install_static_path_not_captured_as_plugin_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = {"source_type": "path", "source": "/tmp/nonexistent_plugin"}
|
||
# Act — POST /plugins/install 应路由到 install_plugin(admin=superadmin 通过
|
||
# get_superadmin_user 守门),而非被 /{plugin_id} 捕获为 plugin_id="install"。
|
||
# 若被 /{plugin_id} 捕获则返回 404(PLUGIN_NOT_FOUND);命中 install 则返回
|
||
# 400/500(adapter 安装失败)。据此区分路由是否正确。
|
||
response = await test_client.post(f"{PLUGINS_URL}/install", json=body, headers=admin_headers)
|
||
# Assert — 非 404 表示命中 install_plugin(而非被 /{plugin_id} 捕获)
|
||
assert response.status_code in (400, 500), response.text
|