ForcePilot/backend/test/integration/api/channels/test_outbox_router.py
Kris d58331e0c0 test: 批量修复单元测试与集成测试的兼容性问题
本次提交修复了多个测试文件中的问题:
1. 将 ChannelType 枚举调用改为字符串实例化方式
2. 修正了日志断言、异步mock使用、配置参数等多处测试细节
3. 新增了会话聚合根、跨渠道关联策略等单元测试用例
4. 修复了路由测试中的路径方法错误与断言逻辑
5. 调整了依赖导入与测试夹具的兼容性
6. 统一了重试回退调度的列表/元组使用规范
2026-07-04 00:18:04 +08:00

408 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=201 violates le=200
response = await test_client.get(
f"{OUTBOX_URL}/messages",
params={"limit": 201},
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)
# =============================================================================
# === 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