From c658a0b0dac64b4a93bae589980b0be025b1fa3b Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 10 Jan 2023 11:31:23 -0500 Subject: [PATCH] Get terminal tests to pass again Signed-off-by: Sebastian Malton --- src/common/test-utils/use-fake-time.ts | 1 - .../opening-terminal-tab.test.tsx.snap | 38 +++++++++++++++++++ .../terminal/opening-terminal-tab.test.tsx | 17 ++++++++- .../test-utils/application-builder.tsx | 17 +++++++-- src/main/__test__/kube-auth-proxy.test.ts | 2 +- src/main/k8s-request.injectable.ts | 4 +- src/main/lens-proxy/lens-proxy.ts | 2 +- .../api/create-terminal-api.injectable.ts | 10 ++--- src/renderer/api/terminal-api.ts | 7 +++- ...et-agent.global-override-for-injectable.ts | 12 ++++++ .../api/websocket-agent.injectable.ts | 20 ++++++++++ src/renderer/api/websocket-api.ts | 12 +++++- .../terminal-spawning-pool.injectable.ts | 15 +++++--- .../components/dock/terminal/terminal.ts | 6 +-- .../utils/get-element-by-id.injectable.ts | 21 ++++++++++ 15 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 src/renderer/api/websocket-agent.global-override-for-injectable.ts create mode 100644 src/renderer/api/websocket-agent.injectable.ts create mode 100644 src/renderer/utils/get-element-by-id.injectable.ts diff --git a/src/common/test-utils/use-fake-time.ts b/src/common/test-utils/use-fake-time.ts index e455984861..a788a8e2b7 100644 --- a/src/common/test-utils/use-fake-time.ts +++ b/src/common/test-utils/use-fake-time.ts @@ -20,6 +20,5 @@ export const testUsingFakeTime = (dateTime = "2015-10-21T07:28:00Z") => { usingFakeTime = true; jest.useFakeTimers(); - jest.setSystemTime(new Date(dateTime)); }; diff --git a/src/features/terminal/__snapshots__/opening-terminal-tab.test.tsx.snap b/src/features/terminal/__snapshots__/opening-terminal-tab.test.tsx.snap index 3323da6e5f..b4f65871ce 100644 --- a/src/features/terminal/__snapshots__/opening-terminal-tab.test.tsx.snap +++ b/src/features/terminal/__snapshots__/opening-terminal-tab.test.tsx.snap @@ -6,6 +6,44 @@ exports[`test for opening terminal tab within cluster frame when new terminal ta
+
+
+
+
+ +
+ + + close + + +
+ Close +
+
+
+
+
+
diff --git a/src/features/terminal/opening-terminal-tab.test.tsx b/src/features/terminal/opening-terminal-tab.test.tsx index 1244543d08..22703a321f 100644 --- a/src/features/terminal/opening-terminal-tab.test.tsx +++ b/src/features/terminal/opening-terminal-tab.test.tsx @@ -18,6 +18,8 @@ import type { ApplicationBuilder } from "../test-utils/application-builder"; import { setupInitializingApplicationBuilder } from "../test-utils/application-builder"; import type { FindByTextWithMarkup } from "../../test-utils/findByTextWithMarkup"; import { findByTextWithMarkupFor } from "../../test-utils/findByTextWithMarkup"; +import terminalStoreInjectable from "../../renderer/components/dock/terminal/store.injectable"; +import { flushPromises } from "../../common/test-utils/flush-promises"; describe("test for opening terminal tab within cluster frame", () => { let builder: ApplicationBuilder; @@ -113,8 +115,21 @@ describe("test for opening terminal tab within cluster frame", () => { }); describe("when the next data is sent", () => { - beforeEach(() => { + beforeEach(async () => { + const terminalStore = builder.applicationWindow.only.di.inject(terminalStoreInjectable); + const terminalApi = terminalStore.getTerminalApi("terminal"); + + assert(terminalApi); + + const waitForData = new Promise(resolve => terminalApi.on("data", (message) => { + if (message) { + resolve(); + } + })); + + await flushPromises(); sendData("I am a prompt"); + await waitForData; }); it("renders the new data", async () => { diff --git a/src/features/test-utils/application-builder.tsx b/src/features/test-utils/application-builder.tsx index 8cb700f8e2..01ec75c081 100644 --- a/src/features/test-utils/application-builder.tsx +++ b/src/features/test-utils/application-builder.tsx @@ -81,9 +81,10 @@ import { runManySyncFor } from "../../common/runnable/run-many-sync-for"; import type { MemoryHistory } from "history"; import { object } from "../../common/utils"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; -import { testUsingFakeTime } from "../../common/test-utils/use-fake-time"; import createVersionDetectorInjectable from "../../main/cluster-detectors/create-version-detector.injectable"; import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; +import getElementByIdInjectable from "../../renderer/utils/get-element-by-id.injectable"; +import { testUsingFakeTime } from "../../common/test-utils/use-fake-time"; type Callback = (di: DiContainer) => void | Promise; @@ -237,8 +238,8 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB const createElectronWindowFake: CreateElectronWindow = (configuration) => { const windowId = configuration.id; - const windowDi = getRendererDi({ doGeneralOverrides: true }); + let rendered: RenderResult; overrideForWindow(windowDi, windowId); overrideFsWithFakes(windowDi); @@ -258,7 +259,15 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB return computed(() => [...rendererExtensionState.values()]); }); - let rendered: RenderResult; + windowDi.override(getElementByIdInjectable, () => (id) => { + const elem = rendered?.container.querySelector(`#${id}`); + + if (!elem) { + throw new Error(`Missing #${id} in DOM`); + } + + return elem; + }); windowHelpers.set(windowId, { di: windowDi, getRendered: () => rendered }); @@ -357,7 +366,7 @@ export const setupInitializingApplicationBuilder = (init: (builder: ApplicationB windowDi.override(currentLocationInjectable, () => ({ hostname: "localhost", port: `${mainDi.inject(lensProxyPortInjectable).get()}`, - protocol: "http", + protocol: "https", })); }); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index bcb1c33293..6bcd3cf95b 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -5,7 +5,7 @@ import waitUntilPortIsUsedInjectable from "../../common/utils/wait-until-port-is-used/wait-until-port-is-used.injectable"; import type { Cluster } from "../../common/cluster/cluster"; -import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; +import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import type { ChildProcess } from "child_process"; import { Kubectl } from "../kubectl/kubectl"; import type { DeepMockProxy } from "jest-mock-extended"; diff --git a/src/main/k8s-request.injectable.ts b/src/main/k8s-request.injectable.ts index 9797ba98e5..a698b96a7a 100644 --- a/src/main/k8s-request.injectable.ts +++ b/src/main/k8s-request.injectable.ts @@ -2,7 +2,6 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { apiKubePrefix } from "../common/vars"; import type { Cluster } from "../common/cluster/cluster"; import { getInjectable } from "@ogre-tools/injectable"; import type { RequestInit } from "node-fetch"; @@ -23,8 +22,7 @@ const k8sRequestInjectable = getInjectable({ return async (cluster, path, { timeout = 30_000, ...init } = {}) => { const controller = withTimeout(timeout); - - const response = await lensFetch(`/${cluster.id}${apiKubePrefix}${path}`, { + const response = await lensFetch(`/${cluster.id}${path}`, { ...init, signal: controller.signal, }); diff --git a/src/main/lens-proxy/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts index 5be786e28d..1ec1acf04a 100644 --- a/src/main/lens-proxy/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -17,7 +17,7 @@ import type { SetRequired } from "type-fest"; import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; import type { Logger } from "../../common/logger"; import type { SelfSignedCert } from "selfsigned"; -import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable"; +import type { GetClusterForRequest } from "./get-cluster-for-request.injectable"; export type ServerIncomingMessage = SetRequired; export type ProxyRequestHandler = (args: ProxyApiRequestArgs) => void | Promise; diff --git a/src/renderer/api/create-terminal-api.injectable.ts b/src/renderer/api/create-terminal-api.injectable.ts index 59af749968..d0d22d0988 100644 --- a/src/renderer/api/create-terminal-api.injectable.ts +++ b/src/renderer/api/create-terminal-api.injectable.ts @@ -11,17 +11,19 @@ import currentLocationInjectable from "./current-location.injectable"; import defaultWebsocketApiParamsInjectable from "./default-websocket-api-params.injectable"; import type { TerminalApiDependencies, TerminalApiQuery } from "./terminal-api"; import { TerminalApi } from "./terminal-api"; +import websocketAgentInjectable from "./websocket-agent.injectable"; export type CreateTerminalApi = (query: TerminalApiQuery) => TerminalApi; const createTerminalApiInjectable = getInjectable({ id: "create-terminal-api", instantiate: (di): CreateTerminalApi => { - const partialDeps = { + const partialDeps: Omit = { requestShellApiToken: di.inject(requestShellApiTokenInjectable), defaultParams: di.inject(defaultWebsocketApiParamsInjectable), logger: di.inject(loggerInjectable), currentLocation: di.inject(currentLocationInjectable), + websocketAgent: di.inject(websocketAgentInjectable), }; return (query) => { @@ -29,12 +31,10 @@ const createTerminalApiInjectable = getInjectable({ assert(hostedClusterId, "Can only create Terminal APIs within cluster frames"); - const deps: TerminalApiDependencies = { + return new TerminalApi({ ...partialDeps, hostedClusterId, - }; - - return new TerminalApi(deps, query); + }, query); }; }, }); diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts index 2785e294ae..e75a306068 100644 --- a/src/renderer/api/terminal-api.ts +++ b/src/renderer/api/terminal-api.ts @@ -13,6 +13,7 @@ import { type TerminalMessage, TerminalChannels } from "../../common/terminal/ch import type { RequestShellApiToken } from "../../features/terminal/renderer/request-shell-api-token.injectable"; import type { CurrentLocation } from "./current-location.injectable"; import type { ClusterId } from "../../common/cluster-types"; +import type { CloseEvent, Event, MessageEvent } from "ws"; enum TerminalColor { RED = "\u001b[31m", @@ -127,7 +128,9 @@ export class TerminalApi extends WebSocketApi { } } - protected _onMessage({ data, ...evt }: MessageEvent): void { + protected _onMessage(event: MessageEvent): void { + const data = event.data as string; + try { const message = JSON.parse(data) as TerminalMessage; @@ -138,7 +141,7 @@ export class TerminalApi extends WebSocketApi { * don't want this data to survive if the app is closed */ window.localStorage.setItem(`${this.query.id}:last-data`, message.data); - super._onMessage({ data: message.data, ...evt }); + super._onMessage({ ...event, data: message.data }); break; case TerminalChannels.CONNECTED: this.emit("connected"); diff --git a/src/renderer/api/websocket-agent.global-override-for-injectable.ts b/src/renderer/api/websocket-agent.global-override-for-injectable.ts new file mode 100644 index 0000000000..b3b8a239a2 --- /dev/null +++ b/src/renderer/api/websocket-agent.global-override-for-injectable.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { Agent } from "https"; +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import websocketAgentInjectable from "./websocket-agent.injectable"; + +export default getGlobalOverride(websocketAgentInjectable, () => new Agent({ + rejectUnauthorized: false, +})); diff --git a/src/renderer/api/websocket-agent.injectable.ts b/src/renderer/api/websocket-agent.injectable.ts new file mode 100644 index 0000000000..caf14d43d8 --- /dev/null +++ b/src/renderer/api/websocket-agent.injectable.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { Agent } from "https"; +import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; + +const websocketAgentInjectable = getInjectable({ + id: "websocket-agent", + instantiate: (di) => { + const lensProxyCertificate = di.inject(lensProxyCertificateInjectable); + + return new Agent({ + cert: lensProxyCertificate.get().cert, + }); + }, +}); + +export default websocketAgentInjectable; diff --git a/src/renderer/api/websocket-api.ts b/src/renderer/api/websocket-api.ts index 611e0b7e67..285aa9cd12 100644 --- a/src/renderer/api/websocket-api.ts +++ b/src/renderer/api/websocket-api.ts @@ -9,6 +9,9 @@ import type TypedEventEmitter from "typed-emitter"; import type { Defaulted } from "../utils"; import type { DefaultWebsocketApiParams } from "./default-websocket-api-params.injectable"; import type { Logger } from "../../common/logger"; +import type { CloseEvent, Event, MessageEvent } from "ws"; +import { WebSocket } from "ws"; +import type { Agent } from "https"; interface WebsocketApiParams { /** @@ -66,6 +69,7 @@ export interface WebSocketEvents { export interface WebSocketApiDependencies { readonly defaultParams: DefaultWebsocketApiParams; readonly logger: Logger; + readonly websocketAgent: Agent; } export class WebSocketApi extends (EventEmitter as { new(): TypedEventEmitter }) { @@ -100,7 +104,9 @@ export class WebSocketApi extends (EventEmitter this.socket?.close(); // start new connection - this.socket = new WebSocket(url); + this.socket = new WebSocket(url, { + agent: this.dependencies.websocketAgent, + }); this.socket.addEventListener("open", ev => this._onOpen(ev)); this.socket.addEventListener("message", ev => this._onMessage(ev)); this.socket.addEventListener("error", ev => this._onError(ev)); @@ -164,7 +170,9 @@ export class WebSocketApi extends (EventEmitter this.writeLog("%cOPEN", "color:green;font-weight:bold;", evt); } - protected _onMessage({ data }: MessageEvent): void { + protected _onMessage(event: MessageEvent): void { + const data = event.data as string; + (this as TypedEventEmitter).emit("data", data); this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data); } diff --git a/src/renderer/components/dock/terminal/terminal-spawning-pool.injectable.ts b/src/renderer/components/dock/terminal/terminal-spawning-pool.injectable.ts index 7c6f9d984e..e210ab5192 100644 --- a/src/renderer/components/dock/terminal/terminal-spawning-pool.injectable.ts +++ b/src/renderer/components/dock/terminal/terminal-spawning-pool.injectable.ts @@ -3,16 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import assert from "assert"; +import { memoize } from "lodash"; +import getElementByIdInjectable from "../../../utils/get-element-by-id.injectable"; +/** + * It is necessary to have this a function because in a testing environment the DOM isn't + * available until after first render + */ const terminalSpawningPoolInjectable = getInjectable({ id: "terminal-spawning-pool", - instantiate: () => { - const pool = document.getElementById("terminal-init"); + instantiate: (di) => { + const getElementById = di.inject(getElementByIdInjectable); - assert(pool, "DOM MUST contain #terminal-init element"); - - return pool; + return memoize(() => getElementById("terminal-init")); }, }); diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index 1300c64578..41e77f6b3f 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -24,12 +24,12 @@ import { SearchAddon } from "xterm-addon-search"; import { WebglAddon } from "xterm-addon-webgl"; export interface TerminalDependencies { - readonly spawningPool: HTMLElement; readonly terminalConfig: IComputedValue; readonly terminalCopyOnSelect: IComputedValue; readonly isMac: boolean; readonly xtermColorTheme: IComputedValue>; readonly logger: Logger; + spawningPool: () => HTMLElement; openLinkInBrowser: OpenLinkInBrowser; createTerminalRenderer: CreateTerminalRenderer; } @@ -73,7 +73,7 @@ export class Terminal { const { elem } = this; if (elem) { - this.dependencies.spawningPool.appendChild(elem); + this.dependencies.spawningPool().appendChild(elem); } } @@ -106,7 +106,7 @@ export class Terminal { this.xterm.loadAddon(new WebLinksAddon()); this.xterm.loadAddon(new SearchAddon()); - this.xterm.open(this.dependencies.spawningPool); + this.xterm.open(this.dependencies.spawningPool()); try { const webgl = new WebglAddon(); diff --git a/src/renderer/utils/get-element-by-id.injectable.ts b/src/renderer/utils/get-element-by-id.injectable.ts new file mode 100644 index 0000000000..89401eb490 --- /dev/null +++ b/src/renderer/utils/get-element-by-id.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; + +const getElementByIdInjectable = getInjectable({ + id: "get-element-by-id", + instantiate: () => (id: string) => { + const elem = document.getElementById(id); + + if (!elem) { + throw new Error(`Missing #${id} in DOM`); + } + + return elem; + }, + causesSideEffects: true, +}); + +export default getElementByIdInjectable;