Merge pull request #3277 from code-asher/logout

This commit is contained in:
Asher 2021-05-04 15:12:59 -05:00 committed by GitHub
commit 75e9e24e92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 127 additions and 125 deletions

View File

@ -19,3 +19,4 @@
# These are code-server code symlinks. # These are code-server code symlinks.
src/vs/base/node/proxy_agent.ts src/vs/base/node/proxy_agent.ts
src/vs/ipc.d.ts src/vs/ipc.d.ts
src/vs/server/common/util.ts

View File

@ -1,8 +1,10 @@
import * as path from 'vs/base/common/path'; import * as path from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { Options } from 'vs/ipc'; import { Options } from 'vs/ipc';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
@ -11,10 +13,18 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { TelemetryChannelClient } from 'vs/server/common/telemetry'; import { TelemetryChannelClient } from 'vs/server/common/telemetry';
import { getOptions } from 'vs/server/common/util';
import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; import 'vs/workbench/contrib/localizations/browser/localizations.contribution';
import 'vs/workbench/services/localizations/browser/localizationsService'; import 'vs/workbench/services/localizations/browser/localizationsService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
/**
* All client-side customization to VS Code should live in this file when
* possible.
*/
const options = getOptions<Options>();
class TelemetryService extends TelemetryChannelClient { class TelemetryService extends TelemetryChannelClient {
public constructor( public constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService, @IRemoteAgentService remoteAgentService: IRemoteAgentService,
@ -23,26 +33,6 @@ class TelemetryService extends TelemetryChannelClient {
} }
} }
/**
* Remove extra slashes in a URL.
*/
export const normalize = (url: string, keepTrailing = false): string => {
return url.replace(/\/\/+/g, '/').replace(/\/+$/, keepTrailing ? '/' : '');
};
/**
* Get options embedded in the HTML.
*/
export const getOptions = <T extends Options>(): T => {
try {
return JSON.parse(document.getElementById('coder-options')!.getAttribute('data-settings')!);
} catch (error) {
return {} as T;
}
};
const options = getOptions();
const TELEMETRY_SECTION_ID = 'telemetry'; const TELEMETRY_SECTION_ID = 'telemetry';
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({ Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
'id': TELEMETRY_SECTION_ID, 'id': TELEMETRY_SECTION_ID,
@ -173,38 +163,36 @@ export const initialize = async (services: ServiceCollection): Promise<void> =>
if (theme) { if (theme) {
localStorage.setItem('colorThemeData', theme); localStorage.setItem('colorThemeData', theme);
} }
};
export interface Query { // Use to show or hide logout commands and menu options.
[key: string]: string | undefined; const contextKeyService = (services.get(IContextKeyService) as IContextKeyService);
} contextKeyService.createKey('code-server.authed', options.authed);
/** // Add a logout command.
* Split a string up to the delimiter. If the delimiter doesn't exist the first const logoutEndpoint = path.join(options.base, '/logout') + `?base=${options.base}`;
* item will have all the text and the second item will be an empty string. const LOGOUT_COMMAND_ID = 'code-server.logout';
*/ CommandsRegistry.registerCommand(
export const split = (str: string, delimiter: string): [string, string] => { LOGOUT_COMMAND_ID,
const index = str.indexOf(delimiter); () => {
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, '']; window.location.href = logoutEndpoint;
}; },
);
/** // Add logout to command palette.
* Return the URL modified with the specified query variables. It's pretty MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
* stupid so it probably doesn't cover any edge cases. Undefined values will command: {
* unset existing values. Doesn't allow duplicates. id: LOGOUT_COMMAND_ID,
*/ title: localize('logout', "Log out")
export const withQuery = (url: string, replace: Query): string => { },
const uri = URI.parse(url); when: ContextKeyExpr.has('code-server.authed')
const query = { ...replace }; });
uri.query.split('&').forEach((kv) => {
const [key, value] = split(kv, '='); // Add logout to the (web-only) home menu.
if (!(key in query)) { MenuRegistry.appendMenuItem(MenuId.MenubarHomeMenu, {
query[key] = value; command: {
} id: LOGOUT_COMMAND_ID,
title: localize('logout', "Log out")
},
when: ContextKeyExpr.has('code-server.authed')
}); });
return uri.with({
query: Object.keys(query)
.filter((k) => typeof query[k] !== 'undefined')
.map((k) => `${k}=${query[k]}`).join('&'),
}).toString(true);
}; };

View File

@ -1,3 +0,0 @@
export enum Cookie {
Key = 'key',
}

View File

@ -0,0 +1 @@
../../../../../../src/common/util.ts

View File

@ -9,7 +9,7 @@ import { registerThemingParticipant, IThemeService } from 'vs/platform/theme/com
import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { MenuBarVisibility, getTitleBarStyle, IWindowOpenable, getMenuBarVisibility } from 'vs/platform/windows/common/windows';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions';
import { addDisposableListener, Dimension, EventType, getCookieValue } from 'vs/base/browser/dom'; import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform'; import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform';
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
@ -38,8 +38,6 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys';
import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICommandService } from 'vs/platform/commands/common/commands';
import { ILogService } from 'vs/platform/log/common/log';
import { Cookie } from 'vs/server/common/cookie';
export type IOpenRecentAction = IAction & { uri: URI, remoteAuthority?: string }; export type IOpenRecentAction = IAction & { uri: URI, remoteAuthority?: string };
@ -318,8 +316,7 @@ export class CustomMenubarControl extends MenubarControl {
@IThemeService private readonly themeService: IThemeService, @IThemeService private readonly themeService: IThemeService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@IHostService protected readonly hostService: IHostService, @IHostService protected readonly hostService: IHostService,
@ICommandService commandService: ICommandService, @ICommandService commandService: ICommandService
@ILogService private readonly logService: ILogService
) { ) {
super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService); super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService);
@ -721,28 +718,6 @@ export class CustomMenubarControl extends MenubarControl {
webNavigationActions.pop(); webNavigationActions.pop();
} }
webNavigationActions.push(new Action('logout', localize('logout', "Log out"), undefined, true,
async (event?: MouseEvent) => {
const COOKIE_KEY = Cookie.Key;
const loginCookie = getCookieValue(COOKIE_KEY);
this.logService.info('Logging out of code-server');
if(loginCookie) {
this.logService.info(`Removing cookie under ${COOKIE_KEY}`);
if (document && document.cookie) {
// We delete the cookie by setting the expiration to a date/time in the past
document.cookie = COOKIE_KEY +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
window.location.href = '/login';
} else {
this.logService.warn('Could not delete cookie because document and/or document.cookie is undefined');
}
} else {
this.logService.warn('Could not log out because we could not find cookie');
}
}));
return webNavigationActions; return webNavigationActions;
} }

View File

@ -1,3 +1,4 @@
import { logger } from "@coder/logger"
import { getOptions, normalize, logError } from "../common/util" import { getOptions, normalize, logError } from "../common/util"
import "./pages/error.css" import "./pages/error.css"
@ -6,19 +7,21 @@ import "./pages/login.css"
export async function registerServiceWorker(): Promise<void> { export async function registerServiceWorker(): Promise<void> {
const options = getOptions() const options = getOptions()
logger.level = options.logLevel
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`) const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
try { try {
await navigator.serviceWorker.register(path, { await navigator.serviceWorker.register(path, {
scope: options.base + "/", scope: options.base + "/",
}) })
console.log("[Service Worker] registered") logger.info(`[Service Worker] registered`)
} catch (error) { } catch (error) {
logError(`[Service Worker] registration`, error) logError(logger, `[Service Worker] registration`, error)
} }
} }
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) { if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
registerServiceWorker() registerServiceWorker()
} else { } else {
console.error(`[Service Worker] navigator is undefined`) logger.error(`[Service Worker] navigator is undefined`)
} }

View File

@ -1,5 +1,13 @@
import { logger, field } from "@coder/logger" /*
* This file exists in two locations:
* - src/common/util.ts
* - lib/vscode/src/vs/server/common/util.ts
* The second is a symlink to the first.
*/
/**
* Base options included on every page.
*/
export interface Options { export interface Options {
base: string base: string
csStaticBase: string csStaticBase: string
@ -69,6 +77,9 @@ export const getOptions = <T extends Options>(): T => {
options = {} as T options = {} as T
} }
// You can also pass options in stringified form to the options query
// variable. Options provided here will override the ones in the options
// element.
const params = new URLSearchParams(location.search) const params = new URLSearchParams(location.search)
const queryOpts = params.get("options") const queryOpts = params.get("options")
if (queryOpts) { if (queryOpts) {
@ -78,13 +89,9 @@ export const getOptions = <T extends Options>(): T => {
} }
} }
logger.level = options.logLevel
options.base = resolveBase(options.base) options.base = resolveBase(options.base)
options.csStaticBase = resolveBase(options.csStaticBase) options.csStaticBase = resolveBase(options.csStaticBase)
logger.debug("got options", field("options", options))
return options return options
} }
@ -113,7 +120,8 @@ export const getFirstString = (value: string | string[] | object | undefined): s
return typeof value === "string" ? value : undefined return typeof value === "string" ? value : undefined
} }
export function logError(prefix: string, err: any): void { // TODO: Might make sense to add Error handling to the logger itself.
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void {
if (err instanceof Error) { if (err instanceof Error) {
logger.error(`${prefix}: ${err.message} ${err.stack}`) logger.error(`${prefix}: ${err.message} ${err.stack}`)
} else { } else {

View File

@ -37,7 +37,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
reject(err) reject(err)
} else { } else {
// Promise resolved earlier so this is an unrelated error. // Promise resolved earlier so this is an unrelated error.
util.logError("http server error", err) util.logError(logger, "http server error", err)
} }
}) })

View File

@ -20,6 +20,7 @@ import * as apps from "./apps"
import * as domainProxy from "./domainProxy" import * as domainProxy from "./domainProxy"
import * as health from "./health" import * as health from "./health"
import * as login from "./login" import * as login from "./login"
import * as logout from "./logout"
import * as pathProxy from "./pathProxy" import * as pathProxy from "./pathProxy"
// static is a reserved keyword. // static is a reserved keyword.
import * as _static from "./static" import * as _static from "./static"
@ -136,10 +137,10 @@ export const register = async (
if (args.auth === AuthType.Password) { if (args.auth === AuthType.Password) {
app.use("/login", login.router) app.use("/login", login.router)
app.use("/logout", logout.router)
} else { } else {
app.all("/login", (req, res) => { app.all("/login", (req, res) => redirect(req, res, "/", {}))
redirect(req, res, "/", {}) app.all("/logout", (req, res) => redirect(req, res, "/", {}))
})
} }
app.use("/static", _static.router) app.use("/static", _static.router)

17
src/node/routes/logout.ts Normal file
View File

@ -0,0 +1,17 @@
import { Router } from "express"
import { getCookieDomain, redirect } from "../http"
import { Cookie } from "./login"
export const router = Router()
router.get("/", async (req, res) => {
// Must use the *identical* properties used to set the cookie.
res.clearCookie(Cookie.Key, {
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
path: req.body.base || "/",
sameSite: "lax",
})
const to = (typeof req.query.to === "string" && req.query.to) || "/"
return redirect(req, res, to, { to: undefined, base: undefined })
})

View File

@ -3,6 +3,7 @@ import { Request, Router } from "express"
import { promises as fs } from "fs" import { promises as fs } from "fs"
import * as path from "path" import * as path from "path"
import qs from "qs" import qs from "qs"
import * as ipc from "../../../typings/ipc"
import { Emitter } from "../../common/emitter" import { Emitter } from "../../common/emitter"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { getFirstString } from "../../common/util" import { getFirstString } from "../../common/util"
@ -39,12 +40,13 @@ router.get("/", async (req, res) => {
options.productConfiguration.codeServerVersion = version options.productConfiguration.codeServerVersion = version
res.send( res.send(
replaceTemplates( replaceTemplates<ipc.Options>(
req, req,
// Uncomment prod blocks if not in development. TODO: Would this be // Uncomment prod blocks if not in development. TODO: Would this be
// better as a build step? Or maintain two HTML files again? // better as a build step? Or maintain two HTML files again?
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content, commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
{ {
authed: req.args.auth !== "none",
disableTelemetry: !!req.args["disable-telemetry"], disableTelemetry: !!req.args["disable-telemetry"],
disableUpdateCheck: !!req.args["disable-update-check"], disableUpdateCheck: !!req.args["disable-update-check"],
}, },

View File

@ -22,11 +22,11 @@ describe("register", () => {
}) })
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks()
jest.mock("@coder/logger", () => loggerModule) jest.mock("@coder/logger", () => loggerModule)
}) })
afterEach(() => { afterEach(() => {
mockRegisterFn.mockClear()
jest.resetModules() jest.resetModules()
}) })
@ -39,6 +39,7 @@ describe("register", () => {
global.navigator = (undefined as unknown) as Navigator & typeof globalThis global.navigator = (undefined as unknown) as Navigator & typeof globalThis
global.location = (undefined as unknown) as Location & typeof globalThis global.location = (undefined as unknown) as Location & typeof globalThis
}) })
it("test should have access to browser globals from beforeAll", () => { it("test should have access to browser globals from beforeAll", () => {
expect(typeof global.window).not.toBeFalsy() expect(typeof global.window).not.toBeFalsy()
expect(typeof global.document).not.toBeFalsy() expect(typeof global.document).not.toBeFalsy()
@ -74,24 +75,24 @@ describe("register", () => {
}) })
describe("when navigator and serviceWorker are NOT defined", () => { describe("when navigator and serviceWorker are NOT defined", () => {
let spy: jest.SpyInstance
beforeEach(() => { beforeEach(() => {
spy = jest.spyOn(console, "error") jest.clearAllMocks()
jest.mock("@coder/logger", () => loggerModule)
}) })
afterAll(() => { afterAll(() => {
jest.restoreAllMocks() jest.restoreAllMocks()
}) })
it("should log an error to the console", () => { it("should log an error", () => {
// Load service worker like you would in the browser // Load service worker like you would in the browser
require("../../src/browser/register") require("../../src/browser/register")
expect(spy).toHaveBeenCalled() expect(loggerModule.logger.error).toHaveBeenCalled()
expect(spy).toHaveBeenCalledTimes(1) expect(loggerModule.logger.error).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith("[Service Worker] navigator is undefined") expect(loggerModule.logger.error).toHaveBeenCalledWith("[Service Worker] navigator is undefined")
}) })
}) })
describe("registerServiceWorker", () => { describe("registerServiceWorker", () => {
let serviceWorkerPath: string let serviceWorkerPath: string
let serviceWorkerScope: string let serviceWorkerScope: string

View File

@ -5,7 +5,7 @@ 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"
describe.skip("update", () => { describe("update", () => {
let version = "1.0.0" let version = "1.0.0"
let spy: string[] = [] let spy: string[] = []
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
@ -75,7 +75,7 @@ describe.skip("update", () => {
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).toBe("2.1.0") expect(update.version).toStrictEqual("2.1.0")
expect(spy).toEqual(["/latest"]) expect(spy).toEqual(["/latest"])
}) })
@ -87,9 +87,9 @@ describe.skip("update", () => {
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)).toBe(false) expect(isNaN(update.checked)).toStrictEqual(false)
expect(update.checked < now).toBe(true) expect(update.checked < now).toBe(true)
expect(update.version).toBe("2.1.0") expect(update.version).toStrictEqual("2.1.0")
expect(spy).toEqual([]) expect(spy).toEqual([])
}) })
@ -101,10 +101,10 @@ describe.skip("update", () => {
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)).toBe(false) expect(isNaN(update.checked)).toStrictEqual(false)
expect(update.checked < Date.now() && update.checked >= now).toBe(true) expect(update.checked < Date.now() && update.checked >= now).toStrictEqual(true)
expect(update.version).toBe("4.1.1") expect(update.version).toStrictEqual("4.1.1")
expect(spy).toBe(["/latest"]) expect(spy).toStrictEqual(["/latest"])
}) })
it("should get latest after interval passes", async () => { it("should get latest after interval passes", async () => {
@ -121,8 +121,8 @@ describe.skip("update", () => {
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.toBe(checked) expect(update.checked).not.toStrictEqual(checked)
expect(spy).toBe(["/latest"]) expect(spy).toStrictEqual(["/latest"])
}) })
it("should check if it's the current version", async () => { it("should check if it's the current version", async () => {
@ -130,24 +130,31 @@ describe.skip("update", () => {
const p = provider() const p = provider()
let update = await p.getUpdate(true) let update = await p.getUpdate(true)
expect(p.isLatestVersion(update)).toBe(false) expect(p.isLatestVersion(update)).toStrictEqual(false)
version = "0.0.0" version = "0.0.0"
update = await p.getUpdate(true) update = await p.getUpdate(true)
expect(p.isLatestVersion(update)).toBe(true) expect(p.isLatestVersion(update)).toStrictEqual(true)
// Old version format; make sure it doesn't report as being later. // Old version format; make sure it doesn't report as being later.
version = "999999.9999-invalid999.99.9" version = "999999.9999-invalid999.99.9"
update = await p.getUpdate(true) update = await p.getUpdate(true)
expect(p.isLatestVersion(update)).toBe(true) expect(p.isLatestVersion(update)).toStrictEqual(true)
}) })
it("should not reject if unable to fetch", async () => { it("should not reject if unable to fetch", async () => {
expect.assertions(2)
let provider = new UpdateProvider("invalid", settings) let provider = new UpdateProvider("invalid", settings)
await expect(() => provider.getUpdate(true)).resolves.toBe(undefined) let now = Date.now()
let update = await provider.getUpdate(true)
expect(isNaN(update.checked)).toStrictEqual(false)
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
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)
await expect(() => provider.getUpdate(true)).resolves.toBe(undefined) now = Date.now()
update = await provider.getUpdate(true)
expect(isNaN(update.checked)).toStrictEqual(false)
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
expect(update.version).toStrictEqual("unknown")
}) })
}) })

View File

@ -18,9 +18,6 @@ global.document = dom.window.document
export type LocationLike = Pick<Location, "pathname" | "origin"> export type LocationLike = Pick<Location, "pathname" | "origin">
// jest.mock is hoisted above the imports so we must use `require` here.
jest.mock("@coder/logger", () => require("../utils/helpers").loggerModule)
describe("util", () => { describe("util", () => {
describe("normalize", () => { describe("normalize", () => {
it("should remove multiple slashes", () => { it("should remove multiple slashes", () => {
@ -236,14 +233,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("ui", error) 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("api", "oh no") 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")

View File

@ -1,3 +1,4 @@
import { logger } from "@coder/logger"
import * as express from "express" import * as express from "express"
import * as http from "http" import * as http from "http"
import * as net from "net" import * as net from "net"
@ -45,7 +46,7 @@ export class HttpServer {
rej(err) rej(err)
} else { } else {
// Promise resolved earlier so this is some other error. // Promise resolved earlier so this is some other error.
util.logError("http server error", err) util.logError(logger, "http server error", err)
} }
}) })
}) })

3
typings/ipc.d.ts vendored
View File

@ -6,9 +6,12 @@
* The second is a symlink to the first. * The second is a symlink to the first.
*/ */
export interface Options { export interface Options {
authed: boolean
base: string base: string
csStaticBase: string
disableTelemetry: boolean disableTelemetry: boolean
disableUpdateCheck: boolean disableUpdateCheck: boolean
logLevel: number
} }
export interface InitMessage { export interface InitMessage {