Implement last opened functionality (#4633)
* Implement last opened functionality Fixes https://github.com/cdr/code-server/issues/4619 * Fix test temp dirs not being cleaned up * Mock logger everywhere This suppresses all the error and debug output we generate which makes it hard to actually find which test has failed. It also gives us a standard way to test logging for the few places we do that. * Use separate data directories for unit test instances Exactly as we do for the e2e tests. * Add integration tests for vscode route * Make settings use --user-data-dir Without this test instances step on each other feet and they also clobber your own non-test settings. * Make redirects consistent They will preserve the trailing slash if there is one. * Remove compilation check If you do a regular non-watch build there are no compilation stats so this bricks VS Code in CI when running the unit tests. I am not sure how best to fix this for the case where you have a build that has not been packaged yet so I just removed it for now and added a message to check if VS Code is compiling when in dev mode. * Update code-server update endpoint name
This commit is contained in:
parent
b990dabed1
commit
c4c480a068
|
@ -38,6 +38,8 @@ implementation (#4414).
|
||||||
vscode-remote-resource endpoint still can.
|
vscode-remote-resource endpoint still can.
|
||||||
- OpenVSX has been made the default marketplace. However this means web
|
- OpenVSX has been made the default marketplace. However this means web
|
||||||
extensions like Vim may be broken.
|
extensions like Vim may be broken.
|
||||||
|
- The last opened folder/workspace is no longer stored separately in the
|
||||||
|
settings file (we rely on the already-existing query object instead).
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { spawn, fork, ChildProcess } from "child_process"
|
import { spawn, fork, ChildProcess } from "child_process"
|
||||||
import { promises as fs } from "fs"
|
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { CompilationStats, onLine, OnLineCallback } from "../../src/node/util"
|
import { onLine, OnLineCallback } from "../../src/node/util"
|
||||||
|
|
||||||
interface DevelopmentCompilers {
|
interface DevelopmentCompilers {
|
||||||
[key: string]: ChildProcess | undefined
|
[key: string]: ChildProcess | undefined
|
||||||
|
@ -16,7 +15,6 @@ class Watcher {
|
||||||
private readonly paths = {
|
private readonly paths = {
|
||||||
/** Path to uncompiled VS Code source. */
|
/** Path to uncompiled VS Code source. */
|
||||||
vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"),
|
vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"),
|
||||||
compilationStatsFile: path.join(this.rootPath, "out", "watcher.json"),
|
|
||||||
pluginDir: process.env.PLUGIN_DIR,
|
pluginDir: process.env.PLUGIN_DIR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +86,6 @@ class Watcher {
|
||||||
|
|
||||||
if (strippedLine.includes("Finished compilation with")) {
|
if (strippedLine.includes("Finished compilation with")) {
|
||||||
console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)")
|
console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)")
|
||||||
this.emitCompilationStats()
|
|
||||||
this.reloadWebServer()
|
this.reloadWebServer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,19 +115,6 @@ class Watcher {
|
||||||
|
|
||||||
//#region Utilities
|
//#region Utilities
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits a file containing compilation data.
|
|
||||||
* This is especially useful when Express needs to determine if VS Code is still compiling.
|
|
||||||
*/
|
|
||||||
private emitCompilationStats(): Promise<void> {
|
|
||||||
const stats: CompilationStats = {
|
|
||||||
lastCompiledAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Writing watcher stats...")
|
|
||||||
return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
private dispose(code: number | null): void {
|
private dispose(code: number | null): void {
|
||||||
for (const [processName, devProcess] of Object.entries(this.compilers)) {
|
for (const [processName, devProcess] of Object.entries(this.compilers)) {
|
||||||
console.log(`[${processName}]`, "Killing...\n")
|
console.log(`[${processName}]`, "Killing...\n")
|
||||||
|
|
|
@ -158,6 +158,7 @@
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^.+\\.(css|less)$": "<rootDir>/test/utils/cssStub.ts"
|
"^.+\\.(css|less)$": "<rootDir>/test/utils/cssStub.ts"
|
||||||
}
|
},
|
||||||
|
"globalSetup": "<rootDir>/test/utils/globalUnitSetup.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { normalize } from "../common/util"
|
||||||
import { AuthType, DefaultedArgs } from "./cli"
|
import { AuthType, DefaultedArgs } from "./cli"
|
||||||
import { version as codeServerVersion } from "./constants"
|
import { version as codeServerVersion } from "./constants"
|
||||||
import { Heart } from "./heart"
|
import { Heart } from "./heart"
|
||||||
|
import { CoderSettings, SettingsProvider } from "./settings"
|
||||||
|
import { UpdateProvider } from "./update"
|
||||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
|
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,6 +31,8 @@ declare global {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
args: DefaultedArgs
|
args: DefaultedArgs
|
||||||
heart: Heart
|
heart: Heart
|
||||||
|
settings: SettingsProvider<CoderSettings>
|
||||||
|
updater: UpdateProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,8 +139,8 @@ export const relativeRoot = (originalUrl: string): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect relatively to `/${to}`. Query variables on the current URI will be preserved.
|
* Redirect relatively to `/${to}`. Query variables on the current URI will be
|
||||||
* `to` should be a simple path without any query parameters
|
* preserved. `to` should be a simple path without any query parameters
|
||||||
* `override` will merge with the existing query (use `undefined` to unset).
|
* `override` will merge with the existing query (use `undefined` to unset).
|
||||||
*/
|
*/
|
||||||
export const redirect = (
|
export const redirect = (
|
||||||
|
@ -284,3 +288,10 @@ export const getCookieOptions = (req: express.Request): express.CookieOptions =>
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the full path to the current page, preserving any trailing slash.
|
||||||
|
*/
|
||||||
|
export const self = (req: express.Request): string => {
|
||||||
|
return normalize(`${req.baseUrl}${req.originalUrl.endsWith("/") ? "/" : ""}`, true)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Request, Router } from "express"
|
import { Request, Router } from "express"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { normalize } from "../../common/util"
|
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
|
||||||
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
|
||||||
import { proxy } from "../proxy"
|
import { proxy } from "../proxy"
|
||||||
import { Router as WsRouter } from "../wsRouter"
|
import { Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
|
@ -56,7 +55,7 @@ router.all("*", async (req, res, next) => {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
// Redirect all other pages to the login.
|
// Redirect all other pages to the login.
|
||||||
const to = normalize(`${req.baseUrl}${req.path}`)
|
const to = self(req)
|
||||||
return redirect(req, res, "login", {
|
return redirect(req, res, "login", {
|
||||||
to: to !== "/" ? to : undefined,
|
to: to !== "/" ? to : undefined,
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { commit, rootPath } from "../constants"
|
||||||
import { Heart } from "../heart"
|
import { Heart } from "../heart"
|
||||||
import { ensureAuthenticated, redirect } from "../http"
|
import { ensureAuthenticated, redirect } from "../http"
|
||||||
import { PluginAPI } from "../plugin"
|
import { PluginAPI } from "../plugin"
|
||||||
|
import { CoderSettings, SettingsProvider } from "../settings"
|
||||||
|
import { UpdateProvider } from "../update"
|
||||||
import { getMediaMime, paths } from "../util"
|
import { getMediaMime, paths } from "../util"
|
||||||
import * as apps from "./apps"
|
import * as apps from "./apps"
|
||||||
import * as domainProxy from "./domainProxy"
|
import * as domainProxy from "./domainProxy"
|
||||||
|
@ -47,6 +49,9 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
||||||
app.router.use(cookieParser())
|
app.router.use(cookieParser())
|
||||||
app.wsRouter.use(cookieParser())
|
app.wsRouter.use(cookieParser())
|
||||||
|
|
||||||
|
const settings = new SettingsProvider<CoderSettings>(path.join(args["user-data-dir"], "coder.json"))
|
||||||
|
const updater = new UpdateProvider("https://api.github.com/repos/coder/code-server/releases/latest", settings)
|
||||||
|
|
||||||
const common: express.RequestHandler = (req, _, next) => {
|
const common: express.RequestHandler = (req, _, next) => {
|
||||||
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
||||||
// it look like code-server is always in use.
|
// it look like code-server is always in use.
|
||||||
|
@ -57,6 +62,8 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
|
||||||
// Add common variables routes can use.
|
// Add common variables routes can use.
|
||||||
req.args = args
|
req.args = args
|
||||||
req.heart = heart
|
req.heart = heart
|
||||||
|
req.settings = settings
|
||||||
|
req.updater = updater
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,7 @@ import * as path from "path"
|
||||||
import * as qs from "qs"
|
import * as qs from "qs"
|
||||||
import * as pluginapi from "../../../typings/pluginapi"
|
import * as pluginapi from "../../../typings/pluginapi"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
import { normalize } from "../../common/util"
|
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
|
||||||
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
|
||||||
import { proxy as _proxy } from "../proxy"
|
import { proxy as _proxy } from "../proxy"
|
||||||
|
|
||||||
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
|
const getProxyTarget = (req: Request, passthroughPath?: boolean): string => {
|
||||||
|
@ -25,7 +24,7 @@ export function proxy(
|
||||||
if (!authenticated(req)) {
|
if (!authenticated(req)) {
|
||||||
// If visiting the root (/:port only) redirect to the login page.
|
// If visiting the root (/:port only) redirect to the login page.
|
||||||
if (!req.params[0] || req.params[0] === "/") {
|
if (!req.params[0] || req.params[0] === "/") {
|
||||||
const to = normalize(`${req.baseUrl}${req.path}`)
|
const to = self(req)
|
||||||
return redirect(req, res, "login", {
|
return redirect(req, res, "login", {
|
||||||
to: to !== "/" ? to : undefined,
|
to: to !== "/" ? to : undefined,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import { version } from "../constants"
|
import { version } from "../constants"
|
||||||
import { ensureAuthenticated } from "../http"
|
import { ensureAuthenticated } from "../http"
|
||||||
import { UpdateProvider } from "../update"
|
|
||||||
|
|
||||||
export const router = Router()
|
export const router = Router()
|
||||||
|
|
||||||
const provider = new UpdateProvider()
|
|
||||||
|
|
||||||
router.get("/check", ensureAuthenticated, async (req, res) => {
|
router.get("/check", ensureAuthenticated, async (req, res) => {
|
||||||
const update = await provider.getUpdate(req.query.force === "true")
|
const update = await req.updater.getUpdate(req.query.force === "true")
|
||||||
res.json({
|
res.json({
|
||||||
checked: update.checked,
|
checked: update.checked,
|
||||||
latest: update.version,
|
latest: update.version,
|
||||||
current: version,
|
current: version,
|
||||||
isLatest: provider.isLatestVersion(update),
|
isLatest: req.updater.isLatestVersion(update),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { logger } from "@coder/logger"
|
||||||
import * as express from "express"
|
import * as express from "express"
|
||||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
import { WebsocketRequest } from "../../../typings/pluginapi"
|
||||||
import { logError } from "../../common/util"
|
import { logError } from "../../common/util"
|
||||||
import { isDevMode } from "../constants"
|
|
||||||
import { toVsCodeArgs } from "../cli"
|
import { toVsCodeArgs } from "../cli"
|
||||||
import { ensureAuthenticated, authenticated, redirect } from "../http"
|
import { isDevMode } from "../constants"
|
||||||
import { loadAMDModule, readCompilationStats } from "../util"
|
import { authenticated, ensureAuthenticated, redirect, self } from "../http"
|
||||||
|
import { loadAMDModule } from "../util"
|
||||||
import { Router as WsRouter } from "../wsRouter"
|
import { Router as WsRouter } from "../wsRouter"
|
||||||
import { errorHandler } from "./errors"
|
import { errorHandler } from "./errors"
|
||||||
|
|
||||||
|
@ -25,12 +25,39 @@ export class CodeServerRouteWrapper {
|
||||||
const isAuthenticated = await authenticated(req)
|
const isAuthenticated = await authenticated(req)
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
|
const to = self(req)
|
||||||
return redirect(req, res, "login", {
|
return redirect(req, res, "login", {
|
||||||
// req.baseUrl can be blank if already at the root.
|
to: to !== "/" ? to : undefined,
|
||||||
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { query } = await req.settings.read()
|
||||||
|
if (query) {
|
||||||
|
// Ew means the workspace was closed so clear the last folder/workspace.
|
||||||
|
if (req.query.ew) {
|
||||||
|
delete query.folder
|
||||||
|
delete query.workspace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to the last folder/workspace if nothing else is opened.
|
||||||
|
if (
|
||||||
|
!req.query.folder &&
|
||||||
|
!req.query.workspace &&
|
||||||
|
(query.folder || query.workspace) &&
|
||||||
|
!req.args["ignore-last-opened"] // This flag disables this behavior.
|
||||||
|
) {
|
||||||
|
const to = self(req)
|
||||||
|
return redirect(req, res, to, {
|
||||||
|
folder: query.folder,
|
||||||
|
workspace: query.workspace,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the query parameters so we can use them on the next load. This
|
||||||
|
// also allows users to create functionality around query parameters.
|
||||||
|
await req.settings.write({ query: req.query })
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,15 +93,6 @@ export class CodeServerRouteWrapper {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDevMode) {
|
|
||||||
// Is the development mode file watcher still busy?
|
|
||||||
const compileStats = await readCompilationStats()
|
|
||||||
|
|
||||||
if (!compileStats || !compileStats.lastCompiledAt) {
|
|
||||||
return next(new Error("VS Code may still be compiling..."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the server...
|
// Create the server...
|
||||||
|
|
||||||
const { args } = req
|
const { args } = req
|
||||||
|
@ -89,9 +107,12 @@ export class CodeServerRouteWrapper {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._codeServerMain = await createVSServer(null, await toVsCodeArgs(args))
|
this._codeServerMain = await createVSServer(null, await toVsCodeArgs(args))
|
||||||
} catch (createServerError) {
|
} catch (error) {
|
||||||
logError(logger, "CodeServerRouteWrapper", createServerError)
|
logError(logger, "CodeServerRouteWrapper", error)
|
||||||
return next(createServerError)
|
if (isDevMode) {
|
||||||
|
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
|
||||||
|
}
|
||||||
|
return next(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
import { Query } from "express-serve-static-core"
|
import { Query } from "express-serve-static-core"
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as path from "path"
|
|
||||||
import { paths } from "./util"
|
|
||||||
|
|
||||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||||
|
|
||||||
|
@ -54,14 +52,5 @@ export interface UpdateSettings {
|
||||||
* Global code-server settings.
|
* Global code-server settings.
|
||||||
*/
|
*/
|
||||||
export interface CoderSettings extends UpdateSettings {
|
export interface CoderSettings extends UpdateSettings {
|
||||||
lastVisited: {
|
query?: Query
|
||||||
url: string
|
|
||||||
workspace: boolean
|
|
||||||
}
|
|
||||||
query: Query
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Global code-server settings file.
|
|
||||||
*/
|
|
||||||
export const settings = new SettingsProvider<CoderSettings>(path.join(paths.data, "coder.json"))
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as https from "https"
|
||||||
import * as semver from "semver"
|
import * as semver from "semver"
|
||||||
import * as url from "url"
|
import * as url from "url"
|
||||||
import { version } from "./constants"
|
import { version } from "./constants"
|
||||||
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
|
import { SettingsProvider, UpdateSettings } from "./settings"
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
checked: number
|
checked: number
|
||||||
|
@ -27,12 +27,11 @@ export class UpdateProvider {
|
||||||
* The URL for getting the latest version of code-server. Should return JSON
|
* The URL for getting the latest version of code-server. Should return JSON
|
||||||
* that fulfills `LatestResponse`.
|
* that fulfills `LatestResponse`.
|
||||||
*/
|
*/
|
||||||
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
|
private readonly latestUrl: string,
|
||||||
/**
|
/**
|
||||||
* Update information will be stored here. If not provided, the global
|
* Update information will be stored here.
|
||||||
* settings will be used.
|
|
||||||
*/
|
*/
|
||||||
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
private readonly settings: SettingsProvider<UpdateSettings>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,15 +3,14 @@ import * as argon2 from "argon2"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import envPaths from "env-paths"
|
import envPaths from "env-paths"
|
||||||
import { promises as fs, Stats } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import safeCompare from "safe-compare"
|
import safeCompare from "safe-compare"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
import xdgBasedir from "xdg-basedir"
|
import xdgBasedir from "xdg-basedir"
|
||||||
import { logError } from "../common/util"
|
import { vsRootPath } from "./constants"
|
||||||
import { isDevMode, rootPath, vsRootPath } from "./constants"
|
|
||||||
|
|
||||||
export interface Paths {
|
export interface Paths {
|
||||||
data: string
|
data: string
|
||||||
|
@ -523,34 +522,3 @@ export const loadAMDModule = async <T>(amdPath: string, exportName: string): Pro
|
||||||
|
|
||||||
return module[exportName] as T
|
return module[exportName] as T
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompilationStats {
|
|
||||||
lastCompiledAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readCompilationStats = async (): Promise<null | CompilationStats> => {
|
|
||||||
if (!isDevMode) {
|
|
||||||
throw new Error("Compilation stats are only present in development")
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(rootPath, "out/watcher.json")
|
|
||||||
let stat: Stats
|
|
||||||
try {
|
|
||||||
stat = await fs.stat(filePath)
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stat.isFile()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = await fs.readFile(filePath)
|
|
||||||
return JSON.parse(file.toString("utf-8"))
|
|
||||||
} catch (error) {
|
|
||||||
logError(logger, "VS Code", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as fs from "fs"
|
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import util from "util"
|
import util from "util"
|
||||||
import { tmpdir } from "../utils/helpers"
|
import { clean, tmpdir } from "../utils/helpers"
|
||||||
import { describe, expect, test } from "./baseFixture"
|
import { describe, expect, test } from "./baseFixture"
|
||||||
|
|
||||||
describe("Integrated Terminal", true, () => {
|
describe("Integrated Terminal", true, () => {
|
||||||
|
@ -10,20 +9,16 @@ describe("Integrated Terminal", true, () => {
|
||||||
// so we don't have to logged in
|
// so we don't have to logged in
|
||||||
const testFileName = "pipe"
|
const testFileName = "pipe"
|
||||||
const testString = "new string test from e2e test"
|
const testString = "new string test from e2e test"
|
||||||
let tmpFolderPath = ""
|
|
||||||
let tmpFile = ""
|
|
||||||
|
|
||||||
|
const testName = "integrated-terminal"
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
tmpFolderPath = await tmpdir("integrated-terminal")
|
await clean(testName)
|
||||||
tmpFile = path.join(tmpFolderPath, testFileName)
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
|
||||||
// Ensure directory was removed
|
|
||||||
await fs.promises.rmdir(tmpFolderPath, { recursive: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should echo a string to a file", async ({ codeServerPage }) => {
|
test("should echo a string to a file", async ({ codeServerPage }) => {
|
||||||
|
const tmpFolderPath = await tmpdir(testName)
|
||||||
|
const tmpFile = path.join(tmpFolderPath, testFileName)
|
||||||
|
|
||||||
const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'`
|
const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'`
|
||||||
const exec = util.promisify(cp.exec)
|
const exec = util.promisify(cp.exec)
|
||||||
const output = exec(command, { encoding: "utf8" })
|
const output = exec(command, { encoding: "utf8" })
|
||||||
|
|
|
@ -12,7 +12,7 @@ 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: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
|
retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
|
||||||
globalSetup: require.resolve("./utils/globalSetup.ts"),
|
globalSetup: require.resolve("./utils/globalE2eSetup.ts"),
|
||||||
reporter: "list",
|
reporter: "list",
|
||||||
// Put any shared options on the top level.
|
// Put any shared options on the top level.
|
||||||
use: {
|
use: {
|
||||||
|
|
|
@ -1,24 +1,16 @@
|
||||||
// Note: we need to import logger from the root
|
|
||||||
// because this is the logger used in logError in ../src/common/util
|
|
||||||
import { logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
|
|
||||||
import { Emitter } from "../../../src/common/emitter"
|
import { Emitter } from "../../../src/common/emitter"
|
||||||
|
import { mockLogger } from "../../utils/helpers"
|
||||||
|
|
||||||
describe("emitter", () => {
|
describe("emitter", () => {
|
||||||
let spy: jest.SpyInstance
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spy = jest.spyOn(logger, "error")
|
mockLogger()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should run the correct callbacks", async () => {
|
it("should run the correct callbacks", async () => {
|
||||||
const HELLO_WORLD = "HELLO_WORLD"
|
const HELLO_WORLD = "HELLO_WORLD"
|
||||||
const GOODBYE_WORLD = "GOODBYE_WORLD"
|
const GOODBYE_WORLD = "GOODBYE_WORLD"
|
||||||
|
@ -85,8 +77,8 @@ describe("emitter", () => {
|
||||||
await emitter.emit({ event: HELLO_WORLD, callback: mockCallback })
|
await emitter.emit({ event: HELLO_WORLD, callback: mockCallback })
|
||||||
|
|
||||||
// Check that error was called
|
// Check that error was called
|
||||||
expect(spy).toHaveBeenCalled()
|
expect(logger.error).toHaveBeenCalled()
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(message)
|
expect(logger.error).toHaveBeenCalledWith(message)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
import { JSDOM } from "jsdom"
|
import { JSDOM } from "jsdom"
|
||||||
import * as util from "../../../src/common/util"
|
import * as util from "../../../src/common/util"
|
||||||
import { createLoggerMock } from "../../utils/helpers"
|
import { mockLogger } from "../../utils/helpers"
|
||||||
|
|
||||||
const dom = new JSDOM()
|
const dom = new JSDOM()
|
||||||
global.document = dom.window.document
|
global.document = dom.window.document
|
||||||
|
@ -94,31 +95,29 @@ describe("util", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("logError", () => {
|
describe("logError", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
mockLogger()
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
const loggerModule = createLoggerMock()
|
|
||||||
|
|
||||||
it("should log an error with the message and stack trace", () => {
|
it("should log an error with the message and stack trace", () => {
|
||||||
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)
|
||||||
|
|
||||||
util.logError(loggerModule.logger, "ui", error)
|
util.logError(logger, "ui", error)
|
||||||
|
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
expect(logger.error).toHaveBeenCalled()
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalledWith(`ui: ${error.message} ${error.stack}`)
|
expect(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", () => {
|
||||||
util.logError(loggerModule.logger, "api", "oh no")
|
util.logError(logger, "api", "oh no")
|
||||||
|
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
expect(logger.error).toHaveBeenCalled()
|
||||||
expect(loggerModule.logger.error).toHaveBeenCalledWith("api: oh no")
|
expect(logger.error).toHaveBeenCalledWith("api: oh no")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import { getAvailablePort, tmpdir, useEnv } from "../../test/utils/helpers"
|
import { clean, getAvailablePort, tmpdir, useEnv } from "../../test/utils/helpers"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file is for testing test helpers (not core code).
|
* This file is for testing test helpers (not core code).
|
||||||
*/
|
*/
|
||||||
describe("test helpers", () => {
|
describe("test helpers", () => {
|
||||||
it("should return a temp directory", async () => {
|
|
||||||
const testName = "temp-dir"
|
const testName = "temp-dir"
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clean(testName)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a temp directory", async () => {
|
||||||
const pathToTempDir = await tmpdir(testName)
|
const pathToTempDir = await tmpdir(testName)
|
||||||
expect(pathToTempDir).toContain(testName)
|
expect(pathToTempDir).toContain(testName)
|
||||||
expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined)
|
expect(fs.access(pathToTempDir)).resolves.toStrictEqual(undefined)
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`handleServerError should log an error if resolved is true 1`] = `"Cannot read property 'handle' of undefined"`;
|
|
|
@ -1,27 +1,29 @@
|
||||||
import { logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
import { promises, rmdirSync } from "fs"
|
import { promises } from "fs"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as https from "https"
|
import * as https from "https"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { createApp, ensureAddress, handleArgsSocketCatchError, handleServerError } from "../../../src/node/app"
|
import { createApp, ensureAddress, handleArgsSocketCatchError, handleServerError } from "../../../src/node/app"
|
||||||
import { OptionalString, setDefaults } from "../../../src/node/cli"
|
import { OptionalString, setDefaults } from "../../../src/node/cli"
|
||||||
import { generateCertificate } from "../../../src/node/util"
|
import { generateCertificate } from "../../../src/node/util"
|
||||||
import { getAvailablePort, tmpdir } from "../../utils/helpers"
|
import { clean, mockLogger, getAvailablePort, tmpdir } from "../../utils/helpers"
|
||||||
|
|
||||||
describe("createApp", () => {
|
describe("createApp", () => {
|
||||||
let spy: jest.SpyInstance
|
|
||||||
let unlinkSpy: jest.SpyInstance
|
let unlinkSpy: jest.SpyInstance
|
||||||
let port: number
|
let port: number
|
||||||
let tmpDirPath: string
|
let tmpDirPath: string
|
||||||
let tmpFilePath: string
|
let tmpFilePath: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
tmpDirPath = await tmpdir("unlink-socket")
|
mockLogger()
|
||||||
|
|
||||||
|
const testName = "unlink-socket"
|
||||||
|
await clean(testName)
|
||||||
|
tmpDirPath = await tmpdir(testName)
|
||||||
tmpFilePath = path.join(tmpDirPath, "unlink-socket-file")
|
tmpFilePath = path.join(tmpDirPath, "unlink-socket-file")
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
spy = jest.spyOn(logger, "error")
|
|
||||||
// NOTE:@jsjoeio
|
// NOTE:@jsjoeio
|
||||||
// Be mindful when spying.
|
// Be mindful when spying.
|
||||||
// You can't spy on fs functions if you do import * as fs
|
// You can't spy on fs functions if you do import * as fs
|
||||||
|
@ -36,12 +38,6 @@ describe("createApp", () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
// Ensure directory was removed
|
|
||||||
rmdirSync(tmpDirPath, { recursive: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should return an Express app, a WebSockets Express app and an http server", async () => {
|
it("should return an Express app, a WebSockets Express app and an http server", async () => {
|
||||||
const defaultArgs = await setDefaults({
|
const defaultArgs = await setDefaults({
|
||||||
port,
|
port,
|
||||||
|
@ -70,8 +66,8 @@ describe("createApp", () => {
|
||||||
// By emitting an error event
|
// By emitting an error event
|
||||||
// Ref: https://stackoverflow.com/a/33872506/3015595
|
// Ref: https://stackoverflow.com/a/33872506/3015595
|
||||||
app.server.emit("error", testError)
|
app.server.emit("error", testError)
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(`http server error: ${testError.message} ${testError.stack}`)
|
expect(logger.error).toHaveBeenCalledWith(`http server error: ${testError.message} ${testError.stack}`)
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
app.dispose()
|
app.dispose()
|
||||||
|
@ -152,20 +148,14 @@ describe("ensureAddress", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("handleServerError", () => {
|
describe("handleServerError", () => {
|
||||||
let spy: jest.SpyInstance
|
beforeAll(() => {
|
||||||
|
mockLogger()
|
||||||
beforeEach(() => {
|
|
||||||
spy = jest.spyOn(logger, "error")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should call reject if resolved is false", async () => {
|
it("should call reject if resolved is false", async () => {
|
||||||
const resolved = false
|
const resolved = false
|
||||||
const reject = jest.fn((err: Error) => undefined)
|
const reject = jest.fn((err: Error) => undefined)
|
||||||
|
@ -184,33 +174,27 @@ describe("handleServerError", () => {
|
||||||
|
|
||||||
handleServerError(resolved, error, reject)
|
handleServerError(resolved, error, reject)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toThrowErrorMatchingSnapshot()
|
expect(logger.error).toHaveBeenCalledWith(`http server error: ${error.message} ${error.stack}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("handleArgsSocketCatchError", () => {
|
describe("handleArgsSocketCatchError", () => {
|
||||||
let spy: jest.SpyInstance
|
beforeAll(() => {
|
||||||
|
mockLogger()
|
||||||
beforeEach(() => {
|
|
||||||
spy = jest.spyOn(logger, "error")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should log an error if its not an NodeJS.ErrnoException", () => {
|
it("should log an error if its not an NodeJS.ErrnoException", () => {
|
||||||
const error = new Error()
|
const error = new Error()
|
||||||
|
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(error)
|
expect(logger.error).toHaveBeenCalledWith(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should log an error if its not an NodeJS.ErrnoException (and the error has a message)", () => {
|
it("should log an error if its not an NodeJS.ErrnoException (and the error has a message)", () => {
|
||||||
|
@ -219,8 +203,8 @@ describe("handleArgsSocketCatchError", () => {
|
||||||
|
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(errorMessage)
|
expect(logger.error).toHaveBeenCalledWith(errorMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not log an error if its a iNodeJS.ErrnoException", () => {
|
it("should not log an error if its a iNodeJS.ErrnoException", () => {
|
||||||
|
@ -229,7 +213,7 @@ describe("handleArgsSocketCatchError", () => {
|
||||||
|
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(0)
|
expect(logger.error).toHaveBeenCalledTimes(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should log an error if the code is not ENOENT (and the error has a message)", () => {
|
it("should log an error if the code is not ENOENT (and the error has a message)", () => {
|
||||||
|
@ -240,8 +224,8 @@ describe("handleArgsSocketCatchError", () => {
|
||||||
|
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(errorMessage)
|
expect(logger.error).toHaveBeenCalledWith(errorMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should log an error if the code is not ENOENT", () => {
|
it("should log an error if the code is not ENOENT", () => {
|
||||||
|
@ -250,7 +234,7 @@ describe("handleArgsSocketCatchError", () => {
|
||||||
|
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(1)
|
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||||
expect(spy).toHaveBeenCalledWith(error)
|
expect(logger.error).toHaveBeenCalledWith(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -361,13 +361,11 @@ describe("parser", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("cli", () => {
|
describe("cli", () => {
|
||||||
let testDir: string
|
const testName = "cli"
|
||||||
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
testDir = await tmpdir("cli")
|
await clean(testName)
|
||||||
await fs.rmdir(testDir, { recursive: true })
|
|
||||||
await fs.mkdir(testDir, { recursive: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -416,6 +414,7 @@ describe("cli", () => {
|
||||||
args._ = ["./file"]
|
args._ = ["./file"]
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||||
|
|
||||||
|
const testDir = await tmpdir(testName)
|
||||||
const socketPath = path.join(testDir, "socket")
|
const socketPath = path.join(testDir, "socket")
|
||||||
await fs.writeFile(vscodeIpcPath, socketPath)
|
await fs.writeFile(vscodeIpcPath, socketPath)
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||||
|
@ -635,14 +634,15 @@ describe("readSocketPath", () => {
|
||||||
let tmpDirPath: string
|
let tmpDirPath: string
|
||||||
let tmpFilePath: string
|
let tmpFilePath: string
|
||||||
|
|
||||||
beforeEach(async () => {
|
const testName = "readSocketPath"
|
||||||
tmpDirPath = await tmpdir("readSocketPath")
|
beforeAll(async () => {
|
||||||
tmpFilePath = path.join(tmpDirPath, "readSocketPath.txt")
|
await clean(testName)
|
||||||
await fs.writeFile(tmpFilePath, fileContents)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
beforeEach(async () => {
|
||||||
await fs.rmdir(tmpDirPath, { recursive: true })
|
tmpDirPath = await tmpdir(testName)
|
||||||
|
tmpFilePath = path.join(tmpDirPath, "readSocketPath.txt")
|
||||||
|
await fs.writeFile(tmpFilePath, fileContents)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should throw an error if it can't read the file", async () => {
|
it("should throw an error if it can't read the file", async () => {
|
||||||
|
@ -677,9 +677,10 @@ describe("toVsCodeArgs", () => {
|
||||||
version: false,
|
version: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testName = "vscode-args"
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Clean up temporary directories from the previous run.
|
// Clean up temporary directories from the previous run.
|
||||||
await clean("vscode-args")
|
await clean(testName)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should convert empty args", async () => {
|
it("should convert empty args", async () => {
|
||||||
|
@ -691,7 +692,7 @@ describe("toVsCodeArgs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should convert with workspace", async () => {
|
it("should convert with workspace", async () => {
|
||||||
const workspace = path.join(await tmpdir("vscode-args"), "test.code-workspace")
|
const workspace = path.join(await tmpdir(testName), "test.code-workspace")
|
||||||
await fs.writeFile(workspace, "foobar")
|
await fs.writeFile(workspace, "foobar")
|
||||||
expect(await toVsCodeArgs(await setDefaults(parse([workspace])))).toStrictEqual({
|
expect(await toVsCodeArgs(await setDefaults(parse([workspace])))).toStrictEqual({
|
||||||
...vscodeDefaults,
|
...vscodeDefaults,
|
||||||
|
@ -702,7 +703,7 @@ describe("toVsCodeArgs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should convert with folder", async () => {
|
it("should convert with folder", async () => {
|
||||||
const folder = await tmpdir("vscode-args")
|
const folder = await tmpdir(testName)
|
||||||
expect(await toVsCodeArgs(await setDefaults(parse([folder])))).toStrictEqual({
|
expect(await toVsCodeArgs(await setDefaults(parse([folder])))).toStrictEqual({
|
||||||
...vscodeDefaults,
|
...vscodeDefaults,
|
||||||
folder,
|
folder,
|
||||||
|
@ -712,7 +713,7 @@ describe("toVsCodeArgs", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should ignore regular file", async () => {
|
it("should ignore regular file", async () => {
|
||||||
const file = path.join(await tmpdir("vscode-args"), "file")
|
const file = path.join(await tmpdir(testName), "file")
|
||||||
await fs.writeFile(file, "foobar")
|
await fs.writeFile(file, "foobar")
|
||||||
expect(await toVsCodeArgs(await setDefaults(parse([file])))).toStrictEqual({
|
expect(await toVsCodeArgs(await setDefaults(parse([file])))).toStrictEqual({
|
||||||
...vscodeDefaults,
|
...vscodeDefaults,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { createLoggerMock } from "../../utils/helpers"
|
import { logger } from "@coder/logger"
|
||||||
|
import { mockLogger } from "../../utils/helpers"
|
||||||
|
|
||||||
describe("constants", () => {
|
describe("constants", () => {
|
||||||
let constants: typeof import("../../../src/node/constants")
|
let constants: typeof import("../../../src/node/constants")
|
||||||
|
|
||||||
describe("with package.json defined", () => {
|
describe("with package.json defined", () => {
|
||||||
const loggerModule = createLoggerMock()
|
|
||||||
const mockPackageJson = {
|
const mockPackageJson = {
|
||||||
name: "mock-code-server",
|
name: "mock-code-server",
|
||||||
description: "Run VS Code on a remote server.",
|
description: "Run VS Code on a remote server.",
|
||||||
|
@ -14,7 +14,7 @@ describe("constants", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.mock("@coder/logger", () => loggerModule)
|
mockLogger()
|
||||||
jest.mock("../../../package.json", () => mockPackageJson, { virtual: true })
|
jest.mock("../../../package.json", () => mockPackageJson, { virtual: true })
|
||||||
constants = require("../../../src/node/constants")
|
constants = require("../../../src/node/constants")
|
||||||
})
|
})
|
||||||
|
@ -38,8 +38,8 @@ describe("constants", () => {
|
||||||
|
|
||||||
constants.getPackageJson("./package.json")
|
constants.getPackageJson("./package.json")
|
||||||
|
|
||||||
expect(loggerModule.logger.warn).toHaveBeenCalled()
|
expect(logger.warn).toHaveBeenCalled()
|
||||||
expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage)
|
expect(logger.warn).toHaveBeenCalledWith(expectedErrorMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should find the package.json", () => {
|
it("should find the package.json", () => {
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { shouldEnableProxy } from "../../../src/node/proxy_agent"
|
import { shouldEnableProxy } from "../../../src/node/proxy_agent"
|
||||||
import { useEnv } from "../../utils/helpers"
|
import { mockLogger, useEnv } from "../../utils/helpers"
|
||||||
|
|
||||||
describe("shouldEnableProxy", () => {
|
describe("shouldEnableProxy", () => {
|
||||||
const [setHTTPProxy, resetHTTPProxy] = useEnv("HTTP_PROXY")
|
const [setHTTPProxy, resetHTTPProxy] = useEnv("HTTP_PROXY")
|
||||||
const [setHTTPSProxy, resetHTTPSProxy] = useEnv("HTTPS_PROXY")
|
const [setHTTPSProxy, resetHTTPSProxy] = useEnv("HTTPS_PROXY")
|
||||||
const [setNoProxy, resetNoProxy] = useEnv("NO_PROXY")
|
const [setNoProxy, resetNoProxy] = useEnv("NO_PROXY")
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockLogger()
|
||||||
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetModules() // Most important - it clears the cache
|
jest.resetModules() // Most important - it clears the cache
|
||||||
resetHTTPProxy()
|
resetHTTPProxy()
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { RateLimiter } from "../../../../src/node/routes/login"
|
import { RateLimiter } from "../../../../src/node/routes/login"
|
||||||
|
import { mockLogger } from "../../../utils/helpers"
|
||||||
import * as httpserver from "../../../utils/httpserver"
|
import * as httpserver from "../../../utils/httpserver"
|
||||||
import * as integration from "../../../utils/integration"
|
import * as integration from "../../../utils/integration"
|
||||||
|
|
||||||
describe("login", () => {
|
describe("login", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
mockLogger()
|
||||||
|
})
|
||||||
|
|
||||||
describe("RateLimiter", () => {
|
describe("RateLimiter", () => {
|
||||||
it("should allow one try ", () => {
|
it("should allow one try ", () => {
|
||||||
const limiter = new RateLimiter()
|
const limiter = new RateLimiter()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { rootPath } from "../../../../src/node/constants"
|
import { rootPath } from "../../../../src/node/constants"
|
||||||
import { tmpdir } from "../../../utils/helpers"
|
import { clean, tmpdir } from "../../../utils/helpers"
|
||||||
import * as httpserver from "../../../utils/httpserver"
|
import * as httpserver from "../../../utils/httpserver"
|
||||||
import * as integration from "../../../utils/integration"
|
import * as integration from "../../../utils/integration"
|
||||||
|
|
||||||
|
@ -23,8 +23,10 @@ describe("/_static", () => {
|
||||||
let testFileContent: string | undefined
|
let testFileContent: string | undefined
|
||||||
let nonExistentTestFile: string | undefined
|
let nonExistentTestFile: string | undefined
|
||||||
|
|
||||||
|
const testName = "_static"
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const testDir = await tmpdir("_static")
|
await clean(testName)
|
||||||
|
const testDir = await tmpdir(testName)
|
||||||
testFile = path.join(testDir, "test")
|
testFile = path.join(testDir, "test")
|
||||||
testFileContent = "static file contents"
|
testFileContent = "static file contents"
|
||||||
nonExistentTestFile = path.join(testDir, "i-am-not-here")
|
nonExistentTestFile = path.join(testDir, "i-am-not-here")
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { Response } from "node-fetch"
|
||||||
|
import * as path from "path"
|
||||||
|
import { clean, tmpdir } from "../../../utils/helpers"
|
||||||
|
import * as httpserver from "../../../utils/httpserver"
|
||||||
|
import * as integration from "../../../utils/integration"
|
||||||
|
|
||||||
|
interface WorkbenchConfig {
|
||||||
|
folderUri?: {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
workspaceUri?: {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("vscode", () => {
|
||||||
|
let codeServer: httpserver.HttpServer | undefined
|
||||||
|
|
||||||
|
const testName = "vscode"
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clean(testName)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (codeServer) {
|
||||||
|
await codeServer.dispose()
|
||||||
|
codeServer = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const routes = ["/", "/vscode", "/vscode/"]
|
||||||
|
|
||||||
|
it("should load all route variations", async () => {
|
||||||
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
const resp = await codeServer.fetch(route)
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
const html = await resp.text()
|
||||||
|
const url = new URL(resp.url) // Check there were no redirections.
|
||||||
|
expect(url.pathname + decodeURIComponent(url.search)).toBe(route)
|
||||||
|
switch (route) {
|
||||||
|
case "/":
|
||||||
|
case "/vscode/":
|
||||||
|
expect(html).toContain(`src="./static/`)
|
||||||
|
break
|
||||||
|
case "/vscode":
|
||||||
|
expect(html).toContain(`src="./vscode/static/`)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the workbench config from the provided response.
|
||||||
|
*/
|
||||||
|
const getConfig = async (resp: Response): Promise<WorkbenchConfig> => {
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
const html = await resp.text()
|
||||||
|
const match = html.match(/<meta id="vscode-workbench-web-configuration" data-settings="(.+)">/)
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
throw new Error("Unable to find workbench configuration")
|
||||||
|
}
|
||||||
|
const config = match[1].replace(/"/g, '"')
|
||||||
|
try {
|
||||||
|
return JSON.parse(config)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse workbench configuration", config)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should have no default folder or workspace", async () => {
|
||||||
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
|
|
||||||
|
const config = await getConfig(await codeServer.fetch("/"))
|
||||||
|
expect(config.folderUri).toBeUndefined()
|
||||||
|
expect(config.workspaceUri).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have a default folder", async () => {
|
||||||
|
const defaultDir = await tmpdir(testName)
|
||||||
|
codeServer = await integration.setup(["--auth=none", defaultDir], "")
|
||||||
|
|
||||||
|
// At first it will load the directory provided on the command line.
|
||||||
|
const config = await getConfig(await codeServer.fetch("/"))
|
||||||
|
expect(config.folderUri?.path).toBe(defaultDir)
|
||||||
|
expect(config.workspaceUri).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should have a default workspace", async () => {
|
||||||
|
const defaultWorkspace = path.join(await tmpdir(testName), "test.code-workspace")
|
||||||
|
await fs.writeFile(defaultWorkspace, "")
|
||||||
|
codeServer = await integration.setup(["--auth=none", defaultWorkspace], "")
|
||||||
|
|
||||||
|
// At first it will load the workspace provided on the command line.
|
||||||
|
const config = await getConfig(await codeServer.fetch("/"))
|
||||||
|
expect(config.folderUri).toBeUndefined()
|
||||||
|
expect(config.workspaceUri?.path).toBe(defaultWorkspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should redirect to last query folder/workspace", async () => {
|
||||||
|
codeServer = await integration.setup(["--auth=none"], "")
|
||||||
|
|
||||||
|
const folder = await tmpdir(testName)
|
||||||
|
const workspace = path.join(await tmpdir(testName), "test.code-workspace")
|
||||||
|
let resp = await codeServer.fetch("/", undefined, {
|
||||||
|
folder,
|
||||||
|
workspace,
|
||||||
|
})
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
await resp.text()
|
||||||
|
|
||||||
|
// If you visit again without query parameters it will re-attach them by
|
||||||
|
// redirecting. It should always redirect to the same route.
|
||||||
|
for (const route of routes) {
|
||||||
|
resp = await codeServer.fetch(route)
|
||||||
|
const url = new URL(resp.url)
|
||||||
|
expect(url.pathname).toBe(route)
|
||||||
|
expect(decodeURIComponent(url.search)).toBe(`?folder=${folder}&workspace=${workspace}`)
|
||||||
|
await resp.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closing the folder should stop the redirecting.
|
||||||
|
resp = await codeServer.fetch("/", undefined, { ew: "true" })
|
||||||
|
let url = new URL(resp.url)
|
||||||
|
expect(url.pathname).toBe("/")
|
||||||
|
expect(decodeURIComponent(url.search)).toBe("?ew=true")
|
||||||
|
await resp.text()
|
||||||
|
|
||||||
|
resp = await codeServer.fetch("/")
|
||||||
|
url = new URL(resp.url)
|
||||||
|
expect(url.pathname).toBe("/")
|
||||||
|
expect(decodeURIComponent(url.search)).toBe("")
|
||||||
|
await resp.text()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not redirect when last opened is ignored", async () => {
|
||||||
|
codeServer = await integration.setup(["--auth=none", "--ignore-last-opened"], "")
|
||||||
|
|
||||||
|
const folder = await tmpdir(testName)
|
||||||
|
const workspace = path.join(await tmpdir(testName), "test.code-workspace")
|
||||||
|
let resp = await codeServer.fetch("/", undefined, {
|
||||||
|
folder,
|
||||||
|
workspace,
|
||||||
|
})
|
||||||
|
expect(resp.status).toBe(200)
|
||||||
|
await resp.text()
|
||||||
|
|
||||||
|
// No redirections.
|
||||||
|
resp = await codeServer.fetch("/")
|
||||||
|
const url = new URL(resp.url)
|
||||||
|
expect(url.pathname).toBe("/")
|
||||||
|
expect(decodeURIComponent(url.search)).toBe("")
|
||||||
|
await resp.text()
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,9 +1,8 @@
|
||||||
import { promises as fs } from "fs"
|
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { tmpdir } from "../../../src/node/constants"
|
|
||||||
import { SettingsProvider, UpdateSettings } from "../../../src/node/settings"
|
import { SettingsProvider, UpdateSettings } from "../../../src/node/settings"
|
||||||
import { LatestResponse, UpdateProvider } from "../../../src/node/update"
|
import { LatestResponse, UpdateProvider } from "../../../src/node/update"
|
||||||
|
import { clean, mockLogger, tmpdir } from "../../utils/helpers"
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
let version = "1.0.0"
|
let version = "1.0.0"
|
||||||
|
@ -29,22 +28,31 @@ describe("update", () => {
|
||||||
response.end("not found")
|
response.end("not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
|
let _settings: SettingsProvider<UpdateSettings> | undefined
|
||||||
const settings = new SettingsProvider<UpdateSettings>(jsonPath)
|
const settings = (): SettingsProvider<UpdateSettings> => {
|
||||||
|
if (!_settings) {
|
||||||
|
throw new Error("Settings provider has not been created")
|
||||||
|
}
|
||||||
|
return _settings
|
||||||
|
}
|
||||||
|
|
||||||
let _provider: UpdateProvider | undefined
|
let _provider: UpdateProvider | undefined
|
||||||
const provider = (): UpdateProvider => {
|
const provider = (): UpdateProvider => {
|
||||||
if (!_provider) {
|
if (!_provider) {
|
||||||
const address = server.address()
|
throw new Error("Update provider has not been created")
|
||||||
if (!address || typeof address === "string" || !address.port) {
|
|
||||||
throw new Error("unexpected address")
|
|
||||||
}
|
|
||||||
_provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings)
|
|
||||||
}
|
}
|
||||||
return _provider
|
return _provider
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
mockLogger()
|
||||||
|
|
||||||
|
const testName = "update"
|
||||||
|
await clean(testName)
|
||||||
|
const testDir = await tmpdir(testName)
|
||||||
|
const jsonPath = path.join(testDir, "update.json")
|
||||||
|
_settings = new SettingsProvider<UpdateSettings>(jsonPath)
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
server.on("error", reject)
|
server.on("error", reject)
|
||||||
server.on("listening", resolve)
|
server.on("listening", resolve)
|
||||||
|
@ -53,8 +61,13 @@ describe("update", () => {
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
await fs.rmdir(path.join(tmpdir, "tests/updates"), { recursive: true })
|
|
||||||
await fs.mkdir(path.join(tmpdir, "tests/updates"), { recursive: true })
|
const address = server.address()
|
||||||
|
if (!address || typeof address === "string" || !address.port) {
|
||||||
|
throw new Error("unexpected address")
|
||||||
|
}
|
||||||
|
|
||||||
|
_provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, _settings)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -72,7 +85,7 @@ describe("update", () => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const update = await p.getUpdate()
|
const update = await p.getUpdate()
|
||||||
|
|
||||||
await expect(settings.read()).resolves.toEqual({ update })
|
await expect(settings().read()).resolves.toEqual({ update })
|
||||||
expect(isNaN(update.checked)).toEqual(false)
|
expect(isNaN(update.checked)).toEqual(false)
|
||||||
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
|
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
|
||||||
expect(update.version).toStrictEqual("2.1.0")
|
expect(update.version).toStrictEqual("2.1.0")
|
||||||
|
@ -86,7 +99,7 @@ describe("update", () => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const update = await p.getUpdate()
|
const update = await p.getUpdate()
|
||||||
|
|
||||||
await expect(settings.read()).resolves.toEqual({ update })
|
await expect(settings().read()).resolves.toEqual({ update })
|
||||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||||
expect(update.checked < now).toBe(true)
|
expect(update.checked < now).toBe(true)
|
||||||
expect(update.version).toStrictEqual("2.1.0")
|
expect(update.version).toStrictEqual("2.1.0")
|
||||||
|
@ -100,7 +113,7 @@ describe("update", () => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const update = await p.getUpdate(true)
|
const update = await p.getUpdate(true)
|
||||||
|
|
||||||
await expect(settings.read()).resolves.toEqual({ update })
|
await expect(settings().read()).resolves.toEqual({ update })
|
||||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||||
expect(update.checked < Date.now() && update.checked >= now).toStrictEqual(true)
|
expect(update.checked < Date.now() && update.checked >= now).toStrictEqual(true)
|
||||||
expect(update.version).toStrictEqual("4.1.1")
|
expect(update.version).toStrictEqual("4.1.1")
|
||||||
|
@ -113,12 +126,12 @@ describe("update", () => {
|
||||||
expect(spy).toEqual([])
|
expect(spy).toEqual([])
|
||||||
|
|
||||||
let checked = Date.now() - 1000 * 60 * 60 * 23
|
let checked = Date.now() - 1000 * 60 * 60 * 23
|
||||||
await settings.write({ update: { checked, version } })
|
await settings().write({ update: { checked, version } })
|
||||||
await p.getUpdate()
|
await p.getUpdate()
|
||||||
expect(spy).toEqual([])
|
expect(spy).toEqual([])
|
||||||
|
|
||||||
checked = Date.now() - 1000 * 60 * 60 * 25
|
checked = Date.now() - 1000 * 60 * 60 * 25
|
||||||
await settings.write({ update: { checked, version } })
|
await settings().write({ update: { checked, version } })
|
||||||
|
|
||||||
const update = await p.getUpdate()
|
const update = await p.getUpdate()
|
||||||
expect(update.checked).not.toStrictEqual(checked)
|
expect(update.checked).not.toStrictEqual(checked)
|
||||||
|
@ -143,14 +156,14 @@ describe("update", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not reject if unable to fetch", async () => {
|
it("should not reject if unable to fetch", async () => {
|
||||||
let provider = new UpdateProvider("invalid", settings)
|
let provider = new UpdateProvider("invalid", settings())
|
||||||
let now = Date.now()
|
let now = Date.now()
|
||||||
let update = await provider.getUpdate(true)
|
let update = await provider.getUpdate(true)
|
||||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||||
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
|
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
|
||||||
expect(update.version).toStrictEqual("unknown")
|
expect(update.version).toStrictEqual("unknown")
|
||||||
|
|
||||||
provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings)
|
provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings())
|
||||||
now = Date.now()
|
now = Date.now()
|
||||||
update = await provider.getUpdate(true)
|
update = await provider.getUpdate(true)
|
||||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||||
|
|
|
@ -6,8 +6,8 @@ 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
|
* Perform workspace cleanup and authenticate. This should be ran before e2e
|
||||||
* before our tests execute.
|
* 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")
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { workspaceDir } from "./constants"
|
||||||
|
import { clean } from "./helpers"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform workspace cleanup. This should be ran before unit tests execute.
|
||||||
|
*/
|
||||||
|
export default async function () {
|
||||||
|
await clean(workspaceDir)
|
||||||
|
}
|
|
@ -1,23 +1,26 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a mock of @coder/logger.
|
* Spy on the logger and console and replace with mock implementations to
|
||||||
|
* suppress the output.
|
||||||
*/
|
*/
|
||||||
export function createLoggerMock() {
|
export function mockLogger() {
|
||||||
return {
|
jest.spyOn(logger, "debug").mockImplementation()
|
||||||
field: jest.fn(),
|
jest.spyOn(logger, "error").mockImplementation()
|
||||||
level: 2,
|
jest.spyOn(logger, "info").mockImplementation()
|
||||||
logger: {
|
jest.spyOn(logger, "trace").mockImplementation()
|
||||||
debug: jest.fn(),
|
jest.spyOn(logger, "warn").mockImplementation()
|
||||||
error: jest.fn(),
|
|
||||||
info: jest.fn(),
|
jest.spyOn(console, "log").mockImplementation()
|
||||||
trace: jest.fn(),
|
jest.spyOn(console, "debug").mockImplementation()
|
||||||
warn: jest.fn(),
|
jest.spyOn(console, "error").mockImplementation()
|
||||||
},
|
jest.spyOn(console, "info").mockImplementation()
|
||||||
}
|
jest.spyOn(console, "trace").mockImplementation()
|
||||||
|
jest.spyOn(console, "warn").mockImplementation()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +34,8 @@ export async function clean(testName: string): Promise<void> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a uniquely named temporary directory for a test.
|
* Create a uniquely named temporary directory for a test.
|
||||||
|
*
|
||||||
|
* `tmpdir` should usually be preceeded by at least one call to `clean`.
|
||||||
*/
|
*/
|
||||||
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/${testName}`)
|
const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`)
|
||||||
|
|
|
@ -59,13 +59,17 @@ export class HttpServer {
|
||||||
* fetch fetches the request path.
|
* fetch fetches the request path.
|
||||||
* The request path must be rooted!
|
* The request path must be rooted!
|
||||||
*/
|
*/
|
||||||
public fetch(requestPath: string, opts?: RequestInit): Promise<Response> {
|
public fetch(requestPath: string, opts?: RequestInit, query?: { [key: string]: string }): Promise<Response> {
|
||||||
const address = ensureAddress(this.hs, "http")
|
const address = ensureAddress(this.hs, "http")
|
||||||
if (typeof address === "string") {
|
if (typeof address === "string") {
|
||||||
throw new Error("Cannot fetch socket path")
|
throw new Error("Cannot fetch socket path")
|
||||||
}
|
}
|
||||||
address.pathname = requestPath
|
address.pathname = requestPath
|
||||||
|
if (query) {
|
||||||
|
Object.keys(query).forEach((key) => {
|
||||||
|
address.searchParams.append(key, query[key])
|
||||||
|
})
|
||||||
|
}
|
||||||
return nodeFetch(address.toString(), opts)
|
return nodeFetch(address.toString(), opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,27 @@
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import * as path from "path"
|
||||||
import { parse, parseConfigFile, setDefaults } from "../../src/node/cli"
|
import { parse, parseConfigFile, setDefaults } from "../../src/node/cli"
|
||||||
import { runCodeServer } from "../../src/node/main"
|
import { runCodeServer } from "../../src/node/main"
|
||||||
|
import { workspaceDir } from "./constants"
|
||||||
|
import { tmpdir } from "./helpers"
|
||||||
import * as httpserver from "./httpserver"
|
import * as httpserver from "./httpserver"
|
||||||
|
|
||||||
export async function setup(argv: string[], configFile?: string): Promise<httpserver.HttpServer> {
|
export async function setup(argv: string[], configFile?: string): Promise<httpserver.HttpServer> {
|
||||||
argv = ["--bind-addr=localhost:0", "--log=warn", ...argv]
|
// This will be used as the data directory to ensure instances do not bleed
|
||||||
|
// into each other.
|
||||||
|
const dir = await tmpdir(workspaceDir)
|
||||||
|
|
||||||
const cliArgs = parse(argv)
|
// VS Code complains if the logs dir is missing which spams the output.
|
||||||
|
// TODO: Does that mean we are not creating it when we should be?
|
||||||
|
await fs.mkdir(path.join(dir, "logs"))
|
||||||
|
|
||||||
|
const cliArgs = parse([
|
||||||
|
`--config=${path.join(dir, "config.yaml")}`,
|
||||||
|
`--user-data-dir=${dir}`,
|
||||||
|
"--bind-addr=localhost:0",
|
||||||
|
"--log=warn",
|
||||||
|
...argv,
|
||||||
|
])
|
||||||
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
|
const configArgs = parseConfigFile(configFile || "", "test/integration.ts")
|
||||||
const args = await setDefaults(cliArgs, configArgs)
|
const args = await setDefaults(cliArgs, configArgs)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue