1381735e3b
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
177 lines
5.7 KiB
Python
177 lines
5.7 KiB
Python
"""Tests for the rewritten hooks server."""
|
|
|
|
import asyncio
|
|
import json
|
|
import pytest
|
|
from aiohttp import web
|
|
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop, TestClient, TestServer
|
|
from nanobot.hooks.server import HooksServer
|
|
from nanobot.bus.queue import MessageBus
|
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
|
from nanobot.config.schema import HooksConfig
|
|
|
|
|
|
@pytest.fixture
|
|
def bus():
|
|
return MessageBus()
|
|
|
|
|
|
@pytest.fixture
|
|
def config():
|
|
return HooksConfig(
|
|
enabled=True,
|
|
tokens={"test-hook": "test-secret-123"},
|
|
timeout_seconds=5,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def server(bus, config):
|
|
return HooksServer(host="127.0.0.1", port=0, config=config, bus=bus)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check(server):
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.get("/health")
|
|
assert resp.status == 200
|
|
data = await resp.json()
|
|
assert data["status"] == "ok"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_without_token(server):
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.post("/hooks", json={"message": "test"})
|
|
assert resp.status == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unauthorized_wrong_token(server):
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.post(
|
|
"/hooks",
|
|
json={"message": "test"},
|
|
headers={"Authorization": "Bearer wrong-token"},
|
|
)
|
|
assert resp.status == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_message_field(server, bus):
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.post(
|
|
"/hooks",
|
|
json={"not_message": "test"},
|
|
headers={"Authorization": "Bearer test-secret-123"},
|
|
)
|
|
assert resp.status == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_publishes_to_bus(server, bus):
|
|
"""Hook should publish InboundMessage to bus and the message should contain hook prefix."""
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
# Send hook request in background (it will block waiting for correlation)
|
|
async def send_request():
|
|
return await client.post(
|
|
"/hooks",
|
|
json={"message": "hello from webhook"},
|
|
headers={"Authorization": "Bearer test-secret-123"},
|
|
)
|
|
|
|
task = asyncio.create_task(send_request())
|
|
|
|
# Consume the inbound message
|
|
msg = await asyncio.wait_for(bus.consume_inbound(), timeout=2.0)
|
|
|
|
assert msg.channel == "hook"
|
|
assert msg.chat_id == "test-hook" # defaults to token name
|
|
assert msg.metadata.get("hook_source") == "test-hook"
|
|
assert msg.metadata.get("correlation_id") is not None
|
|
|
|
# Simulate agent response by resolving correlation
|
|
bus.resolve_correlation(OutboundMessage(
|
|
channel="hook",
|
|
chat_id="test-hook",
|
|
content="agent says hi",
|
|
metadata={"correlation_id": msg.metadata["correlation_id"]},
|
|
))
|
|
|
|
resp = await asyncio.wait_for(task, timeout=2.0)
|
|
assert resp.status == 200
|
|
data = await resp.json()
|
|
assert data["ok"] is True
|
|
assert data["response"] == "agent says hi"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_with_custom_channel(server, bus):
|
|
"""Hook targeting telegram should use telegram channel in InboundMessage."""
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
async def send_request():
|
|
return await client.post(
|
|
"/hooks",
|
|
json={"message": "notify user", "channel": "telegram", "chat_id": "239824268"},
|
|
headers={"Authorization": "Bearer test-secret-123"},
|
|
)
|
|
|
|
task = asyncio.create_task(send_request())
|
|
|
|
msg = await asyncio.wait_for(bus.consume_inbound(), timeout=2.0)
|
|
assert msg.channel == "telegram"
|
|
assert msg.chat_id == "239824268"
|
|
assert msg.session_key == "telegram:239824268"
|
|
|
|
bus.resolve_correlation(OutboundMessage(
|
|
channel="telegram",
|
|
chat_id="239824268",
|
|
content="done",
|
|
metadata={"correlation_id": msg.metadata["correlation_id"]},
|
|
))
|
|
|
|
resp = await asyncio.wait_for(task, timeout=2.0)
|
|
assert resp.status == 200
|
|
data = await resp.json()
|
|
assert data["response"] == "done"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_timeout_returns_504(bus):
|
|
"""If agent doesn't respond in time, return 504."""
|
|
config = HooksConfig(enabled=True, tokens={"test-hook": "test-secret-123"}, timeout_seconds=1)
|
|
server = HooksServer(host="127.0.0.1", port=0, config=config, bus=bus)
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.post(
|
|
"/hooks",
|
|
json={"message": "slow request"},
|
|
headers={"Authorization": "Bearer test-secret-123"},
|
|
)
|
|
assert resp.status == 504
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hook_timeout_zero_returns_202(server, bus):
|
|
"""timeout=0 should return 202 immediately without waiting."""
|
|
client = TestClient(TestServer(server._app))
|
|
async with client:
|
|
resp = await client.post(
|
|
"/hooks",
|
|
json={"message": "fire and forget", "timeout": 0},
|
|
headers={"Authorization": "Bearer test-secret-123"},
|
|
)
|
|
assert resp.status == 202
|
|
data = await resp.json()
|
|
assert data["ok"] is True
|
|
|
|
# Message should still be on the bus
|
|
msg = await asyncio.wait_for(bus.consume_inbound(), timeout=1.0)
|
|
assert msg.content == "fire and forget"
|