ForcePilot/backend/test/integration/api/channels/test_webhook_router.py
Kris 04a2472014 test(channel): add full integration test suite for channels module
1. 新增channels模块集成测试目录与基础fixture
2. 为capability、doctor、reports、dashboard、directory、webhook、wizard、health、analytics、allowlist、config等路由编写完整的鉴权、参数校验与异常场景测试
3. 修复基础集成测试setup,新增external/scheduler/channel数据库schema初始化步骤
2026-07-02 03:25:27 +08:00

190 lines
6.0 KiB
Python

"""Integration tests for channels webhook_router endpoints."""
from __future__ import annotations
import httpx
import pytest
from .conftest import BASE_URL, DEFAULT_CHANNEL_TYPE
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
# =============================================================================
# === POST /{channel_type}/webhook (NO AUTH) ===
# =============================================================================
async def test_webhook_does_not_require_auth(test_client: httpx.AsyncClient):
# Arrange — no Authorization header; raw bytes body
body = b'{"event_type": "test.event", "payload": {"test": true}}'
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook",
content=body,
headers={"Content-Type": "application/json"},
)
# Assert — no auth required; channel may not be registered → 404, or accepted → 200
assert response.status_code != 401
assert response.status_code in (200, 404), response.text
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(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook",
content=b"",
headers={"Content-Type": "application/json"},
)
# Assert — fire-and-forget; not 401/400 for auth reasons
assert response.status_code != 401
assert response.status_code in (200, 404), response.text
async def test_webhook_rejects_oversized_body(test_client: httpx.AsyncClient):
# Arrange — body larger than 1 MiB
body = b"x" * (1024 * 1024 + 1)
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook",
content=body,
headers={"Content-Type": "application/octet-stream"},
)
# Assert
assert response.status_code == 400, response.text
# =============================================================================
# === POST /{channel_type}/webhook/signature/verify (NO AUTH) ===
# =============================================================================
async def test_verify_signature_does_not_require_auth(test_client: httpx.AsyncClient):
# Arrange
body = {"raw_event": "test-payload", "headers": {"X-Signature": "test-sig"}}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/signature/verify",
json=body,
)
# Assert — no auth; adapter may not implement → 501, or 200 if implemented
assert response.status_code != 401
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
):
# Arrange
body = {
"raw_event": '{"event_type": "test.event"}',
"headers": {"X-Lark-Signature": "sha256=abc", "X-Lark-Request-Timestamp": "1234567890"},
}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/signature/verify",
json=body,
)
# Assert
assert response.status_code in (200, 501), response.text
async def test_verify_signature_rejects_empty_raw_event(test_client: httpx.AsyncClient):
# Arrange — raw_event min_length=1
body = {"raw_event": "", "headers": {}}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/signature/verify",
json=body,
)
# Assert
assert response.status_code == 422, response.text
# =============================================================================
# === POST /{channel_type}/webhook/test (admin) ===
# =============================================================================
async def test_webhook_test_requires_auth(test_client: httpx.AsyncClient):
# Arrange
body = {"event_type": "test_event", "payload": {"test": True}}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/test",
json=body,
)
# Assert
assert response.status_code == 401
async def test_webhook_test_requires_admin(test_client: httpx.AsyncClient, standard_user):
# Arrange
body = {"event_type": "test_event", "payload": {"test": True}}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/test",
json=body,
headers=standard_user["headers"],
)
# Assert
assert response.status_code == 403
async def test_admin_can_test_webhook_returns_200_or_501(
test_client: httpx.AsyncClient, admin_headers
):
# Arrange
body = {
"account_id": None,
"event_type": "test_event",
"payload": {"pytest": True},
}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/test",
json=body,
headers=admin_headers,
)
# Assert — adapter not registered → 501, or accepted → 200
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
):
# Arrange
body = {
"event_type": "test_event",
"payload": {"msg": "hello"},
}
# Act
response = await test_client.post(
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/webhook/test",
json=body,
headers=admin_headers,
)
# Assert
assert response.status_code in (200, 501), response.text
# =============================================================================
# === Invalid channel_type validation ===
# =============================================================================
async def test_webhook_rejects_invalid_channel_type(test_client: httpx.AsyncClient):
# Arrange — channel_type must match ChannelType enum
invalid_channel = "invalid_channel_type_xyz"
# Act
response = await test_client.post(
f"{BASE_URL}/{invalid_channel}/webhook",
content=b"{}",
headers={"Content-Type": "application/json"},
)
# Assert
assert response.status_code == 422, response.text