mirror of https://github.com/coder/code-server.git
Merge pull request #3664 from code-asher/parallel-tests
This commit is contained in:
commit
f92cbb7470
|
@ -334,8 +334,9 @@ jobs:
|
||||||
needs: package-linux-amd64
|
needs: package-linux-amd64
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
PASSWORD: e45432jklfdsab
|
# Since we build code-server we might as well run tests from the release
|
||||||
CODE_SERVER_ADDRESS: http://localhost:8080
|
# since VS Code will load faster due to the bundling.
|
||||||
|
CODE_SERVER_TEST_ENTRY: "./release-packages/code-server-linux-amd64"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
@ -362,9 +363,11 @@ jobs:
|
||||||
name: release-packages
|
name: release-packages
|
||||||
path: ./release-packages
|
path: ./release-packages
|
||||||
|
|
||||||
- name: Untar code-server file
|
- name: Untar code-server release
|
||||||
run: |
|
run: |
|
||||||
cd release-packages && tar -xzf code-server*-linux-amd64.tar.gz
|
cd release-packages
|
||||||
|
tar -xzf code-server*-linux-amd64.tar.gz
|
||||||
|
mv code-server*-linux-amd64 code-server-linux-amd64
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||||
|
@ -380,9 +383,7 @@ jobs:
|
||||||
yarn install --check-files
|
yarn install --check-files
|
||||||
|
|
||||||
- name: Run end-to-end tests
|
- name: Run end-to-end tests
|
||||||
run: |
|
run: yarn test:e2e
|
||||||
./release-packages/code-server*-linux-amd64/bin/code-server --log trace &
|
|
||||||
yarn test:e2e
|
|
||||||
|
|
||||||
- name: Upload test artifacts
|
- name: Upload test artifacts
|
||||||
if: always()
|
if: always()
|
||||||
|
|
|
@ -3,10 +3,35 @@ set -euo pipefail
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
source ./ci/lib.sh
|
||||||
|
|
||||||
|
local dir="$PWD"
|
||||||
|
if [[ ! ${CODE_SERVER_TEST_ENTRY-} ]]; then
|
||||||
|
echo "Set CODE_SERVER_TEST_ENTRY to test another build of code-server"
|
||||||
|
else
|
||||||
|
pushd "$CODE_SERVER_TEST_ENTRY"
|
||||||
|
dir="$PWD"
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Testing build in '$dir'"
|
||||||
|
|
||||||
|
# Simple sanity checks to see that we've built. There could still be things
|
||||||
|
# wrong (native modules version issues, incomplete build, etc).
|
||||||
|
if [[ ! -d $dir/out ]]; then
|
||||||
|
echo >&2 "No code-server build detected"
|
||||||
|
echo >&2 "You can build it with 'yarn build' or 'yarn watch'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d $dir/lib/vscode/out ]]; then
|
||||||
|
echo >&2 "No VS Code build detected"
|
||||||
|
echo >&2 "You can build it with 'yarn build:vscode' or 'yarn watch'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
cd test
|
cd test
|
||||||
# We set these environment variables because they're used in the e2e tests
|
yarn playwright test "$@"
|
||||||
# they don't have to be these values, but these are the defaults
|
|
||||||
PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 yarn playwright test "$@"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
@ -2,6 +2,7 @@ import browserify from "browserify"
|
||||||
import * as cp from "child_process"
|
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 { onLine } from "../../src/node/util"
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
@ -97,38 +98,6 @@ class Watcher {
|
||||||
path.join(this.rootPath, "out/browser/pages/vscode.js"),
|
path.join(this.rootPath, "out/browser/pages/vscode.js"),
|
||||||
]
|
]
|
||||||
|
|
||||||
// From https://github.com/chalk/ansi-regex
|
|
||||||
const pattern = [
|
|
||||||
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
||||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
|
|
||||||
].join("|")
|
|
||||||
const re = new RegExp(pattern, "g")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Split stdout on newlines and strip ANSI codes.
|
|
||||||
*/
|
|
||||||
const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
|
||||||
let buffer = ""
|
|
||||||
if (!proc.stdout) {
|
|
||||||
throw new Error("no stdout")
|
|
||||||
}
|
|
||||||
proc.stdout.setEncoding("utf8")
|
|
||||||
proc.stdout.on("data", (d) => {
|
|
||||||
const data = buffer + d
|
|
||||||
const split = data.split("\n")
|
|
||||||
const last = split.length - 1
|
|
||||||
|
|
||||||
for (let i = 0; i < last; ++i) {
|
|
||||||
callback(split[i].replace(re, ""), split[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// The last item will either be an empty string (the data ended with a
|
|
||||||
// newline) or a partial line (did not end with a newline) and we must
|
|
||||||
// wait to parse it until we get a full line.
|
|
||||||
buffer = split[last]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let startingVscode = false
|
let startingVscode = false
|
||||||
let startedVscode = false
|
let startedVscode = false
|
||||||
onLine(vscode, (line, original) => {
|
onLine(vscode, (line, original) => {
|
||||||
|
|
|
@ -17,6 +17,38 @@ export interface Paths {
|
||||||
runtime: string
|
runtime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From https://github.com/chalk/ansi-regex
|
||||||
|
const pattern = [
|
||||||
|
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
||||||
|
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
|
||||||
|
].join("|")
|
||||||
|
const re = new RegExp(pattern, "g")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split stdout on newlines and strip ANSI codes.
|
||||||
|
*/
|
||||||
|
export const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
||||||
|
let buffer = ""
|
||||||
|
if (!proc.stdout) {
|
||||||
|
throw new Error("no stdout")
|
||||||
|
}
|
||||||
|
proc.stdout.setEncoding("utf8")
|
||||||
|
proc.stdout.on("data", (d) => {
|
||||||
|
const data = buffer + d
|
||||||
|
const split = data.split("\n")
|
||||||
|
const last = split.length - 1
|
||||||
|
|
||||||
|
for (let i = 0; i < last; ++i) {
|
||||||
|
callback(split[i].replace(re, ""), split[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last item will either be an empty string (the data ended with a
|
||||||
|
// newline) or a partial line (did not end with a newline) and we must
|
||||||
|
// wait to parse it until we get a full line.
|
||||||
|
buffer = split[last]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const paths = getEnvPaths()
|
export const paths = getEnvPaths()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,12 +1,71 @@
|
||||||
|
import { field, logger } from "@coder/logger"
|
||||||
import { test as base } from "@playwright/test"
|
import { test as base } from "@playwright/test"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { CodeServer, CodeServerPage } from "./models/CodeServer"
|
||||||
|
|
||||||
export const test = base.extend<{ codeServerPage: CodeServer }>({
|
/**
|
||||||
codeServerPage: async ({ page }, use) => {
|
* Wraps `test.describe` to create and manage an instance of code-server. If you
|
||||||
const codeServer = new CodeServer(page)
|
* don't use this you will need to create your own code-server instance and pass
|
||||||
await codeServer.navigate()
|
* it to `test.use`.
|
||||||
await use(codeServer)
|
*
|
||||||
|
* If `includeCredentials` is `true` page requests will be authenticated.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Kill code-server after the suite has ended. This may happen even without
|
||||||
|
// doing it explicitly but it seems prudent to be sure.
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await codeServer.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test that spawns code-server if necessary and ensures the page is
|
||||||
|
* ready.
|
||||||
|
*/
|
||||||
|
export const test = base.extend<TestFixtures>({
|
||||||
|
authenticated: false,
|
||||||
|
codeServer: undefined, // No default; should be provided through `test.use`.
|
||||||
|
codeServerPage: async ({ authenticated, codeServer, page }, use) => {
|
||||||
|
// It's possible code-server might prevent navigation because of unsaved
|
||||||
|
// changes (seems to happen based on timing even if no changes have been
|
||||||
|
// made too). In these cases just accept.
|
||||||
|
page.on("dialog", (d) => d.accept())
|
||||||
|
|
||||||
|
const codeServerPage = new CodeServerPage(codeServer, page)
|
||||||
|
await codeServerPage.setup(authenticated)
|
||||||
|
await use(codeServerPage)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** Shorthand for test.expect. */
|
||||||
export const expect = test.expect
|
export const expect = test.expect
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { 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
|
||||||
test.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,12 +1,7 @@
|
||||||
import { CODE_SERVER_ADDRESS, storageState } from "../utils/constants"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
import { test, expect } from "./baseFixture"
|
|
||||||
|
|
||||||
test.describe("CodeServer", () => {
|
describe("CodeServer", true, () => {
|
||||||
test.use({
|
test("should navigate to home page", async ({ codeServerPage }) => {
|
||||||
storageState,
|
|
||||||
})
|
|
||||||
|
|
||||||
test(`should navigate to ${CODE_SERVER_ADDRESS}`, 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
|
||||||
// which means we should be logged in
|
// which means we should be logged in
|
||||||
|
@ -14,7 +9,7 @@ test.describe("CodeServer", () => {
|
||||||
const url = codeServerPage.page.url()
|
const url = codeServerPage.page.url()
|
||||||
// We use match because there may be a / at the end
|
// We use match because there may be a / at the end
|
||||||
// so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/
|
// so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/
|
||||||
expect(url).toMatch(CODE_SERVER_ADDRESS)
|
expect(url).toMatch(await codeServerPage.address())
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should always see the code-server editor", async ({ codeServerPage }) => {
|
test("should always see the code-server editor", async ({ codeServerPage }) => {
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { storageState } from "../utils/constants"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
import { 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
|
||||||
test.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 { test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
test.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 }) => {
|
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,25 +1,7 @@
|
||||||
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
import { test, expect } from "./baseFixture"
|
|
||||||
|
|
||||||
test.describe("logout", () => {
|
|
||||||
// Reset the browser so no cookies are persisted
|
|
||||||
// by emptying the storageState
|
|
||||||
test.use({
|
|
||||||
storageState: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
test("should be able login and logout", async ({ codeServerPage }) => {
|
|
||||||
// Type in password
|
|
||||||
await codeServerPage.page.fill(".password", PASSWORD)
|
|
||||||
// Click the submit button and login
|
|
||||||
await codeServerPage.page.click(".submit")
|
|
||||||
await codeServerPage.page.waitForLoadState("networkidle")
|
|
||||||
// We do this because occassionally code-server doesn't load on Firefox
|
|
||||||
// but loads if you reload once or twice
|
|
||||||
await codeServerPage.reloadUntilEditorIsReady()
|
|
||||||
// Make sure the editor actually loaded
|
|
||||||
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
|
||||||
|
|
||||||
|
describe("logout", true, () => {
|
||||||
|
test("should be able logout", async ({ codeServerPage }) => {
|
||||||
// Click the Application menu
|
// Click the Application menu
|
||||||
await codeServerPage.page.click("[aria-label='Application Menu']")
|
await codeServerPage.page.click("[aria-label='Application Menu']")
|
||||||
|
|
||||||
|
@ -28,17 +10,11 @@ test.describe("logout", () => {
|
||||||
expect(await codeServerPage.page.isVisible(logoutButton)).toBe(true)
|
expect(await codeServerPage.page.isVisible(logoutButton)).toBe(true)
|
||||||
|
|
||||||
await codeServerPage.page.hover(logoutButton)
|
await codeServerPage.page.hover(logoutButton)
|
||||||
// TODO(@jsjoeio)
|
|
||||||
// Look into how we're attaching the handlers for the logout feature
|
|
||||||
// We need to see how it's done upstream and add logging to the
|
|
||||||
// handlers themselves.
|
|
||||||
// They may be attached too slowly, hence why we need this timeout
|
|
||||||
await codeServerPage.page.waitForTimeout(2000)
|
|
||||||
|
|
||||||
// Recommended by Playwright for async navigation
|
// Recommended by Playwright for async navigation
|
||||||
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
||||||
await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.page.click(logoutButton)])
|
await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.page.click(logoutButton)])
|
||||||
const currentUrl = codeServerPage.page.url()
|
const currentUrl = codeServerPage.page.url()
|
||||||
expect(currentUrl).toBe(`${CODE_SERVER_ADDRESS}/login`)
|
expect(currentUrl).toBe(`${await codeServerPage.address()}/login`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,21 +1,163 @@
|
||||||
|
import { Logger, logger } from "@coder/logger"
|
||||||
|
import * as cp from "child_process"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import * as path from "path"
|
||||||
import { Page } from "playwright"
|
import { Page } from "playwright"
|
||||||
import { CODE_SERVER_ADDRESS } from "../../utils/constants"
|
import { onLine } from "../../../src/node/util"
|
||||||
// This is a Page Object Model
|
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
||||||
// We use these to simplify e2e test authoring
|
import { tmpdir } from "../../utils/helpers"
|
||||||
// See Playwright docs: https://playwright.dev/docs/pom/
|
|
||||||
export class CodeServer {
|
|
||||||
page: Page
|
|
||||||
editorSelector = "div.monaco-workbench"
|
|
||||||
|
|
||||||
constructor(page: Page) {
|
interface CodeServerProcess {
|
||||||
this.page = page
|
process: cp.ChildProcess
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for spawning and managing code-server.
|
||||||
|
*/
|
||||||
|
export class CodeServer {
|
||||||
|
private process: Promise<CodeServerProcess> | undefined
|
||||||
|
private readonly logger: Logger
|
||||||
|
private closed = false
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.logger = logger.named(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to CODE_SERVER_ADDRESS
|
* The address at which code-server can be accessed. Spawns code-server if it
|
||||||
|
* has not yet been spawned.
|
||||||
|
*/
|
||||||
|
async address(): Promise<string> {
|
||||||
|
if (!this.process) {
|
||||||
|
this.process = this.spawn()
|
||||||
|
}
|
||||||
|
const { address } = await this.process
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a random workspace and seed it with settings.
|
||||||
|
*/
|
||||||
|
private async createWorkspace(): Promise<string> {
|
||||||
|
const dir = await tmpdir(workspaceDir)
|
||||||
|
await fs.mkdir(path.join(dir, ".vscode"))
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(dir, ".vscode/settings.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
"workbench.startupEditor": "none",
|
||||||
|
}),
|
||||||
|
"utf8",
|
||||||
|
)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a new code-server process with its own workspace and data
|
||||||
|
* directories.
|
||||||
|
*/
|
||||||
|
private async spawn(): Promise<CodeServerProcess> {
|
||||||
|
// This will be used both as the workspace and data directory to ensure
|
||||||
|
// instances don't bleed into each other.
|
||||||
|
const dir = await this.createWorkspace()
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.logger.debug("spawning")
|
||||||
|
const proc = cp.spawn(
|
||||||
|
"node",
|
||||||
|
[
|
||||||
|
process.env.CODE_SERVER_TEST_ENTRY || ".",
|
||||||
|
// Using port zero will spawn on a random port.
|
||||||
|
"--bind-addr",
|
||||||
|
"127.0.0.1:0",
|
||||||
|
// Setting the XDG variables would be easier and more thorough but the
|
||||||
|
// modules we import ignores those variables for non-Linux operating
|
||||||
|
// systems so use these flags instead.
|
||||||
|
"--config",
|
||||||
|
path.join(dir, "config.yaml"),
|
||||||
|
"--user-data-dir",
|
||||||
|
dir,
|
||||||
|
// The last argument is the workspace to open.
|
||||||
|
dir,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: path.join(__dirname, "../../.."),
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PASSWORD,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
proc.on("error", (error) => {
|
||||||
|
this.logger.error(error.message)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
proc.on("close", () => {
|
||||||
|
const error = new Error("closed unexpectedly")
|
||||||
|
if (!this.closed) {
|
||||||
|
this.logger.error(error.message)
|
||||||
|
}
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
let resolved = false
|
||||||
|
proc.stdout.setEncoding("utf8")
|
||||||
|
onLine(proc, (line) => {
|
||||||
|
// Log the line without the timestamp.
|
||||||
|
this.logger.trace(line.replace(/\[.+\]/, ""))
|
||||||
|
if (resolved) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const match = line.trim().match(/HTTP server listening on (https?:\/\/[.:\d]+)$/)
|
||||||
|
if (match) {
|
||||||
|
// Cookies don't seem to work on IP address so swap to localhost.
|
||||||
|
// TODO: Investigate whether this is a bug with code-server.
|
||||||
|
const address = match[1].replace("127.0.0.1", "localhost")
|
||||||
|
this.logger.debug(`spawned on ${address}`)
|
||||||
|
resolved = true
|
||||||
|
resolve({ process: proc, address })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the code-server process.
|
||||||
|
*/
|
||||||
|
async close(): Promise<void> {
|
||||||
|
logger.debug("closing")
|
||||||
|
if (this.process) {
|
||||||
|
const proc = (await this.process).process
|
||||||
|
this.closed = true // To prevent the close handler from erroring.
|
||||||
|
proc.kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a "Page Object Model" (https://playwright.dev/docs/pom/) meant to
|
||||||
|
* wrap over a page and represent actions on that page in a more readable way.
|
||||||
|
* This targets a specific code-server instance which must be passed in.
|
||||||
|
* Navigation and setup performed by this model will cause the code-server
|
||||||
|
* process to spawn if it hasn't yet.
|
||||||
|
*/
|
||||||
|
export class CodeServerPage {
|
||||||
|
private readonly editorSelector = "div.monaco-workbench"
|
||||||
|
|
||||||
|
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {}
|
||||||
|
|
||||||
|
address() {
|
||||||
|
return this.codeServer.address()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to code-server.
|
||||||
*/
|
*/
|
||||||
async navigate() {
|
async navigate() {
|
||||||
await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
|
const address = await this.codeServer.address()
|
||||||
|
await this.page.goto(address, { waitUntil: "networkidle" })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,7 +184,7 @@ export class CodeServer {
|
||||||
await this.page.waitForTimeout(1000)
|
await this.page.waitForTimeout(1000)
|
||||||
reloadCount += 1
|
reloadCount += 1
|
||||||
if ((await this.isEditorVisible()) && (await this.isConnected())) {
|
if ((await this.isEditorVisible()) && (await this.isConnected())) {
|
||||||
console.log(` Editor became ready after ${reloadCount} reloads`)
|
logger.debug(`editor became ready after ${reloadCount} reloads`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
await this.page.reload()
|
await this.page.reload()
|
||||||
|
@ -64,7 +206,7 @@ export class CodeServer {
|
||||||
async isConnected() {
|
async isConnected() {
|
||||||
await this.page.waitForLoadState("networkidle")
|
await this.page.waitForLoadState("networkidle")
|
||||||
|
|
||||||
const host = new URL(CODE_SERVER_ADDRESS).host
|
const host = new URL(await this.codeServer.address()).host
|
||||||
const hostSelector = `[title="Editing on ${host}"]`
|
const hostSelector = `[title="Editing on ${host}"]`
|
||||||
await this.page.waitForSelector(hostSelector)
|
await this.page.waitForSelector(hostSelector)
|
||||||
|
|
||||||
|
@ -104,13 +246,16 @@ export class CodeServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigates to CODE_SERVER_ADDRESS
|
* Navigates to code-server then reloads until the editor is ready.
|
||||||
* and reloads until the editor is ready
|
|
||||||
*
|
*
|
||||||
* Helpful for running before 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 { test, expect } from "./baseFixture"
|
|
||||||
|
|
||||||
test.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 ({
|
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 { expect, test } from "./baseFixture"
|
import { describe, expect, test } from "./baseFixture"
|
||||||
|
|
||||||
test.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 @@ test.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)
|
||||||
|
|
|
@ -6,14 +6,13 @@ import path from "path"
|
||||||
const config: PlaywrightTestConfig = {
|
const config: PlaywrightTestConfig = {
|
||||||
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
||||||
timeout: 60000, // Each test is given 60 seconds.
|
timeout: 60000, // Each test is given 60 seconds.
|
||||||
retries: 3, // Retry failing tests 2 times
|
retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
|
||||||
workers: 1,
|
|
||||||
globalSetup: require.resolve("./utils/globalSetup.ts"),
|
globalSetup: require.resolve("./utils/globalSetup.ts"),
|
||||||
reporter: "list",
|
reporter: "list",
|
||||||
// Put any shared options on the top level.
|
// Put any shared options on the top level.
|
||||||
use: {
|
use: {
|
||||||
headless: true, // Run tests in headless browsers.
|
headless: true, // Run tests in headless browsers.
|
||||||
video: "on",
|
video: "retain-on-failure",
|
||||||
},
|
},
|
||||||
|
|
||||||
projects: [
|
projects: [
|
||||||
|
@ -34,10 +33,4 @@ const config: PlaywrightTestConfig = {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.CI) {
|
|
||||||
// In CI, retry failing tests 2 times
|
|
||||||
// in the event of flakiness
|
|
||||||
config.retries = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import {
|
import * as cp from "child_process"
|
||||||
hash,
|
import { generateUuid } from "../../../src/common/util"
|
||||||
isHashMatch,
|
import * as util from "../../../src/node/util"
|
||||||
handlePasswordValidation,
|
|
||||||
PasswordMethod,
|
|
||||||
getPasswordMethod,
|
|
||||||
hashLegacy,
|
|
||||||
isHashLegacyMatch,
|
|
||||||
isCookieValid,
|
|
||||||
sanitizeString,
|
|
||||||
} from "../../../src/node/util"
|
|
||||||
|
|
||||||
describe("getEnvPaths", () => {
|
describe("getEnvPaths", () => {
|
||||||
describe("on darwin", () => {
|
describe("on darwin", () => {
|
||||||
|
@ -161,7 +153,7 @@ describe("getEnvPaths", () => {
|
||||||
describe("hash", () => {
|
describe("hash", () => {
|
||||||
it("should return a hash of the string passed in", async () => {
|
it("should return a hash of the string passed in", async () => {
|
||||||
const plainTextPassword = "mySecretPassword123"
|
const plainTextPassword = "mySecretPassword123"
|
||||||
const hashed = await hash(plainTextPassword)
|
const hashed = await util.hash(plainTextPassword)
|
||||||
expect(hashed).not.toBe(plainTextPassword)
|
expect(hashed).not.toBe(plainTextPassword)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -169,32 +161,32 @@ describe("hash", () => {
|
||||||
describe("isHashMatch", () => {
|
describe("isHashMatch", () => {
|
||||||
it("should return true if the password matches the hash", async () => {
|
it("should return true if the password matches the hash", async () => {
|
||||||
const password = "codeserver1234"
|
const password = "codeserver1234"
|
||||||
const _hash = await hash(password)
|
const _hash = await util.hash(password)
|
||||||
const actual = await isHashMatch(password, _hash)
|
const actual = await util.isHashMatch(password, _hash)
|
||||||
expect(actual).toBe(true)
|
expect(actual).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false if the password does not match the hash", async () => {
|
it("should return false if the password does not match the hash", async () => {
|
||||||
const password = "password123"
|
const password = "password123"
|
||||||
const _hash = await hash(password)
|
const _hash = await util.hash(password)
|
||||||
const actual = await isHashMatch("otherPassword123", _hash)
|
const actual = await util.isHashMatch("otherPassword123", _hash)
|
||||||
expect(actual).toBe(false)
|
expect(actual).toBe(false)
|
||||||
})
|
})
|
||||||
it("should return true with actual hash", async () => {
|
it("should return true with actual hash", async () => {
|
||||||
const password = "password123"
|
const password = "password123"
|
||||||
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
||||||
const actual = await isHashMatch(password, _hash)
|
const actual = await util.isHashMatch(password, _hash)
|
||||||
expect(actual).toBe(true)
|
expect(actual).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false if the password is empty", async () => {
|
it("should return false if the password is empty", async () => {
|
||||||
const password = ""
|
const password = ""
|
||||||
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
||||||
const actual = await isHashMatch(password, _hash)
|
const actual = await util.isHashMatch(password, _hash)
|
||||||
expect(actual).toBe(false)
|
expect(actual).toBe(false)
|
||||||
})
|
})
|
||||||
it("should return false if the hash is empty", async () => {
|
it("should return false if the hash is empty", async () => {
|
||||||
const password = "hellowpasssword"
|
const password = "hellowpasssword"
|
||||||
const _hash = ""
|
const _hash = ""
|
||||||
const actual = await isHashMatch(password, _hash)
|
const actual = await util.isHashMatch(password, _hash)
|
||||||
expect(actual).toBe(false)
|
expect(actual).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -202,7 +194,7 @@ describe("isHashMatch", () => {
|
||||||
describe("hashLegacy", () => {
|
describe("hashLegacy", () => {
|
||||||
it("should return a hash of the string passed in", () => {
|
it("should return a hash of the string passed in", () => {
|
||||||
const plainTextPassword = "mySecretPassword123"
|
const plainTextPassword = "mySecretPassword123"
|
||||||
const hashed = hashLegacy(plainTextPassword)
|
const hashed = util.hashLegacy(plainTextPassword)
|
||||||
expect(hashed).not.toBe(plainTextPassword)
|
expect(hashed).not.toBe(plainTextPassword)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -210,40 +202,40 @@ describe("hashLegacy", () => {
|
||||||
describe("isHashLegacyMatch", () => {
|
describe("isHashLegacyMatch", () => {
|
||||||
it("should return true if is match", () => {
|
it("should return true if is match", () => {
|
||||||
const password = "password123"
|
const password = "password123"
|
||||||
const _hash = hashLegacy(password)
|
const _hash = util.hashLegacy(password)
|
||||||
expect(isHashLegacyMatch(password, _hash)).toBe(true)
|
expect(util.isHashLegacyMatch(password, _hash)).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false if is match", () => {
|
it("should return false if is match", () => {
|
||||||
const password = "password123"
|
const password = "password123"
|
||||||
const _hash = hashLegacy(password)
|
const _hash = util.hashLegacy(password)
|
||||||
expect(isHashLegacyMatch("otherPassword123", _hash)).toBe(false)
|
expect(util.isHashLegacyMatch("otherPassword123", _hash)).toBe(false)
|
||||||
})
|
})
|
||||||
it("should return true if hashed from command line", () => {
|
it("should return true if hashed from command line", () => {
|
||||||
const password = "password123"
|
const password = "password123"
|
||||||
// Hashed using printf "password123" | sha256sum | cut -d' ' -f1
|
// Hashed using printf "password123" | sha256sum | cut -d' ' -f1
|
||||||
const _hash = "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
|
const _hash = "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
|
||||||
expect(isHashLegacyMatch(password, _hash)).toBe(true)
|
expect(util.isHashLegacyMatch(password, _hash)).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("getPasswordMethod", () => {
|
describe("getPasswordMethod", () => {
|
||||||
it("should return PLAIN_TEXT for no hashed password", () => {
|
it("should return PLAIN_TEXT for no hashed password", () => {
|
||||||
const hashedPassword = undefined
|
const hashedPassword = undefined
|
||||||
const passwordMethod = getPasswordMethod(hashedPassword)
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
||||||
const expected: PasswordMethod = "PLAIN_TEXT"
|
const expected: util.PasswordMethod = "PLAIN_TEXT"
|
||||||
expect(passwordMethod).toEqual(expected)
|
expect(passwordMethod).toEqual(expected)
|
||||||
})
|
})
|
||||||
it("should return ARGON2 for password with 'argon2'", () => {
|
it("should return ARGON2 for password with 'argon2'", () => {
|
||||||
const hashedPassword =
|
const hashedPassword =
|
||||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY"
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY"
|
||||||
const passwordMethod = getPasswordMethod(hashedPassword)
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
||||||
const expected: PasswordMethod = "ARGON2"
|
const expected: util.PasswordMethod = "ARGON2"
|
||||||
expect(passwordMethod).toEqual(expected)
|
expect(passwordMethod).toEqual(expected)
|
||||||
})
|
})
|
||||||
it("should return SHA256 for password with legacy hash", () => {
|
it("should return SHA256 for password with legacy hash", () => {
|
||||||
const hashedPassword = "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af"
|
const hashedPassword = "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af"
|
||||||
const passwordMethod = getPasswordMethod(hashedPassword)
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
||||||
const expected: PasswordMethod = "SHA256"
|
const expected: util.PasswordMethod = "SHA256"
|
||||||
expect(passwordMethod).toEqual(expected)
|
expect(passwordMethod).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -251,63 +243,63 @@ describe("getPasswordMethod", () => {
|
||||||
describe("handlePasswordValidation", () => {
|
describe("handlePasswordValidation", () => {
|
||||||
it("should return true with a hashedPassword for a PLAIN_TEXT password", async () => {
|
it("should return true with a hashedPassword for a PLAIN_TEXT password", async () => {
|
||||||
const p = "password"
|
const p = "password"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "PLAIN_TEXT",
|
passwordMethod: "PLAIN_TEXT",
|
||||||
passwordFromRequestBody: p,
|
passwordFromRequestBody: p,
|
||||||
passwordFromArgs: p,
|
passwordFromArgs: p,
|
||||||
hashedPasswordFromArgs: undefined,
|
hashedPasswordFromArgs: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = await isHashMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(true)
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
||||||
expect(matchesHash).toBe(true)
|
expect(matchesHash).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false when PLAIN_TEXT password doesn't match args", async () => {
|
it("should return false when PLAIN_TEXT password doesn't match args", async () => {
|
||||||
const p = "password"
|
const p = "password"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "PLAIN_TEXT",
|
passwordMethod: "PLAIN_TEXT",
|
||||||
passwordFromRequestBody: "password1",
|
passwordFromRequestBody: "password1",
|
||||||
passwordFromArgs: p,
|
passwordFromArgs: p,
|
||||||
hashedPasswordFromArgs: undefined,
|
hashedPasswordFromArgs: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = await isHashMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(false)
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
||||||
expect(matchesHash).toBe(false)
|
expect(matchesHash).toBe(false)
|
||||||
})
|
})
|
||||||
it("should return true with a hashedPassword for a SHA256 password", async () => {
|
it("should return true with a hashedPassword for a SHA256 password", async () => {
|
||||||
const p = "helloworld"
|
const p = "helloworld"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "SHA256",
|
passwordMethod: "SHA256",
|
||||||
passwordFromRequestBody: p,
|
passwordFromRequestBody: p,
|
||||||
passwordFromArgs: undefined,
|
passwordFromArgs: undefined,
|
||||||
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = util.isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(true)
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
||||||
expect(matchesHash).toBe(true)
|
expect(matchesHash).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false when SHA256 password doesn't match hash", async () => {
|
it("should return false when SHA256 password doesn't match hash", async () => {
|
||||||
const p = "helloworld1"
|
const p = "helloworld1"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "SHA256",
|
passwordMethod: "SHA256",
|
||||||
passwordFromRequestBody: p,
|
passwordFromRequestBody: p,
|
||||||
passwordFromArgs: undefined,
|
passwordFromArgs: undefined,
|
||||||
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = util.isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(false)
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
||||||
expect(matchesHash).toBe(false)
|
expect(matchesHash).toBe(false)
|
||||||
})
|
})
|
||||||
it("should return true with a hashedPassword for a ARGON2 password", async () => {
|
it("should return true with a hashedPassword for a ARGON2 password", async () => {
|
||||||
const p = "password"
|
const p = "password"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "ARGON2",
|
passwordMethod: "ARGON2",
|
||||||
passwordFromRequestBody: p,
|
passwordFromRequestBody: p,
|
||||||
passwordFromArgs: undefined,
|
passwordFromArgs: undefined,
|
||||||
|
@ -315,14 +307,14 @@ describe("handlePasswordValidation", () => {
|
||||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = await isHashMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(true)
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
||||||
expect(matchesHash).toBe(true)
|
expect(matchesHash).toBe(true)
|
||||||
})
|
})
|
||||||
it("should return false when ARGON2 password doesn't match hash", async () => {
|
it("should return false when ARGON2 password doesn't match hash", async () => {
|
||||||
const p = "password1"
|
const p = "password1"
|
||||||
const passwordValidation = await handlePasswordValidation({
|
const passwordValidation = await util.handlePasswordValidation({
|
||||||
passwordMethod: "ARGON2",
|
passwordMethod: "ARGON2",
|
||||||
passwordFromRequestBody: p,
|
passwordFromRequestBody: p,
|
||||||
passwordFromArgs: undefined,
|
passwordFromArgs: undefined,
|
||||||
|
@ -330,7 +322,7 @@ describe("handlePasswordValidation", () => {
|
||||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchesHash = await isHashMatch(p, passwordValidation.hashedPassword)
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
||||||
|
|
||||||
expect(passwordValidation.isPasswordValid).toBe(false)
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
||||||
expect(matchesHash).toBe(false)
|
expect(matchesHash).toBe(false)
|
||||||
|
@ -339,7 +331,7 @@ describe("handlePasswordValidation", () => {
|
||||||
|
|
||||||
describe("isCookieValid", () => {
|
describe("isCookieValid", () => {
|
||||||
it("should be valid if hashed-password for SHA256 matches cookie.key", async () => {
|
it("should be valid if hashed-password for SHA256 matches cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "SHA256",
|
passwordMethod: "SHA256",
|
||||||
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
||||||
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
||||||
|
@ -348,7 +340,7 @@ describe("isCookieValid", () => {
|
||||||
expect(isValid).toBe(true)
|
expect(isValid).toBe(true)
|
||||||
})
|
})
|
||||||
it("should be invalid if hashed-password for SHA256 does not match cookie.key", async () => {
|
it("should be invalid if hashed-password for SHA256 does not match cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "SHA256",
|
passwordMethod: "SHA256",
|
||||||
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb9442bb6f8f8f07af",
|
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb9442bb6f8f8f07af",
|
||||||
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
||||||
|
@ -357,7 +349,7 @@ describe("isCookieValid", () => {
|
||||||
expect(isValid).toBe(false)
|
expect(isValid).toBe(false)
|
||||||
})
|
})
|
||||||
it("should be valid if hashed-password for ARGON2 matches cookie.key", async () => {
|
it("should be valid if hashed-password for ARGON2 matches cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "ARGON2",
|
passwordMethod: "ARGON2",
|
||||||
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||||
hashedPasswordFromArgs:
|
hashedPasswordFromArgs:
|
||||||
|
@ -367,7 +359,7 @@ describe("isCookieValid", () => {
|
||||||
expect(isValid).toBe(true)
|
expect(isValid).toBe(true)
|
||||||
})
|
})
|
||||||
it("should be invalid if hashed-password for ARGON2 does not match cookie.key", async () => {
|
it("should be invalid if hashed-password for ARGON2 does not match cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "ARGON2",
|
passwordMethod: "ARGON2",
|
||||||
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
||||||
hashedPasswordFromArgs:
|
hashedPasswordFromArgs:
|
||||||
|
@ -377,7 +369,7 @@ describe("isCookieValid", () => {
|
||||||
expect(isValid).toBe(false)
|
expect(isValid).toBe(false)
|
||||||
})
|
})
|
||||||
it("should be valid if password for PLAIN_TEXT matches cookie.key", async () => {
|
it("should be valid if password for PLAIN_TEXT matches cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "PLAIN_TEXT",
|
passwordMethod: "PLAIN_TEXT",
|
||||||
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||||
passwordFromArgs: "password",
|
passwordFromArgs: "password",
|
||||||
|
@ -386,7 +378,7 @@ describe("isCookieValid", () => {
|
||||||
expect(isValid).toBe(true)
|
expect(isValid).toBe(true)
|
||||||
})
|
})
|
||||||
it("should be invalid if hashed-password for PLAIN_TEXT does not match cookie.key", async () => {
|
it("should be invalid if hashed-password for PLAIN_TEXT does not match cookie.key", async () => {
|
||||||
const isValid = await isCookieValid({
|
const isValid = await util.isCookieValid({
|
||||||
passwordMethod: "PLAIN_TEXT",
|
passwordMethod: "PLAIN_TEXT",
|
||||||
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
||||||
passwordFromArgs: "password1234",
|
passwordFromArgs: "password1234",
|
||||||
|
@ -398,12 +390,47 @@ describe("isCookieValid", () => {
|
||||||
|
|
||||||
describe("sanitizeString", () => {
|
describe("sanitizeString", () => {
|
||||||
it("should return an empty string if passed a type other than a string", () => {
|
it("should return an empty string if passed a type other than a string", () => {
|
||||||
expect(sanitizeString({} as string)).toBe("")
|
expect(util.sanitizeString({} as string)).toBe("")
|
||||||
})
|
})
|
||||||
it("should trim whitespace", () => {
|
it("should trim whitespace", () => {
|
||||||
expect(sanitizeString(" hello ")).toBe("hello")
|
expect(util.sanitizeString(" hello ")).toBe("hello")
|
||||||
})
|
})
|
||||||
it("should always return an empty string", () => {
|
it("should always return an empty string", () => {
|
||||||
expect(sanitizeString(" ")).toBe("")
|
expect(util.sanitizeString(" ")).toBe("")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("onLine", () => {
|
||||||
|
// Spawn a process that outputs anything given on stdin.
|
||||||
|
let proc: cp.ChildProcess | undefined
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
proc = cp.spawn("node", ["-e", 'process.stdin.setEncoding("utf8");process.stdin.on("data", console.log)'])
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
proc?.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call with individual lines", async () => {
|
||||||
|
const size = 100
|
||||||
|
const received = new Promise<string[]>((resolve) => {
|
||||||
|
const lines: string[] = []
|
||||||
|
util.onLine(proc!, (line) => {
|
||||||
|
lines.push(line)
|
||||||
|
if (lines.length === size) {
|
||||||
|
resolve(lines)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const expected: string[] = []
|
||||||
|
for (let i = 0; i < size; ++i) {
|
||||||
|
expected.push(generateUuid(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
proc?.stdin?.write(expected.join("\n"))
|
||||||
|
|
||||||
|
expect(await received).toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
import {
|
import * as util from "../../src/common/util"
|
||||||
arrayify,
|
|
||||||
generateUuid,
|
|
||||||
getFirstString,
|
|
||||||
getOptions,
|
|
||||||
logError,
|
|
||||||
plural,
|
|
||||||
resolveBase,
|
|
||||||
split,
|
|
||||||
trimSlashes,
|
|
||||||
normalize,
|
|
||||||
} from "../../src/common/util"
|
|
||||||
import { createLoggerMock } from "../utils/helpers"
|
import { createLoggerMock } from "../utils/helpers"
|
||||||
|
|
||||||
const dom = new JSDOM()
|
const dom = new JSDOM()
|
||||||
|
@ -21,67 +10,67 @@ export type LocationLike = Pick<Location, "pathname" | "origin">
|
||||||
describe("util", () => {
|
describe("util", () => {
|
||||||
describe("normalize", () => {
|
describe("normalize", () => {
|
||||||
it("should remove multiple slashes", () => {
|
it("should remove multiple slashes", () => {
|
||||||
expect(normalize("//foo//bar//baz///mumble")).toBe("/foo/bar/baz/mumble")
|
expect(util.normalize("//foo//bar//baz///mumble")).toBe("/foo/bar/baz/mumble")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should remove trailing slashes", () => {
|
it("should remove trailing slashes", () => {
|
||||||
expect(normalize("qux///")).toBe("qux")
|
expect(util.normalize("qux///")).toBe("qux")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should preserve trailing slash if it exists", () => {
|
it("should preserve trailing slash if it exists", () => {
|
||||||
expect(normalize("qux///", true)).toBe("qux/")
|
expect(util.normalize("qux///", true)).toBe("qux/")
|
||||||
expect(normalize("qux", true)).toBe("qux")
|
expect(util.normalize("qux", true)).toBe("qux")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("split", () => {
|
describe("split", () => {
|
||||||
it("should split at a comma", () => {
|
it("should split at a comma", () => {
|
||||||
expect(split("Hello,world", ",")).toStrictEqual(["Hello", "world"])
|
expect(util.split("Hello,world", ",")).toStrictEqual(["Hello", "world"])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("shouldn't split if the delimiter doesn't exist", () => {
|
it("shouldn't split if the delimiter doesn't exist", () => {
|
||||||
expect(split("Hello world", ",")).toStrictEqual(["Hello world", ""])
|
expect(util.split("Hello world", ",")).toStrictEqual(["Hello world", ""])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("plural", () => {
|
describe("plural", () => {
|
||||||
it("should add an s if count is greater than 1", () => {
|
it("should add an s if count is greater than 1", () => {
|
||||||
expect(plural(2, "dog")).toBe("dogs")
|
expect(util.plural(2, "dog")).toBe("dogs")
|
||||||
})
|
})
|
||||||
it("should NOT add an s if the count is 1", () => {
|
it("should NOT add an s if the count is 1", () => {
|
||||||
expect(plural(1, "dog")).toBe("dog")
|
expect(util.plural(1, "dog")).toBe("dog")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("generateUuid", () => {
|
describe("generateUuid", () => {
|
||||||
it("should generate a unique uuid", () => {
|
it("should generate a unique uuid", () => {
|
||||||
const uuid = generateUuid()
|
const uuid = util.generateUuid()
|
||||||
const uuid2 = generateUuid()
|
const uuid2 = util.generateUuid()
|
||||||
expect(uuid).toHaveLength(24)
|
expect(uuid).toHaveLength(24)
|
||||||
expect(typeof uuid).toBe("string")
|
expect(typeof uuid).toBe("string")
|
||||||
expect(uuid).not.toBe(uuid2)
|
expect(uuid).not.toBe(uuid2)
|
||||||
})
|
})
|
||||||
it("should generate a uuid of a specific length", () => {
|
it("should generate a uuid of a specific length", () => {
|
||||||
const uuid = generateUuid(10)
|
const uuid = util.generateUuid(10)
|
||||||
expect(uuid).toHaveLength(10)
|
expect(uuid).toHaveLength(10)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("trimSlashes", () => {
|
describe("trimSlashes", () => {
|
||||||
it("should remove leading slashes", () => {
|
it("should remove leading slashes", () => {
|
||||||
expect(trimSlashes("/hello-world")).toBe("hello-world")
|
expect(util.trimSlashes("/hello-world")).toBe("hello-world")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should remove trailing slashes", () => {
|
it("should remove trailing slashes", () => {
|
||||||
expect(trimSlashes("hello-world/")).toBe("hello-world")
|
expect(util.trimSlashes("hello-world/")).toBe("hello-world")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should remove both leading and trailing slashes", () => {
|
it("should remove both leading and trailing slashes", () => {
|
||||||
expect(trimSlashes("/hello-world/")).toBe("hello-world")
|
expect(util.trimSlashes("/hello-world/")).toBe("hello-world")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should remove multiple leading and trailing slashes", () => {
|
it("should remove multiple leading and trailing slashes", () => {
|
||||||
expect(trimSlashes("///hello-world////")).toBe("hello-world")
|
expect(util.trimSlashes("///hello-world////")).toBe("hello-world")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -101,23 +90,23 @@ describe("util", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should resolve a base", () => {
|
it("should resolve a base", () => {
|
||||||
expect(resolveBase("localhost:8080")).toBe("/localhost:8080")
|
expect(util.resolveBase("localhost:8080")).toBe("/localhost:8080")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should resolve a base with a forward slash at the beginning", () => {
|
it("should resolve a base with a forward slash at the beginning", () => {
|
||||||
expect(resolveBase("/localhost:8080")).toBe("/localhost:8080")
|
expect(util.resolveBase("/localhost:8080")).toBe("/localhost:8080")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should resolve a base with query params", () => {
|
it("should resolve a base with query params", () => {
|
||||||
expect(resolveBase("localhost:8080?folder=hello-world")).toBe("/localhost:8080")
|
expect(util.resolveBase("localhost:8080?folder=hello-world")).toBe("/localhost:8080")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should resolve a base with a path", () => {
|
it("should resolve a base with a path", () => {
|
||||||
expect(resolveBase("localhost:8080/hello/world")).toBe("/localhost:8080/hello/world")
|
expect(util.resolveBase("localhost:8080/hello/world")).toBe("/localhost:8080/hello/world")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should resolve a base to an empty string when not provided", () => {
|
it("should resolve a base to an empty string when not provided", () => {
|
||||||
expect(resolveBase()).toBe("")
|
expect(util.resolveBase()).toBe("")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -142,7 +131,7 @@ describe("util", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return options with base and cssStaticBase even if it doesn't exist", () => {
|
it("should return options with base and cssStaticBase even if it doesn't exist", () => {
|
||||||
expect(getOptions()).toStrictEqual({
|
expect(util.getOptions()).toStrictEqual({
|
||||||
base: "",
|
base: "",
|
||||||
csStaticBase: "",
|
csStaticBase: "",
|
||||||
})
|
})
|
||||||
|
@ -162,7 +151,7 @@ describe("util", () => {
|
||||||
// it returns the element
|
// it returns the element
|
||||||
spy.mockImplementation(() => mockElement)
|
spy.mockImplementation(() => mockElement)
|
||||||
|
|
||||||
expect(getOptions()).toStrictEqual({
|
expect(util.getOptions()).toStrictEqual({
|
||||||
base: "",
|
base: "",
|
||||||
csStaticBase: "/static/development/Users/jp/Dev/code-server",
|
csStaticBase: "/static/development/Users/jp/Dev/code-server",
|
||||||
disableTelemetry: false,
|
disableTelemetry: false,
|
||||||
|
@ -179,7 +168,7 @@ describe("util", () => {
|
||||||
// spreads the original options
|
// spreads the original options
|
||||||
// then parses the queryOpts
|
// then parses the queryOpts
|
||||||
location.search = '?options={"logLevel":2}'
|
location.search = '?options={"logLevel":2}'
|
||||||
expect(getOptions()).toStrictEqual({
|
expect(util.getOptions()).toStrictEqual({
|
||||||
base: "",
|
base: "",
|
||||||
csStaticBase: "",
|
csStaticBase: "",
|
||||||
logLevel: 2,
|
logLevel: 2,
|
||||||
|
@ -189,12 +178,12 @@ describe("util", () => {
|
||||||
|
|
||||||
describe("arrayify", () => {
|
describe("arrayify", () => {
|
||||||
it("should return value it's already an array", () => {
|
it("should return value it's already an array", () => {
|
||||||
expect(arrayify(["hello", "world"])).toStrictEqual(["hello", "world"])
|
expect(util.arrayify(["hello", "world"])).toStrictEqual(["hello", "world"])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should wrap the value in an array if not an array", () => {
|
it("should wrap the value in an array if not an array", () => {
|
||||||
expect(
|
expect(
|
||||||
arrayify({
|
util.arrayify({
|
||||||
name: "Coder",
|
name: "Coder",
|
||||||
version: "3.8",
|
version: "3.8",
|
||||||
}),
|
}),
|
||||||
|
@ -202,21 +191,21 @@ describe("util", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return an empty array if the value is undefined", () => {
|
it("should return an empty array if the value is undefined", () => {
|
||||||
expect(arrayify(undefined)).toStrictEqual([])
|
expect(util.arrayify(undefined)).toStrictEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("getFirstString", () => {
|
describe("getFirstString", () => {
|
||||||
it("should return the string if passed a string", () => {
|
it("should return the string if passed a string", () => {
|
||||||
expect(getFirstString("Hello world!")).toBe("Hello world!")
|
expect(util.getFirstString("Hello world!")).toBe("Hello world!")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should get the first string from an array", () => {
|
it("should get the first string from an array", () => {
|
||||||
expect(getFirstString(["Hello", "World"])).toBe("Hello")
|
expect(util.getFirstString(["Hello", "World"])).toBe("Hello")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return undefined if the value isn't an array or a string", () => {
|
it("should return undefined if the value isn't an array or a string", () => {
|
||||||
expect(getFirstString({ name: "Coder" })).toBe(undefined)
|
expect(util.getFirstString({ name: "Coder" })).toBe(undefined)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -235,14 +224,14 @@ describe("util", () => {
|
||||||
const message = "You don't have access to that folder."
|
const message = "You don't have access to that folder."
|
||||||
const error = new Error(message)
|
const error = new Error(message)
|
||||||
|
|
||||||
logError(loggerModule.logger, "ui", error)
|
util.logError(loggerModule.logger, "ui", error)
|
||||||
|
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
expect(loggerModule.logger.error).toHaveBeenCalled()
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`)
|
expect(loggerModule.logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should log an error, even if not an instance of error", () => {
|
it("should log an error, even if not an instance of error", () => {
|
||||||
logError(loggerModule.logger, "api", "oh no")
|
util.logError(loggerModule.logger, "api", "oh no")
|
||||||
|
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
expect(loggerModule.logger.error).toHaveBeenCalled()
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalledWith("api: oh no")
|
expect(loggerModule.logger.error).toHaveBeenCalledWith("api: oh no")
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
|
export const PASSWORD = "e45432jklfdsab"
|
||||||
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
|
export const workspaceDir = "workspaces"
|
||||||
export const storageState = JSON.parse(process.env.STORAGE || "{}")
|
|
||||||
|
|
|
@ -1,40 +1,43 @@
|
||||||
// This setup runs before our e2e tests
|
import { Cookie } from "playwright"
|
||||||
// so that it authenticates us into code-server
|
|
||||||
// ensuring that we're logged in before we run any tests
|
|
||||||
import { chromium } from "playwright"
|
|
||||||
import { hash } from "../../src/node/util"
|
import { hash } from "../../src/node/util"
|
||||||
import { PASSWORD } from "./constants"
|
import { PASSWORD, workspaceDir } from "./constants"
|
||||||
|
import { clean } from "./helpers"
|
||||||
import * as wtfnode from "./wtfnode"
|
import * as wtfnode from "./wtfnode"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform workspace cleanup and authenticate. This should be set up to run
|
||||||
|
* before our tests execute.
|
||||||
|
*/
|
||||||
export default async function () {
|
export default async function () {
|
||||||
console.log("\n🚨 Running Global Setup for Playwright End-to-End Tests")
|
console.log("\n🚨 Running Global Setup for Playwright End-to-End Tests")
|
||||||
console.log(" Please hang tight...")
|
console.log(" Please hang tight...")
|
||||||
|
|
||||||
const cookieToStore = {
|
// Cleanup workspaces from previous tests.
|
||||||
sameSite: "Lax" as const,
|
await clean(workspaceDir)
|
||||||
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.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as fs from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
|
||||||
|
@ -20,13 +20,20 @@ export function createLoggerMock() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a uniquely named temporary directory.
|
* Clean up directories left by a test. It is recommended to do this when a test
|
||||||
*
|
* starts to avoid potentially accumulating infinite test directories.
|
||||||
* These directories are placed under a single temporary code-server directory.
|
*/
|
||||||
|
export async function clean(testName: string): Promise<void> {
|
||||||
|
const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`)
|
||||||
|
await fs.rmdir(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a uniquely named temporary directory for a test.
|
||||||
*/
|
*/
|
||||||
export async function tmpdir(testName: string): Promise<string> {
|
export async function tmpdir(testName: string): Promise<string> {
|
||||||
const dir = path.join(os.tmpdir(), "code-server/tests")
|
const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`)
|
||||||
await fs.promises.mkdir(dir, { recursive: true })
|
await fs.mkdir(dir, { recursive: true })
|
||||||
|
|
||||||
return await fs.promises.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" })
|
return await fs.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" })
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue