From f2fa7701a965e95a11be2a22676629d8f0f3615c Mon Sep 17 00:00:00 2001 From: Asher Date: Wed, 23 Jun 2021 17:41:36 -0500 Subject: [PATCH] Centralize credential handling My thinking is that this may reduce the cognitive overhead for developers writing new test suites. This also allows us to perform different setup steps (like ensuring the editor is visible when authenticated). --- test/e2e/baseFixture.ts | 29 ++++++++++++++++++++++----- test/e2e/browser.test.ts | 2 +- test/e2e/codeServer.test.ts | 7 +------ test/e2e/globalSetup.test.ts | 7 +------ test/e2e/login.test.ts | 8 +------- test/e2e/logout.test.ts | 8 +------- test/e2e/models/CodeServer.ts | 8 ++++++-- test/e2e/openHelpAbout.test.ts | 7 +------ test/e2e/terminal.test.ts | 7 +------ test/utils/constants.ts | 1 - test/utils/globalSetup.ts | 36 ++++++++++++++++------------------ 11 files changed, 54 insertions(+), 66 deletions(-) diff --git a/test/e2e/baseFixture.ts b/test/e2e/baseFixture.ts index b4105c227..e67d09b8d 100644 --- a/test/e2e/baseFixture.ts +++ b/test/e2e/baseFixture.ts @@ -6,8 +6,10 @@ import { CodeServer, CodeServerPage } from "./models/CodeServer" * Wraps `test.describe` to create and manage an instance of code-server. If you * don't use this you will need to create your own code-server instance and pass * it to `test.use`. + * + * If `includeCredentials` is `true` page requests will be authenticated. */ -export const describe = (name: string, fn: (codeServer: CodeServer) => void) => { +export const describe = (name: string, includeCredentials: boolean, fn: (codeServer: CodeServer) => void) => { test.describe(name, () => { // This will spawn on demand so nothing is necessary on before. const codeServer = new CodeServer(name) @@ -18,14 +20,30 @@ export const describe = (name: string, fn: (codeServer: CodeServer) => void) => await codeServer.close() }) - // This makes `codeServer` available to the extend call below. - test.use({ codeServer }) + const storageState = JSON.parse(process.env.STORAGE || "{}") + + // Sanity check to ensure the cookie is set. + const cookies = storageState?.cookies + if (includeCredentials && (!cookies || cookies.length !== 1 || !!cookies[0].key)) { + logger.error("no cookies", field("storage", JSON.stringify(cookies))) + throw new Error("no credentials to include") + } + + test.use({ + // Makes `codeServer` and `authenticated` available to the extend call + // below. + codeServer, + authenticated: includeCredentials, + // This provides a cookie that authenticates with code-server. + storageState: includeCredentials ? storageState : {}, + }) fn(codeServer) }) } interface TestFixtures { + authenticated: boolean codeServer: CodeServer codeServerPage: CodeServerPage } @@ -35,10 +53,11 @@ interface TestFixtures { * ready. */ export const test = base.extend({ + authenticated: false, codeServer: undefined, // No default; should be provided through `test.use`. - codeServerPage: async ({ codeServer, page }, use) => { + codeServerPage: async ({ authenticated, codeServer, page }, use) => { const codeServerPage = new CodeServerPage(codeServer, page) - await codeServerPage.navigate() + await codeServerPage.setup(authenticated) await use(codeServerPage) }, }) diff --git a/test/e2e/browser.test.ts b/test/e2e/browser.test.ts index 078b4e309..3c3b2411c 100644 --- a/test/e2e/browser.test.ts +++ b/test/e2e/browser.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "./baseFixture" // This is a "gut-check" test to make sure playwright is working as expected -describe("browser", () => { +describe("browser", true, () => { test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => { const displayNames = { chromium: "Chrome", diff --git a/test/e2e/codeServer.test.ts b/test/e2e/codeServer.test.ts index a40e30d89..cfac6f74c 100644 --- a/test/e2e/codeServer.test.ts +++ b/test/e2e/codeServer.test.ts @@ -1,11 +1,6 @@ -import { storageState } from "../utils/constants" import { describe, test, expect } from "./baseFixture" -describe("CodeServer", () => { - test.use({ - storageState, - }) - +describe("CodeServer", true, () => { test("should navigate to home page", async ({ codeServerPage }) => { // We navigate codeServer before each test // and we start the test with a storage state diff --git a/test/e2e/globalSetup.test.ts b/test/e2e/globalSetup.test.ts index ed3506ea9..8b4589b15 100644 --- a/test/e2e/globalSetup.test.ts +++ b/test/e2e/globalSetup.test.ts @@ -1,13 +1,8 @@ -import { storageState } from "../utils/constants" import { describe, test, expect } from "./baseFixture" // This test is to make sure the globalSetup works as expected // meaning globalSetup ran and stored the storageState -describe("globalSetup", () => { - test.use({ - storageState, - }) - +describe("globalSetup", true, () => { test("should keep us logged in using the storageState", async ({ codeServerPage }) => { // Make sure the editor actually loaded expect(await codeServerPage.isEditorVisible()).toBe(true) diff --git a/test/e2e/login.test.ts b/test/e2e/login.test.ts index b2289740a..bc9d5e8e9 100644 --- a/test/e2e/login.test.ts +++ b/test/e2e/login.test.ts @@ -1,13 +1,7 @@ import { PASSWORD } from "../utils/constants" import { describe, test, expect } from "./baseFixture" -describe("login", () => { - // Reset the browser so no cookies are persisted - // by emptying the storageState - test.use({ - storageState: {}, - }) - +describe("login", false, () => { test("should see the login page", async ({ codeServerPage }) => { // It should send us to the login page expect(await codeServerPage.page.title()).toBe("code-server login") diff --git a/test/e2e/logout.test.ts b/test/e2e/logout.test.ts index ec480c4f1..ac932fa47 100644 --- a/test/e2e/logout.test.ts +++ b/test/e2e/logout.test.ts @@ -1,13 +1,7 @@ import { PASSWORD } from "../utils/constants" import { describe, test, expect } from "./baseFixture" -describe("logout", () => { - // Reset the browser so no cookies are persisted - // by emptying the storageState - test.use({ - storageState: {}, - }) - +describe("logout", false, () => { test("should be able login and logout", async ({ codeServerPage }) => { // Type in password await codeServerPage.page.fill(".password", PASSWORD) diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index b3e2bdafa..42600df3d 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -250,8 +250,12 @@ export class CodeServerPage { * * It is recommended to run setup before using this model in any tests. */ - async setup() { + async setup(authenticated: boolean) { await this.navigate() - await this.reloadUntilEditorIsReady() + // If we aren't authenticated we'll see a login page so we can't wait until + // the editor is ready. + if (authenticated) { + await this.reloadUntilEditorIsReady() + } } } diff --git a/test/e2e/openHelpAbout.test.ts b/test/e2e/openHelpAbout.test.ts index 1cb030540..47daaaf14 100644 --- a/test/e2e/openHelpAbout.test.ts +++ b/test/e2e/openHelpAbout.test.ts @@ -1,11 +1,6 @@ -import { storageState } from "../utils/constants" import { describe, test, expect } from "./baseFixture" -describe("Open Help > About", () => { - test.use({ - storageState, - }) - +describe("Open Help > About", true, () => { test("should see a 'Help' then 'About' button in the Application Menu that opens a dialog", async ({ codeServerPage, }) => { diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index b2b284c74..836583a8d 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -2,11 +2,10 @@ import * as cp from "child_process" import * as fs from "fs" import * as path from "path" import util from "util" -import { storageState } from "../utils/constants" import { tmpdir } from "../utils/helpers" import { describe, expect, test } from "./baseFixture" -describe("Integrated Terminal", () => { +describe("Integrated Terminal", true, () => { // Create a new context with the saved storage state // so we don't have to logged in const testFileName = "pipe" @@ -14,10 +13,6 @@ describe("Integrated Terminal", () => { let tmpFolderPath = "" let tmpFile = "" - test.use({ - storageState, - }) - test.beforeAll(async () => { tmpFolderPath = await tmpdir("integrated-terminal") tmpFile = path.join(tmpFolderPath, testFileName) diff --git a/test/utils/constants.ts b/test/utils/constants.ts index 08acb9788..9b5c2b6e8 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -1,3 +1,2 @@ export const PASSWORD = "e45432jklfdsab" -export const storageState = JSON.parse(process.env.STORAGE || "{}") export const workspaceDir = "workspaces" diff --git a/test/utils/globalSetup.ts b/test/utils/globalSetup.ts index 8d94c7438..eace7f9b2 100644 --- a/test/utils/globalSetup.ts +++ b/test/utils/globalSetup.ts @@ -1,4 +1,4 @@ -import { chromium } from "playwright" +import { Cookie } from "playwright" import { hash } from "../../src/node/util" import { PASSWORD, workspaceDir } from "./constants" import { clean } from "./helpers" @@ -15,31 +15,29 @@ export default async function () { // Cleanup workspaces from previous tests. await clean(workspaceDir) - const cookieToStore = { - sameSite: "Lax" as const, - name: "key", - value: await hash(PASSWORD), - domain: "localhost", - path: "/", - expires: -1, - httpOnly: false, - secure: false, - } - - const browser = await chromium.launch() - const page = await browser.newPage() - const storage = await page.context().storageState() - if (process.env.WTF_NODE) { wtfnode.setup() } - storage.cookies = [cookieToStore] + // TODO: Replace this with a call to code-server to get the cookie. To avoid + // too much overhead we can do an http POST request and avoid spawning a + // browser for it. + const cookies: Cookie[] = [ + { + domain: "localhost", + expires: -1, + httpOnly: false, + name: "key", + path: "/", + sameSite: "Lax", + secure: false, + value: await hash(PASSWORD), + }, + ] // Save storage state and store as an env variable // More info: https://playwright.dev/docs/auth/#reuse-authentication-state - process.env.STORAGE = JSON.stringify(storage) - await browser.close() + process.env.STORAGE = JSON.stringify({ cookies }) console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.") }