"""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