2026-07-02 03:25:27 +08:00
|
|
|
|
"""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"},
|
|
|
|
|
|
)
|
2026-07-04 00:18:04 +08:00
|
|
|
|
# Assert — 无鉴权要求;入站管道 ack → 200,nack → 500,适配器未注册 → 501
|
2026-07-02 03:25:27 +08:00
|
|
|
|
assert response.status_code != 401
|
2026-07-04 00:18:04 +08:00
|
|
|
|
assert response.status_code in (200, 500, 501), response.text
|
2026-07-02 03:25:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
|
async def test_webhook_accepts_empty_body_without_auth_error(test_client: httpx.AsyncClient):
|
2026-07-02 03:25:27 +08:00
|
|
|
|
# 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"},
|
|
|
|
|
|
)
|
2026-07-04 00:18:04 +08:00
|
|
|
|
# Assert — empty body 被 ReceiveInboundCmd.__post_init__ 拒绝(ValidationError → 400),
|
|
|
|
|
|
# 不返回 401(鉴权不适用于 webhook 入站端点)。
|
2026-07-02 03:25:27 +08:00
|
|
|
|
assert response.status_code != 401
|
2026-07-04 00:18:04 +08:00
|
|
|
|
assert response.status_code == 400, response.text
|
2026-07-02 03:25:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
|
async def test_verify_signature_with_valid_body_returns_200_or_501(test_client: httpx.AsyncClient):
|
2026-07-02 03:25:27 +08:00
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
|
async def test_admin_can_test_webhook_returns_200_or_501(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
|
async def test_webhook_test_with_valid_body_returns_200_or_501(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
|
# 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 ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-04 00:18:04 +08:00
|
|
|
|
async def test_webhook_unregistered_channel_type(test_client: httpx.AsyncClient):
|
|
|
|
|
|
# Arrange — channel_type 为 str 子类无白名单,未注册插件返回 404
|
|
|
|
|
|
unregistered_channel = "invalid_channel_type_xyz"
|
2026-07-02 03:25:27 +08:00
|
|
|
|
# Act
|
|
|
|
|
|
response = await test_client.post(
|
2026-07-04 00:18:04 +08:00
|
|
|
|
f"{BASE_URL}/{unregistered_channel}/webhook",
|
2026-07-02 03:25:27 +08:00
|
|
|
|
content=b"{}",
|
|
|
|
|
|
headers={"Content-Type": "application/json"},
|
|
|
|
|
|
)
|
|
|
|
|
|
# Assert
|
2026-07-04 00:18:04 +08:00
|
|
|
|
assert response.status_code == 404, response.text
|