"""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 # ============================================================================= # === 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 且无 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 # ============================================================================= # === 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