import { 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 { CodeServer, CodeServerPage } from "../e2e/models/CodeServer" import { REVERSE_PROXY_PORT, REVERSE_PROXY_BASE_PATH } from "./constants" /** * Spy on the logger and console and replace with mock implementations to * suppress the output. */ export function mockLogger() { jest.spyOn(logger, "debug").mockImplementation() jest.spyOn(logger, "error").mockImplementation() jest.spyOn(logger, "info").mockImplementation() jest.spyOn(logger, "trace").mockImplementation() jest.spyOn(logger, "warn").mockImplementation() jest.spyOn(console, "log").mockImplementation() jest.spyOn(console, "debug").mockImplementation() jest.spyOn(console, "error").mockImplementation() jest.spyOn(console, "info").mockImplementation() jest.spyOn(console, "trace").mockImplementation() jest.spyOn(console, "warn").mockImplementation() } /** * Clean up directories left by a test. It is recommended to do this when a test * starts to avoid potentially accumulating infinite test directories. */ export async function clean(testName: string): Promise { const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`) await fs.rm(dir, { force: true, recursive: true }) } /** * Create a uniquely named temporary directory for a test. * * `tmpdir` should usually be preceeded by at least one call to `clean`. */ export async function tmpdir(testName: string): Promise { const dir = path.join(os.tmpdir(), `code-server/tests/${testName}`) await fs.mkdir(dir, { recursive: true }) return await fs.mkdtemp(path.join(dir, `${testName}-`), { encoding: "utf8" }) } /** * @description Helper function to use an environment variable. * * @returns an array (similar to useState in React) with a function * to set the value and reset the value */ export function useEnv(key: string): [(nextValue: string | undefined) => string | undefined, () => void] { const initialValue = process.env[key] const setValue = (nextValue: string | undefined) => (process.env[key] = nextValue) // Node automatically converts undefined to string 'undefined' // when assigning an environment variable. // which is why we need to delete it if it's supposed to be undefined // Source: https://stackoverflow.com/a/60147167 const resetValue = () => { if (initialValue !== undefined) { process.env[key] = initialValue } else { delete process.env[key] } } return [setValue, resetValue] } /** * Helper function to get a random port. * * Source: https://github.com/sindresorhus/get-port/blob/main/index.js#L23-L33 */ export const getAvailablePort = (options?: net.ListenOptions): Promise => new Promise((resolve, reject) => { const server = net.createServer() server.unref() server.on("error", reject) server.listen(options, () => { // NOTE@jsjoeio: not a huge fan of the type assertion // but it works for now. const { port } = server.address() as net.AddressInfo server.close(() => { resolve(port) }) }) }) /** * Return a timer that will not reject as long as it is disposed or continually * reset before the delay elapses. */ export function idleTimer(message: string, reject: (error: Error) => void, delay = 5000) { const start = () => setTimeout(() => reject(new Error(message)), delay) let timeout = start() return { reset: () => { clearTimeout(timeout) timeout = start() }, dispose: () => { clearTimeout(timeout) }, } } /** * A helper function which returns a boolean indicating whether * the given address is AddressInfo and has .address * and a .port property. */ export function isAddressInfo(address: unknown): address is net.AddressInfo { return ( address !== null && typeof address !== "string" && (address as net.AddressInfo).port !== undefined && (address as net.AddressInfo).address !== undefined ) } /** * If using a proxy, return the address of the proxy. * * Otherwise, return the direct address of code-server. */ export async function getMaybeProxiedCodeServer(codeServer: CodeServerPage | CodeServer): Promise { const address = await codeServer.address() if (process.env.USE_PROXY === "1") { const uri = new URL(address) return `http://${uri.hostname}:${REVERSE_PROXY_PORT}/${uri.port}/${REVERSE_PROXY_BASE_PATH}/` } return address } /** * Stripes proxy base from url.pathname * i.e. //ide + route returns just route */ export function getMaybeProxiedPathname(url: URL): string { if (process.env.USE_PROXY === "1") { // Behind proxy, path will be //ide + route const pathWithoutProxy = url.pathname.split(`/${REVERSE_PROXY_BASE_PATH}`)[1] return pathWithoutProxy } return url.pathname }