diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 05ce694a97..ca3a507007 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -31,6 +31,7 @@ import { type Resource, Response, type RichParameter, + type ExternalAuthProviderResource, } from "./provisionerGenerated"; // requiresEnterpriseLicense will skip the test if we're not running with an enterprise license @@ -49,6 +50,7 @@ export const createWorkspace = async ( templateName: string, richParameters: RichParameter[] = [], buildParameters: WorkspaceBuildParameter[] = [], + useExternalAuthProvider: string | undefined = undefined, ): Promise => { await page.goto(`/templates/${templateName}/workspace`, { waitUntil: "domcontentloaded", @@ -59,6 +61,25 @@ export const createWorkspace = async ( await page.getByLabel("name").fill(name); await fillParameters(page, richParameters, buildParameters); + + if (useExternalAuthProvider !== undefined) { + // Create a new context for the popup which will be created when clicking the button + const popupPromise = page.waitForEvent("popup"); + + // Find the "Login with " button + const externalAuthLoginButton = page + .getByRole("button") + .getByText("Login with GitHub"); + await expect(externalAuthLoginButton).toBeVisible(); + + // Click it + await externalAuthLoginButton.click(); + + // Wait for authentication to occur + const popup = await popupPromise; + await popup.waitForSelector("text=You are now authenticated."); + } + await page.getByTestId("form-submit").click(); await expectUrl(page).toHavePathName("/@admin/" + name); @@ -648,6 +669,37 @@ export const echoResponsesWithParameters = ( }; }; +export const echoResponsesWithExternalAuth = ( + providers: ExternalAuthProviderResource[], +): EchoProvisionerResponses => { + return { + parse: [ + { + parse: {}, + }, + ], + plan: [ + { + plan: { + externalAuthProviders: providers, + }, + }, + ], + apply: [ + { + apply: { + externalAuthProviders: providers, + resources: [ + { + name: "example", + }, + ], + }, + }, + ], + }; +}; + export const fillParameters = async ( page: Page, richParameters: RichParameter[] = [], diff --git a/site/e2e/hooks.ts b/site/e2e/hooks.ts index 0fc483319a..c04233bc9c 100644 --- a/site/e2e/hooks.ts +++ b/site/e2e/hooks.ts @@ -1,4 +1,6 @@ -import type { Page } from "@playwright/test"; +import type { BrowserContext, Page } from "@playwright/test"; +import http from "http"; +import { coderPort, gitAuth } from "./constants"; export const beforeCoderTest = async (page: Page) => { // eslint-disable-next-line no-console -- Show everything that was printed with console.log() @@ -45,6 +47,41 @@ export const beforeCoderTest = async (page: Page) => { }); }; +export const resetExternalAuthKey = async (context: BrowserContext) => { + // Find the session token so we can destroy the external auth link between tests, to ensure valid authentication happens each time. + const cookies = await context.cookies(); + const sessionCookie = cookies.find((c) => c.name === "coder_session_token"); + const options = { + method: "DELETE", + hostname: "127.0.0.1", + port: coderPort, + path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`, + }; + + const req = http.request(options, (res) => { + let data = ""; + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + // Both 200 (key deleted successfully) and 500 (key was not found) are valid responses. + if (res.statusCode !== 200 && res.statusCode !== 500) { + console.error("failed to delete external auth link", data); + throw new Error( + `failed to delete external auth link: HTTP response ${res.statusCode}`, + ); + } + }); + }); + + req.on("error", (err) => { + throw err.message; + }); + + req.end(); +}; + const isApiCall = (urlString: string): boolean => { const url = new URL(urlString); const apiPath = "/api/v2"; diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index bb1929a062..7bc8ead8e4 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -115,7 +115,7 @@ export default defineConfig({ // Tests for Deployment / User Authentication / OIDC CODER_OIDC_ISSUER_URL: "https://accounts.google.com", CODER_OIDC_EMAIL_DOMAIN: "coder.com", - CODER_OIDC_CLIENT_ID: "1234567890", // FIXME: https://github.com/coder/coder/issues/12585 + CODER_OIDC_CLIENT_ID: "1234567890", CODER_OIDC_CLIENT_SECRET: "1234567890Secret", CODER_OIDC_ALLOW_SIGNUPS: "false", CODER_OIDC_SIGN_IN_TEXT: "Hello", diff --git a/site/e2e/tests/externalAuth.spec.ts b/site/e2e/tests/externalAuth.spec.ts index 2067582ccc..d5c98228ea 100644 --- a/site/e2e/tests/externalAuth.spec.ts +++ b/site/e2e/tests/externalAuth.spec.ts @@ -2,8 +2,37 @@ import type { Endpoints } from "@octokit/types"; import { test } from "@playwright/test"; import type { ExternalAuthDevice } from "api/typesGenerated"; import { gitAuth } from "../constants"; -import { Awaiter, createServer } from "../helpers"; -import { beforeCoderTest } from "../hooks"; +import { + Awaiter, + createServer, + createTemplate, + createWorkspace, + echoResponsesWithExternalAuth, +} from "../helpers"; +import { beforeCoderTest, resetExternalAuthKey } from "../hooks"; + +test.beforeAll(async ({ baseURL }) => { + const srv = await createServer(gitAuth.webPort); + + // The GitHub validate endpoint returns the currently authenticated user! + srv.use(gitAuth.validatePath, (req, res) => { + res.write(JSON.stringify(ghUser)); + res.end(); + }); + srv.use(gitAuth.tokenPath, (req, res) => { + const r = (Math.random() + 1).toString(36).substring(7); + res.write(JSON.stringify({ access_token: r })); + res.end(); + }); + srv.use(gitAuth.authPath, (req, res) => { + res.redirect( + `${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=` + + req.query.state, + ); + }); +}); + +test.beforeEach(async ({ context }) => resetExternalAuthKey(context)); test.beforeEach(({ page }) => beforeCoderTest(page)); @@ -57,23 +86,7 @@ test("external auth device", async ({ page }) => { await page.waitForSelector("text=1 organization authorized"); }); -test("external auth web", async ({ baseURL, page }) => { - const srv = await createServer(gitAuth.webPort); - // The GitHub validate endpoint returns the currently authenticated user! - srv.use(gitAuth.validatePath, (req, res) => { - res.write(JSON.stringify(ghUser)); - res.end(); - }); - srv.use(gitAuth.tokenPath, (req, res) => { - res.write(JSON.stringify({ access_token: "hello-world" })); - res.end(); - }); - srv.use(gitAuth.authPath, (req, res) => { - res.redirect( - `${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=` + - req.query.state, - ); - }); +test("external auth web", async ({ page }) => { await page.goto(`/external-auth/${gitAuth.webProvider}`, { waitUntil: "domcontentloaded", }); @@ -81,6 +94,17 @@ test("external auth web", async ({ baseURL, page }) => { await page.waitForSelector("text=You've authenticated with GitHub!"); }); +test("successful external auth from workspace", async ({ page }) => { + const templateName = await createTemplate( + page, + echoResponsesWithExternalAuth([ + { id: gitAuth.webProvider, optional: false }, + ]), + ); + + await createWorkspace(page, templateName, [], [], gitAuth.webProvider); +}); + const ghUser: Endpoints["GET /user"]["response"]["data"] = { login: "kylecarbs", id: 7122116,