refactor: match /test/unit structure to /src
This commit is contained in:
465
test/unit/node/cli.test.ts
Normal file
465
test/unit/node/cli.test.ts
Normal file
@ -0,0 +1,465 @@
|
||||
import { Level, logger } from "@coder/logger"
|
||||
import { promises as fs } from "fs"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Args, parse, setDefaults, shouldOpenInExistingInstance, splitOnFirstEquals } from "../../../src/node/cli"
|
||||
import { tmpdir } from "../../../src/node/constants"
|
||||
import { paths } from "../../../src/node/util"
|
||||
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.LOG_LEVEL
|
||||
delete process.env.PASSWORD
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
// The parser should not set any defaults so the caller can determine what
|
||||
// values the user actually set. These are only set after explicitly calling
|
||||
// `setDefaults`.
|
||||
const defaults = {
|
||||
auth: "password",
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
"proxy-domain": [],
|
||||
usingEnvPassword: false,
|
||||
usingEnvHashedPassword: false,
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
}
|
||||
|
||||
it("should parse nothing", () => {
|
||||
expect(parse([])).toStrictEqual({ _: [] })
|
||||
})
|
||||
|
||||
it("should parse all available options", () => {
|
||||
expect(
|
||||
parse([
|
||||
"--enable",
|
||||
"feature1",
|
||||
"--enable",
|
||||
"feature2",
|
||||
"--bind-addr=192.169.0.1:8080",
|
||||
"--auth",
|
||||
"none",
|
||||
"--extensions-dir",
|
||||
"foo",
|
||||
"--builtin-extensions-dir",
|
||||
"foobar",
|
||||
"--extra-extensions-dir",
|
||||
"nozzle",
|
||||
"1",
|
||||
"--extra-builtin-extensions-dir",
|
||||
"bazzle",
|
||||
"--verbose",
|
||||
"2",
|
||||
"--log",
|
||||
"error",
|
||||
"--help",
|
||||
"--open",
|
||||
"--socket=mumble",
|
||||
"3",
|
||||
"--user-data-dir",
|
||||
"bar",
|
||||
"--cert=baz",
|
||||
"--cert-key",
|
||||
"qux",
|
||||
"--version",
|
||||
"--json",
|
||||
"--port=8081",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"4",
|
||||
"--",
|
||||
"-5",
|
||||
"--6",
|
||||
]),
|
||||
).toEqual({
|
||||
_: ["1", "2", "3", "4", "-5", "--6"],
|
||||
auth: "none",
|
||||
"builtin-extensions-dir": path.resolve("foobar"),
|
||||
"cert-key": path.resolve("qux"),
|
||||
cert: {
|
||||
value: path.resolve("baz"),
|
||||
},
|
||||
enable: ["feature1", "feature2"],
|
||||
"extensions-dir": path.resolve("foo"),
|
||||
"extra-builtin-extensions-dir": [path.resolve("bazzle")],
|
||||
"extra-extensions-dir": [path.resolve("nozzle")],
|
||||
help: true,
|
||||
host: "0.0.0.0",
|
||||
json: true,
|
||||
log: "error",
|
||||
open: true,
|
||||
port: 8081,
|
||||
socket: path.resolve("mumble"),
|
||||
"user-data-dir": path.resolve("bar"),
|
||||
verbose: true,
|
||||
version: true,
|
||||
"bind-addr": "192.169.0.1:8080",
|
||||
})
|
||||
})
|
||||
|
||||
it("should work with short options", () => {
|
||||
expect(parse(["-vvv", "-v"])).toEqual({
|
||||
_: [],
|
||||
verbose: true,
|
||||
version: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use log level env var", async () => {
|
||||
const args = parse([])
|
||||
expect(args).toEqual({ _: [] })
|
||||
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
const defaults = await setDefaults(args)
|
||||
expect(defaults).toStrictEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "debug",
|
||||
verbose: false,
|
||||
})
|
||||
expect(process.env.LOG_LEVEL).toEqual("debug")
|
||||
expect(logger.level).toEqual(Level.Debug)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
const updated = await setDefaults(args)
|
||||
expect(updated).toStrictEqual({
|
||||
...updated,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
expect(process.env.LOG_LEVEL).toEqual("trace")
|
||||
expect(logger.level).toEqual(Level.Trace)
|
||||
})
|
||||
|
||||
it("should prefer --log to env var and --verbose to --log", async () => {
|
||||
let args = parse(["--log", "info"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
log: "info",
|
||||
})
|
||||
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
const defaults = await setDefaults(args)
|
||||
expect(defaults).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
expect(process.env.LOG_LEVEL).toEqual("info")
|
||||
expect(logger.level).toEqual(Level.Info)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
const updated = await setDefaults(args)
|
||||
expect(updated).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
expect(process.env.LOG_LEVEL).toEqual("info")
|
||||
expect(logger.level).toEqual(Level.Info)
|
||||
|
||||
args = parse(["--log", "info", "--verbose"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
process.env.LOG_LEVEL = "warn"
|
||||
const updatedAgain = await setDefaults(args)
|
||||
expect(updatedAgain).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
expect(process.env.LOG_LEVEL).toEqual("trace")
|
||||
expect(logger.level).toEqual(Level.Trace)
|
||||
})
|
||||
|
||||
it("should ignore invalid log level env var", async () => {
|
||||
process.env.LOG_LEVEL = "bogus"
|
||||
const defaults = await setDefaults(parse([]))
|
||||
expect(defaults).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
})
|
||||
})
|
||||
|
||||
it("should error if value isn't provided", () => {
|
||||
expect(() => parse(["--auth"])).toThrowError(/--auth requires a value/)
|
||||
expect(() => parse(["--auth=", "--log=debug"])).toThrowError(/--auth requires a value/)
|
||||
expect(() => parse(["--auth", "--log"])).toThrowError(/--auth requires a value/)
|
||||
expect(() => parse(["--auth", "--invalid"])).toThrowError(/--auth requires a value/)
|
||||
expect(() => parse(["--bind-addr"])).toThrowError(/--bind-addr requires a value/)
|
||||
})
|
||||
|
||||
it("should error if value is invalid", () => {
|
||||
expect(() => parse(["--port", "foo"])).toThrowError(/--port must be a number/)
|
||||
expect(() => parse(["--auth", "invalid"])).toThrowError(/--auth valid values: \[password, none\]/)
|
||||
expect(() => parse(["--log", "invalid"])).toThrowError(/--log valid values: \[trace, debug, info, warn, error\]/)
|
||||
})
|
||||
|
||||
it("should error if the option doesn't exist", () => {
|
||||
expect(() => parse(["--foo"])).toThrowError(/Unknown option --foo/)
|
||||
})
|
||||
|
||||
it("should not error if the value is optional", () => {
|
||||
expect(parse(["--cert"])).toEqual({
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should not allow option-like values", () => {
|
||||
expect(() => parse(["--socket", "--socket-path-value"])).toThrowError(/--socket requires a value/)
|
||||
// If you actually had a path like this you would do this instead:
|
||||
expect(parse(["--socket", "./--socket-path-value"])).toEqual({
|
||||
_: [],
|
||||
socket: path.resolve("--socket-path-value"),
|
||||
})
|
||||
expect(() => parse(["--cert", "--socket-path-value"])).toThrowError(/Unknown option --socket-path-value/)
|
||||
})
|
||||
|
||||
it("should allow positional arguments before options", () => {
|
||||
expect(parse(["foo", "test", "--auth", "none"])).toEqual({
|
||||
_: ["foo", "test"],
|
||||
auth: "none",
|
||||
})
|
||||
})
|
||||
|
||||
it("should support repeatable flags", () => {
|
||||
expect(parse(["--proxy-domain", "*.coder.com"])).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
expect(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"])).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com", "test.com"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should enforce cert-key with cert value or otherwise generate one", async () => {
|
||||
const args = parse(["--cert"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
})
|
||||
expect(() => parse(["--cert", "test"])).toThrowError(/--cert-key is missing/)
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
_: [],
|
||||
...defaults,
|
||||
cert: {
|
||||
value: path.join(paths.data, "localhost.crt"),
|
||||
},
|
||||
"cert-key": path.join(paths.data, "localhost.key"),
|
||||
})
|
||||
})
|
||||
|
||||
it("should override with --link", async () => {
|
||||
const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" "))
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
_: [],
|
||||
...defaults,
|
||||
auth: "none",
|
||||
host: "localhost",
|
||||
link: {
|
||||
value: "test",
|
||||
},
|
||||
port: 0,
|
||||
cert: undefined,
|
||||
"cert-key": path.resolve("test"),
|
||||
socket: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use env var password", async () => {
|
||||
process.env.PASSWORD = "test"
|
||||
const args = parse([])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
password: "test",
|
||||
usingEnvPassword: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use env var hashed password", async () => {
|
||||
process.env.HASHED_PASSWORD =
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY" // test
|
||||
const args = parse([])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
"hashed-password":
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||
usingEnvHashedPassword: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should filter proxy domains", async () => {
|
||||
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com", "coder.com", "coder.org"],
|
||||
})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
"proxy-domain": ["coder.com", "coder.org"],
|
||||
})
|
||||
})
|
||||
it("should allow '=,$/' in strings", async () => {
|
||||
const args = parse([
|
||||
"--enable-proposed-api",
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"enable-proposed-api": [
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
],
|
||||
})
|
||||
})
|
||||
it("should parse options with double-dash and multiple equal signs ", async () => {
|
||||
const args = parse(
|
||||
[
|
||||
"--hashed-password=$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
],
|
||||
{
|
||||
configFile: "/pathtoconfig",
|
||||
},
|
||||
)
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"hashed-password":
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cli", () => {
|
||||
let args: Mutable<Args> = { _: [] }
|
||||
const testDir = path.join(tmpdir, "tests/cli")
|
||||
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.rmdir(testDir, { recursive: true })
|
||||
await fs.mkdir(testDir, { recursive: true })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
delete process.env.VSCODE_IPC_HOOK_CLI
|
||||
args = { _: [] }
|
||||
await fs.rmdir(vscodeIpcPath, { recursive: true })
|
||||
})
|
||||
|
||||
it("should use existing if inside code-server", async () => {
|
||||
process.env.VSCODE_IPC_HOOK_CLI = "test"
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
|
||||
args.port = 8081
|
||||
args._.push("./file")
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
})
|
||||
|
||||
it("should use existing if --reuse-window is set", async () => {
|
||||
args["reuse-window"] = true
|
||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined)
|
||||
|
||||
await fs.writeFile(vscodeIpcPath, "test")
|
||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test")
|
||||
|
||||
args.port = 8081
|
||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test")
|
||||
})
|
||||
|
||||
it("should use existing if --new-window is set", async () => {
|
||||
args["new-window"] = true
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
await fs.writeFile(vscodeIpcPath, "test")
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
|
||||
args.port = 8081
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
})
|
||||
|
||||
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
args._.push("./file")
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
const socketPath = path.join(testDir, "socket")
|
||||
await fs.writeFile(vscodeIpcPath, socketPath)
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const server = net.createServer(() => {
|
||||
// Close after getting the first connection.
|
||||
server.close()
|
||||
})
|
||||
server.once("listening", () => resolve(server))
|
||||
server.listen(socketPath)
|
||||
})
|
||||
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath)
|
||||
|
||||
args.port = 8081
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("splitOnFirstEquals", () => {
|
||||
it("should split on the first equals", () => {
|
||||
const testStr = "enabled-proposed-api=test=value"
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = ["enabled-proposed-api", "test=value"]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
it("should split on first equals regardless of multiple equals signs", () => {
|
||||
const testStr =
|
||||
"hashed-password=$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY"
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = [
|
||||
"hashed-password",
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||
]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
it("should always return the first element before an equals", () => {
|
||||
const testStr = "auth="
|
||||
const actual = splitOnFirstEquals(testStr)
|
||||
const expected = ["auth"]
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
76
test/unit/node/constants.test.ts
Normal file
76
test/unit/node/constants.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { createLoggerMock } from "../../utils/helpers"
|
||||
|
||||
describe("constants", () => {
|
||||
let constants: typeof import("../../../src/node/constants")
|
||||
|
||||
describe("with package.json defined", () => {
|
||||
const loggerModule = createLoggerMock()
|
||||
const mockPackageJson = {
|
||||
name: "mock-code-server",
|
||||
description: "Run VS Code on a remote server.",
|
||||
repository: "https://github.com/cdr/code-server",
|
||||
version: "1.0.0",
|
||||
commit: "f6b2be2838f4afb217c2fd8f03eafedd8d55ef9b",
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.mock("@coder/logger", () => loggerModule)
|
||||
jest.mock("../../../package.json", () => mockPackageJson, { virtual: true })
|
||||
constants = require("../../../src/node/constants")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it("should provide the commit", () => {
|
||||
expect(constants.commit).toBe(mockPackageJson.commit)
|
||||
})
|
||||
|
||||
it("should return the package.json version", () => {
|
||||
expect(constants.version).toBe(mockPackageJson.version)
|
||||
})
|
||||
|
||||
describe("getPackageJson", () => {
|
||||
it("should log a warning if package.json not found", () => {
|
||||
const expectedErrorMessage = "Cannot find module './package.json' from 'src/node/constants.ts'"
|
||||
|
||||
constants.getPackageJson("./package.json")
|
||||
|
||||
expect(loggerModule.logger.warn).toHaveBeenCalled()
|
||||
expect(loggerModule.logger.warn).toHaveBeenCalledWith(expectedErrorMessage)
|
||||
})
|
||||
|
||||
it("should find the package.json", () => {
|
||||
// the function calls require from src/node/constants
|
||||
// so to get the root package.json we need to use ../../
|
||||
const packageJson = constants.getPackageJson("../../package.json")
|
||||
expect(packageJson).toStrictEqual(mockPackageJson)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("with incomplete package.json", () => {
|
||||
const mockPackageJson = {
|
||||
name: "mock-code-server",
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
jest.mock("../../../package.json", () => mockPackageJson, { virtual: true })
|
||||
constants = require("../../../src/node/constants")
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it("version should return 'development'", () => {
|
||||
expect(constants.version).toBe("development")
|
||||
})
|
||||
it("commit should return 'development'", () => {
|
||||
expect(constants.commit).toBe("development")
|
||||
})
|
||||
})
|
||||
})
|
116
test/unit/node/plugin.test.ts
Normal file
116
test/unit/node/plugin.test.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as express from "express"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { HttpCode } from "../../../src/common/http"
|
||||
import { AuthType } from "../../../src/node/cli"
|
||||
import { codeServer, PluginAPI } from "../../../src/node/plugin"
|
||||
import * as apps from "../../../src/node/routes/apps"
|
||||
import * as httpserver from "../../utils/httpserver"
|
||||
const fsp = fs.promises
|
||||
|
||||
// Jest overrides `require` so our usual override doesn't work.
|
||||
jest.mock("code-server", () => codeServer, { virtual: true })
|
||||
|
||||
/**
|
||||
* Use $LOG_LEVEL=debug to see debug logs.
|
||||
*/
|
||||
describe("plugin", () => {
|
||||
let papi: PluginAPI
|
||||
let s: httpserver.HttpServer
|
||||
|
||||
beforeAll(async () => {
|
||||
// Only include the test plugin to avoid contaminating results with other
|
||||
// plugins that might be on the filesystem.
|
||||
papi = new PluginAPI(logger, `${path.resolve(__dirname, "test-plugin")}:meow`, "")
|
||||
await papi.loadPlugins(false)
|
||||
|
||||
const app = express.default()
|
||||
const wsApp = express.default()
|
||||
|
||||
const common: express.RequestHandler = (req, _, next) => {
|
||||
// Routes might use these arguments.
|
||||
req.args = {
|
||||
_: [],
|
||||
auth: AuthType.None,
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
"proxy-domain": [],
|
||||
config: "~/.config/code-server/config.yaml",
|
||||
verbose: false,
|
||||
usingEnvPassword: false,
|
||||
usingEnvHashedPassword: false,
|
||||
"extensions-dir": "",
|
||||
"user-data-dir": "",
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
app.use(common)
|
||||
wsApp.use(common)
|
||||
|
||||
papi.mount(app, wsApp)
|
||||
app.use("/api/applications", apps.router(papi))
|
||||
|
||||
s = new httpserver.HttpServer()
|
||||
await s.listen(app)
|
||||
s.listenUpgrade(wsApp)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await s.close()
|
||||
})
|
||||
|
||||
it("/api/applications", async () => {
|
||||
const resp = await s.fetch("/api/applications")
|
||||
expect(resp.status).toBe(200)
|
||||
const body = await resp.json()
|
||||
logger.debug(`${JSON.stringify(body)}`)
|
||||
expect(body).toStrictEqual([
|
||||
{
|
||||
name: "Test App",
|
||||
version: "4.0.0",
|
||||
|
||||
description: "This app does XYZ.",
|
||||
iconPath: "/test-plugin/test-app/icon.svg",
|
||||
homepageURL: "https://example.com",
|
||||
path: "/test-plugin/test-app",
|
||||
|
||||
plugin: {
|
||||
name: "test-plugin",
|
||||
version: "1.0.0",
|
||||
modulePath: path.join(__dirname, "test-plugin"),
|
||||
|
||||
displayName: "Test Plugin",
|
||||
description: "Plugin used in code-server tests.",
|
||||
routerPath: "/test-plugin",
|
||||
homepageURL: "https://example.com",
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("/test-plugin/test-app", async () => {
|
||||
const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), {
|
||||
encoding: "utf8",
|
||||
})
|
||||
const resp = await s.fetch("/test-plugin/test-app")
|
||||
expect(resp.status).toBe(200)
|
||||
const body = await resp.text()
|
||||
expect(body).toBe(indexHTML)
|
||||
})
|
||||
|
||||
it("/test-plugin/test-app (websocket)", async () => {
|
||||
const ws = s.ws("/test-plugin/test-app")
|
||||
const message = await new Promise((resolve) => {
|
||||
ws.once("message", (message) => resolve(message))
|
||||
})
|
||||
ws.terminate()
|
||||
expect(message).toBe("hello")
|
||||
})
|
||||
|
||||
it("/test-plugin/error", async () => {
|
||||
const resp = await s.fetch("/test-plugin/error")
|
||||
expect(resp.status).toBe(HttpCode.LargePayload)
|
||||
})
|
||||
})
|
105
test/unit/node/proxy.test.ts
Normal file
105
test/unit/node/proxy.test.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import bodyParser from "body-parser"
|
||||
import * as express from "express"
|
||||
import * as httpserver from "../../utils/httpserver"
|
||||
import * as integration from "../../utils/integration"
|
||||
|
||||
describe("proxy", () => {
|
||||
const nhooyrDevServer = new httpserver.HttpServer()
|
||||
let codeServer: httpserver.HttpServer | undefined
|
||||
let proxyPath: string
|
||||
let absProxyPath: string
|
||||
let e: express.Express
|
||||
|
||||
beforeAll(async () => {
|
||||
await nhooyrDevServer.listen((req, res) => {
|
||||
e(req, res)
|
||||
})
|
||||
proxyPath = `/proxy/${nhooyrDevServer.port()}/wsup`
|
||||
absProxyPath = proxyPath.replace("/proxy/", "/absproxy/")
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nhooyrDevServer.close()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
e = express.default()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (codeServer) {
|
||||
await codeServer.close()
|
||||
codeServer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it("should rewrite the base path", async () => {
|
||||
e.get("/wsup", (req, res) => {
|
||||
res.json("asher is the best")
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch(proxyPath)
|
||||
expect(resp.status).toBe(200)
|
||||
const json = await resp.json()
|
||||
expect(json).toBe("asher is the best")
|
||||
})
|
||||
|
||||
it("should not rewrite the base path", async () => {
|
||||
e.get(absProxyPath, (req, res) => {
|
||||
res.json("joe is the best")
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch(absProxyPath)
|
||||
expect(resp.status).toBe(200)
|
||||
const json = await resp.json()
|
||||
expect(json).toBe("joe is the best")
|
||||
})
|
||||
|
||||
it("should rewrite redirects", async () => {
|
||||
e.post("/wsup", (req, res) => {
|
||||
res.redirect(307, "/finale")
|
||||
})
|
||||
e.post("/finale", (req, res) => {
|
||||
res.json("redirect success")
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch(proxyPath, {
|
||||
method: "POST",
|
||||
})
|
||||
expect(resp.status).toBe(200)
|
||||
expect(await resp.json()).toBe("redirect success")
|
||||
})
|
||||
|
||||
it("should not rewrite redirects", async () => {
|
||||
const finalePath = absProxyPath.replace("/wsup", "/finale")
|
||||
e.post(absProxyPath, (req, res) => {
|
||||
res.redirect(307, finalePath)
|
||||
})
|
||||
e.post(finalePath, (req, res) => {
|
||||
res.json("redirect success")
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch(absProxyPath, {
|
||||
method: "POST",
|
||||
})
|
||||
expect(resp.status).toBe(200)
|
||||
expect(await resp.json()).toBe("redirect success")
|
||||
})
|
||||
|
||||
it("should allow post bodies", async () => {
|
||||
e.use(bodyParser.json({ strict: false }))
|
||||
e.post("/wsup", (req, res) => {
|
||||
res.json(req.body)
|
||||
})
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch(proxyPath, {
|
||||
method: "post",
|
||||
body: JSON.stringify("coder is the best"),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
expect(resp.status).toBe(200)
|
||||
expect(await resp.json()).toBe("coder is the best")
|
||||
})
|
||||
})
|
40
test/unit/node/routes/health.test.ts
Normal file
40
test/unit/node/routes/health.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as httpserver from "../../../utils/httpserver"
|
||||
import * as integration from "../../../utils/integration"
|
||||
|
||||
describe("health", () => {
|
||||
let codeServer: httpserver.HttpServer | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
if (codeServer) {
|
||||
await codeServer.close()
|
||||
codeServer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it("/healthz", async () => {
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const resp = await codeServer.fetch("/healthz")
|
||||
expect(resp.status).toBe(200)
|
||||
const json = await resp.json()
|
||||
expect(json).toStrictEqual({ lastHeartbeat: 0, status: "expired" })
|
||||
})
|
||||
|
||||
it("/healthz (websocket)", async () => {
|
||||
codeServer = await integration.setup(["--auth=none"], "")
|
||||
const ws = codeServer.ws("/healthz")
|
||||
const message = await new Promise((resolve, reject) => {
|
||||
ws.on("error", console.error)
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
const j = JSON.parse(message.toString())
|
||||
resolve(j)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
ws.on("open", () => ws.send(JSON.stringify({ event: "health" })))
|
||||
})
|
||||
ws.terminate()
|
||||
expect(message).toStrictEqual({ event: "health", status: "expired", lastHeartbeat: 0 })
|
||||
})
|
||||
})
|
76
test/unit/node/routes/login.test.ts
Normal file
76
test/unit/node/routes/login.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { RateLimiter } from "../../../../src/node/routes/login"
|
||||
import * as httpserver from "../../../utils/httpserver"
|
||||
import * as integration from "../../../utils/integration"
|
||||
|
||||
describe("login", () => {
|
||||
describe("RateLimiter", () => {
|
||||
it("should allow one try ", () => {
|
||||
const limiter = new RateLimiter()
|
||||
expect(limiter.removeToken()).toBe(true)
|
||||
})
|
||||
|
||||
it("should pull tokens from both limiters (minute & hour)", () => {
|
||||
const limiter = new RateLimiter()
|
||||
|
||||
// Try twice, which pulls two from the minute bucket
|
||||
limiter.removeToken()
|
||||
limiter.removeToken()
|
||||
|
||||
// Check that we can still try
|
||||
// which should be true since there are 12 remaining in the hour bucket
|
||||
expect(limiter.canTry()).toBe(true)
|
||||
expect(limiter.removeToken()).toBe(true)
|
||||
})
|
||||
|
||||
it("should not allow more than 14 tries in less than an hour", () => {
|
||||
const limiter = new RateLimiter()
|
||||
|
||||
// The limiter allows 2 tries per minute plus 12 per hour
|
||||
// so if we run it 15 times, 14 should return true and the last
|
||||
// should return false
|
||||
for (let i = 1; i <= 14; i++) {
|
||||
expect(limiter.removeToken()).toBe(true)
|
||||
}
|
||||
|
||||
expect(limiter.canTry()).toBe(false)
|
||||
expect(limiter.removeToken()).toBe(false)
|
||||
})
|
||||
})
|
||||
describe("/login", () => {
|
||||
let _codeServer: httpserver.HttpServer | undefined
|
||||
function codeServer(): httpserver.HttpServer {
|
||||
if (!_codeServer) {
|
||||
throw new Error("tried to use code-server before setting it up")
|
||||
}
|
||||
return _codeServer
|
||||
}
|
||||
|
||||
// Store whatever might be in here so we can restore it afterward.
|
||||
// TODO: We should probably pass this as an argument somehow instead of
|
||||
// manipulating the environment.
|
||||
const previousEnvPassword = process.env.PASSWORD
|
||||
|
||||
beforeEach(async () => {
|
||||
process.env.PASSWORD = "test"
|
||||
_codeServer = await integration.setup(["--auth=password"], "")
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
process.env.PASSWORD = previousEnvPassword
|
||||
if (_codeServer) {
|
||||
await _codeServer.close()
|
||||
_codeServer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
it("should return HTML with 'Missing password' message", async () => {
|
||||
const resp = await codeServer().fetch("/login", { method: "POST" })
|
||||
|
||||
expect(resp.status).toBe(200)
|
||||
|
||||
const htmlContent = await resp.text()
|
||||
|
||||
expect(htmlContent).toContain("Missing password")
|
||||
})
|
||||
})
|
||||
})
|
136
test/unit/node/routes/static.test.ts
Normal file
136
test/unit/node/routes/static.test.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { tmpdir } from "../../../utils/helpers"
|
||||
import * as httpserver from "../../../utils/httpserver"
|
||||
import * as integration from "../../../utils/integration"
|
||||
|
||||
describe("/static", () => {
|
||||
let _codeServer: httpserver.HttpServer | undefined
|
||||
function codeServer(): httpserver.HttpServer {
|
||||
if (!_codeServer) {
|
||||
throw new Error("tried to use code-server before setting it up")
|
||||
}
|
||||
return _codeServer
|
||||
}
|
||||
|
||||
let testFile: string | undefined
|
||||
let testFileContent: string | undefined
|
||||
let nonExistentTestFile: string | undefined
|
||||
|
||||
// The static endpoint expects a commit and then the full path of the file.
|
||||
// The commit is just for cache busting so we can use anything we want. `-`
|
||||
// and `development` are specially recognized in that they will cause the
|
||||
// static endpoint to avoid sending cache headers.
|
||||
const commit = "-"
|
||||
|
||||
beforeAll(async () => {
|
||||
const testDir = await tmpdir("static")
|
||||
testFile = path.join(testDir, "test")
|
||||
testFileContent = "static file contents"
|
||||
nonExistentTestFile = path.join(testDir, "i-am-not-here")
|
||||
await fs.writeFile(testFile, testFileContent)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (_codeServer) {
|
||||
await _codeServer.close()
|
||||
_codeServer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
function commonTests() {
|
||||
it("should return a 404 when a commit and file are not provided", async () => {
|
||||
const resp = await codeServer().fetch("/static")
|
||||
expect(resp.status).toBe(404)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content).toStrictEqual({ error: "Not Found" })
|
||||
})
|
||||
|
||||
it("should return a 404 when a file is not provided", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}`)
|
||||
expect(resp.status).toBe(404)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content).toStrictEqual({ error: "Not Found" })
|
||||
})
|
||||
}
|
||||
|
||||
describe("disabled authentication", () => {
|
||||
beforeEach(async () => {
|
||||
_codeServer = await integration.setup(["--auth=none"], "")
|
||||
})
|
||||
|
||||
commonTests()
|
||||
|
||||
it("should return a 404 for a nonexistent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`)
|
||||
expect(resp.status).toBe(404)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content.error).toMatch("ENOENT")
|
||||
})
|
||||
|
||||
it("should return a 200 and file contents for an existent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}${testFile}`)
|
||||
expect(resp.status).toBe(200)
|
||||
|
||||
const content = await resp.text()
|
||||
expect(content).toStrictEqual(testFileContent)
|
||||
})
|
||||
})
|
||||
|
||||
describe("enabled authentication", () => {
|
||||
// Store whatever might be in here so we can restore it afterward.
|
||||
// TODO: We should probably pass this as an argument somehow instead of
|
||||
// manipulating the environment.
|
||||
const previousEnvPassword = process.env.PASSWORD
|
||||
|
||||
beforeEach(async () => {
|
||||
process.env.PASSWORD = "test"
|
||||
_codeServer = await integration.setup(["--auth=password"], "")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.PASSWORD = previousEnvPassword
|
||||
})
|
||||
|
||||
commonTests()
|
||||
|
||||
describe("inside code-server root", () => {
|
||||
it("should return a 404 for a nonexistent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}/${__filename}-does-not-exist`)
|
||||
expect(resp.status).toBe(404)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content.error).toMatch("ENOENT")
|
||||
})
|
||||
|
||||
it("should return a 200 and file contents for an existent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}${__filename}`)
|
||||
expect(resp.status).toBe(200)
|
||||
|
||||
const content = await resp.text()
|
||||
expect(content).toStrictEqual(await fs.readFile(__filename, "utf8"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("outside code-server root", () => {
|
||||
it("should return a 401 for a nonexistent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}/${nonExistentTestFile}`)
|
||||
expect(resp.status).toBe(401)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content).toStrictEqual({ error: "Unauthorized" })
|
||||
})
|
||||
|
||||
it("should return a 401 for an existent file", async () => {
|
||||
const resp = await codeServer().fetch(`/static/${commit}${testFile}`)
|
||||
expect(resp.status).toBe(401)
|
||||
|
||||
const content = await resp.json()
|
||||
expect(content).toStrictEqual({ error: "Unauthorized" })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
127
test/unit/node/socket.test.ts
Normal file
127
test/unit/node/socket.test.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import { promises as fs } from "fs"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as tls from "tls"
|
||||
import { Emitter } from "../../../src/common/emitter"
|
||||
import { tmpdir } from "../../../src/node/constants"
|
||||
import { SocketProxyProvider } from "../../../src/node/socket"
|
||||
import { generateCertificate } from "../../../src/node/util"
|
||||
|
||||
describe("SocketProxyProvider", () => {
|
||||
const provider = new SocketProxyProvider()
|
||||
|
||||
const onServerError = new Emitter<{ event: string; error: Error }>()
|
||||
const onClientError = new Emitter<{ event: string; error: Error }>()
|
||||
const onProxyError = new Emitter<{ event: string; error: Error }>()
|
||||
const fromServerToClient = new Emitter<Buffer>()
|
||||
const fromClientToServer = new Emitter<Buffer>()
|
||||
const fromClientToProxy = new Emitter<Buffer>()
|
||||
|
||||
let errors = 0
|
||||
let close = false
|
||||
const onError = ({ event, error }: { event: string; error: Error }): void => {
|
||||
if (!close || event === "error") {
|
||||
logger.error(event, field("error", error.message))
|
||||
++errors
|
||||
}
|
||||
}
|
||||
onServerError.event(onError)
|
||||
onClientError.event(onError)
|
||||
onProxyError.event(onError)
|
||||
|
||||
let server: tls.TLSSocket
|
||||
let proxy: net.Socket
|
||||
let client: tls.TLSSocket
|
||||
|
||||
const getData = <T>(emitter: Emitter<T>): Promise<T> => {
|
||||
return new Promise((resolve) => {
|
||||
const d = emitter.event((t) => {
|
||||
d.dispose()
|
||||
resolve(t)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const cert = await generateCertificate("localhost")
|
||||
const options = {
|
||||
cert: await fs.readFile(cert.cert),
|
||||
key: await fs.readFile(cert.certKey),
|
||||
rejectUnauthorized: false,
|
||||
}
|
||||
|
||||
await fs.mkdir(path.join(tmpdir, "tests"), { recursive: true })
|
||||
const socketPath = await provider.findFreeSocketPath(path.join(tmpdir, "tests/tls-socket-proxy"))
|
||||
await fs.rmdir(socketPath, { recursive: true })
|
||||
|
||||
return new Promise<void>((_resolve) => {
|
||||
const resolved: { [key: string]: boolean } = { client: false, server: false }
|
||||
const resolve = (type: "client" | "server"): void => {
|
||||
resolved[type] = true
|
||||
if (resolved.client && resolved.server) {
|
||||
// We don't need any more connections.
|
||||
main.close() // eslint-disable-line @typescript-eslint/no-use-before-define
|
||||
_resolve()
|
||||
}
|
||||
}
|
||||
const main = tls
|
||||
.createServer(options, (s) => {
|
||||
server = s
|
||||
server
|
||||
.on("data", (d) => fromClientToServer.emit(d))
|
||||
.on("error", (error) => onServerError.emit({ event: "error", error }))
|
||||
.on("end", () => onServerError.emit({ event: "end", error: new Error("unexpected end") }))
|
||||
.on("close", () => onServerError.emit({ event: "close", error: new Error("unexpected close") }))
|
||||
resolve("server")
|
||||
})
|
||||
.on("error", (error) => onServerError.emit({ event: "error", error }))
|
||||
.on("end", () => onServerError.emit({ event: "end", error: new Error("unexpected end") }))
|
||||
.on("close", () => onServerError.emit({ event: "close", error: new Error("unexpected close") }))
|
||||
.listen(socketPath, () => {
|
||||
client = tls
|
||||
.connect({ ...options, path: socketPath })
|
||||
.on("data", (d) => fromServerToClient.emit(d))
|
||||
.on("error", (error) => onClientError.emit({ event: "error", error }))
|
||||
.on("end", () => onClientError.emit({ event: "end", error: new Error("unexpected end") }))
|
||||
.on("close", () => onClientError.emit({ event: "close", error: new Error("unexpected close") }))
|
||||
.once("connect", () => resolve("client"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should work without a proxy", async () => {
|
||||
server.write("server->client")
|
||||
const dataFromServerToClient = (await getData(fromServerToClient)).toString()
|
||||
expect(dataFromServerToClient).toBe("server->client")
|
||||
client.write("client->server")
|
||||
const dataFromClientToServer = (await getData(fromClientToServer)).toString()
|
||||
expect(dataFromClientToServer).toBe("client->server")
|
||||
expect(errors).toEqual(0)
|
||||
})
|
||||
|
||||
it("should work with a proxy", async () => {
|
||||
expect(server instanceof tls.TLSSocket).toBe(true)
|
||||
proxy = (await provider.createProxy(server))
|
||||
.on("data", (d) => fromClientToProxy.emit(d))
|
||||
.on("error", (error) => onProxyError.emit({ event: "error", error }))
|
||||
.on("end", () => onProxyError.emit({ event: "end", error: new Error("unexpected end") }))
|
||||
.on("close", () => onProxyError.emit({ event: "close", error: new Error("unexpected close") }))
|
||||
|
||||
provider.stop() // We don't need more proxies.
|
||||
|
||||
proxy.write("server proxy->client")
|
||||
const dataFromServerToClient = (await getData(fromServerToClient)).toString()
|
||||
expect(dataFromServerToClient).toBe("server proxy->client")
|
||||
client.write("client->server proxy")
|
||||
const dataFromClientToProxy = (await getData(fromClientToProxy)).toString()
|
||||
expect(dataFromClientToProxy).toBe("client->server proxy")
|
||||
expect(errors).toEqual(0)
|
||||
})
|
||||
|
||||
it("should close", async () => {
|
||||
close = true
|
||||
client.end()
|
||||
proxy.end()
|
||||
})
|
||||
})
|
5
test/unit/node/test-plugin/.eslintrc.yaml
Normal file
5
test/unit/node/test-plugin/.eslintrc.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
settings:
|
||||
import/resolver:
|
||||
alias:
|
||||
map:
|
||||
- [code-server, ./typings/pluginapi.d.ts]
|
1
test/unit/node/test-plugin/.gitignore
vendored
Normal file
1
test/unit/node/test-plugin/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
out
|
6
test/unit/node/test-plugin/Makefile
Normal file
6
test/unit/node/test-plugin/Makefile
Normal file
@ -0,0 +1,6 @@
|
||||
out/index.js: src/index.ts
|
||||
# Typescript always emits, even on errors.
|
||||
yarn build || rm out/index.js
|
||||
|
||||
node_modules: package.json yarn.lock
|
||||
yarn
|
16
test/unit/node/test-plugin/package.json
Normal file
16
test/unit/node/test-plugin/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "test-plugin",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"code-server": "^3.7.0"
|
||||
},
|
||||
"main": "out/index.js",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.8",
|
||||
"typescript": "^4.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
}
|
||||
}
|
1
test/unit/node/test-plugin/public/icon.svg
Normal file
1
test/unit/node/test-plugin/public/icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="121" height="131" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="9.612%" y1="66.482%" x2="89.899%" y2="33.523%" id="a"><stop stop-color="#FCEE39" offset="0%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="8.601%" y1="15.03%" x2="99.641%" y2="89.058%" id="b"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#F26F4E" offset="57%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="90.118%" y1="69.931%" x2="17.938%" y2="38.628%" id="c"><stop stop-color="#7C59A4" offset="0%"/><stop stop-color="#AF4C92" offset="38.52%"/><stop stop-color="#DC4183" offset="76.54%"/><stop stop-color="#ED3D7D" offset="95.7%"/></linearGradient><linearGradient x1="91.376%" y1="19.144%" x2="18.895%" y2="70.21%" id="d"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#EE4E72" offset="36.4%"/><stop stop-color="#ED3D7D" offset="100%"/></linearGradient></defs><g fill="none"><path d="M118.623 71.8c.9-.8 1.4-1.9 1.5-3.2.1-2.6-1.8-4.7-4.4-4.9-1.2-.1-2.4.4-3.3 1.1l-83.8 45.9c-1.9.8-3.6 2.2-4.7 4.1-2.9 4.8-1.3 11 3.6 13.9 3.4 2 7.5 1.8 10.7-.2.2-.2.5-.3.7-.5l78-54.8c.4-.3 1.5-1.1 1.7-1.4z" fill="url(#a)" transform="translate(-.023)"/><path d="M118.823 65.1l-63.8-62.6c-1.4-1.5-3.4-2.5-5.7-2.5-4.3 0-7.7 3.5-7.7 7.7 0 2.1.8 3.9 2.1 5.3.4.4.8.7 1.2 1l67.4 57.7c.8.7 1.8 1.2 3 1.3 2.6.1 4.7-1.8 4.9-4.4 0-1.3-.5-2.6-1.4-3.5z" fill="url(#b)" transform="translate(-.023)"/><path d="M57.123 59.5c-.1 0-39.4-31-40.2-31.5l-1.8-.9c-5.8-2.2-12.2.8-14.4 6.6-1.9 5.1.2 10.7 4.6 13.4.7.4 1.3.7 2 .9.4.2 45.4 18.8 45.4 18.8 1.8.8 3.9.3 5.1-1.2 1.5-1.9 1.2-4.6-.7-6.1z" fill="url(#c)" transform="translate(-.023)"/><path d="M49.323 0c-1.7 0-3.3.6-4.6 1.5l-39.8 26.8c-.1.1-.2.1-.2.2h-.1c-1.7 1.2-3.1 3-3.9 5.1-2.2 5.8.8 12.3 6.6 14.4 3.6 1.4 7.5.7 10.4-1.4.7-.5 1.3-1 1.8-1.6l34.6-31.2c1.8-1.4 3-3.6 3-6.1 0-4.2-3.5-7.7-7.8-7.7z" fill="url(#d)" transform="translate(-.023)"/><path fill="#000" d="M34.6 37.4h51v51h-51z"/><path fill="#FFF" d="M39 78.8h19.1V82H39zm-.2-28l1.5-1.4c.4.5.8.8 1.3.8.6 0 .9-.4.9-1.2v-5.3h2.3V49c0 1-.3 1.8-.8 2.3-.5.5-1.3.8-2.3.8-1.5.1-2.3-.5-2.9-1.3zm6.5-7H52v1.9h-4.4V47h4v1.8h-4v1.3h4.5v2h-6.7zm9.7 2h-2.5v-2h7.3v2h-2.5v6.3H55zM39 54h4.3c1 0 1.8.3 2.3.7.3.3.5.8.5 1.4 0 1-.5 1.5-1.3 1.9 1 .3 1.6.9 1.6 2 0 1.4-1.2 2.3-3.1 2.3H39V54zm4.8 2.6c0-.5-.4-.7-1-.7h-1.5v1.5h1.4c.7-.1 1.1-.3 1.1-.8zM43 59h-1.8v1.5H43c.7 0 1.1-.3 1.1-.8s-.4-.7-1.1-.7zm3.8-5h3.9c1.3 0 2.1.3 2.7.9.5.5.7 1.1.7 1.9 0 1.3-.7 2.1-1.7 2.6l2 2.9h-2.6l-1.7-2.5h-1v2.5h-2.3V54zm3.8 4c.8 0 1.2-.4 1.2-1 0-.7-.5-1-1.2-1h-1.5v2h1.5z"/><path d="M56.8 54H59l3.5 8.4H60l-.6-1.5h-3.2l-.6 1.5h-2.4l3.6-8.4zm2 5l-.9-2.3L57 59h1.8zm4-5h2.3v8.3h-2.3zm2.9 0h2.1l3.4 4.4V54h2.3v8.3h-2L68 57.8v4.6h-2.3zm8 7.1l1.3-1.5c.8.7 1.7 1 2.7 1 .6 0 1-.2 1-.6 0-.4-.3-.5-1.4-.8-1.8-.4-3.1-.9-3.1-2.6 0-1.5 1.2-2.7 3.2-2.7 1.4 0 2.5.4 3.4 1.1l-1.2 1.6c-.8-.5-1.6-.8-2.3-.8-.6 0-.8.2-.8.5 0 .4.3.5 1.4.8 1.9.4 3.1 1 3.1 2.6 0 1.7-1.3 2.7-3.4 2.7-1.5.1-2.9-.4-3.9-1.3z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 3.0 KiB |
10
test/unit/node/test-plugin/public/index.html
Normal file
10
test/unit/node/test-plugin/public/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Plugin</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Welcome to the test plugin!</p>
|
||||
</body>
|
||||
</html>
|
52
test/unit/node/test-plugin/src/index.ts
Normal file
52
test/unit/node/test-plugin/src/index.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import * as cs from "code-server"
|
||||
import * as fspath from "path"
|
||||
|
||||
export const plugin: cs.Plugin = {
|
||||
displayName: "Test Plugin",
|
||||
routerPath: "/test-plugin",
|
||||
homepageURL: "https://example.com",
|
||||
description: "Plugin used in code-server tests.",
|
||||
|
||||
init(config) {
|
||||
config.logger.debug("test-plugin loaded!")
|
||||
},
|
||||
|
||||
router() {
|
||||
const r = cs.express.Router()
|
||||
r.get("/test-app", (_, res) => {
|
||||
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
|
||||
})
|
||||
r.get("/goland/icon.svg", (_, res) => {
|
||||
res.sendFile(fspath.resolve(__dirname, "../public/icon.svg"))
|
||||
})
|
||||
r.get("/error", () => {
|
||||
throw new cs.HttpError("error", cs.HttpCode.LargePayload)
|
||||
})
|
||||
return r
|
||||
},
|
||||
|
||||
wsRouter() {
|
||||
const wr = cs.WsRouter()
|
||||
wr.ws("/test-app", (req) => {
|
||||
cs.wss.handleUpgrade(req, req.ws, req.head, (ws) => {
|
||||
req.ws.resume()
|
||||
ws.send("hello")
|
||||
})
|
||||
})
|
||||
return wr
|
||||
},
|
||||
|
||||
applications() {
|
||||
return [
|
||||
{
|
||||
name: "Test App",
|
||||
version: "4.0.0",
|
||||
iconPath: "/icon.svg",
|
||||
path: "/test-app",
|
||||
|
||||
description: "This app does XYZ.",
|
||||
homepageURL: "https://example.com",
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
71
test/unit/node/test-plugin/tsconfig.json
Normal file
71
test/unit/node/test-plugin/tsconfig.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./out" /* Redirect output structure to the directory. */,
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
|
||||
"paths": {
|
||||
"code-server": ["../../../../typings/pluginapi"]
|
||||
} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
70
test/unit/node/test-plugin/yarn.lock
Normal file
70
test/unit/node/test-plugin/yarn.lock
Normal file
@ -0,0 +1,70 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.33"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
|
||||
integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express-serve-static-core@*":
|
||||
version "4.17.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084"
|
||||
integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
|
||||
"@types/express@^4.17.8":
|
||||
version "4.17.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a"
|
||||
integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/mime@*":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
|
||||
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
|
||||
|
||||
"@types/node@*":
|
||||
version "14.14.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f"
|
||||
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
|
||||
integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1"
|
||||
integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==
|
||||
dependencies:
|
||||
"@types/mime" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
typescript@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
|
||||
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
|
160
test/unit/node/update.test.ts
Normal file
160
test/unit/node/update.test.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { promises as fs } from "fs"
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import { tmpdir } from "../../../src/node/constants"
|
||||
import { SettingsProvider, UpdateSettings } from "../../../src/node/settings"
|
||||
import { LatestResponse, UpdateProvider } from "../../../src/node/update"
|
||||
|
||||
describe("update", () => {
|
||||
let version = "1.0.0"
|
||||
let spy: string[] = []
|
||||
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (!request.url) {
|
||||
throw new Error("no url")
|
||||
}
|
||||
|
||||
spy.push(request.url)
|
||||
|
||||
// Return the latest version.
|
||||
if (request.url === "/latest") {
|
||||
const latest: LatestResponse = {
|
||||
name: version,
|
||||
}
|
||||
response.writeHead(200)
|
||||
return response.end(JSON.stringify(latest))
|
||||
}
|
||||
|
||||
// Anything else is a 404.
|
||||
response.writeHead(404)
|
||||
response.end("not found")
|
||||
})
|
||||
|
||||
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
|
||||
const settings = new SettingsProvider<UpdateSettings>(jsonPath)
|
||||
|
||||
let _provider: UpdateProvider | undefined
|
||||
const provider = (): UpdateProvider => {
|
||||
if (!_provider) {
|
||||
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)
|
||||
}
|
||||
return _provider
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.on("error", reject)
|
||||
server.on("listening", resolve)
|
||||
server.listen({
|
||||
port: 0,
|
||||
host: "localhost",
|
||||
})
|
||||
})
|
||||
await fs.rmdir(path.join(tmpdir, "tests/updates"), { recursive: true })
|
||||
await fs.mkdir(path.join(tmpdir, "tests/updates"), { recursive: true })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
spy = []
|
||||
})
|
||||
|
||||
it("should get the latest", async () => {
|
||||
version = "2.1.0"
|
||||
|
||||
const p = provider()
|
||||
const now = Date.now()
|
||||
const update = await p.getUpdate()
|
||||
|
||||
await expect(settings.read()).resolves.toEqual({ update })
|
||||
expect(isNaN(update.checked)).toEqual(false)
|
||||
expect(update.checked < Date.now() && update.checked >= now).toEqual(true)
|
||||
expect(update.version).toStrictEqual("2.1.0")
|
||||
expect(spy).toEqual(["/latest"])
|
||||
})
|
||||
|
||||
it("should keep existing information", async () => {
|
||||
version = "3.0.1"
|
||||
|
||||
const p = provider()
|
||||
const now = Date.now()
|
||||
const update = await p.getUpdate()
|
||||
|
||||
await expect(settings.read()).resolves.toEqual({ update })
|
||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||
expect(update.checked < now).toBe(true)
|
||||
expect(update.version).toStrictEqual("2.1.0")
|
||||
expect(spy).toEqual([])
|
||||
})
|
||||
|
||||
it("should force getting the latest", async () => {
|
||||
version = "4.1.1"
|
||||
|
||||
const p = provider()
|
||||
const now = Date.now()
|
||||
const update = await p.getUpdate(true)
|
||||
|
||||
await expect(settings.read()).resolves.toEqual({ update })
|
||||
expect(isNaN(update.checked)).toStrictEqual(false)
|
||||
expect(update.checked < Date.now() && update.checked >= now).toStrictEqual(true)
|
||||
expect(update.version).toStrictEqual("4.1.1")
|
||||
expect(spy).toStrictEqual(["/latest"])
|
||||
})
|
||||
|
||||
it("should get latest after interval passes", async () => {
|
||||
const p = provider()
|
||||
await p.getUpdate()
|
||||
expect(spy).toEqual([])
|
||||
|
||||
let checked = Date.now() - 1000 * 60 * 60 * 23
|
||||
await settings.write({ update: { checked, version } })
|
||||
await p.getUpdate()
|
||||
expect(spy).toEqual([])
|
||||
|
||||
checked = Date.now() - 1000 * 60 * 60 * 25
|
||||
await settings.write({ update: { checked, version } })
|
||||
|
||||
const update = await p.getUpdate()
|
||||
expect(update.checked).not.toStrictEqual(checked)
|
||||
expect(spy).toStrictEqual(["/latest"])
|
||||
})
|
||||
|
||||
it("should check if it's the current version", async () => {
|
||||
version = "9999999.99999.9999"
|
||||
|
||||
const p = provider()
|
||||
let update = await p.getUpdate(true)
|
||||
expect(p.isLatestVersion(update)).toStrictEqual(false)
|
||||
|
||||
version = "0.0.0"
|
||||
update = await p.getUpdate(true)
|
||||
expect(p.isLatestVersion(update)).toStrictEqual(true)
|
||||
|
||||
// Old version format; make sure it doesn't report as being later.
|
||||
version = "999999.9999-invalid999.99.9"
|
||||
update = await p.getUpdate(true)
|
||||
expect(p.isLatestVersion(update)).toStrictEqual(true)
|
||||
})
|
||||
|
||||
it("should not reject if unable to fetch", async () => {
|
||||
let provider = new UpdateProvider("invalid", settings)
|
||||
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)
|
||||
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")
|
||||
})
|
||||
})
|
@ -1,9 +1,9 @@
|
||||
import * as cp from "child_process"
|
||||
import * as path from "path"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { generateUuid } from "../../../src/common/util"
|
||||
import * as util from "../../../src/node/util"
|
||||
import { tmpdir } from "../../../src/node/constants"
|
||||
import * as util from "../../../src/node/util"
|
||||
|
||||
describe("getEnvPaths", () => {
|
||||
describe("on darwin", () => {
|
||||
|
Reference in New Issue
Block a user