feat(scouts): gmail pub/sub webhook with JWT verification
This commit is contained in:
106
tests/test_scout_webhook.py
Normal file
106
tests/test_scout_webhook.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user