feat(scouts): add GmailConnector
Implements GmailConnector — the first concrete SourceConnector. Wraps existing GmailClient + low-level Gmail API service for metadata-only fetch, trash archive, incremental history polling, and Pub/Sub watch setup. Adds GMAIL_PUBSUB_TOPIC setting (empty string default for dev). Adds 3 passing unit tests (mocked API, no real credentials required). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
77
tests/test_scout_connectors_gmail.py
Normal file
77
tests/test_scout_connectors_gmail.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for GmailConnector."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import CloudScoutConfig
|
||||
from app.scouts.connectors.base import ItemRef
|
||||
from app.scouts.connectors.gmail import GmailConnector
|
||||
|
||||
|
||||
def _make_scout():
|
||||
return CloudScoutConfig(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id="00000000-0000-0000-0000-000000000003",
|
||||
provider="gmail",
|
||||
name="Inbox",
|
||||
data_types=[],
|
||||
prompt_template="",
|
||||
oauth_token_encrypted="encrypted-blob",
|
||||
schedule_cron="0 * * * *",
|
||||
enabled=True,
|
||||
auto_trash_spam=False,
|
||||
device_inactivity_pause_days=14,
|
||||
gmail_history_id="100",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_metadata_returns_subject_and_snippet():
|
||||
scout = _make_scout()
|
||||
conn = GmailConnector()
|
||||
fake_message = {
|
||||
"id": "msg-1",
|
||||
"snippet": "preview text",
|
||||
"payload": {"headers": [
|
||||
{"name": "Subject", "value": "Hello"},
|
||||
{"name": "From", "value": "alice@example.com"},
|
||||
{"name": "Date", "value": "Wed, 14 May 2026 10:00:00 +0000"},
|
||||
]},
|
||||
}
|
||||
with patch("app.scouts.connectors.gmail._get_gmail_service") as mock_svc:
|
||||
mock_svc.return_value.users().messages().get().execute.return_value = fake_message
|
||||
meta = await conn.fetch_metadata(scout, ItemRef(source_msg_ref="msg-1"))
|
||||
assert meta.subject == "Hello"
|
||||
assert meta.sender == "alice@example.com"
|
||||
assert meta.snippet == "preview text"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_content_returns_body_text():
|
||||
scout = _make_scout()
|
||||
conn = GmailConnector()
|
||||
# decrypt_token is patched because the test doesn't set OAUTH_ENCRYPTION_KEY.
|
||||
with patch("app.scouts.connectors.gmail.decrypt_token", return_value={}), \
|
||||
patch("app.scouts.connectors.gmail.GmailClient") as MockClient:
|
||||
instance = MockClient.return_value
|
||||
instance.fetch_messages = AsyncMock(return_value=[
|
||||
MagicMock(id="msg-1", subject="S", sender="a@b", body_text="hello world",
|
||||
date=datetime.now(tz=timezone.utc), labels=[]),
|
||||
])
|
||||
content = await conn.fetch_content(scout, ItemRef(source_msg_ref="msg-1"))
|
||||
assert content.body_text == "hello world"
|
||||
assert content.metadata.subject == "S"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_archive_calls_trash():
|
||||
scout = _make_scout()
|
||||
conn = GmailConnector()
|
||||
with patch("app.scouts.connectors.gmail._get_gmail_service") as mock_svc:
|
||||
await conn.archive(scout, ItemRef(source_msg_ref="msg-1"))
|
||||
mock_svc.return_value.users().messages().trash.assert_called()
|
||||
Reference in New Issue
Block a user