Files
api/tests/test_scout_webhook.py

107 lines
3.2 KiB
Python

"""Tests for the Gmail Pub/Sub webhook route.
Covers:
- Happy path: valid JWT + known user + enabled scout → 204, engine triggered.
- Rejection: invalid JWT → 401.
"""
from __future__ import annotations
import base64
import json
import uuid
from unittest.mock import AsyncMock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
from app.models import CloudScoutConfig, User
from tests.conftest import _TestSessionLocal
def _pubsub_payload(email: str, history_id: str) -> dict:
"""Build a minimal Pub/Sub push envelope."""
inner = json.dumps({"emailAddress": email, "historyId": history_id}).encode()
return {
"message": {"data": base64.b64encode(inner).decode(), "messageId": "m1"},
"subscription": "projects/x/subscriptions/gmail-watch-sub",
}
@pytest.mark.asyncio
async def test_webhook_triggers_scout_for_matching_user():
"""204 returned and ScoutEngine.trigger_scout awaited for the matching scout."""
user_id = "00000000-0000-0000-0000-000000000003" # seeded 'power' user
scout_id = str(uuid.uuid4())
# Mutate the seeded user email so the webhook can resolve it,
# and add a cloud scout config for gmail.
async with _TestSessionLocal() as session:
user = await session.get(User, user_id)
user.email = "alice@example.com"
session.add(
CloudScoutConfig(
id=scout_id,
user_id=user_id,
provider="gmail",
name="Inbox",
data_types=[],
prompt_template="",
schedule_cron="0 * * * *",
enabled=True,
auto_trash_spam=False,
device_inactivity_pause_days=14,
)
)
await session.commit()
payload = _pubsub_payload("alice@example.com", "200")
with (
patch(
"app.api.routes.scout_webhooks._verify_pubsub_jwt",
return_value=True,
),
patch(
"app.api.routes.scout_webhooks.async_session",
_TestSessionLocal,
),
patch(
"app.scouts.engine.ScoutEngine.trigger_scout",
new=AsyncMock(),
) as mock_trigger,
):
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
resp = await client.post(
"/api/v1/scouts/webhooks/gmail",
json=payload,
headers={"Authorization": "Bearer fake-google-jwt"},
)
assert resp.status_code == 204
mock_trigger.assert_awaited_once_with(uuid.UUID(scout_id))
@pytest.mark.asyncio
async def test_webhook_rejects_unverified_jwt():
"""401 returned when JWT verification fails."""
payload = _pubsub_payload("alice@example.com", "200")
with patch(
"app.api.routes.scout_webhooks._verify_pubsub_jwt",
return_value=False,
):
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as client:
resp = await client.post(
"/api/v1/scouts/webhooks/gmail",
json=payload,
headers={"Authorization": "Bearer bogus"},
)
assert resp.status_code == 401