ForcePilot/backend/test/integration/api/channels/test_webhook_router.py
Kris df116a069c test: 补全全量接口参数校验与权限控制测试用例
新增并完善多模块接口测试用例,覆盖:
1. 权限校验:认证缺失、普通用户越权访问场景
2. 参数合法性校验:空值、格式错误、边界值、枚举约束
3. 响应字段增强:补充业务字段断言与分页回传校验
4. 异常场景:不存在资源、超限参数、无效格式等400/404/422场景
2026-07-11 21:41:45 +08:00

220 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 — 无鉴权要求;入站管道 ack → 200nack → 500适配器未注册 → 501
assert response.status_code != 401
assert response.status_code in (200, 500, 501), 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 — empty body 被 ReceiveInboundCmd.__post_init__ 拒绝ValidationError → 400
# 不返回 401鉴权不适用于 webhook 入站端点)。
assert response.status_code != 401
assert response.status_code == 400, 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
async def test_verify_signature_rejects_missing_raw_event(test_client: httpx.AsyncClient):
# Arrange — raw_event 必填字段缺失
body = {"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
async def test_verify_signature_rejects_missing_headers(test_client: httpx.AsyncClient):
# Arrange — headers 必填字段缺失
body = {"raw_event": "test-payload"}
# 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
async def test_webhook_test_rejects_empty_event_type(test_client: httpx.AsyncClient, admin_headers):
# Arrange — event_type min_length=1空串违反约束
body = {"event_type": "", "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 == 422, response.text
# =============================================================================
# === Invalid channel_type validation ===
# =============================================================================
async def test_webhook_unregistered_channel_type(test_client: httpx.AsyncClient):
# Arrange — channel_type 为 str 子类无白名单,未注册插件返回 404
unregistered_channel = "invalid_channel_type_xyz"
# Act
response = await test_client.post(
f"{BASE_URL}/{unregistered_channel}/webhook",
content=b"{}",
headers={"Content-Type": "application/json"},
)
# Assert
assert response.status_code == 404, response.text