mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
test(backend): add Notion E2E fakes
This commit is contained in:
parent
fcf5208ee6
commit
15709b82f7
4 changed files with 285 additions and 0 deletions
64
surfsense_backend/tests/e2e/fakes/fixtures/notion_pages.json
Normal file
64
surfsense_backend/tests/e2e/fakes/fixtures/notion_pages.json
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"object": "page",
|
||||||
|
"id": "fake-notion-page-canary-001",
|
||||||
|
"created_time": "2026-05-07T00:00:00.000Z",
|
||||||
|
"last_edited_time": "2026-05-07T00:00:00.000Z",
|
||||||
|
"url": "https://notion.so/fake-notion-page-canary-001",
|
||||||
|
"properties": {
|
||||||
|
"Name": {
|
||||||
|
"id": "title",
|
||||||
|
"type": "title",
|
||||||
|
"title": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"plain_text": "E2E Canary Notion Page",
|
||||||
|
"text": {
|
||||||
|
"content": "E2E Canary Notion Page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"blocks": {
|
||||||
|
"fake-notion-page-canary-001": [
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"id": "fake-notion-block-heading-001",
|
||||||
|
"type": "heading_2",
|
||||||
|
"has_children": false,
|
||||||
|
"heading_2": {
|
||||||
|
"rich_text": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"plain_text": "E2E Notion Canary",
|
||||||
|
"text": {
|
||||||
|
"content": "E2E Notion Canary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"object": "block",
|
||||||
|
"id": "fake-notion-block-body-001",
|
||||||
|
"type": "paragraph",
|
||||||
|
"has_children": false,
|
||||||
|
"paragraph": {
|
||||||
|
"rich_text": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"plain_text": "This Notion page proves the indexed connector fetched Notion blocks through OAuth credentials. SURFSENSE_E2E_CANARY_TOKEN_NOTION_001",
|
||||||
|
"text": {
|
||||||
|
"content": "This Notion page proves the indexed connector fetched Notion blocks through OAuth credentials. SURFSENSE_E2E_CANARY_TOKEN_NOTION_001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
197
surfsense_backend/tests/e2e/fakes/notion_module.py
Normal file
197
surfsense_backend/tests/e2e/fakes/notion_module.py
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""Strict Notion OAuth/API fakes for Playwright E2E."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "notion_pages.json"
|
||||||
|
_TOKEN_URL = "https://api.notion.com/v1/oauth/token"
|
||||||
|
_ACCESS_TOKEN = "fake-notion-access-token"
|
||||||
|
_REFRESH_TOKEN = "fake-notion-refresh-token"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_fixture() -> dict[str, Any]:
|
||||||
|
with _FIXTURE_PATH.open() as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
_FIXTURE = _load_fixture()
|
||||||
|
|
||||||
|
|
||||||
|
class _StrictFakeMixin:
|
||||||
|
_component_name: str = "<unknown>"
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"E2E Notion fake missing surface: {self._component_name}.{name!r}. "
|
||||||
|
"Add it to surfsense_backend/tests/e2e/fakes/notion_module.py."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIResponseError(Exception):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
status: int = 400,
|
||||||
|
code: str = "validation_error",
|
||||||
|
headers: dict[str, str] | None = None,
|
||||||
|
body: Any | None = None,
|
||||||
|
):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status = status
|
||||||
|
self.code = code
|
||||||
|
self.headers = headers or {}
|
||||||
|
self.body = body or {"message": message}
|
||||||
|
|
||||||
|
|
||||||
|
errors = types.ModuleType("notion_client.errors")
|
||||||
|
errors.APIResponseError = APIResponseError
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeBlocksChildren(_StrictFakeMixin):
|
||||||
|
_component_name = "notion.blocks.children"
|
||||||
|
|
||||||
|
async def list(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
block_id = kwargs.get("block_id")
|
||||||
|
start_cursor = kwargs.get("start_cursor")
|
||||||
|
if start_cursor is not None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"E2E Notion fake does not model block pagination cursor={start_cursor!r}."
|
||||||
|
)
|
||||||
|
|
||||||
|
blocks = _FIXTURE.get("blocks", {}).get(block_id)
|
||||||
|
if blocks is None:
|
||||||
|
raise APIResponseError(
|
||||||
|
f"Could not find block: {block_id}",
|
||||||
|
status=404,
|
||||||
|
code="object_not_found",
|
||||||
|
body={"message": f"Could not find block: {block_id}"},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"object": "list",
|
||||||
|
"results": blocks,
|
||||||
|
"has_more": False,
|
||||||
|
"next_cursor": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeBlocks(_StrictFakeMixin):
|
||||||
|
_component_name = "notion.blocks"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.children = _FakeBlocksChildren()
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncClient(_StrictFakeMixin):
|
||||||
|
_component_name = "notion.AsyncClient"
|
||||||
|
|
||||||
|
def __init__(self, *, auth: str, **kwargs: Any):
|
||||||
|
del kwargs
|
||||||
|
if auth != _ACCESS_TOKEN:
|
||||||
|
raise ValueError(f"Unexpected fake Notion auth token: {auth!r}")
|
||||||
|
self.auth = auth
|
||||||
|
self.blocks = _FakeBlocks()
|
||||||
|
|
||||||
|
async def search(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
unsupported = set(kwargs) - {"filter", "sort", "start_cursor"}
|
||||||
|
if unsupported:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"E2E Notion fake search got unsupported kwargs: {sorted(unsupported)}"
|
||||||
|
)
|
||||||
|
if kwargs.get("start_cursor") is not None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"E2E Notion fake does not model search cursor={kwargs['start_cursor']!r}."
|
||||||
|
)
|
||||||
|
expected_filter = {"value": "page", "property": "object"}
|
||||||
|
if kwargs.get("filter") != expected_filter:
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"E2E Notion fake search expected filter={expected_filter!r}, "
|
||||||
|
f"got {kwargs.get('filter')!r}."
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"object": "list",
|
||||||
|
"results": _FIXTURE.get("pages", []),
|
||||||
|
"has_more": False,
|
||||||
|
"next_cursor": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def aclose(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeTokenResponse(_StrictFakeMixin):
|
||||||
|
_component_name = "notion.oauth.response"
|
||||||
|
|
||||||
|
def __init__(self, payload: dict[str, Any], status_code: int = 200):
|
||||||
|
self._payload = payload
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = json.dumps(payload, sort_keys=True)
|
||||||
|
|
||||||
|
def json(self) -> dict[str, Any]:
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHttpxAsyncClient(_StrictFakeMixin):
|
||||||
|
_component_name = "httpx.AsyncClient"
|
||||||
|
|
||||||
|
async def __aenter__(self) -> _FakeHttpxAsyncClient:
|
||||||
|
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, **kwargs: Any) -> _FakeTokenResponse:
|
||||||
|
if url != _TOKEN_URL:
|
||||||
|
raise NotImplementedError(f"Unexpected Notion OAuth POST url={url!r}")
|
||||||
|
|
||||||
|
data = kwargs.get("json") or {}
|
||||||
|
headers = kwargs.get("headers") or {}
|
||||||
|
if "Authorization" not in headers:
|
||||||
|
raise ValueError(
|
||||||
|
"Notion OAuth token exchange missing Authorization header."
|
||||||
|
)
|
||||||
|
|
||||||
|
grant_type = data.get("grant_type")
|
||||||
|
if grant_type == "authorization_code":
|
||||||
|
if data.get("code") != "fake-notion-oauth-code":
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected fake Notion OAuth code: {data.get('code')!r}"
|
||||||
|
)
|
||||||
|
elif grant_type == "refresh_token":
|
||||||
|
if data.get("refresh_token") != _REFRESH_TOKEN:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected fake Notion refresh token: {data.get('refresh_token')!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected fake Notion grant_type: {grant_type!r}")
|
||||||
|
|
||||||
|
return _FakeTokenResponse(
|
||||||
|
{
|
||||||
|
"access_token": _ACCESS_TOKEN,
|
||||||
|
"refresh_token": _REFRESH_TOKEN,
|
||||||
|
"expires_in": 3600,
|
||||||
|
"workspace_id": "fake-notion-workspace-001",
|
||||||
|
"workspace_name": "SurfSense E2E Notion Workspace",
|
||||||
|
"workspace_icon": "https://surfsense.example/notion-icon.png",
|
||||||
|
"bot_id": "fake-notion-bot-001",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install(active_patches: list[Any]) -> None:
|
||||||
|
"""Patch production bindings that cannot be covered by sys.modules hijack."""
|
||||||
|
targets = [
|
||||||
|
(
|
||||||
|
"app.routes.notion_add_connector_route.httpx.AsyncClient",
|
||||||
|
_FakeHttpxAsyncClient,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for target, replacement in targets:
|
||||||
|
p = patch(target, replacement)
|
||||||
|
p.start()
|
||||||
|
active_patches.append(p)
|
||||||
|
|
@ -43,8 +43,11 @@ if _BACKEND_ROOT not in sys.path:
|
||||||
sys.path.insert(0, _BACKEND_ROOT)
|
sys.path.insert(0, _BACKEND_ROOT)
|
||||||
|
|
||||||
import tests.e2e.fakes.composio_module as _fake_composio # noqa: E402
|
import tests.e2e.fakes.composio_module as _fake_composio # noqa: E402
|
||||||
|
import tests.e2e.fakes.notion_module as _fake_notion # noqa: E402
|
||||||
|
|
||||||
sys.modules["composio"] = _fake_composio
|
sys.modules["composio"] = _fake_composio
|
||||||
|
sys.modules["notion_client"] = _fake_notion
|
||||||
|
sys.modules["notion_client.errors"] = _fake_notion.errors
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -54,6 +57,12 @@ sys.modules["composio"] = _fake_composio
|
||||||
from dotenv import load_dotenv # noqa: E402
|
from dotenv import load_dotenv # noqa: E402
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
||||||
|
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
||||||
|
os.environ.setdefault(
|
||||||
|
"NOTION_REDIRECT_URI",
|
||||||
|
"http://localhost:8000/api/v1/auth/notion/connector/callback",
|
||||||
|
)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -82,6 +91,7 @@ from app.app import app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
embeddings as _fake_embeddings,
|
embeddings as _fake_embeddings,
|
||||||
native_google as _fake_native_google,
|
native_google as _fake_native_google,
|
||||||
|
notion_module as _fake_notion_module,
|
||||||
)
|
)
|
||||||
from tests.e2e.fakes.chat_llm import ( # noqa: E402
|
from tests.e2e.fakes.chat_llm import ( # noqa: E402
|
||||||
fake_create_chat_litellm_from_agent_config,
|
fake_create_chat_litellm_from_agent_config,
|
||||||
|
|
@ -98,6 +108,7 @@ def _patch_llm_bindings() -> None:
|
||||||
"app.services.llm_service.get_user_long_context_llm",
|
"app.services.llm_service.get_user_long_context_llm",
|
||||||
"app.tasks.connector_indexers.google_drive_indexer.get_user_long_context_llm",
|
"app.tasks.connector_indexers.google_drive_indexer.get_user_long_context_llm",
|
||||||
"app.tasks.connector_indexers.google_gmail_indexer.get_user_long_context_llm",
|
"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.local_folder_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._save.get_user_long_context_llm",
|
||||||
"app.tasks.document_processors.markdown_processor.get_user_long_context_llm",
|
"app.tasks.document_processors.markdown_processor.get_user_long_context_llm",
|
||||||
|
|
@ -150,6 +161,7 @@ def _patch_llm_bindings() -> None:
|
||||||
_patch_llm_bindings()
|
_patch_llm_bindings()
|
||||||
_fake_embeddings.install(_active_patches)
|
_fake_embeddings.install(_active_patches)
|
||||||
_fake_native_google.install(_active_patches)
|
_fake_native_google.install(_active_patches)
|
||||||
|
_fake_notion_module.install(_active_patches)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,11 @@ if _BACKEND_ROOT not in sys.path:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
import tests.e2e.fakes.composio_module as _fake_composio # noqa: E402
|
import tests.e2e.fakes.composio_module as _fake_composio # noqa: E402
|
||||||
|
import tests.e2e.fakes.notion_module as _fake_notion # noqa: E402
|
||||||
|
|
||||||
sys.modules["composio"] = _fake_composio
|
sys.modules["composio"] = _fake_composio
|
||||||
|
sys.modules["notion_client"] = _fake_notion
|
||||||
|
sys.modules["notion_client.errors"] = _fake_notion.errors
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -41,6 +44,12 @@ sys.modules["composio"] = _fake_composio
|
||||||
from dotenv import load_dotenv # noqa: E402
|
from dotenv import load_dotenv # noqa: E402
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
||||||
|
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
||||||
|
os.environ.setdefault(
|
||||||
|
"NOTION_REDIRECT_URI",
|
||||||
|
"http://localhost:8000/api/v1/auth/notion/connector/callback",
|
||||||
|
)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -67,6 +76,7 @@ from app.celery_app import celery_app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
embeddings as _fake_embeddings,
|
embeddings as _fake_embeddings,
|
||||||
native_google as _fake_native_google,
|
native_google as _fake_native_google,
|
||||||
|
notion_module as _fake_notion_module,
|
||||||
)
|
)
|
||||||
from tests.e2e.fakes.chat_llm import ( # noqa: E402
|
from tests.e2e.fakes.chat_llm import ( # noqa: E402
|
||||||
fake_create_chat_litellm_from_agent_config,
|
fake_create_chat_litellm_from_agent_config,
|
||||||
|
|
@ -82,6 +92,7 @@ def _patch_llm_bindings() -> None:
|
||||||
"app.services.llm_service.get_user_long_context_llm",
|
"app.services.llm_service.get_user_long_context_llm",
|
||||||
"app.tasks.connector_indexers.google_drive_indexer.get_user_long_context_llm",
|
"app.tasks.connector_indexers.google_drive_indexer.get_user_long_context_llm",
|
||||||
"app.tasks.connector_indexers.google_gmail_indexer.get_user_long_context_llm",
|
"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.local_folder_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._save.get_user_long_context_llm",
|
||||||
"app.tasks.document_processors.markdown_processor.get_user_long_context_llm",
|
"app.tasks.document_processors.markdown_processor.get_user_long_context_llm",
|
||||||
|
|
@ -134,6 +145,7 @@ def _patch_llm_bindings() -> None:
|
||||||
_patch_llm_bindings()
|
_patch_llm_bindings()
|
||||||
_fake_embeddings.install(_active_patches)
|
_fake_embeddings.install(_active_patches)
|
||||||
_fake_native_google.install(_active_patches)
|
_fake_native_google.install(_active_patches)
|
||||||
|
_fake_notion_module.install(_active_patches)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue