Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 962608c

Browse files
authored
chore: allow signing in as non-admin users in e2e tests (#15892)
Closes coder/internal#168 Gets rid of the "global state" authentication, and adds a `login` helper which should be called at the beginning of each test. This means that not every test needs to authenticated as admin, and we can even have tests that encompass multiple permission levels. We also now create more than just the single admin user during setup, so that we can have a set of users to pick from as appropriate.
1 parent 1ead56f commit 962608c

40 files changed

+429
-155
lines changed

site/e2e/api.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,9 @@ import { findSessionToken, randomName } from "./helpers";
99
let currentOrgId: string;
1010

1111
export const setupApiCalls = async (page: Page) => {
12-
try {
13-
const token = await findSessionToken(page);
14-
API.setSessionToken(token);
15-
} catch {
16-
// If this fails, we have an unauthenticated client.
17-
}
18-
1912
API.setHost(`http://127.0.0.1:${coderPort}`);
13+
const token = await findSessionToken(page);
14+
API.setSessionToken(token);
2015
};
2116

2217
export const getCurrentOrgId = async (): Promise<string> => {

site/e2e/constants.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,30 @@ export const coderdPProfPort = 6062;
1515

1616
// The name of the organization that should be used by default when needed.
1717
export const defaultOrganizationName = "coder";
18+
export const defaultPassword = "SomeSecurePassword!";
1819

19-
// Credentials for the first user
20-
export const username = "admin";
21-
export const password = "SomeSecurePassword!";
22-
export const email = "[email protected]";
20+
// Credentials for users
21+
export const users = {
22+
admin: {
23+
username: "admin",
24+
password: defaultPassword,
25+
26+
},
27+
auditor: {
28+
username: "auditor",
29+
password: defaultPassword,
30+
31+
roles: ["Template Admin", "Auditor"],
32+
},
33+
user: {
34+
username: "user",
35+
password: defaultPassword,
36+
37+
},
38+
} satisfies Record<
39+
string,
40+
{ username: string; password: string; email: string; roles?: string[] }
41+
>;
2342

2443
export const gitAuth = {
2544
deviceProvider: "device",

site/e2e/helpers.ts

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { type ChildProcess, exec, spawn } from "node:child_process";
22
import { randomUUID } from "node:crypto";
3-
import * as fs from "node:fs";
43
import net from "node:net";
54
import path from "node:path";
65
import { Duplex } from "node:stream";
@@ -19,10 +18,12 @@ import {
1918
coderMain,
2019
coderPort,
2120
defaultOrganizationName,
21+
defaultPassword,
2222
license,
2323
premiumTestsRequired,
2424
prometheusPort,
2525
requireTerraformTests,
26+
users,
2627
} from "./constants";
2728
import { expectUrl } from "./expectUrl";
2829
import {
@@ -60,28 +61,75 @@ export function requireTerraformProvisioner() {
6061
test.skip(!requireTerraformTests);
6162
}
6263

64+
type LoginOptions = {
65+
username: string;
66+
email: string;
67+
password: string;
68+
};
69+
70+
export async function login(page: Page, options: LoginOptions = users.admin) {
71+
const ctx = page.context();
72+
// biome-ignore lint/suspicious/noExplicitAny: reset the current user
73+
(ctx as any)[Symbol.for("currentUser")] = undefined;
74+
await ctx.clearCookies();
75+
await page.goto("/login");
76+
await page.getByLabel("Email").fill(options.email);
77+
await page.getByLabel("Password").fill(options.password);
78+
await page.getByRole("button", { name: "Sign In" }).click();
79+
await expectUrl(page).toHavePathName("/workspaces");
80+
// biome-ignore lint/suspicious/noExplicitAny: update once logged in
81+
(ctx as any)[Symbol.for("currentUser")] = options;
82+
}
83+
84+
export function currentUser(page: Page): LoginOptions {
85+
const ctx = page.context();
86+
// biome-ignore lint/suspicious/noExplicitAny: get the current user
87+
const user = (ctx as any)[Symbol.for("currentUser")];
88+
89+
if (!user) {
90+
throw new Error("page context does not have a user. did you call `login`?");
91+
}
92+
93+
return user;
94+
}
95+
96+
type CreateWorkspaceOptions = {
97+
richParameters?: RichParameter[];
98+
buildParameters?: WorkspaceBuildParameter[];
99+
useExternalAuth?: boolean;
100+
};
101+
63102
/**
64103
* createWorkspace creates a workspace for a template. It does not wait for it
65104
* to be running, but it does navigate to the page.
66105
*/
67106
export const createWorkspace = async (
68107
page: Page,
69-
templateName: string,
70-
richParameters: RichParameter[] = [],
71-
buildParameters: WorkspaceBuildParameter[] = [],
72-
useExternalAuthProvider: string | undefined = undefined,
108+
template: string | { organization: string; name: string },
109+
options: CreateWorkspaceOptions = {},
73110
): Promise<string> => {
74-
await page.goto(`/templates/${templateName}/workspace`, {
111+
const {
112+
richParameters = [],
113+
buildParameters = [],
114+
useExternalAuth,
115+
} = options;
116+
117+
const templatePath =
118+
typeof template === "string"
119+
? template
120+
: `${template.organization}/${template.name}`;
121+
122+
await page.goto(`/templates/${templatePath}/workspace`, {
75123
waitUntil: "domcontentloaded",
76124
});
77-
await expectUrl(page).toHavePathName(`/templates/${templateName}/workspace`);
125+
await expectUrl(page).toHavePathName(`/templates/${templatePath}/workspace`);
78126

79127
const name = randomName();
80128
await page.getByLabel("name").fill(name);
81129

82130
await fillParameters(page, richParameters, buildParameters);
83131

84-
if (useExternalAuthProvider !== undefined) {
132+
if (useExternalAuth) {
85133
// Create a new context for the popup which will be created when clicking the button
86134
const popupPromise = page.waitForEvent("popup");
87135

@@ -101,7 +149,9 @@ export const createWorkspace = async (
101149

102150
await page.getByTestId("form-submit").click();
103151

104-
await expectUrl(page).toHavePathName(`/@admin/${name}`);
152+
const user = currentUser(page);
153+
154+
await expectUrl(page).toHavePathName(`/@${user.username}/${name}`);
105155

106156
await page.waitForSelector("[data-testid='build-status'] >> text=Running", {
107157
state: "visible",
@@ -214,6 +264,12 @@ export const createTemplate = async (
214264
const orgPicker = page.getByLabel("Belongs to *");
215265
const organizationsEnabled = await orgPicker.isVisible();
216266
if (organizationsEnabled) {
267+
if (orgName !== defaultOrganizationName) {
268+
throw new Error(
269+
`No provisioners registered for ${orgName}, creating this template will fail`,
270+
);
271+
}
272+
217273
await orgPicker.click();
218274
await page.getByText(orgName, { exact: true }).click();
219275
}
@@ -659,8 +715,9 @@ const createTemplateVersionTar = async (
659715
);
660716
};
661717

662-
export const randomName = () => {
663-
return randomUUID().slice(0, 8);
718+
export const randomName = (annotation?: string) => {
719+
const base = randomUUID().slice(0, 8);
720+
return annotation ? `${annotation}-${base}` : base;
664721
};
665722

666723
/**
@@ -1002,6 +1059,7 @@ type UserValues = {
10021059
username: string;
10031060
email: string;
10041061
password: string;
1062+
roles: string[];
10051063
};
10061064

10071065
export async function createUser(
@@ -1019,7 +1077,8 @@ export async function createUser(
10191077
const username = userValues.username ?? randomName();
10201078
const name = userValues.name ?? username;
10211079
const email = userValues.email ?? `${username}@coder.com`;
1022-
const password = userValues.password || "s3cure&password!";
1080+
const password = userValues.password || defaultPassword;
1081+
const roles = userValues.roles ?? [];
10231082

10241083
await page.getByLabel("Username").fill(username);
10251084
if (name) {
@@ -1036,10 +1095,18 @@ export async function createUser(
10361095
await expect(page.getByText("Successfully created user.")).toBeVisible();
10371096

10381097
await expect(page).toHaveTitle("Users - Coder");
1039-
await expect(page.locator("tr", { hasText: email })).toBeVisible();
1098+
const addedRow = page.locator("tr", { hasText: email });
1099+
await expect(addedRow).toBeVisible();
1100+
1101+
// Give them a role
1102+
await addedRow.getByLabel("Edit user roles").click();
1103+
for (const role of roles) {
1104+
await page.getByText(role, { exact: true }).click();
1105+
}
1106+
await page.mouse.click(10, 10); // close the popover by clicking outside of it
10401107

10411108
await page.goto(returnTo, { waitUntil: "domcontentloaded" });
1042-
return { name, username, email, password };
1109+
return { name, username, email, password, roles };
10431110
}
10441111

10451112
export async function createOrganization(page: Page): Promise<{

site/e2e/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import http from "node:http";
22
import type { BrowserContext, Page } from "@playwright/test";
33
import { coderPort, gitAuth } from "./constants";
44

5-
export const beforeCoderTest = async (page: Page) => {
5+
export const beforeCoderTest = (page: Page) => {
66
page.on("console", (msg) => console.info(`[onConsole] ${msg.text()}`));
77

88
page.on("request", (request) => {

site/e2e/playwright.config.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import {
1313

1414
export const wsEndpoint = process.env.CODER_E2E_WS_ENDPOINT;
1515

16-
// This is where auth cookies are stored!
17-
export const storageState = path.join(__dirname, ".auth.json");
18-
1916
// If running terraform tests, verify the requirements exist in the
2017
// environment.
2118
//
@@ -58,13 +55,12 @@ export default defineConfig({
5855
projects: [
5956
{
6057
name: "testsSetup",
61-
testMatch: /global.setup\.ts/,
58+
testMatch: /setup\/.*\.spec\.ts/,
6259
},
6360
{
6461
name: "tests",
65-
testMatch: /.*\.spec\.ts/,
62+
testMatch: /tests\/.*\.spec\.ts/,
6663
dependencies: ["testsSetup"],
67-
use: { storageState },
6864
timeout: 30_000,
6965
},
7066
],

site/e2e/global.setup.ts renamed to site/e2e/setup/createUsers.spec.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { expect, test } from "@playwright/test";
22
import { API } from "api/api";
33
import { Language } from "pages/CreateUserPage/CreateUserForm";
4-
import { setupApiCalls } from "./api";
5-
import * as constants from "./constants";
6-
import { expectUrl } from "./expectUrl";
7-
import { storageState } from "./playwright.config";
4+
import { coderPort, license, premiumTestsRequired, users } from "../constants";
5+
import { expectUrl } from "../expectUrl";
6+
import { createUser } from "../helpers";
87

98
test("setup deployment", async ({ page }) => {
109
await page.goto("/", { waitUntil: "domcontentloaded" });
11-
await setupApiCalls(page);
10+
API.setHost(`http://127.0.0.1:${coderPort}`);
1211
const exists = await API.hasFirstUser();
1312
// First user already exists, abort early. All tests execute this as a dependency,
1413
// if you run multiple tests in the UI, this will fail unless we check this.
@@ -17,28 +16,35 @@ test("setup deployment", async ({ page }) => {
1716
}
1817

1918
// Setup first user
20-
await page.getByLabel(Language.usernameLabel).fill(constants.username);
21-
await page.getByLabel(Language.emailLabel).fill(constants.email);
22-
await page.getByLabel(Language.passwordLabel).fill(constants.password);
19+
await page.getByLabel(Language.usernameLabel).fill(users.admin.username);
20+
await page.getByLabel(Language.emailLabel).fill(users.admin.email);
21+
await page.getByLabel(Language.passwordLabel).fill(users.admin.password);
2322
await page.getByTestId("create").click();
2423

2524
await expectUrl(page).toHavePathName("/workspaces");
26-
await page.context().storageState({ path: storageState });
27-
2825
await page.getByTestId("button-select-template").isVisible();
2926

27+
for (const user of Object.values(users)) {
28+
// Already created as first user
29+
if (user.username === "admin") {
30+
continue;
31+
}
32+
33+
await createUser(page, user);
34+
}
35+
3036
// Setup license
31-
if (constants.premiumTestsRequired || constants.license) {
37+
if (premiumTestsRequired || license) {
3238
// Make sure that we have something that looks like a real license
33-
expect(constants.license).toBeTruthy();
34-
expect(constants.license.length).toBeGreaterThan(92); // the signature alone should be this long
35-
expect(constants.license.split(".").length).toBe(3); // otherwise it's invalid
39+
expect(license).toBeTruthy();
40+
expect(license.length).toBeGreaterThan(92); // the signature alone should be this long
41+
expect(license.split(".").length).toBe(3); // otherwise it's invalid
3642

3743
await page.goto("/deployment/licenses", { waitUntil: "domcontentloaded" });
3844
await expect(page).toHaveTitle("License Settings - Coder");
3945

4046
await page.getByText("Add a license").click();
41-
await page.getByRole("textbox").fill(constants.license);
47+
await page.getByRole("textbox").fill(license);
4248
await page.getByText("Upload License").click();
4349

4450
await expect(

site/e2e/tests/app.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import { test } from "@playwright/test";
44
import {
55
createTemplate,
66
createWorkspace,
7+
login,
78
startAgent,
89
stopAgent,
910
stopWorkspace,
1011
} from "../helpers";
1112
import { beforeCoderTest } from "../hooks";
1213

13-
test.beforeEach(({ page }) => beforeCoderTest(page));
14+
test.beforeEach(async ({ page }) => {
15+
beforeCoderTest(page);
16+
await login(page);
17+
});
1418

1519
test("app", async ({ context, page }) => {
1620
test.setTimeout(75_000);

0 commit comments

Comments
 (0)