ForcePilot/backend/test/integration/api/channels/test_plugin_router.py

256 lines
11 KiB
Python
Raw Normal View History

"""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
# =============================================================================
# === 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
# =============================================================================
# === DELETE /plugins/{plugin_id} — superadmin only ===
# =============================================================================
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_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_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_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
# =============================================================================
# === Lifecycle endpoints — non-existent plugin (404) ===
# =============================================================================
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_with_empty_plugin_ids(test_client: httpx.AsyncClient, admin_headers):
# Arrange — 空 plugin_ids 且无 filterBatchPluginLifecycleCmd.__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
# =============================================================================
# === 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_pluginadmin=superadmin 通过
# get_superadmin_user 守门),而非被 /{plugin_id} 捕获为 plugin_id="install"。
# 若被 /{plugin_id} 捕获则返回 404PLUGIN_NOT_FOUND命中 install 则返回
# 400/500adapter 安装失败)。据此区分路由是否正确。
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