test: 格式化所有渠道相关测试用例的函数定义
1. 将多行的测试函数声明合并为单行,统一代码风格 2. 新增临时的微信woc向导验证脚本,用于全流程验证后可删除 3. 统一调整长请求行的换行写法,简化代码结构
This commit is contained in:
parent
12ec5af0de
commit
5c4ef3a709
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
114
backend/test/integration/api/channels/test_woc_wizard_verify.py
Normal file
114
backend/test/integration/api/channels/test_woc_wizard_verify.py
Normal file
@ -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()))
|
||||
Loading…
Reference in New Issue
Block a user