Merge pull request #3277 from code-asher/logout
This commit is contained in:
commit
75e9e24e92
|
@ -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
|
||||||
|
|
|
@ -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);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export enum Cookie {
|
|
||||||
Key = 'key',
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
../../../../../../src/common/util.ts
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 })
|
||||||
|
})
|
|
@ -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"],
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue