From 5c4ef3a709f8fa07a5fa60691ef7ae1867eaea9c Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Tue, 7 Jul 2026 16:27:06 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=A0=BC=E5=BC=8F=E5=8C=96=E6=89=80?= =?UTF-8?q?=E6=9C=89=E6=B8=A0=E9=81=93=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E7=9A=84=E5=87=BD=E6=95=B0=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将多行的测试函数声明合并为单行,统一代码风格 2. 新增临时的微信woc向导验证脚本,用于全流程验证后可删除 3. 统一调整长请求行的换行写法,简化代码结构 --- .../api/channels/test_account_router.py | 68 ++----- .../api/channels/test_allowlist_router.py | 90 +++------- .../api/channels/test_audit_router.py | 80 +++------ .../api/channels/test_capability_router.py | 16 +- .../api/channels/test_config_router.py | 80 +++------ .../channels/test_content_review_router.py | 37 +--- .../api/channels/test_dashboard_router.py | 48 ++--- .../api/channels/test_directory_router.py | 167 +++++++++++++----- .../api/channels/test_doctor_router.py | 48 ++--- .../api/channels/test_health_router.py | 68 ++----- .../api/channels/test_login_router.py | 40 ++--- .../api/channels/test_message_router.py | 20 +-- .../api/channels/test_outbox_router.py | 61 ++----- .../api/channels/test_pairing_router.py | 52 ++---- .../api/channels/test_plugin_router.py | 140 ++++----------- .../api/channels/test_reports_router.py | 60 ++----- .../api/channels/test_session_router.py | 24 +-- .../api/channels/test_webhook_router.py | 16 +- .../api/channels/test_wizard_router.py | 48 ++--- .../api/channels/test_woc_wizard_verify.py | 114 ++++++++++++ 20 files changed, 487 insertions(+), 790 deletions(-) create mode 100644 backend/test/integration/api/channels/test_woc_wizard_verify.py diff --git a/backend/test/integration/api/channels/test_account_router.py b/backend/test/integration/api/channels/test_account_router.py index 7413087e..0e9140d9 100644 --- a/backend/test/integration/api/channels/test_account_router.py +++ b/backend/test/integration/api/channels/test_account_router.py @@ -96,9 +96,7 @@ async def test_batch_enable_accounts_requires_auth(test_client: httpx.AsyncClien assert response.status_code == 401, response.text -async def test_batch_enable_accounts_forbids_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_batch_enable_accounts_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"account_ids": [NON_EXISTENT_ACCOUNT_ID]} # Act @@ -111,9 +109,7 @@ async def test_batch_enable_accounts_forbids_standard_user( assert response.status_code == 403, response.text -async def test_batch_enable_accounts_rejects_empty_body( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_enable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — account_ids 与 filter 同时为空 body = {"account_ids": None, "filter": None} # Act @@ -126,9 +122,7 @@ async def test_batch_enable_accounts_rejects_empty_body( assert response.status_code in (400, 422), response.text -async def test_batch_disable_accounts_rejects_empty_body( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_disable_accounts_rejects_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"account_ids": None, "filter": None} # Act @@ -171,9 +165,7 @@ async def test_create_account_forbids_standard_user(test_client: httpx.AsyncClie assert response.status_code == 403, response.text -async def test_create_account_invalid_channel_type_returns_422( - test_client: httpx.AsyncClient, admin_headers -): +async def test_create_account_invalid_channel_type_returns_422(test_client: httpx.AsyncClient, admin_headers): # Arrange — 适配器未注册时返回 422 body = _make_create_account_body() # Act @@ -186,9 +178,7 @@ async def test_create_account_invalid_channel_type_returns_422( assert response.status_code == 422, response.text -async def test_create_account_with_invalid_body_returns_not_4xx( - test_client: httpx.AsyncClient, admin_headers -): +async def test_create_account_with_invalid_body_returns_not_4xx(test_client: httpx.AsyncClient, admin_headers): # Arrange — display_name 过长触发校验失败 body = { "display_name": "x" * 200, @@ -219,9 +209,7 @@ async def test_get_account_non_existent_returns_404(test_client: httpx.AsyncClie assert response.status_code == 404, response.text -async def test_update_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"display_name": "updated_name"} # Act @@ -247,9 +235,7 @@ async def test_update_account_rejects_empty_body(test_client: httpx.AsyncClient, assert response.status_code in (400, 422, 404), response.text -async def test_delete_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_delete_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.delete( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}", @@ -259,9 +245,7 @@ async def test_delete_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_enable_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_enable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/enable", @@ -271,9 +255,7 @@ async def test_enable_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_disable_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_disable_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/disable", @@ -283,9 +265,7 @@ async def test_disable_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_recover_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_recover_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/recover", @@ -295,9 +275,7 @@ async def test_recover_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_get_runtime_status_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_runtime_status_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/runtime-status", @@ -307,9 +285,7 @@ async def test_get_runtime_status_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_rotate_credentials_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_rotate_credentials_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/rotate-credentials", @@ -319,9 +295,7 @@ async def test_rotate_credentials_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_test_connection_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_test_connection_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/test-connection", @@ -336,9 +310,7 @@ async def test_test_connection_non_existent_returns_404( # ============================================================================= -async def test_export_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_export_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/export", @@ -348,9 +320,7 @@ async def test_export_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_export_account_with_secrets_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_export_account_with_secrets_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange — admin_headers 为 superadmin,include_secrets=True 的 superadmin # 校验在 dispatch 阶段通过;账户不存在 → 404。 # 注:include_secrets=True 拒绝非 superadmin 管理员的场景无法用 admin_headers @@ -371,9 +341,7 @@ async def test_export_account_with_secrets_non_existent_returns_404( # ============================================================================= -async def test_clone_account_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clone_account_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"new_display_name": "cloned_account", "include_credentials": False} # Act @@ -386,9 +354,7 @@ async def test_clone_account_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_clone_account_rejects_invalid_body( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clone_account_rejects_invalid_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — new_display_name 过长触发校验失败 body = {"new_display_name": "x" * 200, "include_credentials": False} # Act diff --git a/backend/test/integration/api/channels/test_allowlist_router.py b/backend/test/integration/api/channels/test_allowlist_router.py index 26978967..62a785ac 100644 --- a/backend/test/integration/api/channels/test_allowlist_router.py +++ b/backend/test/integration/api/channels/test_allowlist_router.py @@ -41,18 +41,14 @@ async def test_list_whitelist_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_list_whitelist_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_whitelist_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(_path(), headers=standard_user["headers"]) # Assert assert response.status_code == 403 -async def test_admin_list_whitelist_returns_404_for_non_existent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_list_whitelist_returns_404_for_non_existent_account(test_client: httpx.AsyncClient, admin_headers): # Act — 非存在账户 → 404 response = await test_client.get(_path(), headers=admin_headers) # Assert @@ -64,9 +60,7 @@ async def test_admin_list_whitelist_returns_404_for_non_existent_account( # ============================================================================= -async def test_list_whitelist_rejects_invalid_policy_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_whitelist_rejects_invalid_policy_type(test_client: httpx.AsyncClient, admin_headers): # Act — policy_type="foo" 非法,_parse_policy_type 抛 ValidationError → 400 response = await test_client.get(_path(policy_type="foo"), headers=admin_headers) # Assert @@ -78,9 +72,7 @@ async def test_list_whitelist_rejects_invalid_policy_type( # ============================================================================= -async def test_add_whitelist_rejects_invalid_expires_at( - test_client: httpx.AsyncClient, admin_headers -): +async def test_add_whitelist_rejects_invalid_expires_at(test_client: httpx.AsyncClient, admin_headers): # Arrange — expires_at 格式非法 body = {"peer_id": NON_EXISTENT_PEER_ID, "expires_at": "not-a-date"} # Act @@ -94,26 +86,16 @@ async def test_add_whitelist_rejects_invalid_expires_at( # ============================================================================= -async def test_batch_add_whitelist_rejects_empty_items( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_add_whitelist_rejects_empty_items(test_client: httpx.AsyncClient, admin_headers): # Act — items 为空违反 min_length=1 - response = await test_client.post( - _path(suffix="/batch"), json={"items": []}, headers=admin_headers - ) + response = await test_client.post(_path(suffix="/batch"), json={"items": []}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_batch_add_whitelist_rejects_too_many_items( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_add_whitelist_rejects_too_many_items(test_client: httpx.AsyncClient, admin_headers): # Arrange — 501 条超过 max_length=500 - body = { - "items": [ - {"peer_id": f"peer_{i}"} for i in range(501) - ] - } + body = {"items": [{"peer_id": f"peer_{i}"} for i in range(501)]} # Act response = await test_client.post(_path(suffix="/batch"), json=body, headers=admin_headers) # Assert @@ -125,13 +107,9 @@ async def test_batch_add_whitelist_rejects_too_many_items( # ============================================================================= -async def test_export_whitelist_rejects_invalid_format( - test_client: httpx.AsyncClient, admin_headers -): +async def test_export_whitelist_rejects_invalid_format(test_client: httpx.AsyncClient, admin_headers): # Act — format="xml" 非法,_parse_export_format 抛 ValidationError → 400 - response = await test_client.get( - _path(suffix="/export"), params={"format": "xml"}, headers=admin_headers - ) + response = await test_client.get(_path(suffix="/export"), params={"format": "xml"}, headers=admin_headers) # Assert assert response.status_code == 400, response.text @@ -141,13 +119,9 @@ async def test_export_whitelist_rejects_invalid_format( # ============================================================================= -async def test_batch_delete_whitelist_rejects_empty_peer_ids( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_delete_whitelist_rejects_empty_peer_ids(test_client: httpx.AsyncClient, admin_headers): # Act — peer_ids 为空违反 min_length=1 - response = await test_client.post( - _path(suffix="/batch-delete"), json={"peer_ids": []}, headers=admin_headers - ) + response = await test_client.post(_path(suffix="/batch-delete"), json={"peer_ids": []}, headers=admin_headers) # Assert assert response.status_code == 422, response.text @@ -157,24 +131,16 @@ async def test_batch_delete_whitelist_rejects_empty_peer_ids( # ============================================================================= -async def test_clean_expired_whitelist_rejects_zero_max_count( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clean_expired_whitelist_rejects_zero_max_count(test_client: httpx.AsyncClient, admin_headers): # Act — max_count=0 违反 ge=1 - response = await test_client.post( - _path(suffix="/clean-expired"), json={"max_count": 0}, headers=admin_headers - ) + response = await test_client.post(_path(suffix="/clean-expired"), json={"max_count": 0}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_clean_expired_whitelist_rejects_max_count_over_limit( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clean_expired_whitelist_rejects_max_count_over_limit(test_client: httpx.AsyncClient, admin_headers): # Act — max_count=1001 违反 le=1000 - response = await test_client.post( - _path(suffix="/clean-expired"), json={"max_count": 1001}, headers=admin_headers - ) + response = await test_client.post(_path(suffix="/clean-expired"), json={"max_count": 1001}, headers=admin_headers) # Assert assert response.status_code == 422, response.text @@ -184,15 +150,11 @@ async def test_clean_expired_whitelist_rejects_max_count_over_limit( # ============================================================================= -async def test_update_whitelist_returns_404_for_non_existent_peer( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_whitelist_returns_404_for_non_existent_peer(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"peer_name": "updated_name"} # Act — router 声明 PATCH(非 PUT),错误方法会返回 405 - response = await test_client.patch( - _path(suffix=f"/{NON_EXISTENT_PEER_ID}"), json=body, headers=admin_headers - ) + response = await test_client.patch(_path(suffix=f"/{NON_EXISTENT_PEER_ID}"), json=body, headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -202,13 +164,9 @@ async def test_update_whitelist_returns_404_for_non_existent_peer( # ============================================================================= -async def test_delete_whitelist_returns_404_for_non_existent_peer( - test_client: httpx.AsyncClient, admin_headers -): +async def test_delete_whitelist_returns_404_for_non_existent_peer(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.delete( - _path(suffix=f"/{NON_EXISTENT_PEER_ID}"), headers=admin_headers - ) + response = await test_client.delete(_path(suffix=f"/{NON_EXISTENT_PEER_ID}"), headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -218,15 +176,11 @@ async def test_delete_whitelist_returns_404_for_non_existent_peer( # ============================================================================= -async def test_batch_static_path_not_captured_as_policy_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_static_path_not_captured_as_policy_type(test_client: httpx.AsyncClient, admin_headers): # Arrange — 合法批量请求体,policy_type=dm,静态后缀 /batch body = {"items": [{"peer_id": "p1"}]} # Act - response = await test_client.post( - _path(suffix="/batch"), json=body, headers=admin_headers - ) + response = await test_client.post(_path(suffix="/batch"), json=body, headers=admin_headers) # Assert — 路由到 batch 端点:非存在账户 404;若 /batch 被当作 policy_type # 则会返回 400(unsupported policy_type)。此处断言非 400 以确认路由正确。 assert response.status_code != 400, response.text diff --git a/backend/test/integration/api/channels/test_audit_router.py b/backend/test/integration/api/channels/test_audit_router.py index 5a217f0e..66862554 100644 --- a/backend/test/integration/api/channels/test_audit_router.py +++ b/backend/test/integration/api/channels/test_audit_router.py @@ -24,20 +24,14 @@ async def test_query_audit_logs_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_query_audit_logs_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_query_audit_logs_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act - response = await test_client.get( - f"{AUDIT_URL}/logs", headers=standard_user["headers"] - ) + response = await test_client.get(f"{AUDIT_URL}/logs", headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text -async def test_admin_can_query_audit_logs( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_query_audit_logs(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{AUDIT_URL}/logs", headers=admin_headers) # Assert @@ -52,35 +46,23 @@ async def test_admin_can_query_audit_logs( # ============================================================================= -async def test_query_audit_logs_rejects_limit_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_query_audit_logs_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 - response = await test_client.get( - f"{AUDIT_URL}/logs", params={"limit": 0}, headers=admin_headers - ) + response = await test_client.get(f"{AUDIT_URL}/logs", params={"limit": 0}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_query_audit_logs_rejects_limit_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_query_audit_logs_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=501 violates le=500 - response = await test_client.get( - f"{AUDIT_URL}/logs", params={"limit": 501}, headers=admin_headers - ) + response = await test_client.get(f"{AUDIT_URL}/logs", params={"limit": 501}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_query_audit_logs_rejects_negative_offset( - test_client: httpx.AsyncClient, admin_headers -): +async def test_query_audit_logs_rejects_negative_offset(test_client: httpx.AsyncClient, admin_headers): # Act — offset=-1 violates ge=0 - response = await test_client.get( - f"{AUDIT_URL}/logs", params={"offset": -1}, headers=admin_headers - ) + response = await test_client.get(f"{AUDIT_URL}/logs", params={"offset": -1}, headers=admin_headers) # Assert assert response.status_code == 422, response.text @@ -90,9 +72,7 @@ async def test_query_audit_logs_rejects_negative_offset( # ============================================================================= -async def test_admin_can_get_audit_log_stats( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_audit_log_stats(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{AUDIT_URL}/logs/stats", headers=admin_headers) # Assert @@ -102,9 +82,7 @@ async def test_admin_can_get_audit_log_stats( assert "data" in payload -async def test_admin_can_list_audit_operations( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_list_audit_operations(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{AUDIT_URL}/operations", headers=admin_headers) # Assert @@ -119,9 +97,7 @@ async def test_admin_can_list_audit_operations( # ============================================================================= -async def test_admin_can_export_audit_logs( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_export_audit_logs(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{AUDIT_URL}/export", headers=admin_headers) # Assert @@ -137,13 +113,9 @@ async def test_admin_can_export_audit_logs( # ============================================================================= -async def test_admin_can_get_retention_policy( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_retention_policy(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - f"{AUDIT_URL}/retention-policy", headers=admin_headers - ) + response = await test_client.get(f"{AUDIT_URL}/retention-policy", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() @@ -165,9 +137,7 @@ async def test_update_retention_policy_requires_auth(test_client: httpx.AsyncCli assert response.status_code == 401, response.text -async def test_update_retention_policy_rejects_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_update_retention_policy_rejects_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"default_retention_days": 90} # Act @@ -180,17 +150,13 @@ async def test_update_retention_policy_rejects_standard_user( assert response.status_code == 403, response.text -async def test_superadmin_can_update_retention_policy( - test_client: httpx.AsyncClient, admin_headers -): +async def test_superadmin_can_update_retention_policy(test_client: httpx.AsyncClient, admin_headers): # Arrange — admin_headers 实际为 superadmin(见 conftest.py + test_auth_router # 中 _require_superadmin 校验),通过 get_superadmin_user 守门;body 合法 # 应返回 200。standard_user 被拒场景由 test_update_retention_policy_rejects_standard_user 覆盖。 body = {"default_retention_days": 90} # Act - response = await test_client.put( - f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers - ) + response = await test_client.put(f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() @@ -212,9 +178,7 @@ async def test_update_retention_policy_rejects_archive_before_ge_retention( "auto_archive_before_days": 10, } # Act - response = await test_client.put( - f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers - ) + response = await test_client.put(f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers) # Assert — DTO __post_init__ 抛 ValidationError -> 400 assert response.status_code == 400, response.text @@ -224,12 +188,8 @@ async def test_update_retention_policy_rejects_archive_before_ge_retention( # ============================================================================= -async def test_get_audit_log_returns_404_for_nonexistent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_audit_log_returns_404_for_nonexistent(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - f"{AUDIT_URL}/logs/{NON_EXISTENT_LOG_ID}", headers=admin_headers - ) + response = await test_client.get(f"{AUDIT_URL}/logs/{NON_EXISTENT_LOG_ID}", headers=admin_headers) # Assert assert response.status_code == 404, response.text diff --git a/backend/test/integration/api/channels/test_capability_router.py b/backend/test/integration/api/channels/test_capability_router.py index 35a73122..8497136b 100644 --- a/backend/test/integration/api/channels/test_capability_router.py +++ b/backend/test/integration/api/channels/test_capability_router.py @@ -29,9 +29,7 @@ async def test_list_capabilities_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_list_capabilities_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_capabilities_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(CAPABILITIES_URL, headers=standard_user["headers"]) # Assert @@ -54,13 +52,9 @@ async def test_admin_can_list_capabilities(test_client: httpx.AsyncClient, admin # ============================================================================= -async def test_admin_can_get_capability_for_channel_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_capability_for_channel_type(test_client: httpx.AsyncClient, admin_headers): # Act — DEFAULT_CHANNEL_TYPE 已注册则 200,未注册则 404 - response = await test_client.get( - f"{CAPABILITIES_URL}/{DEFAULT_CHANNEL_TYPE}", headers=admin_headers - ) + response = await test_client.get(f"{CAPABILITIES_URL}/{DEFAULT_CHANNEL_TYPE}", headers=admin_headers) # Assert assert response.status_code in (200, 404), response.text @@ -69,9 +63,7 @@ async def test_admin_can_get_capability_for_channel_type( assert payload["success"] is True -async def test_get_capability_unregistered_channel_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_capability_unregistered_channel_type(test_client: httpx.AsyncClient, admin_headers): # Act — "foo" 是合法字符串但未注册插件 response = await test_client.get(f"{CAPABILITIES_URL}/foo", headers=admin_headers) # Assert - channel_type 无白名单,任何字符串都合法,但未注册插件返回 404 diff --git a/backend/test/integration/api/channels/test_config_router.py b/backend/test/integration/api/channels/test_config_router.py index 1bca89a9..21935c7c 100644 --- a/backend/test/integration/api/channels/test_config_router.py +++ b/backend/test/integration/api/channels/test_config_router.py @@ -30,9 +30,7 @@ async def test_get_config_schema_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_get_config_schema_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +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 @@ -62,18 +60,14 @@ async def test_export_config_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_export_config_requires_superadmin_for_standard_user( - test_client: httpx.AsyncClient, standard_user -): +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 -): +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 覆盖。 @@ -100,22 +94,16 @@ async def test_import_config_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_import_config_rejects_standard_user( - test_client: httpx.AsyncClient, standard_user -): +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"] - ) + 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 -): +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} @@ -143,33 +131,23 @@ async def test_batch_update_config_requires_auth(test_client: httpx.AsyncClient) assert response.status_code == 401 -async def test_batch_update_config_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +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"] - ) + 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 -): +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 - ) + 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 -): +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 @@ -183,13 +161,9 @@ async def test_batch_update_config_rejects_too_many_updates( # ============================================================================= -async def test_get_config_returns_404_for_non_existent_key( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_config_returns_404_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 - ) + response = await test_client.get(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -199,15 +173,11 @@ async def test_get_config_returns_404_for_non_existent_key( # ============================================================================= -async def test_update_config_returns_404_for_non_existent_key( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_config_returns_404_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 - ) + response = await test_client.put(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}", json=body, headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -217,9 +187,7 @@ async def test_update_config_returns_404_for_non_existent_key( # ============================================================================= -async def test_rollback_config_returns_404_for_non_existent_key( - test_client: httpx.AsyncClient, admin_headers -): +async def test_rollback_config_returns_404_for_non_existent_key(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"target_version": 1} # Act @@ -230,9 +198,7 @@ async def test_rollback_config_returns_404_for_non_existent_key( assert response.status_code == 404, response.text -async def test_rollback_config_rejects_zero_target_version( - test_client: httpx.AsyncClient, admin_headers -): +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", @@ -248,13 +214,9 @@ async def test_rollback_config_rejects_zero_target_version( # ============================================================================= -async def test_get_config_history_returns_404_for_non_existent_key( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_config_history_returns_404_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 - ) + response = await test_client.get(f"{CONFIG_URL}/{NON_EXISTENT_CONFIG_KEY}/history", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -264,9 +226,7 @@ async def test_get_config_history_returns_404_for_non_existent_key( # ============================================================================= -async def test_schema_static_path_not_captured_as_key( - test_client: httpx.AsyncClient, admin_headers -): +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) diff --git a/backend/test/integration/api/channels/test_content_review_router.py b/backend/test/integration/api/channels/test_content_review_router.py index a251dc10..447dbe1d 100644 --- a/backend/test/integration/api/channels/test_content_review_router.py +++ b/backend/test/integration/api/channels/test_content_review_router.py @@ -57,9 +57,7 @@ async def test_preview_requires_admin(test_client: httpx.AsyncClient, standard_u assert response.status_code == 403 -async def test_admin_preview_returns_404_or_400_for_non_existent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_preview_returns_404_or_400_for_non_existent_account(test_client: httpx.AsyncClient, admin_headers): # Arrange — body references non-existent account body = _make_preview_payload() # Act @@ -134,9 +132,7 @@ async def test_admin_can_list_review_history(test_client: httpx.AsyncClient, adm assert isinstance(payload["data"], dict) -async def test_review_history_rejects_invalid_verdict( - test_client: httpx.AsyncClient, admin_headers -): +async def test_review_history_rejects_invalid_verdict(test_client: httpx.AsyncClient, admin_headers): # Arrange — verdict must be pass / review / block params = {"channel_type": DEFAULT_CHANNEL_TYPE, "verdict": "invalid"} # Act @@ -160,9 +156,7 @@ async def test_review_history_rejects_limit_zero(test_client: httpx.AsyncClient, assert response.status_code == 422, response.text -async def test_review_history_rejects_limit_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_review_history_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=101 violates le=100 response = await test_client.get( f"{REVIEW_URL}/history", @@ -201,9 +195,7 @@ async def test_admin_can_get_review_stats(test_client: httpx.AsyncClient, admin_ # ============================================================================= -async def test_batch_decision_rejects_empty_decisions( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_decision_rejects_empty_decisions(test_client: httpx.AsyncClient, admin_headers): # Arrange — decisions min_length=1 body = {"decisions": []} # Act @@ -216,14 +208,9 @@ async def test_batch_decision_rejects_empty_decisions( assert response.status_code == 422, response.text -async def test_batch_decision_rejects_too_many_decisions( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_decision_rejects_too_many_decisions(test_client: httpx.AsyncClient, admin_headers): # Arrange — decisions max_length=100 - decisions = [ - {"review_id": f"review_{i}", "decision": "pass", "reason": None, "categories": []} - for i in range(101) - ] + decisions = [{"review_id": f"review_{i}", "decision": "pass", "reason": None, "categories": []} for i in range(101)] body = {"decisions": decisions} # Act response = await test_client.post( @@ -265,9 +252,7 @@ async def test_batch_decision_with_non_existent_review_returns_200_or_404( # ============================================================================= -async def test_get_review_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_review_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{REVIEW_URL}/history/{NON_EXISTENT_REVIEW_ID}", @@ -282,9 +267,7 @@ async def test_get_review_returns_404_for_non_existent( # ============================================================================= -async def test_stats_path_not_captured_by_review_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_stats_path_not_captured_by_review_id(test_client: httpx.AsyncClient, admin_headers): # Act — /stats must route to stats endpoint, not /history/{review_id} response = await test_client.get( f"{REVIEW_URL}/stats", @@ -295,9 +278,7 @@ async def test_stats_path_not_captured_by_review_id( assert response.status_code == 200, response.text -async def test_batch_decision_path_not_captured_by_review_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_decision_path_not_captured_by_review_id(test_client: httpx.AsyncClient, admin_headers): # Act — /history/batch-decision must route to batch-decision endpoint body = { "decisions": [ diff --git a/backend/test/integration/api/channels/test_dashboard_router.py b/backend/test/integration/api/channels/test_dashboard_router.py index 758a0537..7baa01e7 100644 --- a/backend/test/integration/api/channels/test_dashboard_router.py +++ b/backend/test/integration/api/channels/test_dashboard_router.py @@ -24,24 +24,16 @@ async def test_get_dashboard_overview_requires_auth(test_client: httpx.AsyncClie assert response.status_code == 401, response.text -async def test_get_dashboard_overview_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_get_dashboard_overview_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act - response = await test_client.get( - f"{DASHBOARD_URL}/overview", headers=standard_user["headers"] - ) + response = await test_client.get(f"{DASHBOARD_URL}/overview", headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text -async def test_admin_can_get_dashboard_overview( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_overview(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - f"{DASHBOARD_URL}/overview", headers=admin_headers - ) + response = await test_client.get(f"{DASHBOARD_URL}/overview", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() @@ -54,9 +46,7 @@ async def test_admin_can_get_dashboard_overview( # ============================================================================= -async def test_admin_can_get_dashboard_accounts( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_accounts(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{DASHBOARD_URL}/accounts", @@ -70,9 +60,7 @@ async def test_admin_can_get_dashboard_accounts( assert "data" in payload -async def test_admin_can_get_dashboard_messages( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_messages(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{DASHBOARD_URL}/messages", @@ -90,9 +78,7 @@ async def test_admin_can_get_dashboard_messages( assert "data" in payload -async def test_admin_can_get_dashboard_sessions( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_sessions(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{DASHBOARD_URL}/sessions", @@ -106,9 +92,7 @@ async def test_admin_can_get_dashboard_sessions( assert "data" in payload -async def test_admin_can_get_dashboard_delivery( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_delivery(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{DASHBOARD_URL}/delivery", @@ -126,9 +110,7 @@ async def test_admin_can_get_dashboard_delivery( assert "data" in payload -async def test_admin_can_get_dashboard_realtime( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_dashboard_realtime(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{DASHBOARD_URL}/realtime", @@ -147,9 +129,7 @@ async def test_admin_can_get_dashboard_realtime( # ============================================================================= -async def test_dashboard_realtime_rejects_window_below_min( - test_client: httpx.AsyncClient, admin_headers -): +async def test_dashboard_realtime_rejects_window_below_min(test_client: httpx.AsyncClient, admin_headers): # Act — window_seconds=9 violates ge=10 response = await test_client.get( f"{DASHBOARD_URL}/realtime", @@ -160,9 +140,7 @@ async def test_dashboard_realtime_rejects_window_below_min( assert response.status_code == 422, response.text -async def test_dashboard_realtime_rejects_window_above_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_dashboard_realtime_rejects_window_above_max(test_client: httpx.AsyncClient, admin_headers): # Act — window_seconds=301 violates le=300 response = await test_client.get( f"{DASHBOARD_URL}/realtime", @@ -178,9 +156,7 @@ async def test_dashboard_realtime_rejects_window_above_max( # ============================================================================= -async def test_dashboard_delivery_rejects_invalid_start_time_format( - test_client: httpx.AsyncClient, admin_headers -): +async def test_dashboard_delivery_rejects_invalid_start_time_format(test_client: httpx.AsyncClient, admin_headers): # Act — "not-a-date" is not ISO 8601, parse_datetime raises ValidationError (400) response = await test_client.get( f"{DASHBOARD_URL}/delivery", diff --git a/backend/test/integration/api/channels/test_directory_router.py b/backend/test/integration/api/channels/test_directory_router.py index 75723d21..e726c28b 100644 --- a/backend/test/integration/api/channels/test_directory_router.py +++ b/backend/test/integration/api/channels/test_directory_router.py @@ -40,18 +40,14 @@ async def test_search_directory_users_requires_auth(test_client: httpx.AsyncClie assert response.status_code == 401 -async def test_search_directory_users_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_search_directory_users_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(_path("/users/search"), headers=standard_user["headers"]) # Assert assert response.status_code == 403 -async def test_admin_search_directory_users_returns_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_search_directory_users_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act — 非存在账户:404(账户未找到)或 501(适配器未实现) response = await test_client.get(_path("/users/search"), headers=admin_headers) # Assert @@ -63,36 +59,77 @@ async def test_admin_search_directory_users_returns_404_or_501( # ============================================================================= -async def test_search_directory_users_rejects_limit_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_search_directory_users_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 违反 ge=1 - response = await test_client.get( - _path("/users/search"), params={"limit": 0}, headers=admin_headers - ) + response = await test_client.get(_path("/users/search"), params={"limit": 0}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_search_directory_users_rejects_limit_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_search_directory_users_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=101 违反 le=100 + response = await test_client.get(_path("/users/search"), params={"limit": 101}, headers=admin_headers) + # Assert + assert response.status_code == 422, response.text + + +# ============================================================================= +# === GET /directory/suggestions — auth three-tier & validation === +# ============================================================================= + + +async def test_search_directory_suggestions_requires_auth(test_client: httpx.AsyncClient): + # Act + response = await test_client.get(_path("/suggestions")) + # Assert + assert response.status_code == 401 + + +async def test_search_directory_suggestions_requires_admin(test_client: httpx.AsyncClient, standard_user): + # Act + response = await test_client.get(_path("/suggestions"), params={"keyword": "foo"}, headers=standard_user["headers"]) + # Assert + assert response.status_code == 403 + + +async def test_search_directory_suggestions_rejects_missing_keyword(test_client: httpx.AsyncClient, admin_headers): + # Act — keyword 必填 + response = await test_client.get(_path("/suggestions"), headers=admin_headers) + # Assert + assert response.status_code == 422, response.text + + +async def test_search_directory_suggestions_rejects_empty_keyword(test_client: httpx.AsyncClient, admin_headers): + # Act — keyword 最小长度 1 + response = await test_client.get(_path("/suggestions"), params={"keyword": ""}, headers=admin_headers) + # Assert + assert response.status_code == 422, response.text + + +async def test_search_directory_suggestions_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): + # Act — limit=21 违反 le=20 response = await test_client.get( - _path("/users/search"), params={"limit": 101}, headers=admin_headers + _path("/suggestions"), + params={"keyword": "foo", "limit": 21}, + headers=admin_headers, ) # Assert assert response.status_code == 422, response.text +async def test_search_directory_suggestions_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): + # Act — 非存在账户:404(账户未找到)或 501(适配器未实现) + response = await test_client.get(_path("/suggestions"), params={"keyword": "foo"}, headers=admin_headers) + # Assert + assert response.status_code in (404, 501), response.text + + # ============================================================================= # === GET /directory/groups/search — non-existent account === # ============================================================================= -async def test_search_directory_groups_returns_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_search_directory_groups_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(_path("/groups/search"), headers=admin_headers) # Assert @@ -104,13 +141,9 @@ async def test_search_directory_groups_returns_404_or_501( # ============================================================================= -async def test_get_directory_user_profile_returns_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_directory_user_profile_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - _path(f"/users/{NON_EXISTENT_PEER_ID}"), headers=admin_headers - ) + response = await test_client.get(_path(f"/users/{NON_EXISTENT_PEER_ID}"), headers=admin_headers) # Assert assert response.status_code in (404, 501), response.text @@ -120,13 +153,9 @@ async def test_get_directory_user_profile_returns_404_or_501( # ============================================================================= -async def test_get_directory_group_detail_returns_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_directory_group_detail_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - _path(f"/groups/{NON_EXISTENT_GROUP_ID}"), headers=admin_headers - ) + response = await test_client.get(_path(f"/groups/{NON_EXISTENT_GROUP_ID}"), headers=admin_headers) # Assert assert response.status_code in (404, 501), response.text @@ -136,13 +165,9 @@ async def test_get_directory_group_detail_returns_404_or_501( # ============================================================================= -async def test_get_directory_group_members_returns_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_directory_group_members_returns_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - _path(f"/groups/{NON_EXISTENT_GROUP_ID}/members"), headers=admin_headers - ) + response = await test_client.get(_path(f"/groups/{NON_EXISTENT_GROUP_ID}/members"), headers=admin_headers) # Assert assert response.status_code in (404, 501), response.text @@ -152,13 +177,9 @@ async def test_get_directory_group_members_returns_404_or_501( # ============================================================================= -async def test_clear_directory_cache_returns_404_or_200_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clear_directory_cache_returns_404_or_200_or_501(test_client: httpx.AsyncClient, admin_headers): # Act — 合法 scope=all - response = await test_client.delete( - _path("/cache"), params={"scope": "all"}, headers=admin_headers - ) + response = await test_client.delete(_path("/cache"), params={"scope": "all"}, headers=admin_headers) # Assert assert response.status_code in (200, 404, 501), response.text @@ -167,9 +188,7 @@ async def test_clear_directory_cache_does_not_reject_path_for_invalid_scope( test_client: httpx.AsyncClient, admin_headers ): # Act — scope 为字符串参数(无枚举校验),不应触发路径级 401/403/422 - response = await test_client.delete( - _path("/cache"), params={"scope": "invalid_scope_value"}, headers=admin_headers - ) + response = await test_client.delete(_path("/cache"), params={"scope": "invalid_scope_value"}, headers=admin_headers) # Assert — scope 未做枚举校验,可能 200 或 404/501,但不应为鉴权/路径错误 assert response.status_code not in (401, 403, 422), response.text @@ -179,3 +198,59 @@ async def test_clear_directory_cache_requires_auth(test_client: httpx.AsyncClien response = await test_client.delete(_path("/cache")) # Assert assert response.status_code == 401 + + +# ============================================================================= +# === POST /directory/export — auth / validation / non-existent account === +# ============================================================================= + + +async def test_export_directory_requires_auth(test_client: httpx.AsyncClient): + # Act + response = await test_client.post(_path("/export"), json={"scope": "users"}) + # Assert + assert response.status_code == 401 + + +async def test_export_directory_requires_admin(test_client: httpx.AsyncClient, standard_user): + # Act + response = await test_client.post(_path("/export"), json={"scope": "users"}, headers=standard_user["headers"]) + # Assert + assert response.status_code == 403 + + +async def test_export_directory_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): + # Act — limit=1001 违反 le=1000 + response = await test_client.post( + _path("/export"), + json={"scope": "users", "limit": 1001}, + headers=admin_headers, + ) + # Assert + assert response.status_code == 422, response.text + + +async def test_export_directory_csv_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): + # Act — 非存在账户:200 仅当存在 mock 适配器,否则 404/501 + response = await test_client.post( + _path("/export"), + json={"scope": "users", "format": "csv"}, + headers=admin_headers, + ) + # Assert + assert response.status_code in (200, 404, 501), response.text + if response.status_code == 200: + assert response.headers["content-type"].startswith("text/csv") + + +async def test_export_directory_json_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): + # Act + response = await test_client.post( + _path("/export"), + json={"scope": "groups", "format": "json"}, + headers=admin_headers, + ) + # Assert + assert response.status_code in (200, 404, 501), response.text + if response.status_code == 200: + assert response.headers["content-type"].startswith("application/json") diff --git a/backend/test/integration/api/channels/test_doctor_router.py b/backend/test/integration/api/channels/test_doctor_router.py index 0834d841..50b67ae3 100644 --- a/backend/test/integration/api/channels/test_doctor_router.py +++ b/backend/test/integration/api/channels/test_doctor_router.py @@ -14,9 +14,7 @@ from .conftest import ( pytestmark = [pytest.mark.asyncio, pytest.mark.integration] -CHECKS_URL = ( - f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/doctor/checks" -) +CHECKS_URL = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/doctor/checks" # ============================================================================= @@ -31,20 +29,14 @@ async def test_list_doctor_checks_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_list_doctor_checks_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_doctor_checks_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act - response = await test_client.get( - CHECKS_URL, headers=standard_user["headers"] - ) + response = await test_client.get(CHECKS_URL, headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text -async def test_admin_list_doctor_checks_nonexistent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_list_doctor_checks_nonexistent_account(test_client: httpx.AsyncClient, admin_headers): # Act — non-existent account: 404 (not found) or 501 (adapter not registered) response = await test_client.get(CHECKS_URL, headers=admin_headers) # Assert @@ -56,31 +48,21 @@ async def test_admin_list_doctor_checks_nonexistent_account( # ============================================================================= -async def test_run_all_doctor_checks_nonexistent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_run_all_doctor_checks_nonexistent_account(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.post( - f"{CHECKS_URL}/run-all", headers=admin_headers - ) + response = await test_client.post(f"{CHECKS_URL}/run-all", headers=admin_headers) # Assert assert response.status_code in (404, 501), response.text -async def test_get_doctor_check_nonexistent_check( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_doctor_check_nonexistent_check(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - f"{CHECKS_URL}/{NON_EXISTENT_CHECK_ID}", headers=admin_headers - ) + response = await test_client.get(f"{CHECKS_URL}/{NON_EXISTENT_CHECK_ID}", headers=admin_headers) # Assert assert response.status_code in (404, 501), response.text -async def test_run_doctor_check_nonexistent_check( - test_client: httpx.AsyncClient, admin_headers -): +async def test_run_doctor_check_nonexistent_check(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{CHECKS_URL}/{NON_EXISTENT_CHECK_ID}/run", @@ -90,9 +72,7 @@ async def test_run_doctor_check_nonexistent_check( assert response.status_code in (404, 501), response.text -async def test_preview_doctor_repair_nonexistent_check( - test_client: httpx.AsyncClient, admin_headers -): +async def test_preview_doctor_repair_nonexistent_check(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{CHECKS_URL}/{NON_EXISTENT_CHECK_ID}/repair/preview", @@ -102,9 +82,7 @@ async def test_preview_doctor_repair_nonexistent_check( assert response.status_code in (404, 501), response.text -async def test_execute_doctor_repair_nonexistent_check( - test_client: httpx.AsyncClient, admin_headers -): +async def test_execute_doctor_repair_nonexistent_check(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{CHECKS_URL}/{NON_EXISTENT_CHECK_ID}/repair", @@ -119,9 +97,7 @@ async def test_execute_doctor_repair_nonexistent_check( # ============================================================================= -async def test_run_doctor_check_rejects_invalid_check_id_format( - test_client: httpx.AsyncClient, admin_headers -): +async def test_run_doctor_check_rejects_invalid_check_id_format(test_client: httpx.AsyncClient, admin_headers): # Arrange — "invalid@check" violates regex ^[A-Za-z0-9_-]+$ # _validateCheckId runs before adapter/account lookup, so returns 400 invalid_check_id = "invalid@check" diff --git a/backend/test/integration/api/channels/test_health_router.py b/backend/test/integration/api/channels/test_health_router.py index ee601741..cd571996 100644 --- a/backend/test/integration/api/channels/test_health_router.py +++ b/backend/test/integration/api/channels/test_health_router.py @@ -10,9 +10,7 @@ from .conftest import BASE_URL, DEFAULT_CHANNEL_TYPE, NON_EXISTENT_ACCOUNT_ID pytestmark = [pytest.mark.asyncio, pytest.mark.integration] HEALTH_URL = f"{BASE_URL}/health" -SINGLE_HEALTH_URL = ( - f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/health" -) +SINGLE_HEALTH_URL = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/health" PROBE_URL = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/probe" @@ -35,9 +33,7 @@ async def test_get_health_does_not_require_auth(test_client: httpx.AsyncClient): assert "channels" in data -async def test_get_health_works_with_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_get_health_works_with_standard_user(test_client: httpx.AsyncClient, standard_user): # Act — standard user headers also accepted (no auth required) response = await test_client.get(HEALTH_URL, headers=standard_user["headers"]) # Assert @@ -51,9 +47,7 @@ async def test_get_health_works_with_standard_user( assert "channels" in data -async def test_get_health_not_captured_by_channel_type_path( - test_client: httpx.AsyncClient -): +async def test_get_health_not_captured_by_channel_type_path(test_client: httpx.AsyncClient): # Act — /health must hit the static health endpoint, not be captured as # channel_type="health" by the dynamic /{channel_type}/{account_id}/... path response = await test_client.get(HEALTH_URL) @@ -79,20 +73,14 @@ async def test_get_health_detail_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_get_health_detail_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_get_health_detail_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act - response = await test_client.get( - f"{HEALTH_URL}/detail", headers=standard_user["headers"] - ) + response = await test_client.get(f"{HEALTH_URL}/detail", headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text -async def test_admin_can_get_health_detail( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_get_health_detail(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{HEALTH_URL}/detail", headers=admin_headers) # Assert @@ -109,16 +97,12 @@ async def test_admin_can_get_health_detail( async def test_export_diagnostics_requires_auth(test_client: httpx.AsyncClient): # Act - response = await test_client.post( - f"{HEALTH_URL}/diagnostics/export", json={} - ) + response = await test_client.post(f"{HEALTH_URL}/diagnostics/export", json={}) # Assert assert response.status_code == 401, response.text -async def test_export_diagnostics_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_export_diagnostics_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.post( f"{HEALTH_URL}/diagnostics/export", @@ -129,15 +113,11 @@ async def test_export_diagnostics_requires_admin( assert response.status_code == 403, response.text -async def test_admin_can_export_diagnostics_with_empty_body( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_export_diagnostics_with_empty_body(test_client: httpx.AsyncClient, admin_headers): # Arrange — all fields optional, empty dict is valid body: dict = {} # Act - response = await test_client.post( - f"{HEALTH_URL}/diagnostics/export", json=body, headers=admin_headers - ) + response = await test_client.post(f"{HEALTH_URL}/diagnostics/export", json=body, headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() @@ -150,9 +130,7 @@ async def test_admin_can_export_diagnostics_with_empty_body( # ============================================================================= -async def test_export_diagnostics_rejects_audit_log_count_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_export_diagnostics_rejects_audit_log_count_zero(test_client: httpx.AsyncClient, admin_headers): # Act — audit_log_count=0 violates ge=1 response = await test_client.post( f"{HEALTH_URL}/diagnostics/export", @@ -163,9 +141,7 @@ async def test_export_diagnostics_rejects_audit_log_count_zero( assert response.status_code == 422, response.text -async def test_export_diagnostics_rejects_audit_log_count_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_export_diagnostics_rejects_audit_log_count_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — audit_log_count=501 violates le=500 response = await test_client.post( f"{HEALTH_URL}/diagnostics/export", @@ -181,24 +157,18 @@ async def test_export_diagnostics_rejects_audit_log_count_over_max( # ============================================================================= -async def test_get_single_health_nonexistent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_single_health_nonexistent_account(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(SINGLE_HEALTH_URL, headers=admin_headers) # Assert — non-existent account: 404 (not found) or 501 (adapter not registered) assert response.status_code in (404, 501), response.text -async def test_probe_channel_nonexistent_account( - test_client: httpx.AsyncClient, admin_headers -): +async def test_probe_channel_nonexistent_account(test_client: httpx.AsyncClient, admin_headers): # Arrange — valid probe body with defaults body = {"probe_type": "connectivity", "timeout_ms": 5000} # Act - response = await test_client.post( - PROBE_URL, json=body, headers=admin_headers - ) + response = await test_client.post(PROBE_URL, json=body, headers=admin_headers) # Assert — non-existent account: 404 or 501 assert response.status_code in (404, 501), response.text @@ -208,9 +178,7 @@ async def test_probe_channel_nonexistent_account( # ============================================================================= -async def test_probe_rejects_timeout_below_min( - test_client: httpx.AsyncClient, admin_headers -): +async def test_probe_rejects_timeout_below_min(test_client: httpx.AsyncClient, admin_headers): # Act — timeout_ms=999 violates ge=1000 response = await test_client.post( PROBE_URL, @@ -221,9 +189,7 @@ async def test_probe_rejects_timeout_below_min( assert response.status_code == 422, response.text -async def test_probe_rejects_timeout_above_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_probe_rejects_timeout_above_max(test_client: httpx.AsyncClient, admin_headers): # Act — timeout_ms=30001 violates le=30000 response = await test_client.post( PROBE_URL, diff --git a/backend/test/integration/api/channels/test_login_router.py b/backend/test/integration/api/channels/test_login_router.py index 62daafe2..966c07ba 100644 --- a/backend/test/integration/api/channels/test_login_router.py +++ b/backend/test/integration/api/channels/test_login_router.py @@ -14,9 +14,7 @@ from .conftest import ( pytestmark = [pytest.mark.asyncio, pytest.mark.integration] -LOGIN_BASE = ( - f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/login" -) +LOGIN_BASE = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/accounts/{NON_EXISTENT_ACCOUNT_ID}/login" # ============================================================================= @@ -31,9 +29,7 @@ async def test_get_login_status_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_get_login_status_forbids_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_get_login_status_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get( f"{LOGIN_BASE}/status", @@ -43,9 +39,7 @@ async def test_get_login_status_forbids_standard_user( assert response.status_code == 403, response.text -async def test_get_login_status_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_login_status_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(f"{LOGIN_BASE}/status", headers=admin_headers) # Assert @@ -74,9 +68,7 @@ async def test_qr_start_forbids_standard_user(test_client: httpx.AsyncClient, st assert response.status_code == 403, response.text -async def test_qr_start_non_existent_account_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_qr_start_non_existent_account_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post(f"{LOGIN_BASE}/qr/start", headers=admin_headers) # Assert @@ -95,9 +87,7 @@ async def test_qr_wait_requires_session_id(test_client: httpx.AsyncClient, admin assert response.status_code == 422, response.text -async def test_qr_wait_non_existent_account_returns_404_or_422( - test_client: httpx.AsyncClient, admin_headers -): +async def test_qr_wait_non_existent_account_returns_404_or_422(test_client: httpx.AsyncClient, admin_headers): # Arrange params = {"session_id": NON_EXISTENT_SESSION_ID} # Act @@ -148,9 +138,7 @@ async def test_qr_cancel_requires_session_id(test_client: httpx.AsyncClient, adm assert response.status_code == 422, response.text -async def test_qr_cancel_non_existent_account_returns_404_or_422( - test_client: httpx.AsyncClient, admin_headers -): +async def test_qr_cancel_non_existent_account_returns_404_or_422(test_client: httpx.AsyncClient, admin_headers): # Arrange params = {"session_id": NON_EXISTENT_SESSION_ID} # Act @@ -175,9 +163,7 @@ async def test_qr_refresh_requires_session_id(test_client: httpx.AsyncClient, ad assert response.status_code == 422, response.text -async def test_qr_refresh_non_existent_account_returns_404_or_422( - test_client: httpx.AsyncClient, admin_headers -): +async def test_qr_refresh_non_existent_account_returns_404_or_422(test_client: httpx.AsyncClient, admin_headers): # Arrange params = {"session_id": NON_EXISTENT_SESSION_ID} # Act @@ -212,9 +198,7 @@ async def test_logout_forbids_standard_user(test_client: httpx.AsyncClient, stan assert response.status_code == 403, response.text -async def test_logout_non_existent_account_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_logout_non_existent_account_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post(f"{LOGIN_BASE}/logout", headers=admin_headers) # Assert @@ -235,9 +219,7 @@ async def test_force_logout_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_force_logout_forbids_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_force_logout_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = {"reason": "pytest"} # Act @@ -261,9 +243,7 @@ async def test_force_logout_requires_reason(test_client: httpx.AsyncClient, admi assert response.status_code == 422, response.text -async def test_force_logout_non_existent_account_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_force_logout_non_existent_account_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"reason": "pytest"} # Act diff --git a/backend/test/integration/api/channels/test_message_router.py b/backend/test/integration/api/channels/test_message_router.py index c9ede7e5..a306b9dd 100644 --- a/backend/test/integration/api/channels/test_message_router.py +++ b/backend/test/integration/api/channels/test_message_router.py @@ -205,9 +205,7 @@ async def test_get_message_returns_404_for_non_existent(test_client: httpx.Async assert response.status_code == 404, response.text -async def test_get_message_status_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_message_status_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{MESSAGES_URL}/{NON_EXISTENT_MESSAGE_ID}/status", @@ -233,9 +231,7 @@ async def test_search_path_not_captured_by_message_id(test_client: httpx.AsyncCl assert response.status_code == 200, response.text -async def test_batch_recall_path_not_captured_by_message_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_recall_path_not_captured_by_message_id(test_client: httpx.AsyncClient, admin_headers): # Act — /batch-recall must route to batch-recall endpoint response = await test_client.post( f"{MESSAGES_URL}/batch-recall", @@ -251,9 +247,7 @@ async def test_batch_recall_path_not_captured_by_message_id( # ============================================================================= -async def test_send_admin_message_requires_idempotency_key( - test_client: httpx.AsyncClient, admin_headers -): +async def test_send_admin_message_requires_idempotency_key(test_client: httpx.AsyncClient, admin_headers): # Arrange — missing X-Idempotency-Key header body = { "target": "test_target", @@ -316,9 +310,7 @@ async def test_upload_attachment_requires_file(test_client: httpx.AsyncClient, a # ============================================================================= -async def test_recall_message_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_recall_message_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/messages/{NON_EXISTENT_MESSAGE_ID}/recall", @@ -333,9 +325,7 @@ async def test_recall_message_returns_404_for_non_existent( # ============================================================================= -async def test_resend_message_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_resend_message_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"target": "test_target", "reason": "pytest resend"} # Act diff --git a/backend/test/integration/api/channels/test_outbox_router.py b/backend/test/integration/api/channels/test_outbox_router.py index 677b4e21..26844368 100644 --- a/backend/test/integration/api/channels/test_outbox_router.py +++ b/backend/test/integration/api/channels/test_outbox_router.py @@ -30,9 +30,7 @@ async def test_list_outbox_messages_requires_auth(test_client: httpx.AsyncClient assert response.status_code == 401 -async def test_list_outbox_messages_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_outbox_messages_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(f"{OUTBOX_URL}/messages", headers=standard_user["headers"]) # Assert @@ -56,9 +54,7 @@ async def test_admin_can_list_outbox_messages(test_client: httpx.AsyncClient, ad # ============================================================================= -async def test_list_outbox_messages_rejects_limit_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_outbox_messages_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( f"{OUTBOX_URL}/messages", @@ -69,9 +65,7 @@ async def test_list_outbox_messages_rejects_limit_zero( assert response.status_code == 422, response.text -async def test_list_outbox_messages_rejects_limit_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_outbox_messages_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=201 violates le=200 response = await test_client.get( f"{OUTBOX_URL}/messages", @@ -96,6 +90,10 @@ async def test_admin_can_get_outbox_stats(test_client: httpx.AsyncClient, admin_ payload = response.json() assert payload["success"] is True assert isinstance(payload["data"], dict) + data = payload["data"] + assert "top_errors" in data and isinstance(data["top_errors"], list) + assert "avg_latency_ms" in data + assert "oldest_pending_at" in data # ============================================================================= @@ -256,9 +254,7 @@ async def test_admin_can_update_retry_policy(test_client: httpx.AsyncClient, adm assert isinstance(payload["data"], dict) -async def test_update_retry_policy_rejects_max_retry_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_retry_policy_rejects_max_retry_zero(test_client: httpx.AsyncClient, admin_headers): # Act — max_retry=0 violates ge=1 body = {"max_retry": 0, "ttl_seconds": 3600, "retry_backoff_schedule": [10, 30, 60]} response = await test_client.put( @@ -270,9 +266,7 @@ async def test_update_retry_policy_rejects_max_retry_zero( assert response.status_code == 422, response.text -async def test_update_retry_policy_rejects_max_retry_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_retry_policy_rejects_max_retry_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — max_retry=21 violates le=20 body = {"max_retry": 21, "ttl_seconds": 3600, "retry_backoff_schedule": [10, 30, 60]} response = await test_client.put( @@ -283,9 +277,8 @@ async def test_update_retry_policy_rejects_max_retry_over_max( # Assert assert response.status_code == 422, response.text -async def test_update_retry_policy_rejects_ttl_too_small( - test_client: httpx.AsyncClient, admin_headers -): + +async def test_update_retry_policy_rejects_ttl_too_small(test_client: httpx.AsyncClient, admin_headers): # Act — ttl_seconds=59 violates ge=60 body = {"max_retry": 5, "ttl_seconds": 59, "retry_backoff_schedule": [10, 30, 60]} response = await test_client.put( @@ -297,9 +290,7 @@ async def test_update_retry_policy_rejects_ttl_too_small( assert response.status_code == 422, response.text -async def test_update_retry_policy_rejects_ttl_too_large( - test_client: httpx.AsyncClient, admin_headers -): +async def test_update_retry_policy_rejects_ttl_too_large(test_client: httpx.AsyncClient, admin_headers): # Act — ttl_seconds=2592001 violates le=2592000 body = {"max_retry": 5, "ttl_seconds": 2592001, "retry_backoff_schedule": [10, 30, 60]} response = await test_client.put( @@ -316,9 +307,7 @@ async def test_update_retry_policy_rejects_ttl_too_large( # ============================================================================= -async def test_get_outbox_message_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_outbox_message_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}", @@ -333,9 +322,7 @@ async def test_get_outbox_message_returns_404_for_non_existent( # ============================================================================= -async def test_delete_dead_letter_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_delete_dead_letter_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.delete( f"{OUTBOX_URL}/dead-letter/{NON_EXISTENT_OUTBOX_ID}", @@ -350,9 +337,7 @@ async def test_delete_dead_letter_returns_404_for_non_existent( # ============================================================================= -async def test_retry_outbox_message_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_retry_outbox_message_returns_404_for_non_existent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}/retry", @@ -367,18 +352,14 @@ async def test_retry_outbox_message_returns_404_for_non_existent( # ============================================================================= -async def test_stats_path_not_captured_by_outbox_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_stats_path_not_captured_by_outbox_id(test_client: httpx.AsyncClient, admin_headers): # Act — /stats must route to stats endpoint, not /messages/{outbox_id} response = await test_client.get(f"{OUTBOX_URL}/stats", headers=admin_headers) # Assert assert response.status_code == 200, response.text -async def test_trend_path_not_captured_by_outbox_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_trend_path_not_captured_by_outbox_id(test_client: httpx.AsyncClient, admin_headers): # Act — /trend must route to trend endpoint response = await test_client.get( f"{OUTBOX_URL}/trend", @@ -389,18 +370,14 @@ async def test_trend_path_not_captured_by_outbox_id( assert response.status_code == 200, response.text -async def test_dead_letter_path_not_captured_by_outbox_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_dead_letter_path_not_captured_by_outbox_id(test_client: httpx.AsyncClient, admin_headers): # Act — /dead-letter must route to dead-letter endpoint response = await test_client.get(f"{OUTBOX_URL}/dead-letter", headers=admin_headers) # Assert assert response.status_code == 200, response.text -async def test_retry_policy_path_not_captured_by_outbox_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_retry_policy_path_not_captured_by_outbox_id(test_client: httpx.AsyncClient, admin_headers): # Act — /retry-policy must route to retry-policy endpoint response = await test_client.get(f"{OUTBOX_URL}/retry-policy", headers=admin_headers) # Assert diff --git a/backend/test/integration/api/channels/test_pairing_router.py b/backend/test/integration/api/channels/test_pairing_router.py index 7b81eee5..325dd4e8 100644 --- a/backend/test/integration/api/channels/test_pairing_router.py +++ b/backend/test/integration/api/channels/test_pairing_router.py @@ -43,9 +43,7 @@ async def test_list_pairings_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_list_pairings_forbids_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_pairings_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(PAIRINGS_URL, headers=standard_user["headers"]) # Assert @@ -117,9 +115,7 @@ async def test_create_pairing_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401, response.text -async def test_create_pairing_forbids_standard_user( - test_client: httpx.AsyncClient, standard_user -): +async def test_create_pairing_forbids_standard_user(test_client: httpx.AsyncClient, standard_user): # Arrange body = _make_create_pairing_body() # Act @@ -132,9 +128,7 @@ async def test_create_pairing_forbids_standard_user( assert response.status_code == 403, response.text -async def test_create_pairing_non_existent_account_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_create_pairing_non_existent_account_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = _make_create_pairing_body() # Act @@ -153,9 +147,7 @@ async def test_create_pairing_rejects_expires_zero(test_client: httpx.AsyncClien assert response.status_code == 422, response.text -async def test_create_pairing_rejects_expires_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_create_pairing_rejects_expires_over_max(test_client: httpx.AsyncClient, admin_headers): # Arrange — expires_in_seconds > 2592000 超过上限 body = _make_create_pairing_body() body["expires_in_seconds"] = 2592001 @@ -170,9 +162,7 @@ async def test_create_pairing_rejects_expires_over_max( # ============================================================================= -async def test_batch_approve_pairings_rejects_empty_ids( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_approve_pairings_rejects_empty_ids(test_client: httpx.AsyncClient, admin_headers): # Arrange — pairing_ids 为空违反 min_items=1 body = {"pairing_ids": []} # Act @@ -185,9 +175,7 @@ async def test_batch_approve_pairings_rejects_empty_ids( assert response.status_code == 422, response.text -async def test_batch_approve_pairings_rejects_too_many_ids( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_approve_pairings_rejects_too_many_ids(test_client: httpx.AsyncClient, admin_headers): # Arrange — pairing_ids 超过 max_items=500 body = {"pairing_ids": [f"id_{i}" for i in range(501)]} # Act @@ -200,9 +188,7 @@ async def test_batch_approve_pairings_rejects_too_many_ids( assert response.status_code == 422, response.text -async def test_batch_reject_pairings_rejects_empty_ids( - test_client: httpx.AsyncClient, admin_headers -): +async def test_batch_reject_pairings_rejects_empty_ids(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"pairing_ids": []} # Act @@ -220,9 +206,7 @@ async def test_batch_reject_pairings_rejects_empty_ids( # ============================================================================= -async def test_clean_expired_pairings_rejects_max_count_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clean_expired_pairings_rejects_max_count_zero(test_client: httpx.AsyncClient, admin_headers): # Arrange — max_count=0 低于下限 1 body = {"max_count": 0} # Act @@ -235,9 +219,7 @@ async def test_clean_expired_pairings_rejects_max_count_zero( assert response.status_code == 422, response.text -async def test_clean_expired_pairings_rejects_max_count_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_clean_expired_pairings_rejects_max_count_over_max(test_client: httpx.AsyncClient, admin_headers): # Arrange — max_count=1001 超过上限 1000 body = {"max_count": 1001} # Act @@ -275,9 +257,7 @@ async def test_get_pairings_stats_returns_200(test_client: httpx.AsyncClient, ad # ============================================================================= -async def test_get_pairing_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{PAIRINGS_URL}/{NON_EXISTENT_PAIRING_ID}", @@ -292,9 +272,7 @@ async def test_get_pairing_non_existent_returns_404( # ============================================================================= -async def test_approve_pairing_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_approve_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"approver_id": "admin_xxx", "reason": "pytest"} # Act @@ -312,9 +290,7 @@ async def test_approve_pairing_non_existent_returns_404( # ============================================================================= -async def test_reject_pairing_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_reject_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"approver_id": "admin_xxx", "reason": "pytest"} # Act @@ -332,9 +308,7 @@ async def test_reject_pairing_non_existent_returns_404( # ============================================================================= -async def test_revoke_pairing_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_revoke_pairing_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"reason": "pytest"} # Act diff --git a/backend/test/integration/api/channels/test_plugin_router.py b/backend/test/integration/api/channels/test_plugin_router.py index 0addb7c9..3907ec6d 100644 --- a/backend/test/integration/api/channels/test_plugin_router.py +++ b/backend/test/integration/api/channels/test_plugin_router.py @@ -31,9 +31,7 @@ async def test_list_plugins_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_list_plugins_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +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 @@ -77,31 +75,23 @@ async def test_install_plugin_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_install_plugin_rejects_standard_user( - test_client: httpx.AsyncClient, standard_user -): +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"] - ) + 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 -): +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 - ) + 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 @@ -111,25 +101,17 @@ async def test_superadmin_install_nonexistent_source_fails( # ============================================================================= -async def test_uninstall_plugin_rejects_standard_user( - test_client: httpx.AsyncClient, standard_user -): +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"] - ) + 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 -): +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 - ) + response = await test_client.delete(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -139,13 +121,9 @@ async def test_superadmin_uninstall_nonexistent_plugin_returns_404( # ============================================================================= -async def test_get_plugin_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +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 - ) + response = await test_client.get(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -155,26 +133,18 @@ async def test_get_plugin_returns_404_for_non_existent( # ============================================================================= -async def test_get_plugin_config_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +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 - ) + 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 -): +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 - ) + 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 @@ -184,79 +154,51 @@ async def test_update_plugin_config_returns_404_for_non_existent( # ============================================================================= -async def test_load_plugin_returns_404_for_non_existent( - test_client: httpx.AsyncClient, admin_headers -): +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 - ) + 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 -): +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 - ) + 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 -): +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 - ) + 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 -): +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 - ) + 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 -): +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 - ) + 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 -): +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 - ) + 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 -): +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 - ) + response = await test_client.post(f"{PLUGINS_URL}/{NON_EXISTENT_PLUGIN_ID}/reload", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -266,26 +208,18 @@ async def test_reload_plugin_returns_404_for_non_existent( # ============================================================================= -async def test_batch_start_plugins_with_empty_plugin_ids( - test_client: httpx.AsyncClient, admin_headers -): +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 - ) + 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 -): +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 - ) + response = await test_client.post(f"{PLUGINS_URL}/batch/start", json={}, headers=admin_headers) # Assert assert response.status_code == 400, response.text @@ -309,17 +243,13 @@ async def test_batch_stop_plugins_requires_auth(test_client: httpx.AsyncClient): # ============================================================================= -async def test_install_static_path_not_captured_as_plugin_id( - test_client: httpx.AsyncClient, admin_headers -): +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 - ) + 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 diff --git a/backend/test/integration/api/channels/test_reports_router.py b/backend/test/integration/api/channels/test_reports_router.py index c1ad6a74..5492e2fc 100644 --- a/backend/test/integration/api/channels/test_reports_router.py +++ b/backend/test/integration/api/channels/test_reports_router.py @@ -25,18 +25,14 @@ async def test_list_oneoff_reports_requires_auth(test_client: httpx.AsyncClient) assert response.status_code == 401, response.text -async def test_list_oneoff_reports_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_oneoff_reports_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get(ONEOFF_URL, headers=standard_user["headers"]) # Assert assert response.status_code == 403, response.text -async def test_admin_can_list_oneoff_reports( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_list_oneoff_reports(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get(ONEOFF_URL, headers=admin_headers) # Assert @@ -51,15 +47,11 @@ async def test_admin_can_list_oneoff_reports( # ============================================================================= -async def test_admin_can_create_oneoff_report( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_create_oneoff_report(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"report_type": "message_stats"} # Act - response = await test_client.post( - ONEOFF_URL, json=body, headers=admin_headers - ) + response = await test_client.post(ONEOFF_URL, json=body, headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() @@ -68,15 +60,11 @@ async def test_admin_can_create_oneoff_report( assert "task_id" in payload["data"] -async def test_create_oneoff_report_rejects_invalid_report_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_create_oneoff_report_rejects_invalid_report_type(test_client: httpx.AsyncClient, admin_headers): # Arrange — "invalid_type" is not in the Literal enum body = {"report_type": "invalid_type"} # Act - response = await test_client.post( - ONEOFF_URL, json=body, headers=admin_headers - ) + response = await test_client.post(ONEOFF_URL, json=body, headers=admin_headers) # Assert assert response.status_code == 422, response.text @@ -86,24 +74,16 @@ async def test_create_oneoff_report_rejects_invalid_report_type( # ============================================================================= -async def test_list_oneoff_reports_rejects_limit_zero( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_oneoff_reports_rejects_limit_zero(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 - response = await test_client.get( - ONEOFF_URL, params={"limit": 0}, headers=admin_headers - ) + response = await test_client.get(ONEOFF_URL, params={"limit": 0}, headers=admin_headers) # Assert assert response.status_code == 422, response.text -async def test_list_oneoff_reports_rejects_limit_over_max( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_oneoff_reports_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers): # Act — limit=201 violates le=200 - response = await test_client.get( - ONEOFF_URL, params={"limit": 201}, headers=admin_headers - ) + response = await test_client.get(ONEOFF_URL, params={"limit": 201}, headers=admin_headers) # Assert assert response.status_code == 422, response.text @@ -113,9 +93,7 @@ async def test_list_oneoff_reports_rejects_limit_over_max( # ============================================================================= -async def test_download_oneoff_report_returns_404_or_409_for_nonexistent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_download_oneoff_report_returns_404_or_409_for_nonexistent(test_client: httpx.AsyncClient, admin_headers): # Act — non-existent task returns 404 (not found) or 409 (not ready) response = await test_client.get( f"{ONEOFF_URL}/{NON_EXISTENT_TASK_ID}/download", @@ -125,9 +103,7 @@ async def test_download_oneoff_report_returns_404_or_409_for_nonexistent( assert response.status_code in (404, 409), response.text -async def test_retry_oneoff_report_returns_404_for_nonexistent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_retry_oneoff_report_returns_404_for_nonexistent(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.post( f"{ONEOFF_URL}/{NON_EXISTENT_TASK_ID}/retry", @@ -137,13 +113,9 @@ async def test_retry_oneoff_report_returns_404_for_nonexistent( assert response.status_code == 404, response.text -async def test_get_report_returns_404_for_nonexistent( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_report_returns_404_for_nonexistent(test_client: httpx.AsyncClient, admin_headers): # Act - response = await test_client.get( - f"{REPORTS_URL}/{NON_EXISTENT_REPORT_ID}", headers=admin_headers - ) + response = await test_client.get(f"{REPORTS_URL}/{NON_EXISTENT_REPORT_ID}", headers=admin_headers) # Assert assert response.status_code == 404, response.text @@ -153,9 +125,7 @@ async def test_get_report_returns_404_for_nonexistent( # ============================================================================= -async def test_static_oneoff_path_not_captured_as_report_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_static_oneoff_path_not_captured_as_report_id(test_client: httpx.AsyncClient, admin_headers): # Act — GET /reports/oneoff must hit the list endpoint (200), not # GET /reports/{report_id} with report_id="oneoff" (which would 404) response = await test_client.get(ONEOFF_URL, headers=admin_headers) diff --git a/backend/test/integration/api/channels/test_session_router.py b/backend/test/integration/api/channels/test_session_router.py index 010aa8a4..763d9ac6 100644 --- a/backend/test/integration/api/channels/test_session_router.py +++ b/backend/test/integration/api/channels/test_session_router.py @@ -314,9 +314,7 @@ async def test_list_session_messages_forbids_standard_user(test_client: httpx.As assert response.status_code == 403, response.text -async def test_list_session_messages_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_session_messages_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages", @@ -326,9 +324,7 @@ async def test_list_session_messages_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_list_session_messages_rejects_invalid_limit( - test_client: httpx.AsyncClient, admin_headers -): +async def test_list_session_messages_rejects_invalid_limit(test_client: httpx.AsyncClient, admin_headers): # Act — limit=0 violates ge=1 response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/messages", @@ -361,9 +357,7 @@ async def test_get_session_stats_forbids_standard_user(test_client: httpx.AsyncC assert response.status_code == 403, response.text -async def test_get_session_stats_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_session_stats_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/stats", @@ -378,9 +372,7 @@ async def test_get_session_stats_non_existent_returns_404( # ============================================================================= -async def test_bind_session_user_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_bind_session_user_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Arrange body = {"user_uid": "non_existent_user_uid"} # Act @@ -393,9 +385,7 @@ async def test_bind_session_user_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_unbind_session_user_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_unbind_session_user_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.delete( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/bind-user", @@ -405,9 +395,7 @@ async def test_unbind_session_user_non_existent_returns_404( assert response.status_code == 404, response.text -async def test_get_session_identity_non_existent_returns_404( - test_client: httpx.AsyncClient, admin_headers -): +async def test_get_session_identity_non_existent_returns_404(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{SESSIONS_URL}/{NON_EXISTENT_SESSION_ID}/identity", diff --git a/backend/test/integration/api/channels/test_webhook_router.py b/backend/test/integration/api/channels/test_webhook_router.py index 26528c39..d8f87698 100644 --- a/backend/test/integration/api/channels/test_webhook_router.py +++ b/backend/test/integration/api/channels/test_webhook_router.py @@ -29,9 +29,7 @@ async def test_webhook_does_not_require_auth(test_client: httpx.AsyncClient): assert response.status_code in (200, 500, 501), response.text -async def test_webhook_accepts_empty_body_without_auth_error( - test_client: httpx.AsyncClient -): +async def test_webhook_accepts_empty_body_without_auth_error(test_client: httpx.AsyncClient): # Arrange — empty body, no auth # Act response = await test_client.post( @@ -76,9 +74,7 @@ async def test_verify_signature_does_not_require_auth(test_client: httpx.AsyncCl assert response.status_code in (200, 501), response.text -async def test_verify_signature_with_valid_body_returns_200_or_501( - test_client: httpx.AsyncClient -): +async def test_verify_signature_with_valid_body_returns_200_or_501(test_client: httpx.AsyncClient): # Arrange body = { "raw_event": '{"event_type": "test.event"}', @@ -135,9 +131,7 @@ async def test_webhook_test_requires_admin(test_client: httpx.AsyncClient, stand assert response.status_code == 403 -async def test_admin_can_test_webhook_returns_200_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_test_webhook_returns_200_or_501(test_client: httpx.AsyncClient, admin_headers): # Arrange body = { "account_id": None, @@ -154,9 +148,7 @@ async def test_admin_can_test_webhook_returns_200_or_501( assert response.status_code in (200, 501), response.text -async def test_webhook_test_with_valid_body_returns_200_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_webhook_test_with_valid_body_returns_200_or_501(test_client: httpx.AsyncClient, admin_headers): # Arrange body = { "event_type": "test_event", diff --git a/backend/test/integration/api/channels/test_wizard_router.py b/backend/test/integration/api/channels/test_wizard_router.py index 77577678..29e0aafe 100644 --- a/backend/test/integration/api/channels/test_wizard_router.py +++ b/backend/test/integration/api/channels/test_wizard_router.py @@ -41,9 +41,7 @@ async def test_list_wizard_steps_requires_auth(test_client: httpx.AsyncClient): assert response.status_code == 401 -async def test_list_wizard_steps_requires_admin( - test_client: httpx.AsyncClient, standard_user -): +async def test_list_wizard_steps_requires_admin(test_client: httpx.AsyncClient, standard_user): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps", @@ -53,9 +51,7 @@ async def test_list_wizard_steps_requires_admin( assert response.status_code == 403 -async def test_admin_can_list_wizard_steps_returns_200_or_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_admin_can_list_wizard_steps_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Act response = await test_client.get( f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps", @@ -70,9 +66,7 @@ async def test_admin_can_list_wizard_steps_returns_200_or_404_or_501( # ============================================================================= -async def test_wizard_validate_returns_200_or_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_validate_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Arrange step_id = "account_info" body = _make_validate_payload() @@ -86,9 +80,7 @@ async def test_wizard_validate_returns_200_or_404_or_501( assert response.status_code in (200, 404, 501), response.text -async def test_wizard_validate_rejects_empty_step_id( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_validate_rejects_empty_step_id(test_client: httpx.AsyncClient, admin_headers): # Arrange — step_id 路径参数为空字符串,FastAPI 路由匹配规则下 # 可能不匹配(404)或匹配到空 step_id 后由下游校验拒绝(400/422) body = _make_validate_payload() @@ -107,9 +99,7 @@ async def test_wizard_validate_rejects_empty_step_id( # ============================================================================= -async def test_wizard_apply_returns_200_or_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_apply_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Arrange step_id = "account_info" body = {"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"}} @@ -128,9 +118,7 @@ async def test_wizard_apply_returns_200_or_404_or_501( # ============================================================================= -async def test_wizard_finalize_returns_200_or_404_or_501( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_finalize_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers): # Arrange body = _make_finalize_payload() # Act @@ -143,9 +131,7 @@ async def test_wizard_finalize_returns_200_or_404_or_501( assert response.status_code in (200, 404, 501), response.text -async def test_wizard_finalize_rejects_empty_patches( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_finalize_rejects_empty_patches(test_client: httpx.AsyncClient, admin_headers): # Arrange — patches min_length=1 body = {"patches": []} # Act @@ -163,9 +149,7 @@ async def test_wizard_finalize_rejects_empty_patches( # ============================================================================= -async def test_oauth_initiate_rejects_invalid_redirect_uri_scheme( - test_client: httpx.AsyncClient, admin_headers -): +async def test_oauth_initiate_rejects_invalid_redirect_uri_scheme(test_client: httpx.AsyncClient, admin_headers): # Arrange — redirect_uri 必须为 http/https,ftp 等非法 scheme 拒绝 body = {"redirect_uri": "ftp://example.com/callback"} # Act @@ -224,9 +208,7 @@ async def test_oauth_callback_requires_state(test_client: httpx.AsyncClient, adm assert response.status_code == 422, response.text -async def test_oauth_callback_requires_redirect_uri( - test_client: httpx.AsyncClient, admin_headers -): +async def test_oauth_callback_requires_redirect_uri(test_client: httpx.AsyncClient, admin_headers): # Arrange — redirect_uri required body = {"code": "pytest_code", "state": "pytest_state"} # Act @@ -239,9 +221,7 @@ async def test_oauth_callback_requires_redirect_uri( assert response.status_code == 422, response.text -async def test_oauth_callback_rejects_invalid_redirect_uri_scheme( - test_client: httpx.AsyncClient, admin_headers -): +async def test_oauth_callback_rejects_invalid_redirect_uri_scheme(test_client: httpx.AsyncClient, admin_headers): # Arrange — redirect_uri 必须为 http/https,ftp 等非法 scheme 拒绝 body = { "code": "pytest_code", @@ -258,9 +238,7 @@ async def test_oauth_callback_rejects_invalid_redirect_uri_scheme( assert response.status_code == 422, response.text -async def test_oauth_callback_with_all_params_returns_404_or_501_or_400( - test_client: httpx.AsyncClient, admin_headers -): +async def test_oauth_callback_with_all_params_returns_404_or_501_or_400(test_client: httpx.AsyncClient, admin_headers): # Arrange — all params present but channel not registered body = { "code": "pytest_code", @@ -282,9 +260,7 @@ async def test_oauth_callback_with_all_params_returns_404_or_501_or_400( # ============================================================================= -async def test_wizard_unregistered_channel_type( - test_client: httpx.AsyncClient, admin_headers -): +async def test_wizard_unregistered_channel_type(test_client: httpx.AsyncClient, admin_headers): # Arrange — channel_type 为 str 子类无白名单,未注册插件返回 404 unregistered_channel = "invalid_channel_type_xyz" # Act diff --git a/backend/test/integration/api/channels/test_woc_wizard_verify.py b/backend/test/integration/api/channels/test_woc_wizard_verify.py new file mode 100644 index 00000000..d14f559a --- /dev/null +++ b/backend/test/integration/api/channels/test_woc_wizard_verify.py @@ -0,0 +1,114 @@ +"""临时验证脚本:wechat_woc 向导接入全流程。验证后删除。""" + +from __future__ import annotations + +import asyncio +import os +import sys +import uuid + +import httpx + +BASE_URL = os.getenv("TEST_BASE_URL", "http://localhost:5050").rstrip("/") +USERNAME = os.getenv("TEST_USERNAME", "Kris") +PASSWORD = os.getenv("TEST_PASSWORD", "Admin@Test123") +CHANNEL_TYPE = "wechat_woc" + +WOC_INSTANCE_ID = "c3b5992392" +WOC_BRIDGE_TOKEN = "83c89911e1592cc17a29be3a39107f545d10aeab7938615acb5ec28560cbabad" +BRIDGE_URL = f"http://host.docker.internal:36080/api/bridge/{WOC_INSTANCE_ID}" + + +def _uniq() -> str: + return f"verify_{uuid.uuid4().hex[:8]}" + + +async def _get_token(client: httpx.AsyncClient) -> str: + resp = await client.post( + f"{BASE_URL}/api/auth/token", + data={"username": USERNAME, "password": PASSWORD}, + ) + if resp.status_code != 200: + raise RuntimeError(f"auth failed: {resp.status_code} {resp.text}") + return resp.json()["access_token"] + + +async def _cleanup(client: httpx.AsyncClient, headers: dict[str, str], account_id: str) -> None: + try: + await client.delete(f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/accounts/{account_id}", headers=headers) + except Exception as exc: + print(f"cleanup warning: {exc}") + + +async def main() -> int: + async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client: + token = await _get_token(client) + headers = {"Authorization": f"Bearer {token}"} + account_id = WOC_INSTANCE_ID + + steps_resp = await client.get(f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/wizard/steps", headers=headers) + print(f"steps: {steps_resp.status_code} {steps_resp.text[:200]}") + if steps_resp.status_code in (404, 501): + print("wechat_woc plugin not registered, skip") + return 0 + if steps_resp.status_code != 200: + return 1 + + display_name = f"woc_verify_{_uniq()}" + apply1 = await client.post( + f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/wizard/steps/account_info/apply", + headers=headers, + json={"values": {"display_name": display_name}}, + ) + print(f"apply account_info: {apply1.status_code} {apply1.text[:200]}") + if apply1.status_code != 200: + return 1 + + apply2 = await client.post( + f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/wizard/steps/bridge_config/apply", + headers=headers, + json={"values": {"bridge_url": BRIDGE_URL, "bridge_token": WOC_BRIDGE_TOKEN}}, + ) + print(f"apply bridge_config: {apply2.status_code} {apply2.text[:200]}") + if apply2.status_code != 200: + return 1 + + apply3 = await client.post( + f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/wizard/steps/verify/apply", + headers=headers, + json={"values": {}}, + ) + print(f"apply verify: {apply3.status_code} {apply3.text[:200]}") + if apply3.status_code != 200: + return 1 + + finalize = await client.post( + f"{BASE_URL}/api/channels/{CHANNEL_TYPE}/wizard/finalize", + headers=headers, + json={ + "patches": [ + {"step_id": "account_info", "values": {"display_name": display_name}}, + { + "step_id": "bridge_config", + "values": {"bridge_url": BRIDGE_URL, "bridge_token": WOC_BRIDGE_TOKEN}, + }, + {"step_id": "verify", "values": {"verified": True}}, + ] + }, + ) + print(f"finalize: {finalize.status_code} {finalize.text}") + if finalize.status_code != 200: + return 1 + + payload = finalize.json() + print(f"finalize payload keys: {list(payload.keys())}") + if payload.get("data"): + print(f"data keys: {list(payload['data'].keys())}") + print(f"account: {payload['data'].get('account')}") + + await _cleanup(client, headers, account_id) + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main()))