1. 格式化代码与参数换行,提升可读性 2. 修正测试断言与测试逻辑,覆盖更多边界场景 3. 新增外部系统告警仓库、工具服务、系统服务等测试用例 4. 完善插件注册表的重启状态检测逻辑测试 5. 更新测试断言计数与预期结果,匹配代码变更 6. 修复Instagram适配器的签名校验测试逻辑 7. 优化会话合并、上下文测试的类型检查与断言
385 lines
13 KiB
Python
385 lines
13 KiB
Python
"""Integration tests for channels outbox_router endpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from .conftest import (
|
|
BASE_URL,
|
|
DEFAULT_CHANNEL_TYPE,
|
|
ISO_TIME_END,
|
|
ISO_TIME_START,
|
|
NON_EXISTENT_OUTBOX_ID,
|
|
)
|
|
|
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
|
OUTBOX_URL = f"{BASE_URL}/outbox"
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/messages ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_list_outbox_messages_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/messages")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
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
|
|
assert response.status_code == 403
|
|
|
|
|
|
async def test_admin_can_list_outbox_messages(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/messages", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
assert isinstance(payload["data"]["items"], list)
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/messages pagination validation ===
|
|
# =============================================================================
|
|
|
|
|
|
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",
|
|
params={"limit": 0},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
async def test_list_outbox_messages_rejects_limit_over_max(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — limit=1001 violates le=1000
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/messages",
|
|
params={"limit": 1001},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/stats ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_get_outbox_stats(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/stats", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
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
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/trend ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_outbox_trend_requires_start_time(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — start_time required
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"end_time": ISO_TIME_END},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
async def test_outbox_trend_requires_end_time(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — end_time required
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"start_time": ISO_TIME_START},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
async def test_admin_can_get_outbox_trend(test_client: httpx.AsyncClient, admin_headers):
|
|
# Arrange
|
|
params = {
|
|
"start_time": ISO_TIME_START,
|
|
"end_time": ISO_TIME_END,
|
|
"channel_type": DEFAULT_CHANNEL_TYPE,
|
|
"granularity": "hour",
|
|
"metric": "queue_depth",
|
|
}
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/trend", params=params, headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/dead-letter ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_list_dead_letter(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/retry-policy ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_get_retry_policy(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/retry-policy", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === POST /outbox/dead-letter/batch-retry ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_batch_retry_dead_letter(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/dead-letter/batch-retry",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === POST /outbox/dead-letter/batch-delete ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_batch_delete_dead_letter(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/dead-letter/batch-delete",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/dead-letter/export ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_export_dead_letter(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter/export", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
# =============================================================================
|
|
# === PUT /outbox/retry-policy validation ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_admin_can_update_retry_policy(test_client: httpx.AsyncClient, admin_headers):
|
|
# Arrange
|
|
body = {
|
|
"max_retry": 5,
|
|
"ttl_seconds": 3600,
|
|
"retry_backoff_schedule": [10, 30, 60],
|
|
}
|
|
# Act
|
|
response = await test_client.put(
|
|
f"{OUTBOX_URL}/retry-policy",
|
|
json=body,
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
payload = response.json()
|
|
assert payload["success"] is True
|
|
assert isinstance(payload["data"], dict)
|
|
|
|
|
|
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(
|
|
f"{OUTBOX_URL}/retry-policy",
|
|
json=body,
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
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(
|
|
f"{OUTBOX_URL}/retry-policy",
|
|
json=body,
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
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(
|
|
f"{OUTBOX_URL}/retry-policy",
|
|
json=body,
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
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(
|
|
f"{OUTBOX_URL}/retry-policy",
|
|
json=body,
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/messages/{outbox_id} ===
|
|
# =============================================================================
|
|
|
|
|
|
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}",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === DELETE /outbox/dead-letter/{outbox_id} ===
|
|
# =============================================================================
|
|
|
|
|
|
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}",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === POST /outbox/messages/{outbox_id}/retry ===
|
|
# =============================================================================
|
|
|
|
|
|
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",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 404, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === Static paths not captured by /messages/{outbox_id} ===
|
|
# =============================================================================
|
|
|
|
|
|
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):
|
|
# Act — /trend must route to trend endpoint
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
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):
|
|
# Act — /retry-policy must route to retry-policy endpoint
|
|
response = await test_client.get(f"{OUTBOX_URL}/retry-policy", headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 200, response.text
|