Files
kandev/apps/web/e2e/tests/integrations/jira-settings.spec.ts
T

286 lines
11 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { JiraSettingsPage } from "../../pages/jira-settings-page";
import { assertJiraWatcherQualifiedBranchPersists } from "./jira-watcher-branch-flow";
test.describe("Jira settings", () => {
test("qualified remote branch persists through Jira watcher reload", async ({
testPage,
apiClient,
seedData,
prCapture,
}) => {
await assertJiraWatcherQualifiedBranchPersists({
page: testPage,
apiClient,
seedData,
inputMode: "mouse",
prCapture,
});
});
test("empty workspace shows form with disabled save/test until secret is filled", async ({
testPage,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await expect(settings.siteInput).toHaveValue("");
await expect(settings.secretInput).toHaveValue("");
await expect(settings.statusBanner).toHaveCount(0);
await expect(settings.saveButton).toHaveCount(0);
await expect(settings.testButton).toBeDisabled();
await settings.siteInput.fill("https://acme.atlassian.net");
await settings.emailInput.fill("alice@example.com");
await expect(settings.saveButton).toBeDisabled();
await settings.secretInput.fill("api-token-value");
await expect(settings.saveButton).toBeEnabled();
await expect(settings.testButton).toBeEnabled();
});
test("saving the config persists across reload and shows the auth banner", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await settings.fillForm({
siteUrl: "https://acme.atlassian.net",
email: "alice@example.com",
secret: "api-token-value",
projectKey: "PROJ",
});
await settings.saveButton.click();
await expect(settings.saveButton).toHaveCount(0);
// The post-save probe runs async; await it before reloading so the new
// banner state is in the DB by the time the page re-fetches the config.
await apiClient.waitForIntegrationAuthHealthy("jira", {
workspaceId: seedData.workspaceId,
timeoutMs: 60_000,
});
await testPage.reload();
await settings.siteInput.waitFor();
await expect(settings.siteInput).toHaveValue("https://acme.atlassian.net");
await expect(settings.emailInput).toHaveValue("alice@example.com");
await expect(settings.projectInput).toHaveValue("PROJ");
await expect(settings.statusBanner).toHaveAttribute("data-state", "ok");
});
test("test connection surfaces inline success and failure", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await apiClient.mockJiraSetAuthResult({
ok: true,
displayName: "Alice from Jira",
email: "alice@example.com",
});
await settings.fillForm({
siteUrl: "https://acme.atlassian.net",
email: "alice@example.com",
secret: "api-token-value",
});
await settings.testButton.click();
await expect(testPage.getByText(/Connected as Alice from Jira/i)).toBeVisible();
await apiClient.mockJiraSetAuthResult({ ok: false, error: "401 Unauthorized" });
await settings.testButton.click();
await expect(testPage.getByText(/Failed: 401 Unauthorized/)).toBeVisible();
});
test("seeded auth-health failure renders the failed banner on load", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
// Save first so a config row exists, then simulate the poller writing
// a failure status onto it.
await settings.gotoWorkspace(seedData.workspaceId);
await settings.fillForm({
siteUrl: "https://acme.atlassian.net",
email: "alice@example.com",
secret: "api-token-value",
});
await settings.saveButton.click();
// Wait for the post-save probe to land BEFORE forcing the failure: the
// probe goroutine could otherwise overwrite our forced lastOk=false back
// to true a few ms after the mockJiraSetAuthHealth call, flipping the
// banner to "ok" right when the assertion expects "failed".
await apiClient.waitForIntegrationAuthHealthy("jira", { workspaceId: seedData.workspaceId });
await apiClient.mockJiraSetAuthHealth({
workspaceId: seedData.workspaceId,
ok: false,
error: "session expired",
});
await testPage.reload();
await settings.statusBanner.waitFor();
await expect(settings.statusBanner).toHaveAttribute("data-state", "failed");
await expect(settings.statusBanner).toContainText(/session expired/i);
});
test("delete clears the saved configuration", async ({ testPage, seedData }) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await settings.fillForm({
siteUrl: "https://acme.atlassian.net",
email: "alice@example.com",
secret: "api-token-value",
});
await settings.saveButton.click();
await expect(settings.deleteButton).toBeVisible();
await settings.deleteButton.click();
await testPage.getByTestId("jira-remove-confirm").click();
await expect(settings.deleteButton).toHaveCount(0);
await expect(settings.siteInput).toHaveValue("");
await expect(settings.secretInput).toHaveValue("");
await expect(settings.statusBanner).toHaveCount(0);
});
test("server / data center save flow with PAT persists across reload", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await settings.selectInstance("server");
// Switching to Server auto-selects PAT — the dropdown should now show it
// as the (only) option, and the email input is no longer rendered.
await expect(settings.authSelect).toContainText(/Personal Access Token/i);
await expect(settings.emailInput).toHaveCount(0);
await settings.siteInput.fill("https://jira.acme.com");
await settings.secretInput.fill("pat-token-value");
await expect(settings.saveButton).toBeEnabled();
await settings.saveButton.click();
await expect(settings.saveButton).toHaveCount(0);
await apiClient.waitForIntegrationAuthHealthy("jira", { workspaceId: seedData.workspaceId });
await testPage.reload();
await settings.siteInput.waitFor();
await expect(settings.siteInput).toHaveValue("https://jira.acme.com");
await expect(settings.instanceSelect).toContainText(/Server \/ Data Center/i);
await expect(settings.authSelect).toContainText(/Personal Access Token/i);
await expect(settings.emailInput).toHaveCount(0);
await expect(settings.statusBanner).toHaveAttribute("data-state", "ok");
});
test("switching instance type swaps the auth-method options", async ({ testPage, seedData }) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
// Cloud default: api_token + session_cookie options, no PAT.
await expect(settings.authSelect).toContainText(/API token/i);
await settings.authSelect.click();
await expect(testPage.getByRole("option", { name: /API token/i })).toBeVisible();
await expect(testPage.getByRole("option", { name: /session cookie/i })).toBeVisible();
await expect(testPage.getByRole("option", { name: /Personal Access Token/i })).toHaveCount(0);
// Dismiss the listbox before opening another select.
await testPage.keyboard.press("Escape");
await settings.selectInstance("server");
await expect(settings.authSelect).toContainText(/Personal Access Token/i);
await settings.authSelect.click();
await expect(testPage.getByRole("option", { name: /Personal Access Token/i })).toBeVisible();
await expect(testPage.getByRole("option", { name: /API token/i })).toHaveCount(0);
await expect(testPage.getByRole("option", { name: /session cookie/i })).toHaveCount(0);
await testPage.keyboard.press("Escape");
// Round-trip back to Cloud restores the canonical default.
await settings.selectInstance("cloud");
await expect(settings.authSelect).toContainText(/API token/i);
});
test("email field hides for session cookie and PAT, returns for cloud + api_token", async ({
testPage,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
// Cloud + api_token (default) shows email.
await expect(settings.emailInput).toBeVisible();
// Cloud + session_cookie hides email — the secret is the cookie itself.
await settings.selectAuth("Browser session cookie");
await expect(settings.emailInput).toHaveCount(0);
// Server + PAT also hides email.
await settings.selectInstance("server");
await expect(settings.emailInput).toHaveCount(0);
// Back to Cloud + api_token brings email back.
await settings.selectInstance("cloud");
await settings.selectAuth("API token (recommended)");
await expect(settings.emailInput).toBeVisible();
});
test("saved secret is not reused after switching identity fields", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await settings.fillForm({
siteUrl: "https://acme.atlassian.net",
email: "alice@example.com",
secret: "api-token-value",
});
await settings.saveButton.click();
await expect(settings.saveButton).toHaveCount(0);
await apiClient.waitForIntegrationAuthHealthy("jira", { workspaceId: seedData.workspaceId });
await testPage.reload();
await settings.siteInput.waitFor();
// Saved secret reuse: placeholder shows the masked dots. A clean form has
// no route-level save action until the user changes an identity field.
await expect(settings.secretInput).toHaveAttribute("placeholder", /•/);
await expect(settings.saveButton).toHaveCount(0);
// Change the site URL — the saved secret no longer applies to this host.
await settings.siteInput.fill("https://other.atlassian.net");
await expect(settings.secretInput).toHaveAttribute("placeholder", /paste/i);
await expect(settings.saveButton).toBeDisabled();
// Restoring the host re-enables reuse without re-typing the token.
await settings.siteInput.fill("https://acme.atlassian.net");
await expect(settings.secretInput).toHaveAttribute("placeholder", /•/);
await expect(settings.saveButton).toHaveCount(0);
// Switching instance type also invalidates reuse.
await settings.selectInstance("server");
await expect(settings.secretInput).toHaveAttribute("placeholder", /paste/i);
await expect(settings.saveButton).toBeDisabled();
});
test("PAT help link targets the configured site", async ({ testPage, seedData }) => {
const settings = new JiraSettingsPage(testPage);
await settings.gotoWorkspace(seedData.workspaceId);
await settings.selectInstance("server");
await settings.siteInput.fill("https://jira.acme.com/");
// patHref strips trailing slashes, so the link should point at the bare
// host + /secure/ViewProfile.jspa.
const helpLink = testPage.getByRole("link", {
name: "https://jira.acme.com/secure/ViewProfile.jspa",
});
await expect(helpLink).toBeVisible();
await expect(helpLink).toHaveAttribute("href", "https://jira.acme.com/secure/ViewProfile.jspa");
});
});