107 lines
3.2 KiB
Python
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
|