diff --git a/surfsense_web/tests/fixtures/connectors/notion.fixture.ts b/surfsense_web/tests/fixtures/connectors/notion.fixture.ts new file mode 100644 index 000000000..d0032663c --- /dev/null +++ b/surfsense_web/tests/fixtures/connectors/notion.fixture.ts @@ -0,0 +1,27 @@ +import { type ConnectorRow, deleteConnector, runNotionOAuth } from "../../helpers/api/connectors"; +import { searchSpaceFixtures } from "../search-space.fixture"; + +export type NotionFixtures = { + /** + * A pre-connected Notion connector inside the fixture's `searchSpace`. + * OAuth uses E2E Notion fakes and is cleaned up automatically after the test. + */ + notionConnector: ConnectorRow; +}; + +export const notionFixtures = searchSpaceFixtures.extend({ + notionConnector: async ({ request, apiToken, searchSpace }, use) => { + const { connector } = await runNotionOAuth(request, apiToken, searchSpace.id); + if (!connector) { + throw new Error( + "notionConnector fixture: OAuth completed but no NOTION_CONNECTOR was created. " + + "Check the backend log — the Notion fake likely rejected an unmodelled call." + ); + } + try { + await use(connector); + } finally { + await deleteConnector(request, apiToken, connector.id); + } + }, +}); diff --git a/surfsense_web/tests/fixtures/index.ts b/surfsense_web/tests/fixtures/index.ts index 1c88701fc..9f41adb73 100644 --- a/surfsense_web/tests/fixtures/index.ts +++ b/surfsense_web/tests/fixtures/index.ts @@ -20,6 +20,8 @@ * └─ nativeGmailWithChatTest — chatThread * └─ nativeCalendarFixtures — nativeCalendarConnector * └─ nativeCalendarWithChatTest — chatThread + * └─ notionFixtures — notionConnector + * └─ notionWithChatTest — chatThread * * To add a new connector (Gmail, Slack, manual upload, etc.): * 1. Add a fixture file under `fixtures/connectors/.fixture.ts`. @@ -34,6 +36,7 @@ export { composioGmailFixtures } from "./connectors/composio-gmail.fixture"; export { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; export { nativeDriveFixtures } from "./connectors/native-drive.fixture"; export { nativeGmailFixtures } from "./connectors/native-gmail.fixture"; +export { notionFixtures } from "./connectors/notion.fixture"; export { searchSpaceFixtures } from "./search-space.fixture"; import { type ChatThreadFixtures, chatThreadFixtures } from "./chat-thread.fixture"; @@ -43,6 +46,7 @@ import { composioGmailFixtures } from "./connectors/composio-gmail.fixture"; import { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; import { nativeDriveFixtures } from "./connectors/native-drive.fixture"; import { nativeGmailFixtures } from "./connectors/native-gmail.fixture"; +import { notionFixtures } from "./connectors/notion.fixture"; import { searchSpaceFixtures } from "./search-space.fixture"; /** Default `test` for specs that just need auth + a clean search space. */ @@ -77,3 +81,7 @@ export const nativeCalendarTest = nativeCalendarFixtures; /** `test` for native Calendar specs that also need a chat thread. */ export const nativeCalendarWithChatTest = nativeCalendarFixtures.extend(chatThreadFixtures); +/** `test` for specs that also need a pre-connected Notion connector. */ +export const notionTest = notionFixtures; +/** `test` for Notion specs that also need a chat thread. */ +export const notionWithChatTest = notionFixtures.extend(chatThreadFixtures); diff --git a/surfsense_web/tests/helpers/api/connectors.ts b/surfsense_web/tests/helpers/api/connectors.ts index 99c27ebf2..b650def57 100644 --- a/surfsense_web/tests/helpers/api/connectors.ts +++ b/surfsense_web/tests/helpers/api/connectors.ts @@ -340,3 +340,55 @@ export async function runNativeGoogleCalendarOAuth( return { authUrl: auth_url, finalUrl: location, connector }; } + +/** + * Drives the Notion OAuth flow programmatically. + * + * The E2E backend keeps SurfSense's OAuth add/callback routes real and + * patches only Notion's external token endpoint. Notion's authorization + * URL stays off-origin, so this helper extracts the signed state and calls + * the backend callback directly with the deterministic fake code. + */ +export async function runNotionOAuth( + request: APIRequestContext, + token: string, + searchSpaceId: number +): Promise<{ + authUrl: string; + finalUrl: string; + connector: ConnectorRow | null; +}> { + const initiateResp = await request.get( + `${BACKEND_URL}/api/v1/auth/notion/connector/add?space_id=${searchSpaceId}`, + { headers: authHeaders(token) } + ); + if (!initiateResp.ok()) { + throw new Error( + `Notion initiate failed (${initiateResp.status()}): ${await initiateResp.text()}` + ); + } + const { auth_url } = (await initiateResp.json()) as { auth_url: string }; + if (!auth_url) { + throw new Error("Notion initiate response missing auth_url"); + } + + const state = new URL(auth_url).searchParams.get("state"); + if (!state) { + throw new Error(`Notion auth_url missing state: ${auth_url}`); + } + + const callbackResp = await request.get( + `${BACKEND_URL}/api/v1/auth/notion/connector/callback?code=fake-notion-oauth-code&state=${encodeURIComponent(state)}`, + { + headers: authHeaders(token), + maxRedirects: 0, + failOnStatusCode: false, + } + ); + const location = callbackResp.headers().location ?? auth_url; + + const connectors = await listConnectors(request, token, searchSpaceId); + const connector = connectors.find((c) => c.connector_type === "NOTION_CONNECTOR") ?? null; + + return { authUrl: auth_url, finalUrl: location, connector }; +}