From 69437b1fe4ff58fc8f80d9655cf90604d42eac0a Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 8 May 2026 12:27:45 +0530 Subject: [PATCH] test(e2e): add Dropbox backend fake --- .../tests/e2e/fakes/dropbox_api.py | 179 ++++++++++++++++++ .../e2e/fakes/fixtures/dropbox_files.json | 19 ++ surfsense_backend/tests/e2e/run_backend.py | 9 + surfsense_backend/tests/e2e/run_celery.py | 9 + 4 files changed, 216 insertions(+) create mode 100644 surfsense_backend/tests/e2e/fakes/dropbox_api.py create mode 100644 surfsense_backend/tests/e2e/fakes/fixtures/dropbox_files.json diff --git a/surfsense_backend/tests/e2e/fakes/dropbox_api.py b/surfsense_backend/tests/e2e/fakes/dropbox_api.py new file mode 100644 index 000000000..43bb74c2d --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/dropbox_api.py @@ -0,0 +1,179 @@ +"""Strict Dropbox HTTP/API fakes for Playwright E2E. + +This module patches the Dropbox OAuth route and indexer consumer-site +bindings. It keeps the production add/callback/indexing flow intact while +serving deterministic Dropbox-shaped token, profile, metadata, and file +content responses. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any +from unittest.mock import patch + +import httpx + +_DROPBOX_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "dropbox_files.json" + + +def _load_dropbox_fixture() -> dict[str, Any]: + with _DROPBOX_FIXTURE_PATH.open() as f: + return json.load(f) + + +_DROPBOX_FIXTURE = _load_dropbox_fixture() + + +class _StrictFakeMixin: + _component_name: str = "" + + def __getattr__(self, name: str) -> Any: + raise NotImplementedError( + f"E2E Dropbox fake missing surface: " + f"{self._component_name}.{name!r}. Add it to " + f"surfsense_backend/tests/e2e/fakes/dropbox_api.py." + ) + + +class _FakeDropboxClient(_StrictFakeMixin): + _component_name = "DropboxClient" + + def __init__(self, session: Any, connector_id: int): + self._session = session + self._connector_id = connector_id + + async def _get_valid_token(self) -> str: + return "fake-dropbox-access-token" + + async def list_folder( + self, path: str = "" + ) -> tuple[list[dict[str, Any]], str | None]: + items = _DROPBOX_FIXTURE.get(path) + if not isinstance(items, list): + return [], f"E2E Dropbox fake has no folder for path={path!r}." + return [dict(item) for item in items], None + + async def get_latest_cursor(self, path: str = "") -> tuple[str | None, str | None]: + if path not in _DROPBOX_FIXTURE: + return None, f"E2E Dropbox fake has no cursor for path={path!r}." + return f"fake-dropbox-cursor:{path or 'root'}", None + + async def get_changes( + self, cursor: str + ) -> tuple[list[dict[str, Any]], str | None, str | None]: + return [], cursor, None + + async def get_metadata(self, path: str) -> tuple[dict[str, Any] | None, str | None]: + metadata = _dropbox_get_metadata(path) + if metadata is None: + return None, f"E2E Dropbox fake has no metadata for path={path!r}." + return metadata, None + + async def download_file(self, path: str) -> tuple[bytes | None, str | None]: + content = _DROPBOX_FIXTURE.get("_file_contents", {}).get(path) + if content is None: + return None, f"E2E Dropbox fake has no content for path={path!r}." + return content.encode("utf-8"), None + + async def download_file_to_disk(self, path: str, dest_path: str) -> str | None: + content = _DROPBOX_FIXTURE.get("_file_contents", {}).get(path) + if content is None: + return f"E2E Dropbox fake has no content for path={path!r}." + with open(dest_path, "wb") as f: + f.write(content.encode("utf-8")) + return None + + async def get_current_account(self) -> tuple[dict[str, Any] | None, str | None]: + return _dropbox_current_account(), None + + +class _FakeAsyncClient(_StrictFakeMixin): + _component_name = "httpx.AsyncClient" + + def __init__(self, *args: Any, **kwargs: Any): + del args, kwargs + + async def __aenter__(self) -> _FakeAsyncClient: + return self + + async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: + del exc_type, exc, tb + + async def post(self, url: str, *args: Any, **kwargs: Any) -> httpx.Response: + del args, kwargs + if url == "https://api.dropboxapi.com/oauth2/token": + return _json_response( + "POST", + url, + { + "access_token": "fake-dropbox-access-token", + "refresh_token": "fake-dropbox-refresh-token", + "token_type": "bearer", + "expires_in": 3600, + "account_id": "dbid:fake-dropbox-account", + }, + ) + if url == "https://api.dropboxapi.com/2/users/get_current_account": + return _json_response("POST", url, _dropbox_current_account()) + raise NotImplementedError(f"E2E Dropbox fake unexpected POST URL: {url!r}") + + async def request( + self, method: str, url: str, *args: Any, **kwargs: Any + ) -> httpx.Response: + del args, kwargs + raise NotImplementedError( + f"E2E Dropbox fake unexpected request: {method!r} {url!r}" + ) + + +class _FakeHttpxModule(_StrictFakeMixin): + _component_name = "httpx" + + AsyncClient = _FakeAsyncClient + + +def _json_response( + method: str, url: str, payload: dict[str, Any], status_code: int = 200 +) -> httpx.Response: + return httpx.Response( + status_code=status_code, + json=payload, + request=httpx.Request(method, url), + ) + + +def _dropbox_current_account() -> dict[str, Any]: + return { + "email": "dropbox-e2e@surfsense.example", + "name": {"display_name": "SurfSense Dropbox E2E"}, + "account_id": "dbid:fake-dropbox-account", + } + + +def _dropbox_get_metadata(path: str | None) -> dict[str, Any] | None: + for items in _DROPBOX_FIXTURE.values(): + if not isinstance(items, list): + continue + for entry in items: + if entry.get("path_lower") == path or entry.get("id") == path: + return dict(entry) + return None + + +def install(active_patches: list[Any]) -> None: + """Patch production Dropbox bindings to use strict Dropbox fakes.""" + targets = [ + ("app.routes.dropbox_add_connector_route.httpx", _FakeHttpxModule()), + ("app.routes.dropbox_add_connector_route.DropboxClient", _FakeDropboxClient), + ( + "app.tasks.connector_indexers.dropbox_indexer.DropboxClient", + _FakeDropboxClient, + ), + ("app.connectors.dropbox.client.httpx", _FakeHttpxModule()), + ] + for target, replacement in targets: + p = patch(target, replacement) + p.start() + active_patches.append(p) diff --git a/surfsense_backend/tests/e2e/fakes/fixtures/dropbox_files.json b/surfsense_backend/tests/e2e/fakes/fixtures/dropbox_files.json new file mode 100644 index 000000000..52d30772c --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/fixtures/dropbox_files.json @@ -0,0 +1,19 @@ +{ + "": [ + { + ".tag": "file", + "id": "id:fake-dropbox-canary", + "name": "e2e-dropbox-canary.txt", + "path_lower": "/e2e-dropbox-canary.txt", + "path_display": "/e2e-dropbox-canary.txt", + "size": 152, + "is_downloadable": true, + "server_modified": "2026-05-08T00:00:00Z", + "client_modified": "2026-05-08T00:00:00Z", + "content_hash": "fake-dropbox-hash-001" + } + ], + "_file_contents": { + "/e2e-dropbox-canary.txt": "Canary token for Dropbox E2E tests: SURFSENSE_E2E_CANARY_TOKEN_DROPBOX_001\nThis file proves the Dropbox indexing pipeline ran end-to-end." + } +} diff --git a/surfsense_backend/tests/e2e/run_backend.py b/surfsense_backend/tests/e2e/run_backend.py index 5ca2ea2b5..9c880e0a8 100644 --- a/surfsense_backend/tests/e2e/run_backend.py +++ b/surfsense_backend/tests/e2e/run_backend.py @@ -75,6 +75,12 @@ os.environ.setdefault( "ONEDRIVE_REDIRECT_URI", "http://localhost:8000/api/v1/auth/onedrive/connector/callback", ) +os.environ.setdefault("DROPBOX_APP_KEY", "fake-dropbox-app-key") +os.environ.setdefault("DROPBOX_APP_SECRET", "fake-dropbox-app-secret") +os.environ.setdefault( + "DROPBOX_REDIRECT_URI", + "http://localhost:8000/api/v1/auth/dropbox/connector/callback", +) os.environ["SLACK_CLIENT_ID"] = "fake-slack-mcp-client-id" os.environ["SLACK_CLIENT_SECRET"] = "fake-slack-mcp-client-secret" @@ -105,6 +111,7 @@ from app.app import app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 confluence_indexer as _fake_confluence_indexer, confluence_oauth as _fake_confluence_oauth, + dropbox_api as _fake_dropbox_api, embeddings as _fake_embeddings, jira_module as _fake_jira_module, linear_module as _fake_linear_module, @@ -133,6 +140,7 @@ def _patch_llm_bindings() -> None: "app.tasks.connector_indexers.google_gmail_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.notion_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.onedrive_indexer.get_user_long_context_llm", + "app.tasks.connector_indexers.dropbox_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.local_folder_indexer.get_user_long_context_llm", "app.tasks.document_processors._save.get_user_long_context_llm", "app.tasks.document_processors.markdown_processor.get_user_long_context_llm", @@ -188,6 +196,7 @@ _fake_confluence_oauth.install(_active_patches) _fake_confluence_indexer.install(_active_patches) _fake_native_google.install(_active_patches) _fake_onedrive_graph.install(_active_patches) +_fake_dropbox_api.install(_active_patches) _fake_notion_module.install(_active_patches) _fake_linear_module.install(_active_patches) _fake_jira_module.install(_active_patches) diff --git a/surfsense_backend/tests/e2e/run_celery.py b/surfsense_backend/tests/e2e/run_celery.py index ddf5d8b01..7b084e0ac 100644 --- a/surfsense_backend/tests/e2e/run_celery.py +++ b/surfsense_backend/tests/e2e/run_celery.py @@ -62,6 +62,12 @@ os.environ.setdefault( "ONEDRIVE_REDIRECT_URI", "http://localhost:8000/api/v1/auth/onedrive/connector/callback", ) +os.environ.setdefault("DROPBOX_APP_KEY", "fake-dropbox-app-key") +os.environ.setdefault("DROPBOX_APP_SECRET", "fake-dropbox-app-secret") +os.environ.setdefault( + "DROPBOX_REDIRECT_URI", + "http://localhost:8000/api/v1/auth/dropbox/connector/callback", +) os.environ["SLACK_CLIENT_ID"] = "fake-slack-mcp-client-id" os.environ["SLACK_CLIENT_SECRET"] = "fake-slack-mcp-client-secret" @@ -90,6 +96,7 @@ from app.celery_app import celery_app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 confluence_indexer as _fake_confluence_indexer, confluence_oauth as _fake_confluence_oauth, + dropbox_api as _fake_dropbox_api, embeddings as _fake_embeddings, jira_module as _fake_jira_module, linear_module as _fake_linear_module, @@ -117,6 +124,7 @@ def _patch_llm_bindings() -> None: "app.tasks.connector_indexers.google_gmail_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.notion_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.onedrive_indexer.get_user_long_context_llm", + "app.tasks.connector_indexers.dropbox_indexer.get_user_long_context_llm", "app.tasks.connector_indexers.local_folder_indexer.get_user_long_context_llm", "app.tasks.document_processors._save.get_user_long_context_llm", "app.tasks.document_processors.markdown_processor.get_user_long_context_llm", @@ -172,6 +180,7 @@ _fake_confluence_oauth.install(_active_patches) _fake_confluence_indexer.install(_active_patches) _fake_native_google.install(_active_patches) _fake_onedrive_graph.install(_active_patches) +_fake_dropbox_api.install(_active_patches) _fake_notion_module.install(_active_patches) _fake_linear_module.install(_active_patches) _fake_jira_module.install(_active_patches)