Skip to content

Commit 319fd5b

Browse files
authored
chore: add e2e test against an external auth provider during workspace creation (#12985)
1 parent 75223df commit 319fd5b

File tree

4 files changed

+134
-21
lines changed

4 files changed

+134
-21
lines changed

site/e2e/helpers.ts

+52
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
type Resource,
3232
Response,
3333
type RichParameter,
34+
type ExternalAuthProviderResource,
3435
} from "./provisionerGenerated";
3536

3637
// requiresEnterpriseLicense will skip the test if we're not running with an enterprise license
@@ -49,6 +50,7 @@ export const createWorkspace = async (
4950
templateName: string,
5051
richParameters: RichParameter[] = [],
5152
buildParameters: WorkspaceBuildParameter[] = [],
53+
useExternalAuthProvider: string | undefined = undefined,
5254
): Promise<string> => {
5355
await page.goto(`/templates/${templateName}/workspace`, {
5456
waitUntil: "domcontentloaded",
@@ -59,6 +61,25 @@ export const createWorkspace = async (
5961
await page.getByLabel("name").fill(name);
6062

6163
await fillParameters(page, richParameters, buildParameters);
64+
65+
if (useExternalAuthProvider !== undefined) {
66+
// Create a new context for the popup which will be created when clicking the button
67+
const popupPromise = page.waitForEvent("popup");
68+
69+
// Find the "Login with <Provider>" button
70+
const externalAuthLoginButton = page
71+
.getByRole("button")
72+
.getByText("Login with GitHub");
73+
await expect(externalAuthLoginButton).toBeVisible();
74+
75+
// Click it
76+
await externalAuthLoginButton.click();
77+
78+
// Wait for authentication to occur
79+
const popup = await popupPromise;
80+
await popup.waitForSelector("text=You are now authenticated.");
81+
}
82+
6283
await page.getByTestId("form-submit").click();
6384

6485
await expectUrl(page).toHavePathName("/@admin/" + name);
@@ -648,6 +669,37 @@ export const echoResponsesWithParameters = (
648669
};
649670
};
650671

672+
export const echoResponsesWithExternalAuth = (
673+
providers: ExternalAuthProviderResource[],
674+
): EchoProvisionerResponses => {
675+
return {
676+
parse: [
677+
{
678+
parse: {},
679+
},
680+
],
681+
plan: [
682+
{
683+
plan: {
684+
externalAuthProviders: providers,
685+
},
686+
},
687+
],
688+
apply: [
689+
{
690+
apply: {
691+
externalAuthProviders: providers,
692+
resources: [
693+
{
694+
name: "example",
695+
},
696+
],
697+
},
698+
},
699+
],
700+
};
701+
};
702+
651703
export const fillParameters = async (
652704
page: Page,
653705
richParameters: RichParameter[] = [],

site/e2e/hooks.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { Page } from "@playwright/test";
1+
import type { BrowserContext, Page } from "@playwright/test";
2+
import http from "http";
3+
import { coderPort, gitAuth } from "./constants";
24

35
export const beforeCoderTest = async (page: Page) => {
46
// eslint-disable-next-line no-console -- Show everything that was printed with console.log()
@@ -45,6 +47,41 @@ export const beforeCoderTest = async (page: Page) => {
4547
});
4648
};
4749

50+
export const resetExternalAuthKey = async (context: BrowserContext) => {
51+
// Find the session token so we can destroy the external auth link between tests, to ensure valid authentication happens each time.
52+
const cookies = await context.cookies();
53+
const sessionCookie = cookies.find((c) => c.name === "coder_session_token");
54+
const options = {
55+
method: "DELETE",
56+
hostname: "127.0.0.1",
57+
port: coderPort,
58+
path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`,
59+
};
60+
61+
const req = http.request(options, (res) => {
62+
let data = "";
63+
res.on("data", (chunk) => {
64+
data += chunk;
65+
});
66+
67+
res.on("end", () => {
68+
// Both 200 (key deleted successfully) and 500 (key was not found) are valid responses.
69+
if (res.statusCode !== 200 && res.statusCode !== 500) {
70+
console.error("failed to delete external auth link", data);
71+
throw new Error(
72+
`failed to delete external auth link: HTTP response ${res.statusCode}`,
73+
);
74+
}
75+
});
76+
});
77+
78+
req.on("error", (err) => {
79+
throw err.message;
80+
});
81+
82+
req.end();
83+
};
84+
4885
const isApiCall = (urlString: string): boolean => {
4986
const url = new URL(urlString);
5087
const apiPath = "/api/v2";

site/e2e/playwright.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export default defineConfig({
115115
// Tests for Deployment / User Authentication / OIDC
116116
CODER_OIDC_ISSUER_URL: "https://accounts.google.com",
117117
CODER_OIDC_EMAIL_DOMAIN: "coder.com",
118-
CODER_OIDC_CLIENT_ID: "1234567890", // FIXME: https://github.com/coder/coder/issues/12585
118+
CODER_OIDC_CLIENT_ID: "1234567890",
119119
CODER_OIDC_CLIENT_SECRET: "1234567890Secret",
120120
CODER_OIDC_ALLOW_SIGNUPS: "false",
121121
CODER_OIDC_SIGN_IN_TEXT: "Hello",

site/e2e/tests/externalAuth.spec.ts

+43-19
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,37 @@ import type { Endpoints } from "@octokit/types";
22
import { test } from "@playwright/test";
33
import type { ExternalAuthDevice } from "api/typesGenerated";
44
import { gitAuth } from "../constants";
5-
import { Awaiter, createServer } from "../helpers";
6-
import { beforeCoderTest } from "../hooks";
5+
import {
6+
Awaiter,
7+
createServer,
8+
createTemplate,
9+
createWorkspace,
10+
echoResponsesWithExternalAuth,
11+
} from "../helpers";
12+
import { beforeCoderTest, resetExternalAuthKey } from "../hooks";
13+
14+
test.beforeAll(async ({ baseURL }) => {
15+
const srv = await createServer(gitAuth.webPort);
16+
17+
// The GitHub validate endpoint returns the currently authenticated user!
18+
srv.use(gitAuth.validatePath, (req, res) => {
19+
res.write(JSON.stringify(ghUser));
20+
res.end();
21+
});
22+
srv.use(gitAuth.tokenPath, (req, res) => {
23+
const r = (Math.random() + 1).toString(36).substring(7);
24+
res.write(JSON.stringify({ access_token: r }));
25+
res.end();
26+
});
27+
srv.use(gitAuth.authPath, (req, res) => {
28+
res.redirect(
29+
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=` +
30+
req.query.state,
31+
);
32+
});
33+
});
34+
35+
test.beforeEach(async ({ context }) => resetExternalAuthKey(context));
736

837
test.beforeEach(({ page }) => beforeCoderTest(page));
938

@@ -57,30 +86,25 @@ test("external auth device", async ({ page }) => {
5786
await page.waitForSelector("text=1 organization authorized");
5887
});
5988

60-
test("external auth web", async ({ baseURL, page }) => {
61-
const srv = await createServer(gitAuth.webPort);
62-
// The GitHub validate endpoint returns the currently authenticated user!
63-
srv.use(gitAuth.validatePath, (req, res) => {
64-
res.write(JSON.stringify(ghUser));
65-
res.end();
66-
});
67-
srv.use(gitAuth.tokenPath, (req, res) => {
68-
res.write(JSON.stringify({ access_token: "hello-world" }));
69-
res.end();
70-
});
71-
srv.use(gitAuth.authPath, (req, res) => {
72-
res.redirect(
73-
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=` +
74-
req.query.state,
75-
);
76-
});
89+
test("external auth web", async ({ page }) => {
7790
await page.goto(`/external-auth/${gitAuth.webProvider}`, {
7891
waitUntil: "domcontentloaded",
7992
});
8093
// This endpoint doesn't have the installations URL set intentionally!
8194
await page.waitForSelector("text=You've authenticated with GitHub!");
8295
});
8396

97+
test("successful external auth from workspace", async ({ page }) => {
98+
const templateName = await createTemplate(
99+
page,
100+
echoResponsesWithExternalAuth([
101+
{ id: gitAuth.webProvider, optional: false },
102+
]),
103+
);
104+
105+
await createWorkspace(page, templateName, [], [], gitAuth.webProvider);
106+
});
107+
84108
const ghUser: Endpoints["GET /user"]["response"]["data"] = {
85109
login: "kylecarbs",
86110
id: 7122116,

0 commit comments

Comments
 (0)