mirror of https://github.com/coder/code-server.git
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).
This commit is contained in:
parent
da4de439e0
commit
f2fa7701a9
|
@ -6,8 +6,10 @@ import { CodeServer, CodeServerPage } from "./models/CodeServer"
|
||||||
* Wraps `test.describe` to create and manage an instance of code-server. If you
|
* 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
|
* don't use this you will need to create your own code-server instance and pass
|
||||||
* it to `test.use`.
|
* 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, () => {
|
test.describe(name, () => {
|
||||||
// This will spawn on demand so nothing is necessary on before.
|
// This will spawn on demand so nothing is necessary on before.
|
||||||
const codeServer = new CodeServer(name)
|
const codeServer = new CodeServer(name)
|
||||||
|
@ -18,14 +20,30 @@ export const describe = (name: string, fn: (codeServer: CodeServer) => void) =>
|
||||||
await codeServer.close()
|
await codeServer.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
// This makes `codeServer` available to the extend call below.
|
const storageState = JSON.parse(process.env.STORAGE || "{}")
|
||||||
test.use({ codeServer })
|
|
||||||
|
// 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)
|
fn(codeServer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestFixtures {
|
interface TestFixtures {
|
||||||
|
authenticated: boolean
|
||||||
codeServer: CodeServer
|
codeServer: CodeServer
|
||||||
codeServerPage: CodeServerPage
|
codeServerPage: CodeServerPage
|
||||||
}
|
}
|
||||||
|
@ -35,10 +53,11 @@ interface TestFixtures {
|
||||||
* ready.
|
* ready.
|
||||||
*/
|
*/
|
||||||
export const test = base.extend<TestFixtures>({
|
export const test = base.extend<TestFixtures>({
|
||||||
|
authenticated: false,
|
||||||
codeServer: undefined, // No default; should be provided through `test.use`.
|
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)
|
const codeServerPage = new CodeServerPage(codeServer, page)
|
||||||
await codeServerPage.navigate()
|
await codeServerPage.setup(authenticated)
|
||||||
await use(codeServerPage)
|
await use(codeServerPage)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
// This is a "gut-check" test to make sure playwright is working as expected
|
// 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 }) => {
|
test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => {
|
||||||
const displayNames = {
|
const displayNames = {
|
||||||
chromium: "Chrome",
|
chromium: "Chrome",
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { storageState } from "../utils/constants"
|
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("CodeServer", () => {
|
describe("CodeServer", true, () => {
|
||||||
test.use({
|
|
||||||
storageState,
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should navigate to home page", async ({ codeServerPage }) => {
|
test("should navigate to home page", async ({ codeServerPage }) => {
|
||||||
// We navigate codeServer before each test
|
// We navigate codeServer before each test
|
||||||
// and we start the test with a storage state
|
// and we start the test with a storage state
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { storageState } from "../utils/constants"
|
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
// This test is to make sure the globalSetup works as expected
|
// This test is to make sure the globalSetup works as expected
|
||||||
// meaning globalSetup ran and stored the storageState
|
// meaning globalSetup ran and stored the storageState
|
||||||
describe("globalSetup", () => {
|
describe("globalSetup", true, () => {
|
||||||
test.use({
|
|
||||||
storageState,
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should keep us logged in using the storageState", async ({ codeServerPage }) => {
|
test("should keep us logged in using the storageState", async ({ codeServerPage }) => {
|
||||||
// Make sure the editor actually loaded
|
// Make sure the editor actually loaded
|
||||||
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { PASSWORD } from "../utils/constants"
|
import { PASSWORD } from "../utils/constants"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("login", () => {
|
describe("login", false, () => {
|
||||||
// Reset the browser so no cookies are persisted
|
|
||||||
// by emptying the storageState
|
|
||||||
test.use({
|
|
||||||
storageState: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should see the login page", async ({ codeServerPage }) => {
|
test("should see the login page", async ({ codeServerPage }) => {
|
||||||
// It should send us to the login page
|
// It should send us to the login page
|
||||||
expect(await codeServerPage.page.title()).toBe("code-server login")
|
expect(await codeServerPage.page.title()).toBe("code-server login")
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import { PASSWORD } from "../utils/constants"
|
import { PASSWORD } from "../utils/constants"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("logout", () => {
|
describe("logout", false, () => {
|
||||||
// Reset the browser so no cookies are persisted
|
|
||||||
// by emptying the storageState
|
|
||||||
test.use({
|
|
||||||
storageState: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should be able login and logout", async ({ codeServerPage }) => {
|
test("should be able login and logout", async ({ codeServerPage }) => {
|
||||||
// Type in password
|
// Type in password
|
||||||
await codeServerPage.page.fill(".password", PASSWORD)
|
await codeServerPage.page.fill(".password", PASSWORD)
|
||||||
|
|
|
@ -250,8 +250,12 @@ export class CodeServerPage {
|
||||||
*
|
*
|
||||||
* It is recommended to run setup before using this model in any tests.
|
* It is recommended to run setup before using this model in any tests.
|
||||||
*/
|
*/
|
||||||
async setup() {
|
async setup(authenticated: boolean) {
|
||||||
await this.navigate()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { storageState } from "../utils/constants"
|
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("Open Help > About", () => {
|
describe("Open Help > About", true, () => {
|
||||||
test.use({
|
|
||||||
storageState,
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should see a 'Help' then 'About' button in the Application Menu that opens a dialog", async ({
|
test("should see a 'Help' then 'About' button in the Application Menu that opens a dialog", async ({
|
||||||
codeServerPage,
|
codeServerPage,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -2,11 +2,10 @@ import * as cp from "child_process"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import util from "util"
|
import util from "util"
|
||||||
import { storageState } from "../utils/constants"
|
|
||||||
import { tmpdir } from "../utils/helpers"
|
import { tmpdir } from "../utils/helpers"
|
||||||
import { describe, expect, test } from "./baseFixture"
|
import { describe, expect, test } from "./baseFixture"
|
||||||
|
|
||||||
describe("Integrated Terminal", () => {
|
describe("Integrated Terminal", true, () => {
|
||||||
// Create a new context with the saved storage state
|
// Create a new context with the saved storage state
|
||||||
// so we don't have to logged in
|
// so we don't have to logged in
|
||||||
const testFileName = "pipe"
|
const testFileName = "pipe"
|
||||||
|
@ -14,10 +13,6 @@ describe("Integrated Terminal", () => {
|
||||||
let tmpFolderPath = ""
|
let tmpFolderPath = ""
|
||||||
let tmpFile = ""
|
let tmpFile = ""
|
||||||
|
|
||||||
test.use({
|
|
||||||
storageState,
|
|
||||||
})
|
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
tmpFolderPath = await tmpdir("integrated-terminal")
|
tmpFolderPath = await tmpdir("integrated-terminal")
|
||||||
tmpFile = path.join(tmpFolderPath, testFileName)
|
tmpFile = path.join(tmpFolderPath, testFileName)
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export const PASSWORD = "e45432jklfdsab"
|
export const PASSWORD = "e45432jklfdsab"
|
||||||
export const storageState = JSON.parse(process.env.STORAGE || "{}")
|
|
||||||
export const workspaceDir = "workspaces"
|
export const workspaceDir = "workspaces"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { chromium } from "playwright"
|
import { Cookie } from "playwright"
|
||||||
import { hash } from "../../src/node/util"
|
import { hash } from "../../src/node/util"
|
||||||
import { PASSWORD, workspaceDir } from "./constants"
|
import { PASSWORD, workspaceDir } from "./constants"
|
||||||
import { clean } from "./helpers"
|
import { clean } from "./helpers"
|
||||||
|
@ -15,31 +15,29 @@ export default async function () {
|
||||||
// Cleanup workspaces from previous tests.
|
// Cleanup workspaces from previous tests.
|
||||||
await clean(workspaceDir)
|
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) {
|
if (process.env.WTF_NODE) {
|
||||||
wtfnode.setup()
|
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
|
// Save storage state and store as an env variable
|
||||||
// More info: https://playwright.dev/docs/auth/#reuse-authentication-state
|
// More info: https://playwright.dev/docs/auth/#reuse-authentication-state
|
||||||
process.env.STORAGE = JSON.stringify(storage)
|
process.env.STORAGE = JSON.stringify({ cookies })
|
||||||
await browser.close()
|
|
||||||
|
|
||||||
console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.")
|
console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue