diff --git a/surfsense_web/tests/fixtures/connectors/jira.fixture.ts b/surfsense_web/tests/fixtures/connectors/jira.fixture.ts new file mode 100644 index 000000000..4d1252e2e --- /dev/null +++ b/surfsense_web/tests/fixtures/connectors/jira.fixture.ts @@ -0,0 +1,28 @@ +import { type ConnectorRow, deleteConnector, runJiraOAuth } from "../../helpers/api/connectors"; +import { searchSpaceFixtures } from "../search-space.fixture"; + +export type JiraFixtures = { + /** + * A pre-connected Jira connector inside the fixture's `searchSpace`. + * OAuth and MCP tool calls use E2E Jira fakes and are cleaned up + * automatically after the test. + */ + jiraConnector: ConnectorRow; +}; + +export const jiraFixtures = searchSpaceFixtures.extend({ + jiraConnector: async ({ request, apiToken, searchSpace }, use) => { + const { connector } = await runJiraOAuth(request, apiToken, searchSpace.id); + if (!connector) { + throw new Error( + "jiraConnector fixture: OAuth completed but no JIRA_CONNECTOR was created. " + + "Check the backend log — the Jira MCP 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 dcd4ca327..99235b70d 100644 --- a/surfsense_web/tests/fixtures/index.ts +++ b/surfsense_web/tests/fixtures/index.ts @@ -24,6 +24,8 @@ * └─ notionWithChatTest — chatThread * └─ linearFixtures — linearConnector * └─ linearWithChatTest — chatThread + * └─ jiraFixtures — jiraConnector + * └─ jiraWithChatTest — chatThread * * To add a new connector (Gmail, Slack, manual upload, etc.): * 1. Add a fixture file under `fixtures/connectors/.fixture.ts`. @@ -35,6 +37,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 { jiraFixtures } from "./connectors/jira.fixture"; export { linearFixtures } from "./connectors/linear.fixture"; export { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; export { nativeDriveFixtures } from "./connectors/native-drive.fixture"; @@ -46,6 +49,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 { jiraFixtures } from "./connectors/jira.fixture"; import { linearFixtures } from "./connectors/linear.fixture"; import { nativeCalendarFixtures } from "./connectors/native-calendar.fixture"; import { nativeDriveFixtures } from "./connectors/native-drive.fixture"; @@ -93,3 +97,7 @@ export const notionWithChatTest = notionFixtures.extend(chat export const linearTest = linearFixtures; /** `test` for Linear specs that also need a chat thread. */ export const linearWithChatTest = linearFixtures.extend(chatThreadFixtures); +/** `test` for specs that also need a pre-connected Jira connector. */ +export const jiraTest = jiraFixtures; +/** `test` for Jira specs that also need a chat thread. */ +export const jiraWithChatTest = jiraFixtures.extend(chatThreadFixtures); diff --git a/surfsense_web/tests/helpers/api/connectors.ts b/surfsense_web/tests/helpers/api/connectors.ts index 34c3b5446..2cf5fe225 100644 --- a/surfsense_web/tests/helpers/api/connectors.ts +++ b/surfsense_web/tests/helpers/api/connectors.ts @@ -442,3 +442,53 @@ export async function runLinearOAuth( return { authUrl: auth_url, finalUrl: location, connector }; } + +/** + * Drives the Jira MCP OAuth flow programmatically. + * + * The E2E backend keeps SurfSense's generic MCP OAuth routes real and + * patches Jira's external discovery/DCR/token/MCP tool boundaries. + */ +export async function runJiraOAuth( + request: APIRequestContext, + token: string, + searchSpaceId: number +): Promise<{ + authUrl: string; + finalUrl: string; + connector: ConnectorRow | null; +}> { + const initiateResp = await request.get( + `${BACKEND_URL}/api/v1/auth/mcp/jira/connector/add?space_id=${searchSpaceId}`, + { headers: authHeaders(token) } + ); + if (!initiateResp.ok()) { + throw new Error( + `Jira MCP initiate failed (${initiateResp.status()}): ${await initiateResp.text()}` + ); + } + const { auth_url } = (await initiateResp.json()) as { auth_url: string }; + if (!auth_url) { + throw new Error("Jira MCP initiate response missing auth_url"); + } + + const state = new URL(auth_url).searchParams.get("state"); + if (!state) { + throw new Error(`Jira MCP auth_url missing state: ${auth_url}`); + } + + const callbackResp = await request.get( + `${BACKEND_URL}/api/v1/auth/mcp/jira/connector/callback?code=fake-jira-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 === "JIRA_CONNECTOR") ?? null; + + return { authUrl: auth_url, finalUrl: location, connector }; +}