mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
test(backend): add Confluence OAuth E2E fakes
This commit is contained in:
parent
1bd7cad495
commit
640ae03030
5 changed files with 210 additions and 2 deletions
144
surfsense_backend/tests/e2e/fakes/confluence_oauth.py
Normal file
144
surfsense_backend/tests/e2e/fakes/confluence_oauth.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
"""Strict Confluence OAuth fakes for Playwright E2E."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "confluence_pages.json"
|
||||||
|
|
||||||
|
_TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||||||
|
_RESOURCES_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
|
||||||
|
_ACCESS_TOKEN = "fake-confluence-access-token"
|
||||||
|
_REFRESH_TOKEN = "fake-confluence-refresh-token"
|
||||||
|
_OAUTH_CODE = "fake-confluence-oauth-code"
|
||||||
|
|
||||||
|
|
||||||
|
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 Confluence OAuth fake missing surface: "
|
||||||
|
f"{self._component_name}.{name!r}. "
|
||||||
|
"Add it to surfsense_backend/tests/e2e/fakes/confluence_oauth.py."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeResponse(_StrictFakeMixin):
|
||||||
|
_component_name = "httpx.Response"
|
||||||
|
|
||||||
|
def __init__(self, payload: Any, status_code: int = 200):
|
||||||
|
self._payload = payload
|
||||||
|
self.status_code = status_code
|
||||||
|
self.text = json.dumps(payload, sort_keys=True)
|
||||||
|
|
||||||
|
def json(self) -> Any:
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHttpxAsyncClient(_StrictFakeMixin):
|
||||||
|
_component_name = "httpx.AsyncClient"
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
|
del args, kwargs
|
||||||
|
|
||||||
|
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) -> _FakeResponse:
|
||||||
|
if url != _TOKEN_URL:
|
||||||
|
raise NotImplementedError(f"Unexpected Confluence OAuth POST url={url!r}")
|
||||||
|
|
||||||
|
data = kwargs.get("json") or {}
|
||||||
|
headers = kwargs.get("headers") or {}
|
||||||
|
if headers.get("Content-Type") != "application/json":
|
||||||
|
raise ValueError("Confluence OAuth token exchange expected JSON headers.")
|
||||||
|
|
||||||
|
grant_type = data.get("grant_type")
|
||||||
|
if grant_type == "authorization_code":
|
||||||
|
if data.get("code") != _OAUTH_CODE:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected fake Confluence OAuth code: {data.get('code')!r}"
|
||||||
|
)
|
||||||
|
if not data.get("client_id") or not data.get("client_secret"):
|
||||||
|
raise ValueError("Confluence OAuth token exchange missing client creds.")
|
||||||
|
if "/api/v1/auth/confluence/connector/callback" not in str(
|
||||||
|
data.get("redirect_uri", "")
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Confluence OAuth token exchange got unexpected redirect_uri: "
|
||||||
|
f"{data.get('redirect_uri')!r}"
|
||||||
|
)
|
||||||
|
elif grant_type == "refresh_token":
|
||||||
|
if data.get("refresh_token") != _REFRESH_TOKEN:
|
||||||
|
raise ValueError(
|
||||||
|
"Unexpected fake Confluence refresh token: "
|
||||||
|
f"{data.get('refresh_token')!r}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unexpected fake Confluence grant_type: {grant_type!r}")
|
||||||
|
|
||||||
|
return _FakeResponse(
|
||||||
|
{
|
||||||
|
"access_token": _ACCESS_TOKEN,
|
||||||
|
"refresh_token": _REFRESH_TOKEN,
|
||||||
|
"expires_in": 3600,
|
||||||
|
"scope": "read:confluence-user read:space:confluence read:page:confluence",
|
||||||
|
"token_type": "Bearer",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(self, url: str, **kwargs: Any) -> _FakeResponse:
|
||||||
|
if url != _RESOURCES_URL:
|
||||||
|
raise NotImplementedError(f"Unexpected Confluence OAuth GET url={url!r}")
|
||||||
|
|
||||||
|
headers = kwargs.get("headers") or {}
|
||||||
|
auth = headers.get("Authorization")
|
||||||
|
if auth != f"Bearer {_ACCESS_TOKEN}":
|
||||||
|
raise ValueError(f"Unexpected Confluence resources Authorization: {auth!r}")
|
||||||
|
|
||||||
|
site = _FIXTURE["site"]
|
||||||
|
return _FakeResponse(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": site["cloud_id"],
|
||||||
|
"name": site["name"],
|
||||||
|
"url": site["url"],
|
||||||
|
"scopes": [
|
||||||
|
"read:confluence-user",
|
||||||
|
"read:space:confluence",
|
||||||
|
"read:page:confluence",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHttpxModule(_StrictFakeMixin):
|
||||||
|
_component_name = "httpx"
|
||||||
|
|
||||||
|
AsyncClient = _FakeHttpxAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
def install(active_patches: list[Any]) -> None:
|
||||||
|
"""Patch only Confluence route-local HTTP OAuth calls."""
|
||||||
|
p = patch(
|
||||||
|
"app.routes.confluence_add_connector_route.httpx",
|
||||||
|
_FakeHttpxModule(),
|
||||||
|
)
|
||||||
|
p.start()
|
||||||
|
active_patches.append(p)
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"site": {
|
||||||
|
"cloud_id": "fake-confluence-cloud-001",
|
||||||
|
"name": "SurfSense E2E Confluence",
|
||||||
|
"url": "https://surfsense-e2e-confluence.atlassian.net"
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"id": "fake-confluence-page-canary-001",
|
||||||
|
"title": "E2E Canary Confluence Page",
|
||||||
|
"spaceId": "fake-confluence-space-001",
|
||||||
|
"body": {
|
||||||
|
"storage": {
|
||||||
|
"value": "<h1>E2E Canary Confluence Page</h1><p>This page proves Confluence OAuth indexing works end-to-end. SURFSENSE_E2E_CANARY_TOKEN_CONFLUENCE_001</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": "fake-confluence-comment-canary-001",
|
||||||
|
"body": {
|
||||||
|
"storage": {
|
||||||
|
"value": "<p>Confluence comment content is included in indexed markdown.</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"authorId": "fake-confluence-user-001",
|
||||||
|
"createdAt": "2026-05-08T00:00:00.000Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -139,6 +139,9 @@ class _FakeTokenResponse(_StrictFakeMixin):
|
||||||
class _FakeHttpxAsyncClient(_StrictFakeMixin):
|
class _FakeHttpxAsyncClient(_StrictFakeMixin):
|
||||||
_component_name = "httpx.AsyncClient"
|
_component_name = "httpx.AsyncClient"
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
|
del args, kwargs
|
||||||
|
|
||||||
async def __aenter__(self) -> _FakeHttpxAsyncClient:
|
async def __aenter__(self) -> _FakeHttpxAsyncClient:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
@ -183,12 +186,18 @@ class _FakeHttpxAsyncClient(_StrictFakeMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHttpxModule(_StrictFakeMixin):
|
||||||
|
_component_name = "httpx"
|
||||||
|
|
||||||
|
AsyncClient = _FakeHttpxAsyncClient
|
||||||
|
|
||||||
|
|
||||||
def install(active_patches: list[Any]) -> None:
|
def install(active_patches: list[Any]) -> None:
|
||||||
"""Patch production bindings that cannot be covered by sys.modules hijack."""
|
"""Patch production bindings that cannot be covered by sys.modules hijack."""
|
||||||
targets = [
|
targets = [
|
||||||
(
|
(
|
||||||
"app.routes.notion_add_connector_route.httpx.AsyncClient",
|
"app.routes.notion_add_connector_route.httpx",
|
||||||
_FakeHttpxAsyncClient,
|
_FakeHttpxModule(),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
for target, replacement in targets:
|
for target, replacement in targets:
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ sys.modules["notion_client.errors"] = _fake_notion.errors
|
||||||
from dotenv import load_dotenv # noqa: E402
|
from dotenv import load_dotenv # noqa: E402
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
os.environ.setdefault("ATLASSIAN_CLIENT_ID", "fake-atlassian-client-id")
|
||||||
|
os.environ.setdefault("ATLASSIAN_CLIENT_SECRET", "fake-atlassian-client-secret")
|
||||||
|
os.environ.setdefault(
|
||||||
|
"CONFLUENCE_REDIRECT_URI",
|
||||||
|
"http://localhost:8000/api/v1/auth/confluence/connector/callback",
|
||||||
|
)
|
||||||
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
||||||
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
|
|
@ -89,6 +95,8 @@ from unittest.mock import patch # noqa: E402
|
||||||
|
|
||||||
from app.app import app # noqa: E402
|
from app.app import app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
|
confluence_indexer as _fake_confluence_indexer,
|
||||||
|
confluence_oauth as _fake_confluence_oauth,
|
||||||
embeddings as _fake_embeddings,
|
embeddings as _fake_embeddings,
|
||||||
jira_module as _fake_jira_module,
|
jira_module as _fake_jira_module,
|
||||||
linear_module as _fake_linear_module,
|
linear_module as _fake_linear_module,
|
||||||
|
|
@ -110,6 +118,7 @@ def _patch_llm_bindings() -> None:
|
||||||
"""Replace LLM factories at every known binding site."""
|
"""Replace LLM factories at every known binding site."""
|
||||||
targets = [
|
targets = [
|
||||||
"app.services.llm_service.get_user_long_context_llm",
|
"app.services.llm_service.get_user_long_context_llm",
|
||||||
|
"app.tasks.connector_indexers.confluence_indexer.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.notion_indexer.get_user_long_context_llm",
|
||||||
|
|
@ -164,6 +173,8 @@ def _patch_llm_bindings() -> None:
|
||||||
|
|
||||||
_patch_llm_bindings()
|
_patch_llm_bindings()
|
||||||
_fake_embeddings.install(_active_patches)
|
_fake_embeddings.install(_active_patches)
|
||||||
|
_fake_confluence_oauth.install(_active_patches)
|
||||||
|
_fake_confluence_indexer.install(_active_patches)
|
||||||
_fake_native_google.install(_active_patches)
|
_fake_native_google.install(_active_patches)
|
||||||
_fake_notion_module.install(_active_patches)
|
_fake_notion_module.install(_active_patches)
|
||||||
_fake_linear_module.install(_active_patches)
|
_fake_linear_module.install(_active_patches)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,12 @@ sys.modules["notion_client.errors"] = _fake_notion.errors
|
||||||
from dotenv import load_dotenv # noqa: E402
|
from dotenv import load_dotenv # noqa: E402
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
os.environ.setdefault("ATLASSIAN_CLIENT_ID", "fake-atlassian-client-id")
|
||||||
|
os.environ.setdefault("ATLASSIAN_CLIENT_SECRET", "fake-atlassian-client-secret")
|
||||||
|
os.environ.setdefault(
|
||||||
|
"CONFLUENCE_REDIRECT_URI",
|
||||||
|
"http://localhost:8000/api/v1/auth/confluence/connector/callback",
|
||||||
|
)
|
||||||
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
os.environ.setdefault("NOTION_CLIENT_ID", "fake-notion-client-id")
|
||||||
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
os.environ.setdefault("NOTION_CLIENT_SECRET", "fake-notion-client-secret")
|
||||||
os.environ.setdefault(
|
os.environ.setdefault(
|
||||||
|
|
@ -74,6 +80,8 @@ from unittest.mock import patch # noqa: E402
|
||||||
|
|
||||||
from app.celery_app import celery_app # noqa: E402
|
from app.celery_app import celery_app # noqa: E402
|
||||||
from tests.e2e.fakes import ( # noqa: E402
|
from tests.e2e.fakes import ( # noqa: E402
|
||||||
|
confluence_indexer as _fake_confluence_indexer,
|
||||||
|
confluence_oauth as _fake_confluence_oauth,
|
||||||
embeddings as _fake_embeddings,
|
embeddings as _fake_embeddings,
|
||||||
jira_module as _fake_jira_module,
|
jira_module as _fake_jira_module,
|
||||||
linear_module as _fake_linear_module,
|
linear_module as _fake_linear_module,
|
||||||
|
|
@ -94,6 +102,7 @@ _active_patches: list = []
|
||||||
def _patch_llm_bindings() -> None:
|
def _patch_llm_bindings() -> None:
|
||||||
targets = [
|
targets = [
|
||||||
"app.services.llm_service.get_user_long_context_llm",
|
"app.services.llm_service.get_user_long_context_llm",
|
||||||
|
"app.tasks.connector_indexers.confluence_indexer.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.notion_indexer.get_user_long_context_llm",
|
||||||
|
|
@ -148,6 +157,8 @@ def _patch_llm_bindings() -> None:
|
||||||
|
|
||||||
_patch_llm_bindings()
|
_patch_llm_bindings()
|
||||||
_fake_embeddings.install(_active_patches)
|
_fake_embeddings.install(_active_patches)
|
||||||
|
_fake_confluence_oauth.install(_active_patches)
|
||||||
|
_fake_confluence_indexer.install(_active_patches)
|
||||||
_fake_native_google.install(_active_patches)
|
_fake_native_google.install(_active_patches)
|
||||||
_fake_notion_module.install(_active_patches)
|
_fake_notion_module.install(_active_patches)
|
||||||
_fake_linear_module.install(_active_patches)
|
_fake_linear_module.install(_active_patches)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue