From a5c04cb38dded60987d0ea83ae3585d59c3d6a00 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Thu, 7 May 2026 04:22:59 +0530 Subject: [PATCH] test(backend): generalize native Google E2E fakes for Gmail --- ...ative_google_drive.py => native_google.py} | 96 +++++++++++++++++-- surfsense_backend/tests/e2e/run_backend.py | 4 +- surfsense_backend/tests/e2e/run_celery.py | 4 +- 3 files changed, 90 insertions(+), 14 deletions(-) rename surfsense_backend/tests/e2e/fakes/{native_google_drive.py => native_google.py} (71%) diff --git a/surfsense_backend/tests/e2e/fakes/native_google_drive.py b/surfsense_backend/tests/e2e/fakes/native_google.py similarity index 71% rename from surfsense_backend/tests/e2e/fakes/native_google_drive.py rename to surfsense_backend/tests/e2e/fakes/native_google.py index 524ce77ba..183ed37c6 100644 --- a/surfsense_backend/tests/e2e/fakes/native_google_drive.py +++ b/surfsense_backend/tests/e2e/fakes/native_google.py @@ -1,12 +1,13 @@ -"""Strict native Google Drive fakes for Playwright E2E. +"""Strict native Google SDK fakes for Playwright E2E. This module patches the production Google OAuth and Drive SDK bindings used by -the native ``GOOGLE_DRIVE_CONNECTOR`` happy path. It deliberately does not -replace the whole Google package; unmodelled service methods fail loudly. +the native Google connector happy paths. It deliberately does not replace the +whole Google package; unmodelled service methods fail loudly. """ from __future__ import annotations +import base64 import json from datetime import UTC, datetime, timedelta from pathlib import Path @@ -17,6 +18,7 @@ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from google.oauth2.credentials import Credentials _DRIVE_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "drive_files.json" +_GMAIL_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "gmail_messages.json" def _load_drive_fixture() -> dict[str, Any]: @@ -27,14 +29,22 @@ def _load_drive_fixture() -> dict[str, Any]: _DRIVE_FIXTURE = _load_drive_fixture() +def _load_gmail_fixture() -> dict[str, Any]: + with _GMAIL_FIXTURE_PATH.open() as f: + return json.load(f) + + +_GMAIL_FIXTURE = _load_gmail_fixture() + + class _StrictFakeMixin: _component_name: str = "" def __getattr__(self, name: str) -> Any: raise NotImplementedError( - f"E2E native Google Drive fake missing surface: " + f"E2E native Google fake missing surface: " f"{self._component_name}.{name!r}. Add it to " - f"surfsense_backend/tests/e2e/fakes/native_google_drive.py." + f"surfsense_backend/tests/e2e/fakes/native_google.py." ) @@ -149,7 +159,7 @@ class _FakeDriveFiles(_StrictFakeMixin): content = _DRIVE_FIXTURE.get("_file_contents", {}).get(file_id) if content is None: raise NotImplementedError( - f"E2E native Google Drive fake has no content for fileId={file_id!r}." + f"E2E native Google fake has no content for fileId={file_id!r}." ) return _FakeMediaRequest(content.encode("utf-8")) @@ -158,7 +168,7 @@ class _FakeDriveFiles(_StrictFakeMixin): content = _DRIVE_FIXTURE.get("_file_contents", {}).get(file_id) if content is None: raise NotImplementedError( - f"E2E native Google Drive fake has no export content for fileId={file_id!r}." + f"E2E native Google fake has no export content for fileId={file_id!r}." ) return _FakeRequest(content.encode("utf-8")) @@ -194,6 +204,46 @@ class _FakeGmailUsers(_StrictFakeMixin): raise NotImplementedError(f"Unexpected fake Gmail profile userId={user_id!r}") return _FakeRequest({"emailAddress": "native-drive-e2e@surfsense.example"}) + def messages(self) -> _FakeGmailMessages: + return _FakeGmailMessages() + + +class _FakeGmailMessages(_StrictFakeMixin): + _component_name = "gmail.messages" + + def list(self, **kwargs: Any) -> _FakeRequest: + user_id = kwargs.get("userId") + if user_id != "me": + raise NotImplementedError(f"Unexpected fake Gmail list userId={user_id!r}") + + max_results = int(kwargs.get("maxResults", 10) or 10) + messages = list(_GMAIL_FIXTURE.get("messages", []))[:max_results] + return _FakeRequest( + { + "messages": [ + { + "id": message["id"], + "threadId": message.get("threadId"), + } + for message in messages + ], + "resultSizeEstimate": len(messages), + } + ) + + def get(self, **kwargs: Any) -> _FakeRequest: + user_id = kwargs.get("userId") + if user_id != "me": + raise NotImplementedError(f"Unexpected fake Gmail get userId={user_id!r}") + + message_id = kwargs.get("id") + detail = _GMAIL_FIXTURE.get("details", {}).get(message_id) + if detail is None: + raise NotImplementedError( + f"E2E native Gmail fake has no message detail for id={message_id!r}." + ) + return _FakeRequest(_gmail_detail_to_native_message(detail)) + class _FakeGmailService(_StrictFakeMixin): _component_name = "gmail_service" @@ -208,7 +258,7 @@ def _fake_build(service_name: str, version: str, **_: Any) -> Any: if service_name == "gmail" and version == "v1": return _FakeGmailService() raise NotImplementedError( - f"E2E native Google Drive fake cannot build {service_name!r} {version!r}." + f"E2E native Google fake cannot build {service_name!r} {version!r}." ) @@ -256,14 +306,40 @@ def _drive_get_metadata(file_id: str | None) -> dict[str, Any]: if entry.get("id") == file_id: return dict(entry) raise NotImplementedError( - f"E2E native Google Drive fake has no metadata for fileId={file_id!r}." + f"E2E native Google fake has no metadata for fileId={file_id!r}." ) +def _gmail_detail_to_native_message(detail: dict[str, Any]) -> dict[str, Any]: + message_text = detail.get("messageText", "") + encoded_body = base64.urlsafe_b64encode(message_text.encode("utf-8")).decode("ascii") + + return { + "id": detail.get("id"), + "threadId": detail.get("threadId"), + "labelIds": ["INBOX", "IMPORTANT"], + "snippet": message_text[:160], + "payload": { + "mimeType": "text/plain", + "headers": [ + {"name": "Subject", "value": detail.get("subject", "No Subject")}, + {"name": "From", "value": detail.get("from", "Unknown Sender")}, + {"name": "To", "value": detail.get("to", "Unknown Recipient")}, + {"name": "Date", "value": detail.get("date", "Unknown Date")}, + ], + "body": { + "data": encoded_body, + "size": len(message_text), + }, + }, + } + + def install(active_patches: list[Any]) -> None: - """Patch production bindings to use native Google Drive fakes.""" + """Patch production bindings to use native Google SDK fakes.""" targets = [ ("app.routes.google_drive_add_connector_route.Flow", _FakeFlow), + ("app.routes.google_gmail_add_connector_route.Flow", _FakeFlow), ("app.connectors.google_drive.client.build", _fake_build), ("app.connectors.google_gmail_connector.build", _fake_build), ("googleapiclient.http.MediaIoBaseDownload", _FakeMediaIoBaseDownload), diff --git a/surfsense_backend/tests/e2e/run_backend.py b/surfsense_backend/tests/e2e/run_backend.py index a985fee73..9a9a2b7ce 100644 --- a/surfsense_backend/tests/e2e/run_backend.py +++ b/surfsense_backend/tests/e2e/run_backend.py @@ -81,7 +81,7 @@ from unittest.mock import patch # noqa: E402 from app.app import app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 embeddings as _fake_embeddings, - native_google_drive as _fake_native_google_drive, + native_google as _fake_native_google, ) from tests.e2e.fakes.chat_llm import ( # noqa: E402 fake_create_chat_litellm_from_agent_config, @@ -148,7 +148,7 @@ def _patch_llm_bindings() -> None: _patch_llm_bindings() _fake_embeddings.install(_active_patches) -_fake_native_google_drive.install(_active_patches) +_fake_native_google.install(_active_patches) # --------------------------------------------------------------------------- diff --git a/surfsense_backend/tests/e2e/run_celery.py b/surfsense_backend/tests/e2e/run_celery.py index 31904cbe5..2cbba966f 100644 --- a/surfsense_backend/tests/e2e/run_celery.py +++ b/surfsense_backend/tests/e2e/run_celery.py @@ -66,7 +66,7 @@ from unittest.mock import patch # noqa: E402 from app.celery_app import celery_app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 embeddings as _fake_embeddings, - native_google_drive as _fake_native_google_drive, + native_google as _fake_native_google, ) from tests.e2e.fakes.chat_llm import ( # noqa: E402 fake_create_chat_litellm_from_agent_config, @@ -132,7 +132,7 @@ def _patch_llm_bindings() -> None: _patch_llm_bindings() _fake_embeddings.install(_active_patches) -_fake_native_google_drive.install(_active_patches) +_fake_native_google.install(_active_patches) # ---------------------------------------------------------------------------