本次提交包含多类代码优化: 1. 修复多处单行代码换行格式,统一代码排版 2. 为外部系统模块新增快捷菜单配置 3. 优化前端API请求参数命名一致性 4. 补充媒体下载超限错误码与领域异常类 5. 完善仓储层更新逻辑,支持显式清空字段 6. 优化部分测试用例与工具函数代码结构 7. 为微信插件白名单缓存增加过期时间
689 lines
24 KiB
Python
689 lines
24 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 — router declares status_code=202 for async batch-retry
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/dead-letter/batch-retry",
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 202, 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
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/stats ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_get_outbox_stats_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/stats")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_get_outbox_stats_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/stats", headers=standard_user["headers"])
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/trend ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_get_outbox_trend_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/trend")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_get_outbox_trend_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END},
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/dead-letter ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_list_dead_letter_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_list_dead_letter_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter", headers=standard_user["headers"])
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/retry-policy ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_get_retry_policy_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/retry-policy")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_get_retry_policy_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/retry-policy", headers=standard_user["headers"])
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for POST /outbox/dead-letter/batch-retry ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_batch_retry_dead_letter_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.post(f"{OUTBOX_URL}/dead-letter/batch-retry")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_batch_retry_dead_letter_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/dead-letter/batch-retry",
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for POST /outbox/dead-letter/batch-delete ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_batch_delete_dead_letter_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.post(f"{OUTBOX_URL}/dead-letter/batch-delete")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_batch_delete_dead_letter_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/dead-letter/batch-delete",
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/dead-letter/export ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_export_dead_letter_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter/export")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_export_dead_letter_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/dead-letter/export", headers=standard_user["headers"])
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for PUT /outbox/retry-policy ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_update_retry_policy_requires_auth(test_client: httpx.AsyncClient):
|
|
# 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)
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_update_retry_policy_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# 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=standard_user["headers"])
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for GET /outbox/messages/{outbox_id} ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_get_outbox_message_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.get(f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_get_outbox_message_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}",
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for DELETE /outbox/dead-letter/{outbox_id} ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_delete_dead_letter_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.delete(f"{OUTBOX_URL}/dead-letter/{NON_EXISTENT_OUTBOX_ID}")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_delete_dead_letter_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.delete(
|
|
f"{OUTBOX_URL}/dead-letter/{NON_EXISTENT_OUTBOX_ID}",
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === Auth three-tier for POST /outbox/messages/{outbox_id}/retry ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_retry_outbox_message_requires_auth(test_client: httpx.AsyncClient):
|
|
# Act
|
|
response = await test_client.post(f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}/retry")
|
|
# Assert
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_retry_outbox_message_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
|
# Act
|
|
response = await test_client.post(
|
|
f"{OUTBOX_URL}/messages/{NON_EXISTENT_OUTBOX_ID}/retry",
|
|
headers=standard_user["headers"],
|
|
)
|
|
# Assert
|
|
assert response.status_code == 403
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/trend — granularity / metric Literal validation (422) ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_outbox_trend_rejects_invalid_granularity(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — granularity="invalid" violates Literal["minute", "hour", "day"]
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END, "granularity": "invalid"},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
async def test_outbox_trend_rejects_invalid_metric(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — metric="invalid" violates Literal["queue_depth", "retry_count", "dead_count"]
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/trend",
|
|
params={"start_time": ISO_TIME_START, "end_time": ISO_TIME_END, "metric": "invalid"},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === GET /outbox/dead-letter/export — format Literal validation (422) ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_export_dead_letter_rejects_invalid_format(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — format="xml" violates Literal["json", "csv"]
|
|
response = await test_client.get(
|
|
f"{OUTBOX_URL}/dead-letter/export",
|
|
params={"format": "xml"},
|
|
headers=admin_headers,
|
|
)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
# =============================================================================
|
|
# === PUT /outbox/retry-policy — retry_backoff_schedule validation (422) ===
|
|
# =============================================================================
|
|
|
|
|
|
async def test_update_retry_policy_rejects_empty_backoff_schedule(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — retry_backoff_schedule=[] violates min_length=1
|
|
body = {"max_retry": 5, "ttl_seconds": 3600, "retry_backoff_schedule": []}
|
|
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_too_many_backoff_schedule(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — 21 entries violates max_length=20
|
|
body = {"max_retry": 5, "ttl_seconds": 3600, "retry_backoff_schedule": [10] * 21}
|
|
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_non_positive_backoff_schedule(test_client: httpx.AsyncClient, admin_headers):
|
|
# Act — 0 in schedule violates field_validator (positive integers only)
|
|
body = {"max_retry": 5, "ttl_seconds": 3600, "retry_backoff_schedule": [10, 0, 60]}
|
|
response = await test_client.put(f"{OUTBOX_URL}/retry-policy", json=body, headers=admin_headers)
|
|
# Assert
|
|
assert response.status_code == 422, response.text
|