diff --git a/surfsense_web/tests/fixtures/connectors/confluence.fixture.ts b/surfsense_web/tests/fixtures/connectors/confluence.fixture.ts new file mode 100644 index 000000000..bea950279 --- /dev/null +++ b/surfsense_web/tests/fixtures/connectors/confluence.fixture.ts @@ -0,0 +1,32 @@ +import { + type ConnectorRow, + deleteConnector, + runConfluenceOAuth, +} from "../../helpers/api/connectors"; +import { searchSpaceFixtures } from "../search-space.fixture"; + +export type ConfluenceFixtures = { + /** + * A pre-connected Confluence connector inside the fixture's `searchSpace`. + * OAuth uses E2E Atlassian fakes and is cleaned up automatically after + * the test. + */ + confluenceConnector: ConnectorRow; +}; + +export const confluenceFixtures = searchSpaceFixtures.extend({ + confluenceConnector: async ({ request, apiToken, searchSpace }, use) => { + const { connector } = await runConfluenceOAuth(request, apiToken, searchSpace.id); + if (!connector) { + throw new Error( + "confluenceConnector fixture: OAuth completed but no CONFLUENCE_CONNECTOR was created. " + + "Check the backend log — the Confluence OAuth 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 99235b70d..f63ee058a 100644 --- a/surfsense_web/tests/fixtures/index.ts +++ b/surfsense_web/tests/fixtures/index.ts @@ -22,6 +22,8 @@ * └─ nativeCalendarWithChatTest — chatThread * └─ notionFixtures — notionConnector * └─ notionWithChatTest — chatThread + * └─ confluenceFixtures — confluenceConnector + * └─ confluenceWithChatTest — chatThread * └─ linearFixtures — linearConnector * └─ linearWithChatTest — chatThread * └─ jiraFixtures — jiraConnector @@ -37,6 +39,7 @@ export { chatThreadFixtures } from "./chat-thread.fixture"; export { composioCalendarFixtures } from "./connectors/composio-calendar.fixture"; export { composioDriveFixtures } from "./connectors/composio-drive.fixture"; export { composioGmailFixtures } from "./connectors/composio-gmail.fixture"; +export { confluenceFixtures } from "./connectors/confluence.fixture"; export { jiraFixtures } from "./connectors/jira.fixture"; export { linearFixtures } from "./connectors/linear.fixture"; export { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; @@ -49,6 +52,7 @@ import { type ChatThreadFixtures, chatThreadFixtures } from "./chat-thread.fixtu import { composioCalendarFixtures } from "./connectors/composio-calendar.fixture"; import { composioDriveFixtures } from "./connectors/composio-drive.fixture"; import { composioGmailFixtures } from "./connectors/composio-gmail.fixture"; +import { confluenceFixtures } from "./connectors/confluence.fixture"; import { jiraFixtures } from "./connectors/jira.fixture"; import { linearFixtures } from "./connectors/linear.fixture"; import { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; @@ -93,6 +97,11 @@ export const nativeCalendarWithChatTest = export const notionTest = notionFixtures; /** `test` for Notion specs that also need a chat thread. */ export const notionWithChatTest = notionFixtures.extend(chatThreadFixtures); +/** `test` for specs that also need a pre-connected Confluence connector. */ +export const confluenceTest = confluenceFixtures; +/** `test` for Confluence specs that also need a chat thread. */ +export const confluenceWithChatTest = + confluenceFixtures.extend(chatThreadFixtures); /** `test` for specs that also need a pre-connected Linear connector. */ export const linearTest = linearFixtures; /** `test` for Linear specs that also need a chat thread. */ diff --git a/surfsense_web/tests/helpers/api/connectors.ts b/surfsense_web/tests/helpers/api/connectors.ts index 2cf5fe225..20e4414a9 100644 --- a/surfsense_web/tests/helpers/api/connectors.ts +++ b/surfsense_web/tests/helpers/api/connectors.ts @@ -393,6 +393,56 @@ export async function runNotionOAuth( return { authUrl: auth_url, finalUrl: location, connector }; } +/** + * Drives the Confluence OAuth flow programmatically. + * + * The E2E backend keeps SurfSense's OAuth add/callback routes real and + * patches only Atlassian's external token/resource endpoints. + */ +export async function runConfluenceOAuth( + request: APIRequestContext, + token: string, + searchSpaceId: number +): Promise<{ + authUrl: string; + finalUrl: string; + connector: ConnectorRow | null; +}> { + const initiateResp = await request.get( + `${BACKEND_URL}/api/v1/auth/confluence/connector/add?space_id=${searchSpaceId}`, + { headers: authHeaders(token) } + ); + if (!initiateResp.ok()) { + throw new Error( + `Confluence initiate failed (${initiateResp.status()}): ${await initiateResp.text()}` + ); + } + const { auth_url } = (await initiateResp.json()) as { auth_url: string }; + if (!auth_url) { + throw new Error("Confluence initiate response missing auth_url"); + } + + const state = new URL(auth_url).searchParams.get("state"); + if (!state) { + throw new Error(`Confluence auth_url missing state: ${auth_url}`); + } + + const callbackResp = await request.get( + `${BACKEND_URL}/api/v1/auth/confluence/connector/callback?code=fake-confluence-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 === "CONFLUENCE_CONNECTOR") ?? null; + + return { authUrl: auth_url, finalUrl: location, connector }; +} + /** * Drives the Linear MCP OAuth flow programmatically. *