Files
kandev/apps/web/e2e/tests/settings/github-app-registration.spec.ts
T
2026-08-12 11:13:05 +01:00

308 lines
13 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { GitHubAuthSettingsPage } from "../../pages/github-auth-settings-page";
const appConnection = (registrationId: string, account: string, installationId: number) => ({
source: "github_app_installation" as const,
status: "active" as const,
app_registration_id: registrationId,
installation_id: installationId,
installation_account_login: account,
installation_account_type: "Organization",
capabilities: { repository_read: true, pull_request_write: true },
});
test.describe("Workspace GitHub App onboarding", () => {
test.beforeEach(async ({ apiClient }) => {
await apiClient.mockGitHubReset();
});
test("selects reusable Apps and explains cross-workspace sharing before installation", async ({
testPage,
apiClient,
seedData,
}) => {
const otherWorkspace = await apiClient.createWorkspace("Shared App Workspace");
await apiClient.mockGitHubSetAppRegistration({
id: "registration-shared",
display_name: "Company automation",
app_id: 101,
});
await apiClient.mockGitHubSetAppRegistration({
id: "registration-personal",
display_name: "Personal projects",
app_id: 202,
});
await apiClient.mockGitHubSetWorkspaceConnection(
seedData.workspaceId,
appConnection("registration-shared", "acme", 41),
);
await apiClient.mockGitHubSetWorkspaceConnection(
otherWorkspace.id,
appConnection("registration-shared", "acme-sandbox", 42),
);
const settings = new GitHubAuthSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await expect(
settings.automation().getByText("Company automation", { exact: true }),
).toBeVisible({
timeout: 15_000,
});
await expect(settings.automation().getByText(/shared by 2 workspaces/i)).toBeVisible();
const connection = await settings.openConnection();
await settings.chooseMethod("GitHub App");
await expect(connection.getByText("Company automation", { exact: true })).toBeVisible();
await expect(connection.getByText("Personal projects", { exact: true })).toBeVisible();
await expect(connection.getByText(/Used by 2 workspaces/)).toBeVisible();
await expect(
connection.getByText(/installation access remains workspace-specific/i),
).toHaveCount(0);
await settings.chooseApp("registration-personal");
let installBody: Record<string, unknown> | undefined;
await testPage.route("**/api/v1/github/app/install/start", async (route) => {
installBody = route.request().postDataJSON() as Record<string, unknown>;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
url: "https://github.com/apps/mock-registration-personal/installations/new",
}),
});
});
await testPage.route(
"https://github.com/apps/mock-registration-personal/installations/new",
(route) =>
route.fulfill({
status: 200,
contentType: "text/html",
body: "<main><h1>Install Personal projects</h1></main>",
}),
);
await connection.getByTestId("github-app-install-button").click();
await expect(
testPage.getByRole("heading", { name: "Install Personal projects" }),
).toBeVisible();
expect(installBody).toEqual({
workspace_id: seedData.workspaceId,
app_registration_id: "registration-personal",
});
});
test("guides manifest creation and returns to the initiating workspace callback", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new GitHubAuthSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
const connection = await settings.openConnection();
await settings.chooseMethod("GitHub App");
await connection.getByRole("button", { name: "Create new App" }).click();
await connection.getByLabel("Name in Kandev").fill("Work automation");
await connection.getByLabel("GitHub owner login").fill("acme");
await connection.getByLabel("Public Kandev URL").fill("https://1.1.1.1");
await connection.getByRole("button", { name: "Review permissions" }).click();
const policy = testPage.getByRole("dialog", { name: "Required GitHub App policy" });
await expect(policy.getByText("Contents", { exact: true })).toBeVisible();
await expect(policy.getByText("Workflows", { exact: true })).toBeVisible();
await expect(policy.getByText("Push", { exact: true })).toBeVisible();
await expect(policy.getByText("Check Run", { exact: true })).toBeVisible();
await expect(policy.getByText("Installation", { exact: true })).toHaveCount(0);
await policy.getByRole("button", { name: "Close" }).click();
let manifest: Record<string, unknown> | undefined;
let registrationState: string | null = null;
await testPage.route(
"https://github.com/organizations/acme/settings/apps/new?**",
async (route) => {
registrationState = new URL(route.request().url()).searchParams.get("state");
const fields = new URLSearchParams(route.request().postData() ?? "");
manifest = JSON.parse(fields.get("manifest") ?? "{}") as Record<string, unknown>;
await route.fulfill({
status: 200,
contentType: "text/html",
body: "<main><h1>Confirm Work automation on GitHub</h1></main>",
});
},
);
await connection.getByRole("button", { name: "Prepare App on GitHub" }).click();
await expect(connection.getByText("GitHub is ready to create the App")).toBeVisible();
await connection.getByRole("button", { name: "Continue on GitHub" }).click();
await expect(
testPage.getByRole("heading", { name: "Confirm Work automation on GitHub" }),
).toBeVisible();
expect(registrationState).toBeTruthy();
const redirectUrl = String(manifest?.redirect_url ?? "");
expect(new URL(redirectUrl).search).toBe("");
const registrationId = redirectUrl.match(
/\/app\/registrations\/([^/]+)\/manifest\/callback/,
)?.[1];
expect(registrationId).toBeTruthy();
expect(manifest).toMatchObject({
url: "https://1.1.1.1",
public: false,
default_events: ["push", "check_run"],
hook_attributes: {
url: `https://1.1.1.1/api/v1/github/app/registrations/${registrationId}/webhook`,
active: true,
},
callback_urls: [
`https://1.1.1.1/api/v1/github/app/registrations/${registrationId}/install/callback`,
`https://1.1.1.1/api/v1/github/app/registrations/${registrationId}/personal/callback`,
],
});
expect(manifest).not.toHaveProperty("setup_url");
await apiClient.mockGitHubSetAppRegistration({
id: registrationId!,
display_name: "Work automation",
app_id: 303,
});
await settings.goto(
seedData.workspaceId,
`?github_result=app_registered&workspace_id=${encodeURIComponent(seedData.workspaceId)}`,
);
const notice = testPage.getByTestId("github-callback-notice");
await expect(notice).toContainText("GitHub App added");
await expect(notice).toContainText("ready to select and install for this workspace");
await expect(settings.automation().getByText("No automation connection")).toBeVisible();
});
test("prepares exact existing-App instructions and imports the reserved registration", async ({
testPage,
apiClient,
seedData,
}) => {
const settings = new GitHubAuthSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
const connection = await settings.openConnection();
await settings.chooseMethod("GitHub App");
await connection.getByRole("button", { name: "Add existing App" }).click();
await connection.getByLabel("Public Kandev URL").fill("https://1.1.1.1");
const prepareResponse = testPage.waitForResponse(
(response) =>
response.url().endsWith("/api/v1/github/app/registrations/import/prepare") &&
response.request().method() === "POST",
);
await connection.getByRole("button", { name: "Generate setup instructions" }).click();
const preparation = (await (await prepareResponse).json()) as {
registration_id: string;
personal_callback_url: string;
install_callback_url: string;
webhook_url: string;
};
await expect(
connection.getByText(preparation.personal_callback_url, { exact: true }),
).toBeVisible();
await expect(
connection.getByText(preparation.install_callback_url, { exact: true }),
).toBeVisible();
await expect(connection.getByText("User authorization callback URL 1")).toBeVisible();
await expect(connection.getByText("User authorization callback URL 2")).toBeVisible();
await expect(connection.getByText("Setup URL")).toHaveCount(0);
await expect(connection.getByText(preparation.webhook_url, { exact: true })).toBeVisible();
await expect(connection.getByText(/application\/json/)).toBeVisible();
await expect(connection.getByText(/SSL verification enabled/)).toBeVisible();
let importedBody: Record<string, unknown> | undefined;
await testPage.route("**/api/v1/github/app/registrations/import", async (route) => {
importedBody = route.request().postDataJSON() as Record<string, unknown>;
const registration = await apiClient.mockGitHubSetAppRegistration({
id: preparation.registration_id,
display_name: "Existing company App",
app_id: 404,
});
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify(registration),
});
});
await connection.getByLabel("Name in Kandev").fill("Existing company App");
await connection.getByLabel("GitHub App slug").fill("company-kandev");
await connection.getByLabel("GitHub owner login").fill("acme");
await connection.getByRole("textbox", { name: "App ID" }).fill("404");
await connection.getByLabel("Client ID").fill("Iv1.client-id");
await connection.getByLabel("Client secret").fill("client-secret-value");
await connection.getByLabel("Webhook secret").fill("webhook-secret-value");
await connection.getByLabel("Private key (.pem)").fill("private-key-value");
await connection.getByRole("button", { name: "Verify and import App" }).click();
await expect(testPage.getByText("GitHub App imported. It is ready to install.")).toBeVisible();
expect(importedBody).toMatchObject({
registration_id: preparation.registration_id,
workspace_id: seedData.workspaceId,
display_name: "Existing company App",
app_id: 404,
client_id: "Iv1.client-id",
client_secret: "client-secret-value",
private_key: "private-key-value",
webhook_secret: "webhook-secret-value",
slug: "company-kandev",
owner_login: "acme",
owner_type: "Organization",
visibility: "private",
public_base_url: "https://1.1.1.1",
});
await expect(connection.getByLabel("Client secret")).toHaveCount(0);
await expect(connection.getByText("Existing company App", { exact: true })).toBeVisible();
await expect(connection.locator(`#github-app-${preparation.registration_id}`)).toHaveAttribute(
"data-state",
"checked",
);
});
test("resets App selection and onboarding drafts when the workspace changes", async ({
testPage,
apiClient,
seedData,
}) => {
const otherWorkspace = await apiClient.createWorkspace("Personal Workspace");
for (const registration of [
{ id: "registration-work", display_name: "Work App", app_id: 501 },
{ id: "registration-home", display_name: "Home App", app_id: 502 },
]) {
await apiClient.mockGitHubSetAppRegistration(registration);
}
await apiClient.mockGitHubSetWorkspaceConnection(
seedData.workspaceId,
appConnection("registration-work", "work-org", 51),
);
await apiClient.mockGitHubSetWorkspaceConnection(
otherWorkspace.id,
appConnection("registration-home", "home-user", 52),
);
const settings = new GitHubAuthSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
let connection = await settings.openConnection();
await settings.chooseMethod("GitHub App");
await connection.getByRole("button", { name: "Create new App" }).click();
await connection.getByLabel("Name in Kandev").fill("Unsaved draft");
await testPage.evaluate((path) => {
window.history.pushState({}, "", path);
window.dispatchEvent(new Event("kandev:navigation"));
}, `/settings/workspaces/${otherWorkspace.id}/integrations/github`);
await expect(settings.automation().getByText("home-user", { exact: true })).toBeVisible({
timeout: 15_000,
});
await expect(testPage.getByText("Unsaved draft", { exact: true })).toHaveCount(0);
connection = await settings.openConnection();
await settings.chooseMethod("GitHub App");
await expect(connection.locator("#github-app-registration-home")).toHaveAttribute(
"data-state",
"checked",
);
await connection.getByRole("button", { name: "Create new App" }).click();
await expect(connection.getByLabel("Name in Kandev")).toHaveValue("");
});
});