From 81f7bd3c2c34bb1ea5183862f3b186ce7b2b2c00 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Tue, 23 Aug 2022 13:04:00 +0300 Subject: [PATCH] Replace jest.mock's with overriding dependencies (#6014) Signed-off-by: Janne Savolainen Signed-off-by: Janne Savolainen --- src/common/cluster/cluster.ts | 9 +- .../stat.global-override-for-injectable.ts | 10 ++ src/common/fs/stat/stat.injectable.ts | 14 ++ .../fs/validate-directory.injectable.ts | 76 ++++++++ .../watch.global-override-for-injectable.ts | 10 ++ src/common/fs/watch/watch.injectable.ts | 18 ++ .../ipc/broadcast-message.injectable.ts | 4 +- src/common/ipc/ipc.ts | 15 ++ src/common/protocol-handler/router.ts | 15 +- .../__tests__/extension-loader.test.ts | 105 ++++++------ .../extension-discovery.injectable.ts | 8 + .../extension-discovery.test.ts | 57 +++--- .../extension-discovery.ts | 45 ++--- .../extension-loader/extension-loader.ts | 8 +- src/main/__test__/cluster.test.ts | 7 +- src/main/__test__/kube-auth-proxy.test.ts | 65 +++---- .../create-cluster.injectable.ts | 2 + src/main/getDiForUnitTesting.ts | 2 - src/main/helm/__mocks__/helm-chart-manager.ts | 153 ----------------- src/main/helm/__tests__/helm-service.test.ts | 151 +++++++++++++++- .../helm-chart-manager-cache.injectable.ts | 19 ++ .../helm/helm-chart-manager.injectable.ts | 26 +++ src/main/helm/helm-chart-manager.ts | 27 ++- .../get-helm-chart-values.injectable.ts | 6 +- .../helm-service/get-helm-chart.injectable.ts | 6 +- .../list-helm-charts.injectable.ts | 6 +- .../create-kube-auth-proxy.injectable.ts | 2 + src/main/kube-auth-proxy/kube-auth-proxy.ts | 14 +- ...-is-used.global-override-for-injectable.ts | 15 ++ .../wait-until-port-is-used.injectable.ts | 20 +++ .../protocol-handler/__test__/router.test.ts | 28 +-- .../lens-protocol-router-main.injectable.ts | 4 + .../lens-protocol-router-main.ts | 16 +- .../start-main-application.injectable.ts | 1 - ...ipc-main.global-override-for-injectable.ts | 13 ++ .../api/catalog/entity/registry.injectable.ts | 2 + src/renderer/api/catalog/entity/registry.ts | 11 +- .../components/+catalog/catalog.test.tsx | 162 ++++++++---------- .../__tests__/secret-details.test.tsx | 7 +- .../__tests__/network-policy-details.test.tsx | 17 +- .../cluster-local-terminal-settings.test.tsx | 46 +++-- .../cluster-local-terminal-settings.tsx | 95 +++------- .../kube-object-menu.test.tsx.snap | 24 ++- .../kube-object-menu.test.tsx | 6 - .../create-cluster.injectable.ts | 2 + src/renderer/getDiForUnitTesting.tsx | 7 - src/renderer/ipc/index.ts | 11 +- ...ens-protocol-router-renderer.injectable.ts | 5 +- .../lens-protocol-router-renderer.tsx | 9 +- ...renderer.global-override-for-injectable.ts | 12 ++ 50 files changed, 788 insertions(+), 605 deletions(-) create mode 100644 src/common/fs/stat/stat.global-override-for-injectable.ts create mode 100644 src/common/fs/stat/stat.injectable.ts create mode 100644 src/common/fs/validate-directory.injectable.ts create mode 100644 src/common/fs/watch/watch.global-override-for-injectable.ts create mode 100644 src/common/fs/watch/watch.injectable.ts delete mode 100644 src/main/helm/__mocks__/helm-chart-manager.ts create mode 100644 src/main/helm/helm-chart-manager-cache.injectable.ts create mode 100644 src/main/helm/helm-chart-manager.injectable.ts create mode 100644 src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.global-override-for-injectable.ts create mode 100644 src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable.ts create mode 100644 src/main/utils/channel/ipc-main/ipc-main.global-override-for-injectable.ts create mode 100644 src/renderer/utils/channel/ipc-renderer.global-override-for-injectable.ts diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 11b67bd002..f1a943732c 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -4,7 +4,6 @@ */ import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; -import { broadcastMessage } from "../ipc"; import type { ClusterContextHandler } from "../../main/context-handler/context-handler"; import type { KubeConfig } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node"; @@ -25,6 +24,7 @@ import type { CanI } from "./authorization-review.injectable"; import type { ListNamespaces } from "./list-namespaces.injectable"; import assert from "assert"; import type { Logger } from "../logger"; +import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -36,6 +36,7 @@ export interface ClusterDependencies { createAuthorizationReview: (config: KubeConfig) => CanI; createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; + broadcastMessage: BroadcastMessage; } /** @@ -602,7 +603,7 @@ export class Cluster implements ClusterModel, ClusterState { */ pushState(state = this.getState()) { this.dependencies.logger.silly(`[CLUSTER]: push-state`, state); - broadcastMessage("cluster:state", this.id, state); + this.dependencies.broadcastMessage("cluster:state", this.id, state); } // get cluster system meta, e.g. use in "logger" @@ -625,7 +626,7 @@ export class Cluster implements ClusterModel, ClusterState { const update: KubeAuthUpdate = { message, isError }; this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() }); - broadcastMessage(`cluster:${this.id}:connection-update`, update); + this.dependencies.broadcastMessage(`cluster:${this.id}:connection-update`, update); } protected async getAllowedNamespaces(proxyConfig: KubeConfig) { @@ -645,7 +646,7 @@ export class Cluster implements ClusterModel, ClusterState { const { response } = error as HttpError & { response: Response }; this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body }); - broadcastMessage(clusterListNamespaceForbiddenChannel, this.id); + this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.id); } return namespaceList; diff --git a/src/common/fs/stat/stat.global-override-for-injectable.ts b/src/common/fs/stat/stat.global-override-for-injectable.ts new file mode 100644 index 0000000000..2afeda7b77 --- /dev/null +++ b/src/common/fs/stat/stat.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import statInjectable from "./stat.injectable"; +import { getGlobalOverride } from "../../test-utils/get-global-override"; + +export default getGlobalOverride(statInjectable, () => () => { + throw new Error("Tried to call stat without explicit override"); +}); diff --git a/src/common/fs/stat/stat.injectable.ts b/src/common/fs/stat/stat.injectable.ts new file mode 100644 index 0000000000..aa1ce44447 --- /dev/null +++ b/src/common/fs/stat/stat.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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 fsInjectable from "../fs.injectable"; + +const statInjectable = getInjectable({ + id: "stat", + + instantiate: (di) => di.inject(fsInjectable).stat, +}); + +export default statInjectable; diff --git a/src/common/fs/validate-directory.injectable.ts b/src/common/fs/validate-directory.injectable.ts new file mode 100644 index 0000000000..9d68ede8d5 --- /dev/null +++ b/src/common/fs/validate-directory.injectable.ts @@ -0,0 +1,76 @@ +/** + * 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 type { AsyncResult } from "../utils/async-result"; +import { isErrnoException } from "../utils"; +import type { Stats } from "fs-extra"; +import { lowerFirst } from "lodash/fp"; +import statInjectable from "./stat/stat.injectable"; + +export type ValidateDirectory = (path: string) => Promise>; + +function getUserReadableFileType(stats: Stats): string { + if (stats.isFile()) { + return "a file"; + } + + if (stats.isFIFO()) { + return "a pipe"; + } + + if (stats.isSocket()) { + return "a socket"; + } + + if (stats.isBlockDevice()) { + return "a block device"; + } + + if (stats.isCharacterDevice()) { + return "a character device"; + } + + return "an unknown file type"; +} + +const validateDirectoryInjectable = getInjectable({ + id: "validate-directory", + + instantiate: (di): ValidateDirectory => { + const stat = di.inject(statInjectable); + + return async (path) => { + try { + const stats = await stat(path); + + if (stats.isDirectory()) { + return { callWasSuccessful: true, response: undefined }; + } + + return { callWasSuccessful: false, error: `the provided path is ${getUserReadableFileType(stats)} and not a directory.` }; + } catch (error) { + if (!isErrnoException(error)) { + return { callWasSuccessful: false, error: "of an unknown error, please try again." }; + } + + const humanReadableErrors: Record = { + ENOENT: "the provided path does not exist.", + EACCES: "search permissions is denied for one of the directories in the prefix of the provided path.", + ELOOP: "the provided path is a sym-link which points to a chain of sym-links that is too long to resolve. Perhaps it is cyclic.", + ENAMETOOLONG: "the pathname is too long to be used.", + ENOTDIR: "a prefix of the provided path is not a directory.", + }; + + const humanReadableError = error.code + ? humanReadableErrors[error.code] + : lowerFirst(String(error)); + + return { callWasSuccessful: false, error: humanReadableError }; + } + }; + }, +}); + +export default validateDirectoryInjectable; diff --git a/src/common/fs/watch/watch.global-override-for-injectable.ts b/src/common/fs/watch/watch.global-override-for-injectable.ts new file mode 100644 index 0000000000..689c7150cf --- /dev/null +++ b/src/common/fs/watch/watch.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getGlobalOverride } from "../../test-utils/get-global-override"; +import watchInjectable from "./watch.injectable"; + +export default getGlobalOverride(watchInjectable, () => () => { + throw new Error("Tried to call file system watch without explicit override"); +}); diff --git a/src/common/fs/watch/watch.injectable.ts b/src/common/fs/watch/watch.injectable.ts new file mode 100644 index 0000000000..44d34f20f5 --- /dev/null +++ b/src/common/fs/watch/watch.injectable.ts @@ -0,0 +1,18 @@ +/** + * 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 type { FSWatcher, WatchOptions } from "chokidar"; +import { watch } from "chokidar"; + +export type Watch = (path: string, options?: WatchOptions) => FSWatcher; + +// TODO: Introduce wrapper to allow simpler API +const watchInjectable = getInjectable({ + id: "watch", + instantiate: (): Watch => watch, + causesSideEffects: true, +}); + +export default watchInjectable; diff --git a/src/common/ipc/broadcast-message.injectable.ts b/src/common/ipc/broadcast-message.injectable.ts index 9df36ac27a..41db749d65 100644 --- a/src/common/ipc/broadcast-message.injectable.ts +++ b/src/common/ipc/broadcast-message.injectable.ts @@ -5,9 +5,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import { broadcastMessage } from "./ipc"; +export type BroadcastMessage = (channel: string, ...args: any[]) => Promise; + const broadcastMessageInjectable = getInjectable({ id: "broadcast-message", - instantiate: () => broadcastMessage, + instantiate: (): BroadcastMessage => broadcastMessage, causesSideEffects: true, }); diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index bddeb0ff3c..a714af73a4 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -13,10 +13,17 @@ import logger from "../../main/logger"; import type { ClusterFrameInfo } from "../cluster-frames"; import { clusterFrameMap } from "../cluster-frames"; import type { Disposer } from "../utils"; +import ipcMainInjectable from "../../main/utils/channel/ipc-main/ipc-main.injectable"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable"; export const broadcastMainChannel = "ipc:broadcast-main"; export function ipcMainHandle(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { + const di = getLegacyGlobalDiForExtensionApi(); + + const ipcMain = di.inject(ipcMainInjectable); + ipcMain.handle(channel, async (event, ...args) => { return sanitizePayload(await listener(event, ...args)); }); @@ -78,12 +85,20 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise } export function ipcMainOn(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): Disposer { + const di = getLegacyGlobalDiForExtensionApi(); + + const ipcMain = di.inject(ipcMainInjectable); + ipcMain.on(channel, listener); return () => ipcMain.off(channel, listener); } export function ipcRendererOn(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer { + const di = getLegacyGlobalDiForExtensionApi(); + + const ipcRenderer = di.inject(ipcRendererInjectable); + ipcRenderer.on(channel, listener); return () => ipcRenderer.off(channel, listener); diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index 804f70c80b..1c0ae909eb 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -8,7 +8,6 @@ import { matchPath } from "react-router"; import { countBy } from "lodash"; import { isDefined, iter } from "../utils"; import { pathToRegexp } from "path-to-regexp"; -import logger from "../../main/logger"; import type Url from "url-parse"; import { RoutingError, RoutingErrorType } from "./error"; import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; @@ -17,6 +16,7 @@ import type { LensExtension } from "../../extensions/lens-extension"; import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler"; import { when } from "mobx"; import { ipcRenderer } from "electron"; +import type { Logger } from "../logger"; // IPC channel for protocol actions. Main broadcasts the open-url events to this channel. export const ProtocolHandlerIpcPrefix = "protocol-handler"; @@ -66,6 +66,7 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R export interface LensProtocolRouterDependencies { readonly extensionLoader: ExtensionLoader; readonly extensionsStore: ExtensionsStore; + readonly logger: Logger; } export abstract class LensProtocolRouter { @@ -130,7 +131,7 @@ export abstract class LensProtocolRouter { data.extensionName = extensionName; } - logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data); + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data); return RouteAttempt.MISSING; } @@ -183,7 +184,7 @@ export abstract class LensProtocolRouter { timeout: 5_000, }); } catch (error) { - logger.info( + this.dependencies.logger.info( `${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`, ); @@ -193,18 +194,18 @@ export abstract class LensProtocolRouter { const extension = extensionLoader.getInstanceByName(name); if (!extension) { - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`); + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`); return name; } if (!this.dependencies.extensionsStore.isEnabled(extension)) { - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); return name; } - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`); + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`); return extension; } @@ -250,7 +251,7 @@ export abstract class LensProtocolRouter { */ public addInternalHandler(urlSchema: string, handler: RouteHandler): this { pathToRegexp(urlSchema); // verify now that the schema is valid - logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`); + this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`); this.internalRoutes.set(urlSchema, handler); return this; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index 765762c9bc..fa14f5856f 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -10,8 +10,10 @@ import extensionLoaderInjectable from "../extension-loader/extension-loader.inje import { runInAction } from "mobx"; import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; import mockFs from "mock-fs"; -import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { delay } from "../../renderer/utils"; +import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; +import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable"; +import type { IpcRenderer } from "electron"; console = new Console(stdout, stderr); @@ -19,10 +21,14 @@ const manifestPath = "manifest/path"; const manifestPath2 = "manifest/path2"; const manifestPath3 = "manifest/path3"; -jest.mock( - "electron", - () => ({ - ipcRenderer: { +describe("ExtensionLoader", () => { + let extensionLoader: ExtensionLoader; + let updateExtensionStateMock: jest.Mock; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(ipcRendererInjectable, () => ({ invoke: jest.fn(async (channel: string) => { if (channel === "extension-loader:main:state") { return [ @@ -59,59 +65,46 @@ jest.mock( return []; }), - on: jest.fn( - (channel: string, listener: (event: any, ...args: any[]) => void) => { - if (channel === "extension-loader:main:state") { - // First initialize with extensions 1 and 2 - // and then broadcast event to remove extension 2 and add extension number 3 - setTimeout(() => { - listener({}, [ - [ + + on: (channel: string, listener: (event: any, ...args: any[]) => void) => { + if (channel === "extension-loader:main:state") { + // First initialize with extensions 1 and 2 + // and then broadcast event to remove extension 2 and add extension number 3 + setTimeout(() => { + listener({}, [ + [ + manifestPath, + { + manifest: { + name: "TestExtension", + version: "1.0.0", + }, + id: manifestPath, + absolutePath: "/test/1", manifestPath, - { - manifest: { - name: "TestExtension", - version: "1.0.0", - }, - id: manifestPath, - absolutePath: "/test/1", - manifestPath, - isBundled: false, - isEnabled: true, + isBundled: false, + isEnabled: true, + }, + ], + [ + manifestPath3, + { + manifest: { + name: "TestExtension3", + version: "3.0.0", }, - ], - [ - manifestPath3, - { - manifest: { - name: "TestExtension3", - version: "3.0.0", - }, - id: manifestPath3, - absolutePath: "/test/3", - manifestPath: manifestPath3, - isBundled: false, - isEnabled: true, - }, - ], - ]); - }, 10); - } - }, - ), - }, - }), - { - virtual: true, - }, -); - -describe("ExtensionLoader", () => { - let extensionLoader: ExtensionLoader; - let updateExtensionStateMock: jest.Mock; - - beforeEach(() => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + id: manifestPath3, + absolutePath: "/test/3", + manifestPath: manifestPath3, + isBundled: false, + isEnabled: true, + }, + ], + ]); + }, 10); + } + }, + }) as unknown as IpcRenderer); mockFs(); diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index f7ffda8a80..e6ebaa769c 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -13,6 +13,10 @@ import installExtensionInjectable from "../extension-installer/install-extension import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable"; import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; +import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; +import loggerInjectable from "../../common/logger.injectable"; +import pathExistsInjectable from "../../common/fs/path-exists.injectable"; +import watchInjectable from "../../common/fs/watch/watch.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -40,6 +44,10 @@ const extensionDiscoveryInjectable = getInjectable({ ), staticFilesDirectory: di.inject(staticFilesDirectoryInjectable), + readJsonFile: di.inject(readJsonFileInjectable), + pathExists: di.inject(pathExistsInjectable), + watch: di.inject(watchInjectable), + logger: di.inject(loggerInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index cfbad4e11c..7feca8f99d 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -4,53 +4,29 @@ */ import type { FSWatcher } from "chokidar"; -import { watch } from "chokidar"; import path from "path"; import os from "os"; import { Console } from "console"; -import * as fse from "fs-extra"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable"; import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery"; -import installExtensionInjectable - from "../extension-installer/install-extension/install-extension.injectable"; -import directoryForUserDataInjectable - from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import mockFs from "mock-fs"; import { delay } from "../../renderer/utils"; import { observable, when } from "mobx"; import appVersionInjectable from "../../common/vars/app-version.injectable"; - -jest.setTimeout(60_000); - -jest.mock("../../common/ipc"); -jest.mock("chokidar", () => ({ - watch: jest.fn(), -})); - -jest.mock("fs-extra"); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); +import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; +import pathExistsInjectable from "../../common/fs/path-exists.injectable"; +import watchInjectable from "../../common/fs/watch/watch.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS -const mockedWatch = watch as jest.MockedFunction; -const mockedFse = fse as jest.Mocked; describe("ExtensionDiscovery", () => { let extensionDiscovery: ExtensionDiscovery; + let readJsonFileMock: jest.Mock; + let pathExistsMock: jest.Mock; + let watchMock: jest.Mock; beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -59,6 +35,15 @@ describe("ExtensionDiscovery", () => { di.override(installExtensionInjectable, () => () => Promise.resolve()); di.override(appVersionInjectable, () => "5.0.0"); + readJsonFileMock = jest.fn(); + di.override(readJsonFileInjectable, () => readJsonFileMock); + + pathExistsMock = jest.fn(() => Promise.resolve(true)); + di.override(pathExistsInjectable, () => pathExistsMock); + + watchMock = jest.fn(); + di.override(watchInjectable, () => watchMock); + mockFs(); extensionDiscovery = di.inject(extensionDiscoveryInjectable); @@ -72,7 +57,7 @@ describe("ExtensionDiscovery", () => { const letTestFinish = observable.box(false); let addHandler!: (filePath: string) => void; - mockedFse.readJson.mockImplementation((p) => { + readJsonFileMock.mockImplementation((p) => { expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json")); return { @@ -84,8 +69,6 @@ describe("ExtensionDiscovery", () => { }; }); - mockedFse.pathExists.mockImplementation(() => true); - const mockWatchInstance = { on: jest.fn((event: string, handler: typeof addHandler) => { if (event === "add") { @@ -96,7 +79,7 @@ describe("ExtensionDiscovery", () => { }), } as unknown as FSWatcher; - mockedWatch.mockImplementationOnce(() => mockWatchInstance); + watchMock.mockImplementationOnce(() => mockWatchInstance); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; @@ -139,7 +122,7 @@ describe("ExtensionDiscovery", () => { }), } as unknown as FSWatcher; - mockedWatch.mockImplementationOnce(() => mockWatchInstance); + watchMock.mockImplementationOnce(() => mockWatchInstance); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index 3576e66362..1de6425c19 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { watch } from "chokidar"; import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import fse from "fs-extra"; @@ -12,7 +11,6 @@ import os from "os"; import path from "path"; import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc"; import { isErrnoException, toJS } from "../../common/utils"; -import logger from "../../main/logger"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; @@ -21,6 +19,10 @@ import type { ExtensionInstallationStateStore } from "../extension-installation- import type { PackageJson } from "type-fest"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { requestInitialExtensionDiscovery } from "../../renderer/ipc"; +import type { ReadJson } from "../../common/fs/read-json-file.injectable"; +import type { Logger } from "../../common/logger"; +import type { PathExists } from "../../common/fs/path-exists.injectable"; +import type { Watch } from "../../common/fs/watch/watch.injectable"; interface Dependencies { extensionLoader: ExtensionLoader; @@ -35,6 +37,10 @@ interface Dependencies { installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise; extensionPackageRootDirectory: string; staticFilesDirectory: string; + readJsonFile: ReadJson; + pathExists: PathExists; + watch: Watch; + logger: Logger; } export interface InstalledExtension { @@ -155,13 +161,12 @@ export class ExtensionDiscovery { * Dependencies are installed automatically after an extension folder is copied. */ async watchExtensions(): Promise { - logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`); + this.dependencies.logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`); // Wait until .load() has been called and has been resolved await this.whenLoaded; - // chokidar works better than fs.watch - watch(this.localFolderPath, { + this.dependencies.watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, ignoreInitial: true, @@ -206,11 +211,11 @@ export class ExtensionDiscovery { await this.dependencies.installExtension(extension.absolutePath); this.extensions.set(extension.id, extension); - logger.info(`${logModule} Added extension ${extension.manifest.name}`); + this.dependencies.logger.info(`${logModule} Added extension ${extension.manifest.name}`); this.events.emit("add", extension); } } catch (error) { - logger.error(`${logModule}: failed to add extension: ${error}`, { error }); + this.dependencies.logger.error(`${logModule}: failed to add extension: ${error}`, { error }); } finally { this.dependencies.extensionInstallationStateStore.clearInstallingFromMain(manifestPath); } @@ -247,13 +252,13 @@ export class ExtensionDiscovery { const lensExtensionId = extension.manifestPath; this.extensions.delete(extension.id); - logger.info(`${logModule} removed extension ${extensionName}`); + this.dependencies.logger.info(`${logModule} removed extension ${extensionName}`); this.events.emit("remove", lensExtensionId); return; } - logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`); + this.dependencies.logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`); }; /** @@ -275,12 +280,12 @@ export class ExtensionDiscovery { const extension = this.extensions.get(extensionId) ?? this.dependencies.extensionLoader.getExtension(extensionId); if (!extension) { - return void logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId }); + return void this.dependencies.logger.warn(`${logModule} could not uninstall extension, not found`, { id: extensionId }); } const { manifest, absolutePath } = extension; - logger.info(`${logModule} Uninstalling ${manifest.name}`); + this.dependencies.logger.info(`${logModule} Uninstalling ${manifest.name}`); await this.removeSymlinkByPackageName(manifest.name); @@ -296,7 +301,7 @@ export class ExtensionDiscovery { this.loadStarted = true; - logger.info( + this.dependencies.logger.info( `${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`, ); @@ -358,12 +363,12 @@ export class ExtensionDiscovery { */ protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise { try { - const manifest = await fse.readJson(manifestPath) as LensExtensionManifest; + const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest; const id = this.getInstalledManifestPath(manifest.name); const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled }); const extensionDir = path.dirname(manifestPath); const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`); - const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir; + const absolutePath = (isProduction && await this.dependencies.pathExists(npmPackage)) ? npmPackage : extensionDir; const isCompatible = (isBundled && this.dependencies.isCompatibleBundledExtension(manifest)) || this.dependencies.isCompatibleExtension(manifest); return { @@ -378,9 +383,9 @@ export class ExtensionDiscovery { } catch (error) { if (isErrnoException(error) && error.code === "ENOTDIR") { // ignore this error, probably from .DS_Store file - logger.debug(`${logModule}: failed to load extension manifest through a not-dir-like at ${manifestPath}`); + this.dependencies.logger.debug(`${logModule}: failed to load extension manifest through a not-dir-like at ${manifestPath}`); } else { - logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`); + this.dependencies.logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`); } return null; @@ -395,7 +400,7 @@ export class ExtensionDiscovery { const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name)); for (const extension of userExtensions) { - if ((await fse.pathExists(extension.manifestPath)) === false) { + if (!(await this.dependencies.pathExists(extension.manifestPath))) { try { await this.dependencies.installExtension(extension.absolutePath); } catch (error) { @@ -404,7 +409,7 @@ export class ExtensionDiscovery { : String(error || "unknown error"); const { name, version } = extension.manifest; - logger.error(`${logModule}: failed to install user extension ${name}@${version}: ${message}`); + this.dependencies.logger.error(`${logModule}: failed to install user extension ${name}@${version}: ${message}`); } } } @@ -438,7 +443,7 @@ export class ExtensionDiscovery { extensions.push(extension); } } - logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions }); + this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions }); return extensions; } @@ -473,7 +478,7 @@ export class ExtensionDiscovery { } } - logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions }); + this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions }); return extensions; } diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index d26fd92d7c..16b28de8e1 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { ipcRenderer } from "electron"; +import { ipcMain, ipcRenderer } from "electron"; import { isEqual } from "lodash"; import type { ObservableMap } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; @@ -127,10 +127,10 @@ export class ExtensionLoader { @action async init() { - if (ipcRenderer) { - await this.initRenderer(); - } else { + if (ipcMain) { await this.initMain(); + } else { + await this.initRenderer(); } await Promise.all([this.whenLoaded]); diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 5e42c8e5fe..0244141ab3 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -2,11 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -jest.mock("../../common/ipc"); -jest.mock("request"); -jest.mock("request-promise-native"); - +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; import { Console } from "console"; import type { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; @@ -41,6 +37,7 @@ describe("create clusters", () => { di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); + di.override(broadcastMessageInjectable, () => async () => {}); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createContextHandlerInjectable, () => (cluster) => ({ diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 1e66d0d42b..47064ec215 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -3,45 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -jest.mock("winston", () => ({ - format: { - colorize: jest.fn(), - combine: jest.fn(), - simple: jest.fn(), - label: jest.fn(), - timestamp: jest.fn(), - printf: jest.fn(), - padLevels: jest.fn(), - ms: jest.fn(), - splat: jest.fn(), - }, - createLogger: jest.fn().mockReturnValue({ - silly: jest.fn(), - debug: jest.fn(), - log: jest.fn(), - info: jest.fn(), - error: jest.fn(), - crit: jest.fn(), - }), - transports: { - Console: jest.fn(), - File: jest.fn(), - }, -})); - -jest.mock("../../common/ipc"); -jest.mock("child_process"); -jest.mock("tcp-port-used"); - +import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/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 { broadcastMessage } from "../../common/ipc"; import type { ChildProcess } from "child_process"; -import { spawn } from "child_process"; import { Kubectl } from "../kubectl/kubectl"; import type { DeepMockProxy } from "jest-mock-extended"; import { mockDeep, mock } from "jest-mock-extended"; -import { waitUntilUsed } from "tcp-port-used"; import type { Readable } from "stream"; import { EventEmitter } from "stream"; import { Console } from "console"; @@ -59,17 +27,18 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; console = new Console(stdout, stderr); -const mockBroadcastIpc = broadcastMessage as jest.MockedFunction; -const mockSpawn = spawn as jest.MockedFunction; -const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction; const clusterServerUrl = "https://192.168.64.3:8443"; describe("kube auth proxy tests", () => { let createCluster: CreateCluster; let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; + let spawnMock: jest.Mock; + let waitUntilPortIsUsedMock: jest.Mock; + let broadcastMessageMock: jest.Mock; beforeEach(async () => { jest.clearAllMocks(); @@ -105,7 +74,15 @@ describe("kube auth proxy tests", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForTempInjectable, () => "some-directory-for-temp"); - di.override(spawnInjectable, () => mockSpawn); + spawnMock = jest.fn(); + di.override(spawnInjectable, () => spawnMock); + + waitUntilPortIsUsedMock = jest.fn(); + di.override(waitUntilPortIsUsedInjectable, () => waitUntilPortIsUsedMock); + + broadcastMessageMock = jest.fn(); + di.override(broadcastMessageInjectable, () => broadcastMessageMock); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(normalizedPlatformInjectable, () => "darwin"); @@ -211,12 +188,12 @@ describe("kube auth proxy tests", () => { return stdout; }); - mockSpawn.mockImplementationOnce((command: string): ChildProcess => { + spawnMock.mockImplementationOnce((command: string): ChildProcess => { expect(path.basename(command).split(".")[0]).toBe("lens-k8s-proxy"); return mockedCP; }); - mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()); + waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve()); const cluster = createCluster({ id: "foobar", @@ -233,34 +210,34 @@ describe("kube auth proxy tests", () => { await proxy.run(); listeners.emit("error", { message: "foobarbat" }); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", isError: true }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", isError: true }); }); it("should call spawn and broadcast exit", async () => { await proxy.run(); listeners.emit("exit", 0); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", isError: false }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", isError: false }); }); it("should call spawn and broadcast errors from stderr", async () => { await proxy.run(); listeners.emit("stderr/data", "an error"); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", isError: true }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", isError: true }); }); it("should call spawn and broadcast stdout serving info", async () => { await proxy.run(); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", isError: false }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", isError: false }); }); it("should call spawn and broadcast stdout other info", async () => { await proxy.run(); listeners.emit("stdout/data", "some info"); - expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", isError: false }); + expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", isError: false }); }); }); }); diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 07c2ee3247..d8bf4209f0 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -15,6 +15,7 @@ import listNamespacesInjectable from "../../common/cluster/list-namespaces.injec import loggerInjectable from "../../common/logger.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -30,6 +31,7 @@ const createClusterInjectable = getInjectable({ logger: di.inject(loggerInjectable), detectorRegistry: di.inject(detectorRegistryInjectable), createVersionDetector: di.inject(createVersionDetectorInjectable), + broadcastMessage: di.inject(broadcastMessageInjectable), }; return (model, configData) => new Cluster(dependencies, model, configData); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index a333e5b1d4..af891b1412 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -58,7 +58,6 @@ import type { ClusterFrameInfo } from "../common/cluster-frames"; import { observable } from "mobx"; import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable"; import setupListenerForCurrentClusterFrameInjectable from "./start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable"; -import ipcMainInjectable from "./utils/channel/ipc-main/ipc-main.injectable"; import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable"; import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable"; @@ -252,7 +251,6 @@ const overrideElectronFeatures = (di: DiContainer) => { di.override(shouldStartHiddenInjectable, () => false); di.override(showMessagePopupInjectable, () => () => {}); di.override(waitForElectronToBeReadyInjectable, () => () => Promise.resolve()); - di.override(ipcMainInjectable, () => ({})); di.override(getElectronThemeInjectable, () => () => "dark"); di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} })); di.override(electronQuitAndInstallUpdateInjectable, () => () => {}); diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts deleted file mode 100644 index 62c4ce4508..0000000000 --- a/src/main/helm/__mocks__/helm-chart-manager.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { sortCharts } from "../../../common/utils"; -import type { HelmRepo } from "../../../common/helm/helm-repo"; - -const charts = new Map([ - ["stable", { - "invalid-semver": sortCharts([ - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "I am not semver", - repo: "stable", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "v4.3.0", - repo: "stable", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "I am not semver but more", - repo: "stable", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "weird-versioning", - version: "v4.4.0", - repo: "stable", - digest: "test", - created: "now", - }, - ]), - "apm-server": sortCharts([ - { - apiVersion: "3.0.0", - name: "apm-server", - version: "2.1.7", - repo: "stable", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "apm-server", - version: "2.1.6", - repo: "stable", - digest: "test", - created: "now", - }, - ]), - "redis": sortCharts([ - { - apiVersion: "3.0.0", - name: "apm-server", - version: "1.0.0", - repo: "stable", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "apm-server", - version: "0.0.9", - repo: "stable", - digest: "test", - created: "now", - }, - ]), - }], - ["experiment", { - "fairwind": sortCharts([ - { - apiVersion: "3.0.0", - name: "fairwind", - version: "0.0.1", - repo: "experiment", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "fairwind", - version: "0.0.2", - repo: "experiment", - digest: "test", - deprecated: true, - created: "now", - }, - ]), - }], - ["bitnami", { - "hotdog": sortCharts([ - { - apiVersion: "3.0.0", - name: "hotdog", - version: "1.0.1", - repo: "bitnami", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "hotdog", - version: "1.0.2", - repo: "bitnami", - digest: "test", - created: "now", - }, - ]), - "pretzel": sortCharts([ - { - apiVersion: "3.0.0", - name: "pretzel", - version: "1.0", - repo: "bitnami", - digest: "test", - created: "now", - }, - { - apiVersion: "3.0.0", - name: "pretzel", - version: "1.0.1", - repo: "bitnami", - digest: "test", - created: "now", - }, - ]), - }], -]); - -export class HelmChartManager { - constructor(private repo: HelmRepo){ } - - static forRepo(repo: HelmRepo) { - return new this(repo); - } - - public async charts(): Promise { - return charts.get(this.repo.name) ?? {}; - } -} diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts index fab93bf1e5..ea0a28b1e9 100644 --- a/src/main/helm/__tests__/helm-service.test.ts +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -8,8 +8,8 @@ import listHelmChartsInjectable from "../helm-service/list-helm-charts.injectabl import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable"; import type { AsyncResult } from "../../../common/utils/async-result"; import type { HelmRepo } from "../../../common/helm/helm-repo"; - -jest.mock("../helm-chart-manager"); +import { sortCharts } from "../../../common/utils"; +import helmChartManagerInjectable from "../helm-chart-manager.injectable"; describe("Helm Service tests", () => { let listHelmCharts: () => Promise; @@ -20,6 +20,11 @@ describe("Helm Service tests", () => { getActiveHelmRepositoriesMock = jest.fn(); + di.override( + helmChartManagerInjectable, + (di, repo) => new HelmChartManagerFake(repo) as unknown, + ); + di.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock); di.unoverride(listHelmChartsInjectable); @@ -195,3 +200,145 @@ describe("Helm Service tests", () => { }); }); }); + +const charts = new Map([ + ["stable", { + "invalid-semver": sortCharts([ + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "I am not semver", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "v4.3.0", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "I am not semver but more", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "weird-versioning", + version: "v4.4.0", + repo: "stable", + digest: "test", + created: "now", + }, + ]), + "apm-server": sortCharts([ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test", + created: "now", + }, + ]), + "redis": sortCharts([ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test", + created: "now", + }, + ]), + }], + ["experiment", { + "fairwind": sortCharts([ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true, + created: "now", + }, + ]), + }], + ["bitnami", { + "hotdog": sortCharts([ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + created: "now", + }, + ]), + "pretzel": sortCharts([ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test", + created: "now", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test", + created: "now", + }, + ]), + }], +]); + +class HelmChartManagerFake { + constructor(private repo: HelmRepo){ } + + public async charts(): Promise { + return charts.get(this.repo.name) ?? {}; + } +} diff --git a/src/main/helm/helm-chart-manager-cache.injectable.ts b/src/main/helm/helm-chart-manager-cache.injectable.ts new file mode 100644 index 0000000000..49df5e0843 --- /dev/null +++ b/src/main/helm/helm-chart-manager-cache.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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"; + +export interface ChartCacheEntry { + data: string; // serialized JSON + mtimeMs: number; +} + +export type HelmChartManagerCache = Map; + +const helmChartManagerCacheInjectable = getInjectable({ + id: "helm-chart-manager-cache", + instantiate: (): HelmChartManagerCache => new Map(), +}); + +export default helmChartManagerCacheInjectable; diff --git a/src/main/helm/helm-chart-manager.injectable.ts b/src/main/helm/helm-chart-manager.injectable.ts new file mode 100644 index 0000000000..367c47ae3b --- /dev/null +++ b/src/main/helm/helm-chart-manager.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { HelmRepo } from "../../common/helm/helm-repo"; +import { HelmChartManager } from "./helm-chart-manager"; +import helmChartManagerCacheInjectable from "./helm-chart-manager-cache.injectable"; +import loggerInjectable from "../../common/logger.injectable"; + +const helmChartManagerInjectable = getInjectable({ + id: "helm-chart-manager", + + instantiate: (di, repo: HelmRepo) => { + const cache = di.inject(helmChartManagerCacheInjectable); + const logger = di.inject(loggerInjectable); + + return new HelmChartManager(repo, { cache, logger }); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, repo: HelmRepo) => repo.name, + }), +}); + +export default helmChartManagerInjectable; diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index 8ba7a83101..2eae449aea 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -5,39 +5,34 @@ import fs from "fs"; import * as yaml from "js-yaml"; -import logger from "../logger"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; import { iter, put, sortCharts } from "../../common/utils"; import { execHelm } from "./exec"; import type { SetRequired } from "type-fest"; import { assert } from "console"; import type { HelmRepo } from "../../common/helm/helm-repo"; - -interface ChartCacheEntry { - data: string; // serialized JSON - mtimeMs: number; -} +import type { HelmChartManagerCache } from "./helm-chart-manager-cache.injectable"; +import type { Logger } from "../../common/logger"; export interface HelmCacheFile { apiVersion: string; entries: RepoHelmChartList; } -export class HelmChartManager { - static readonly #cache = new Map(); +interface Dependencies { + cache: HelmChartManagerCache; + logger: Logger; +} +export class HelmChartManager { protected readonly repo: SetRequired; - private constructor(repo: HelmRepo) { + constructor(repo: HelmRepo, private dependencies: Dependencies) { assert(repo.cacheFilePath, "CacheFilePath must be provided on the helm repo"); this.repo = repo as SetRequired; } - static forRepo(repo: HelmRepo) { - return new this(repo); - } - public async chartVersions(name: string) { const charts = await this.charts(); @@ -48,7 +43,7 @@ export class HelmChartManager { try { return await this.cachedYaml(); } catch(error) { - logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); + this.dependencies.logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); return {}; } @@ -84,7 +79,7 @@ export class HelmChartManager { const normalized = normalizeHelmCharts(this.repo.name, data.entries); return put( - HelmChartManager.#cache, + this.dependencies.cache, this.repo.name, { data: JSON.stringify(normalized), @@ -94,7 +89,7 @@ export class HelmChartManager { } protected async cachedYaml(): Promise { - let cacheEntry = HelmChartManager.#cache.get(this.repo.name); + let cacheEntry = this.dependencies.cache.get(this.repo.name); if (!cacheEntry) { cacheEntry = await this.updateYamlCache(); diff --git a/src/main/helm/helm-service/get-helm-chart-values.injectable.ts b/src/main/helm/helm-service/get-helm-chart-values.injectable.ts index 2394a591f0..489f8b8521 100644 --- a/src/main/helm/helm-service/get-helm-chart-values.injectable.ts +++ b/src/main/helm/helm-service/get-helm-chart-values.injectable.ts @@ -3,14 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { HelmChartManager } from "../helm-chart-manager"; import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable"; +import type { HelmRepo } from "../../../common/helm/helm-repo"; +import helmChartManagerInjectable from "../helm-chart-manager.injectable"; const getHelmChartValuesInjectable = getInjectable({ id: "get-helm-chart-values", instantiate: (di) => { const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable); + const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo); return async (repoName: string, chartName: string, version = "") => { const repo = await getActiveHelmRepository(repoName); @@ -19,7 +21,7 @@ const getHelmChartValuesInjectable = getInjectable({ return undefined; } - return HelmChartManager.forRepo(repo).getValues(chartName, version); + return getChartManager(repo).getValues(chartName, version); }; }, diff --git a/src/main/helm/helm-service/get-helm-chart.injectable.ts b/src/main/helm/helm-service/get-helm-chart.injectable.ts index 1a5a43da36..bd9615a3c4 100644 --- a/src/main/helm/helm-service/get-helm-chart.injectable.ts +++ b/src/main/helm/helm-service/get-helm-chart.injectable.ts @@ -3,14 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { HelmChartManager } from "../helm-chart-manager"; import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable"; +import type { HelmRepo } from "../../../common/helm/helm-repo"; +import helmChartManagerInjectable from "../helm-chart-manager.injectable"; const getHelmChartInjectable = getInjectable({ id: "get-helm-chart", instantiate: (di) => { const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable); + const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo); return async (repoName: string, chartName: string, version = "") => { const repo = await getActiveHelmRepository(repoName); @@ -19,7 +21,7 @@ const getHelmChartInjectable = getInjectable({ return undefined; } - const chartManager = HelmChartManager.forRepo(repo); + const chartManager = getChartManager(repo); return { readme: await chartManager.getReadme(chartName, version), diff --git a/src/main/helm/helm-service/list-helm-charts.injectable.ts b/src/main/helm/helm-service/list-helm-charts.injectable.ts index b537021f0f..c5be54fd67 100644 --- a/src/main/helm/helm-service/list-helm-charts.injectable.ts +++ b/src/main/helm/helm-service/list-helm-charts.injectable.ts @@ -5,14 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import assert from "assert"; import { object } from "../../../common/utils"; -import { HelmChartManager } from "../helm-chart-manager"; import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable"; +import type { HelmRepo } from "../../../common/helm/helm-repo"; +import helmChartManagerInjectable from "../helm-chart-manager.injectable"; const listHelmChartsInjectable = getInjectable({ id: "list-helm-charts", instantiate: (di) => { const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable); + const getChartManager = (repo: HelmRepo) => di.inject(helmChartManagerInjectable, repo); return async () => { const result = await getActiveHelmRepositories(); @@ -27,7 +29,7 @@ const listHelmChartsInjectable = getInjectable({ async (repo) => [ repo.name, - await HelmChartManager.forRepo(repo).charts(), + await getChartManager(repo).charts(), ] as const, ), ), diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 8546adb4fb..67cfd8c26e 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -13,6 +13,7 @@ import spawnInjectable from "../child-process/spawn.injectable"; import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; import loggerInjectable from "../../common/logger.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; +import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; @@ -29,6 +30,7 @@ const createKubeAuthProxyInjectable = getInjectable({ proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), spawn: di.inject(spawnInjectable), logger: di.inject(loggerInjectable), + waitUntilPortIsUsed: di.inject(waitUntilPortIsUsedInjectable), }; return new KubeAuthProxy(dependencies, cluster, environmentVariables); diff --git a/src/main/kube-auth-proxy/kube-auth-proxy.ts b/src/main/kube-auth-proxy/kube-auth-proxy.ts index fbbfa67a85..a6067e548b 100644 --- a/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -4,10 +4,8 @@ */ import type { ChildProcess } from "child_process"; -import { waitUntilUsed } from "tcp-port-used"; import { randomBytes } from "crypto"; import type { Cluster } from "../../common/cluster/cluster"; -import logger from "../logger"; import { getPortFrom } from "../utils/get-port"; import { makeObservable, observable, when } from "mobx"; import type { SelfSignedCert } from "selfsigned"; @@ -15,6 +13,7 @@ import assert from "assert"; import { TypedRegEx } from "typed-regex"; import type { Spawn } from "../child-process/spawn.injectable"; import type { Logger } from "../../common/logger"; +import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; const startingServeMatcher = "starting to serve on (?
.+)"; const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { @@ -24,8 +23,9 @@ const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), export interface KubeAuthProxyDependencies { readonly proxyBinPath: string; readonly proxyCert: SelfSignedCert; - spawn: Spawn; + readonly spawn: Spawn; readonly logger: Logger; + readonly waitUntilPortIsUsed: WaitUntilPortIsUsed; } export class KubeAuthProxy { @@ -106,13 +106,13 @@ export class KubeAuthProxy { onFind: () => this.cluster.broadcastConnectUpdate("Authentication proxy started"), }); - logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`); + this.dependencies.logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`); try { - await waitUntilUsed(this.port, 500, 10000); + await this.dependencies.waitUntilPortIsUsed(this.port, 500, 10000); this.ready = true; } catch (error) { - logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error); + this.dependencies.logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error); this.cluster.broadcastConnectUpdate("Proxy port failed to be used within timelimit, restarting...", true); this.exit(); @@ -124,7 +124,7 @@ export class KubeAuthProxy { this.ready = false; if (this.proxyProcess) { - logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); + this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); this.proxyProcess.removeAllListeners(); this.proxyProcess.stderr?.removeAllListeners(); this.proxyProcess.stdout?.removeAllListeners(); diff --git a/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.global-override-for-injectable.ts b/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.global-override-for-injectable.ts new file mode 100644 index 0000000000..86f1e374cd --- /dev/null +++ b/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.global-override-for-injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; +import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used.injectable"; + +export default getGlobalOverride( + waitUntilPortIsUsedInjectable, + () => () => { + throw new Error( + "Tried to wait until port is used without explicit override.", + ); + }, +); diff --git a/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable.ts b/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable.ts new file mode 100644 index 0000000000..2d1662ac6a --- /dev/null +++ b/src/main/kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.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 { waitUntilUsed } from "tcp-port-used"; + +export type WaitUntilPortIsUsed = ( + port: number, + retryAfterMs: number, + timeoutAfterMs: number +) => Promise; + +const waitUntilPortIsUsedInjectable = getInjectable({ + id: "wait-until-port-is-used", + instantiate: (): WaitUntilPortIsUsed => waitUntilUsed, + causesSideEffects: true, +}); + +export default waitUntilPortIsUsedInjectable; diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index a0f26e9764..55aecaa8a0 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -5,7 +5,6 @@ import * as uuid from "uuid"; -import { broadcastMessage } from "../../../common/ipc"; import { ProtocolHandlerExtension, ProtocolHandlerInternal, ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { delay, noop } from "../../../common/utils"; import type { ExtensionsStore, IsEnabledExtensionDescriptor } from "../../../extensions/extensions-store/extensions-store"; @@ -19,8 +18,7 @@ import type { LensExtensionId } from "../../../extensions/lens-extension"; import type { ObservableMap } from "mobx"; import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; - -jest.mock("../../../common/ipc"); +import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; function throwIfDefined(val: any): void { if (val != null) { @@ -32,6 +30,7 @@ describe("protocol router tests", () => { let extensionInstances: ObservableMap; let lpr: LensProtocolRouterMain; let enabledExtensions: Set; + let broadcastMessageMock: jest.Mock; beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -46,6 +45,9 @@ describe("protocol router tests", () => { di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + broadcastMessageMock = jest.fn(); + di.override(broadcastMessageInjectable, () => broadcastMessageMock); + extensionInstances = di.inject(extensionInstancesInjectable); lpr = di.inject(lensProtocolRouterMainInjectable); @@ -54,12 +56,12 @@ describe("protocol router tests", () => { it("should broadcast invalid protocol on non-lens URLs", async () => { await lpr.route("https://google.ca"); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInvalid, "invalid protocol", "https://google.ca"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid protocol", "https://google.ca"); }); it("should broadcast invalid host on non internal or non extension URLs", async () => { await lpr.route("lens://foobar"); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar"); }); it("should not throw when has valid host", async () => { @@ -101,8 +103,8 @@ describe("protocol router tests", () => { } await delay(50); - expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); - expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched"); + expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); + expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched"); }); it("should call handler if matches", async () => { @@ -117,7 +119,7 @@ describe("protocol router tests", () => { } expect(called).toBe(true); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched"); }); it("should call most exact handler", async () => { @@ -133,7 +135,7 @@ describe("protocol router tests", () => { } expect(called).toBe("foo"); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo", "matched"); }); it("should call most exact handler for an extension", async () => { @@ -174,7 +176,7 @@ describe("protocol router tests", () => { await delay(50); expect(called).toBe("foob"); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched"); }); it("should work with non-org extensions", async () => { @@ -244,7 +246,7 @@ describe("protocol router tests", () => { await delay(50); expect(called).toBe(1); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched"); }); it("should throw if urlSchema is invalid", () => { @@ -266,7 +268,7 @@ describe("protocol router tests", () => { } expect(called).toBe(3); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); }); it("should call most exact handler with 2 found handlers", async () => { @@ -283,6 +285,6 @@ describe("protocol router tests", () => { } expect(called).toBe(1); - expect(broadcastMessage).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); + expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); }); }); diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts index 630d63d82c..bce91119c2 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts @@ -7,6 +7,8 @@ import extensionLoaderInjectable from "../../../extensions/extension-loader/exte import { LensProtocolRouterMain } from "./lens-protocol-router-main"; import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; +import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; const lensProtocolRouterMainInjectable = getInjectable({ id: "lens-protocol-router-main", @@ -16,6 +18,8 @@ const lensProtocolRouterMainInjectable = getInjectable({ extensionLoader: di.inject(extensionLoaderInjectable), extensionsStore: di.inject(extensionsStoreInjectable), showApplicationWindow: di.inject(showApplicationWindowInjectable), + broadcastMessage: di.inject(broadcastMessageInjectable), + logger: di.inject(loggerInjectable), }), }); diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index 14b8fa3347..3beb596e4e 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -3,15 +3,14 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import logger from "../../logger"; import * as proto from "../../../common/protocol-handler"; import URLParse from "url-parse"; import type { LensExtension } from "../../../extensions/lens-extension"; -import { broadcastMessage } from "../../../common/ipc"; import { observable, when, makeObservable } from "mobx"; import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler"; import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { disposer, noop } from "../../../common/utils"; +import type { BroadcastMessage } from "../../../common/ipc/broadcast-message.injectable"; export interface FallbackHandler { (name: string): Promise; @@ -36,6 +35,7 @@ function checkHost(url: URLParse): boolean { export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies { showApplicationWindow: () => Promise; + broadcastMessage: BroadcastMessage; } export class LensProtocolRouterMain extends proto.LensProtocolRouter { @@ -73,7 +73,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { this.dependencies.showApplicationWindow().catch(noop); const routeInternally = checkHost(url); - logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); + this.dependencies.logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); if (routeInternally) { this._routeToInternal(url); @@ -81,12 +81,12 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { await this._routeToExtension(url); } } catch (error) { - broadcastMessage(ProtocolHandlerInvalid, error ? String(error) : "unknown error", rawUrl); + this.dependencies.broadcastMessage(ProtocolHandlerInvalid, error ? String(error) : "unknown error", rawUrl); if (error instanceof proto.RoutingError) { - logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url }); + this.dependencies.logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url }); } else { - logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl }); + this.dependencies.logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl }); } } } @@ -119,7 +119,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { const rawUrl = url.toString(); // for sending to renderer const attempt = super._routeToInternal(url); - this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt))); + this.disposers.push(when(() => this.rendererLoaded, () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt))); return attempt; } @@ -136,7 +136,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { */ const attempt = await super._routeToExtension(new URLParse(url.toString(), true)); - this.disposers.push(when(() => this.rendererLoaded, () => broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt))); + this.disposers.push(when(() => this.rendererLoaded, () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt))); return attempt; } diff --git a/src/main/start-main-application/start-main-application.injectable.ts b/src/main/start-main-application/start-main-application.injectable.ts index 0bf77b2fcd..fcd1851575 100644 --- a/src/main/start-main-application/start-main-application.injectable.ts +++ b/src/main/start-main-application/start-main-application.injectable.ts @@ -11,7 +11,6 @@ import { beforeApplicationIsLoadingInjectionToken } from "./runnable-tokens/befo import { onLoadOfApplicationInjectionToken } from "./runnable-tokens/on-load-of-application-injection-token"; import { afterApplicationIsLoadedInjectionToken } from "./runnable-tokens/after-application-is-loaded-injection-token"; import splashWindowInjectable from "./lens-window/splash-window/splash-window.injectable"; - import openDeepLinkInjectable from "../protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable"; import { pipeline } from "@ogre-tools/fp"; import { find, map, startsWith, toLower } from "lodash/fp"; diff --git a/src/main/utils/channel/ipc-main/ipc-main.global-override-for-injectable.ts b/src/main/utils/channel/ipc-main/ipc-main.global-override-for-injectable.ts new file mode 100644 index 0000000000..e770ccdf35 --- /dev/null +++ b/src/main/utils/channel/ipc-main/ipc-main.global-override-for-injectable.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { IpcMain } from "electron"; +import { getGlobalOverride } from "../../../../common/test-utils/get-global-override"; +import ipcMainInjectable from "./ipc-main.injectable"; + +export default getGlobalOverride(ipcMainInjectable, () => ({ + handle: () => {}, + on: () => {}, + off: () => {}, +}) as unknown as IpcMain); diff --git a/src/renderer/api/catalog/entity/registry.injectable.ts b/src/renderer/api/catalog/entity/registry.injectable.ts index 03d4edf52e..a06413afdf 100644 --- a/src/renderer/api/catalog/entity/registry.injectable.ts +++ b/src/renderer/api/catalog/entity/registry.injectable.ts @@ -6,12 +6,14 @@ import { getInjectable } from "@ogre-tools/injectable"; import catalogCategoryRegistryInjectable from "../../../../common/catalog/category-registry.injectable"; import navigateInjectable from "../../../navigation/navigate.injectable"; import { CatalogEntityRegistry } from "./registry"; +import loggerInjectable from "../../../../common/logger.injectable"; const catalogEntityRegistryInjectable = getInjectable({ id: "catalog-entity-registry", instantiate: (di) => new CatalogEntityRegistry({ categoryRegistry: di.inject(catalogCategoryRegistryInjectable), navigate: di.inject(navigateInjectable), + logger: di.inject(loggerInjectable), }), }); diff --git a/src/renderer/api/catalog/entity/registry.ts b/src/renderer/api/catalog/entity/registry.ts index 1f53cf3d94..9971be8462 100644 --- a/src/renderer/api/catalog/entity/registry.ts +++ b/src/renderer/api/catalog/entity/registry.ts @@ -10,12 +10,12 @@ import "../../../../common/catalog-entities"; import { iter } from "../../../utils"; import type { Disposer } from "../../../utils"; import { once } from "lodash"; -import logger from "../../../../common/logger"; import { CatalogRunEvent } from "../../../../common/catalog/catalog-run-event"; import { ipcRenderer } from "electron"; import { catalogInitChannel, catalogItemsChannel, catalogEntityRunListener } from "../../../../common/ipc/catalog"; import { isMainFrame } from "process"; import type { Navigate } from "../../../navigation/navigate.injectable"; +import type { Logger } from "../../../../common/logger"; export type EntityFilter = (entity: CatalogEntity) => any; export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise; @@ -23,6 +23,7 @@ export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promis interface Dependencies { navigate: Navigate; readonly categoryRegistry: CatalogCategoryRegistry; + logger: Logger; } export class CatalogEntityRegistry { @@ -219,7 +220,7 @@ export class CatalogEntityRegistry { * @returns Whether the entities `onRun` method should be executed */ async onBeforeRun(entity: CatalogEntity): Promise { - logger.debug(`[CATALOG-ENTITY-REGISTRY]: run onBeforeRun on ${entity.getId()}`); + this.dependencies.logger.debug(`[CATALOG-ENTITY-REGISTRY]: run onBeforeRun on ${entity.getId()}`); const runEvent = new CatalogRunEvent({ target: entity }); @@ -227,7 +228,7 @@ export class CatalogEntityRegistry { try { await onBeforeRun(runEvent); } catch (error) { - logger.warn(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onBeforeRun threw an error`, error); + this.dependencies.logger.warn(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onBeforeRun threw an error`, error); } if (runEvent.defaultPrevented) { @@ -253,9 +254,9 @@ export class CatalogEntityRegistry { }, }); } else { - logger.debug(`onBeforeRun for ${entity.getId()} returned false`); + this.dependencies.logger.debug(`onBeforeRun for ${entity.getId()} returned false`); } }) - .catch(error => logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error)); + .catch(error => this.dependencies.logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error)); } } diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 899b97bed2..885fa640d5 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -7,10 +7,9 @@ import React from "react"; import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Catalog } from "./catalog"; -import { mockWindow } from "../../../../__mocks__/windowMock"; import type { CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; import { CatalogEntity } from "../../../common/catalog"; -import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry"; +import type { CatalogEntityOnBeforeRun, CatalogEntityRegistry } from "../../api/catalog/entity/registry"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; @@ -19,38 +18,15 @@ import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity- import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import type { DiRender } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor"; -import mockFs from "mock-fs"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import type { AppEvent } from "../../../common/app-event-bus/event-bus"; import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; import { computed } from "mobx"; import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; - -mockWindow(); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, - ipcRenderer: { - on: jest.fn(), - invoke: jest.fn(), - }, -})); - -jest.mock("./hotbar-toggle-menu-item", () => ({ - HotbarToggleMenuItem: () =>
menu item
, -})); +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { flushPromises } from "../../../common/test-utils/flush-promises"; class MockCatalogEntity extends CatalogEntity { public apiVersion = "api"; @@ -95,7 +71,6 @@ describe("", () => { di.permitSideEffects(getConfigurationFileModelInjectable); - mockFs(); CatalogEntityDetailRegistry.createInstance(); render = renderFor(di); @@ -103,8 +78,6 @@ describe("", () => { catalogEntityItem = createMockCatalogEntity(onRun); catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); - di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry); - emitEvent = jest.fn(); di.override(appEventBusInjectable, () => ({ @@ -119,60 +92,71 @@ describe("", () => { afterEach(() => { CatalogEntityDetailRegistry.resetInstance(); - jest.clearAllMocks(); - jest.restoreAllMocks(); - mockFs.restore(); }); - it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => { - catalogEntityRegistry.addOnBeforeRun( - (event) => { - expect(event.target.getId()).toBe("a_catalogEntity_uid"); - expect(event.target.getName()).toBe("a catalog entity"); + describe("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", () => { + let onBeforeRunMock: AsyncFnMock; - setTimeout(() => { - expect(onRun).toHaveBeenCalled(); - done(); - }, 500); - }, - ); + beforeEach(() => { + onBeforeRunMock = asyncFn(); + + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); + + render(); + + userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); + }); + + it("calls on before run event", () => { + const target = onBeforeRunMock.mock.calls[0][0].target; + + const actual = { id: target.getId(), name: target.getName() }; + + expect(actual).toEqual({ + id: "a_catalogEntity_uid", + name: "a catalog entity", + }); + }); + + it("does not call onRun yet", () => { + expect(onRun).not.toHaveBeenCalled(); + }); + + it("when before run event resolves, calls onRun", async () => { + await onBeforeRunMock.resolve(); + + expect(onRun).toHaveBeenCalled(); + }); + }); + + it("onBeforeRun prevents event => onRun wont be triggered", async () => { + const onBeforeRunMock = jest.fn((event) => event.preventDefault()); + + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); + + await flushPromises(); + + expect(onRun).not.toHaveBeenCalled(); }); - it("onBeforeRun prevents event => onRun wont be triggered", (done) => { - catalogEntityRegistry.addOnBeforeRun( - (e) => { - setTimeout(() => { - expect(onRun).not.toHaveBeenCalled(); - done(); - }, 500); - e.preventDefault(); - }, - ); + it("addOnBeforeRun throw an exception => onRun will be triggered", async () => { + const onBeforeRunMock = jest.fn(() => { + throw new Error("some error"); + }); + + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); - }); - it("addOnBeforeRun throw an exception => onRun will be triggered", (done) => { - catalogEntityRegistry.addOnBeforeRun( - () => { - setTimeout(() => { - expect(onRun).toHaveBeenCalled(); - done(); - }, 500); + await flushPromises(); - throw new Error("error!"); - }, - ); - - render(); - - userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); + expect(onRun).toHaveBeenCalled(); }); it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => { @@ -189,40 +173,34 @@ describe("", () => { userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); }); - it("addOnRunHook return a promise and prevents event wont be triggered", (done) => { - catalogEntityRegistry.addOnBeforeRun( - async (e) => { - expect(onRun).not.toBeCalled(); + it("addOnRunHook return a promise and prevents event wont be triggered", async () => { + const onBeforeRunMock = asyncFn(); - setTimeout(() => { - expect(onRun).not.toBeCalled(); - done(); - }, 500); - - e.preventDefault(); - }, - ); + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); + + onBeforeRunMock.mock.calls[0][0].preventDefault(); + + await onBeforeRunMock.resolve(); + + expect(onRun).not.toHaveBeenCalled(); }); - it("addOnRunHook return a promise and reject => onRun will be triggered", (done) => { - catalogEntityRegistry.addOnBeforeRun( - async () => { - setTimeout(() => { - expect(onRun).toHaveBeenCalled(); - done(); - }, 500); + it("addOnRunHook return a promise and reject => onRun will be triggered", async () => { + const onBeforeRunMock = asyncFn(); - throw new Error("rejection!"); - }, - ); + catalogEntityRegistry.addOnBeforeRun(onBeforeRunMock); render(); userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon")); + + await onBeforeRunMock.reject(); + + expect(onRun).toHaveBeenCalled(); }); it("emits catalog open AppEvent", () => { diff --git a/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx index 6903e853bc..dd4044014c 100644 --- a/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx +++ b/src/renderer/components/+config-secrets/__tests__/secret-details.test.tsx @@ -4,17 +4,20 @@ */ import React from "react"; -import { render } from "@testing-library/react"; import { SecretDetails } from "../secret-details"; import { Secret, SecretType } from "../../../../common/k8s-api/endpoints"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import { renderFor } from "../../test-utils/renderFor"; jest.mock("../../kube-object-meta/kube-object-meta", () => ({ KubeObjectMeta: () => null, })); - describe("SecretDetails tests", () => { it("should show the visibility toggle when the secret value is ''", () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + const render = renderFor(di); + const secret = new Secret({ apiVersion: "v1", kind: "secret", diff --git a/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx b/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx index 0a4e9b22d7..e25bebc0c2 100644 --- a/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx +++ b/src/renderer/components/+network-policies/__tests__/network-policy-details.test.tsx @@ -4,15 +4,22 @@ */ import React from "react"; -import { findByTestId, findByText, render } from "@testing-library/react"; +import { findByTestId, findByText } from "@testing-library/react"; import { NetworkPolicy } from "../../../../common/k8s-api/endpoints"; import { NetworkPolicyDetails } from "../network-policy-details"; - -jest.mock("../../kube-object-meta/kube-object-meta", () => ({ - KubeObjectMeta: () => null, -})); +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; describe("NetworkPolicyDetails", () => { + let render: DiRender; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + render = renderFor(di); + }); + it("should render w/o errors", () => { const policy = new NetworkPolicy({ metadata: {} as never, diff --git a/src/renderer/components/cluster-settings/components/__tests__/cluster-local-terminal-settings.test.tsx b/src/renderer/components/cluster-settings/components/__tests__/cluster-local-terminal-settings.test.tsx index 1b845ed9d4..b4a78a5dc0 100644 --- a/src/renderer/components/cluster-settings/components/__tests__/cluster-local-terminal-settings.test.tsx +++ b/src/renderer/components/cluster-settings/components/__tests__/cluster-local-terminal-settings.test.tsx @@ -4,28 +4,38 @@ */ import React from "react"; -import { render, waitFor } from "@testing-library/react"; +import { waitFor } from "@testing-library/react"; import { ClusterLocalTerminalSetting } from "../cluster-local-terminal-settings"; import userEvent from "@testing-library/user-event"; -import { stat } from "fs/promises"; -import { Notifications } from "../../../notifications"; import type { Stats } from "fs"; import type { Cluster } from "../../../../../common/cluster/cluster"; - -const mockStat = stat as jest.MockedFunction; - -jest.mock("fs", () => { - const actual = jest.requireActual("fs"); - - actual.promises.stat = jest.fn(); - - return actual; -}); - -jest.mock("../../../notifications"); +import { getDiForUnitTesting } from "../../../../getDiForUnitTesting"; +import type { DiRender } from "../../../test-utils/renderFor"; +import { renderFor } from "../../../test-utils/renderFor"; +import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable"; +import statInjectable from "../../../../../common/fs/stat/stat.injectable"; describe("ClusterLocalTerminalSettings", () => { + let render: DiRender; + let showErrorNotificationMock: jest.Mock; + let statMock: jest.Mock; + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + showErrorNotificationMock = jest.fn(); + + statMock = jest.fn(); + + di.override(statInjectable, () => statMock); + + di.override( + showErrorNotificationInjectable, + () => showErrorNotificationMock, + ); + + render = renderFor(di); + jest.resetAllMocks(); }); @@ -89,7 +99,7 @@ describe("ClusterLocalTerminalSettings", () => { }); it("should save the new CWD if path is a directory", async () => { - mockStat.mockImplementation(async (path) => { + statMock.mockImplementation(async (path) => { expect(path).toBe("/foobar"); return { @@ -114,7 +124,7 @@ describe("ClusterLocalTerminalSettings", () => { }); it("should not save the new CWD if path is a file", async () => { - mockStat.mockImplementation(async (path) => { + statMock.mockImplementation(async (path) => { expect(path).toBe("/foobar"); return { @@ -136,6 +146,6 @@ describe("ClusterLocalTerminalSettings", () => { userEvent.type(dn, "/foobar"); userEvent.click(dom.baseElement); - await waitFor(() => expect(Notifications.error).toBeCalled()); + await waitFor(() => expect(showErrorNotificationMock).toHaveBeenCalled()); }); }); diff --git a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx index ab1a4b9db0..0ec755cf86 100644 --- a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx @@ -8,79 +8,25 @@ import { observer } from "mobx-react"; import type { Cluster } from "../../../../common/cluster/cluster"; import { Input } from "../../input"; import { SubTitle } from "../../layout/sub-title"; -import { stat } from "fs/promises"; -import { Notifications } from "../../notifications"; -import { isErrnoException, resolveTilde } from "../../../utils"; +import type { ShowNotification } from "../../notifications"; +import { resolveTilde } from "../../../utils"; import { Icon } from "../../icon"; import { PathPicker } from "../../path-picker"; import { isWindows } from "../../../../common/vars"; -import type { Stats } from "fs"; -import logger from "../../../../common/logger"; -import { lowerFirst } from "lodash"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; +import type { ValidateDirectory } from "../../../../common/fs/validate-directory.injectable"; +import validateDirectoryInjectable from "../../../../common/fs/validate-directory.injectable"; export interface ClusterLocalTerminalSettingProps { cluster: Cluster; } - -function getUserReadableFileType(stats: Stats): string { - if (stats.isFile()) { - return "a file"; - } - - if (stats.isFIFO()) { - return "a pipe"; - } - - if (stats.isSocket()) { - return "a socket"; - } - - if (stats.isBlockDevice()) { - return "a block device"; - } - - if (stats.isCharacterDevice()) { - return "a character device"; - } - - return "an unknown file type"; +interface Dependencies { + showErrorNotification: ShowNotification; + validateDirectory: ValidateDirectory; } -/** - * Validate that `dir` currently points to a directory. If so return `false`. - * Otherwise, return a user readable error message string for displaying. - * @param dir The path to be validated - */ -async function validateDirectory(dir: string): Promise { - try { - const stats = await stat(dir); - - if (stats.isDirectory()) { - return false; - } - - return `the provided path is ${getUserReadableFileType(stats)} and not a directory.`; - } catch (error) { - switch (isErrnoException(error) ? error.code : undefined) { - case "ENOENT": - return `the provided path does not exist.`; - case "EACCES": - return `search permissions is denied for one of the directories in the prefix of the provided path.`; - case "ELOOP": - return `the provided path is a sym-link which points to a chain of sym-links that is too long to resolve. Perhaps it is cyclic.`; - case "ENAMETOOLONG": - return `the pathname is too long to be used.`; - case "ENOTDIR": - return `a prefix of the provided path is not a directory.`; - default: - logger.warn(`[CLUSTER-LOCAL-TERMINAL-SETTINGS]: unexpected error in validateDirectory for resolved path=${dir}`, error); - - return error ? lowerFirst(String(error)) : "of an unknown error, please try again."; - } - } -} - -export const ClusterLocalTerminalSetting = observer(({ cluster }: ClusterLocalTerminalSettingProps) => { +const NonInjectedClusterLocalTerminalSetting = observer(({ cluster, showErrorNotification, validateDirectory }: Dependencies & ClusterLocalTerminalSettingProps) => { if (!cluster) { return null; } @@ -109,15 +55,15 @@ export const ClusterLocalTerminalSetting = observer(({ cluster }: ClusterLocalTe cluster.preferences.terminalCWD = undefined; } else { const dir = resolveTilde(directory); - const errorMessage = await validateDirectory(dir); + const result = await validateDirectory(dir); - if (errorMessage) { - Notifications.error( + if (!result.callWasSuccessful) { + showErrorNotification( <> Terminal Working Directory

{"Your changes were not saved because "} - {errorMessage} + {result.error}

, ); @@ -200,3 +146,16 @@ export const ClusterLocalTerminalSetting = observer(({ cluster }: ClusterLocalTe ); }); + +export const ClusterLocalTerminalSetting = withInjectables( + NonInjectedClusterLocalTerminalSetting, + + { + getProps: (di, props) => ({ + showErrorNotification: di.inject(showErrorNotificationInjectable), + validateDirectory: di.inject(validateDirectoryInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index 799e841e5d..372c6482eb 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -19,7 +19,6 @@ exports[`kube-object-menu given kube object renders 1`] = ` +
+ Delete +
@@ -59,7 +61,6 @@ exports[`kube-object-menu given kube object when removing kube object renders 1` +
+ Delete +
@@ -148,7 +152,6 @@ exports[`kube-object-menu given kube object when rerendered with different kube +
+ Delete +
@@ -185,7 +191,6 @@ exports[`kube-object-menu given kube object when rerendered with different kube +
+ Delete +
@@ -277,7 +285,6 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob +
+ Delete +
@@ -369,7 +379,6 @@ exports[`kube-object-menu given kube object without namespace when removing kube +
+ Delete +
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index 0f0b338d8f..80a783c19a 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -26,12 +26,6 @@ import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable"; import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token"; -// TODO: Make tooltips free of side effects by making it deterministic -jest.mock("../tooltip/tooltip"); -jest.mock("../tooltip/withTooltip", () => ({ - withTooltip: (target: any) => target, -})); - // TODO: make `animated={false}` not required to make tests deterministic describe("kube-object-menu", () => { let di: DiContainer; diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index 7e997477a6..af64873fad 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -8,6 +8,7 @@ import { Cluster } from "../../common/cluster/cluster"; import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import loggerInjectable from "../../common/logger.injectable"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -16,6 +17,7 @@ const createClusterInjectable = getInjectable({ const dependencies: ClusterDependencies = { directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), logger: di.inject(loggerInjectable), + broadcastMessage: di.inject(broadcastMessageInjectable), // TODO: Dismantle wrong abstraction // Note: "as never" to get around strictness in unnatural scenario diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index b139dbb9a9..ddd560674f 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -32,8 +32,6 @@ import { ApiManager } from "../common/k8s-api/api-manager"; import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.injectable"; import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; import apiManagerInjectable from "../common/k8s-api/api-manager/manager.injectable"; -import ipcRendererInjectable from "./utils/channel/ipc-renderer.injectable"; -import type { IpcRenderer } from "electron"; import setupOnApiErrorListenersInjectable from "./api/setup-on-api-errors.injectable"; import { observable, computed } from "mobx"; import defaultShellInjectable from "./components/+preferences/default-shell.injectable"; @@ -164,11 +162,6 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) di.override(maximizeWindowInjectable, () => () => {}); di.override(toggleMaximizeWindowInjectable, () => () => {}); - di.override(ipcRendererInjectable, () => ({ - invoke: () => {}, - on: () => {}, - }) as unknown as IpcRenderer); - overrideFunctionalInjectables(di, [ broadcastMessageInjectable, getFilePathsInjectable, diff --git a/src/renderer/ipc/index.ts b/src/renderer/ipc/index.ts index 5e204ef98b..48a005d536 100644 --- a/src/renderer/ipc/index.ts +++ b/src/renderer/ipc/index.ts @@ -4,7 +4,6 @@ */ import type { OpenDialogOptions } from "electron"; -import { ipcRenderer } from "electron"; import { clusterActivateHandler, clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; import type { ClusterId, ClusterState } from "../../common/cluster-types"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window"; @@ -14,12 +13,22 @@ import type { InstalledExtension } from "../../extensions/extension-discovery/ex import type { LensExtensionId } from "../../extensions/lens-extension"; import { toJS } from "../utils"; import type { Location } from "history"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import ipcRendererInjectable from "../utils/channel/ipc-renderer.injectable"; function requestMain(channel: string, ...args: any[]) { + const di = getLegacyGlobalDiForExtensionApi(); + + const ipcRenderer = di.inject(ipcRendererInjectable); + return ipcRenderer.invoke(channel, ...args.map(toJS)); } function emitToMain(channel: string, ...args: any[]) { + const di = getLegacyGlobalDiForExtensionApi(); + + const ipcRenderer = di.inject(ipcRendererInjectable); + return ipcRenderer.send(channel, ...args.map(toJS)); } diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts index 9aa3df8a0a..a3d85e86db 100644 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts +++ b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts @@ -5,8 +5,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer"; -import extensionsStoreInjectable - from "../../../extensions/extensions-store/extensions-store.injectable"; +import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; const lensProtocolRouterRendererInjectable = getInjectable({ id: "lens-protocol-router-renderer", @@ -15,6 +15,7 @@ const lensProtocolRouterRendererInjectable = getInjectable({ new LensProtocolRouterRenderer({ extensionLoader: di.inject(extensionLoaderInjectable), extensionsStore: di.inject(extensionsStoreInjectable), + logger: di.inject(loggerInjectable), }), }); diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx index 5cfeeb3fec..8015678361 100644 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx +++ b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx @@ -8,10 +8,9 @@ import { ipcRenderer } from "electron"; import * as proto from "../../../common/protocol-handler"; import Url from "url-parse"; import { onCorrect } from "../../../common/ipc"; +import type { LensProtocolRouterDependencies } from "../../../common/protocol-handler"; import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler"; import { Notifications } from "../../components/notifications"; -import type { ExtensionLoader } from "../../../extensions/extension-loader"; -import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { if (args.length !== 2) { @@ -32,11 +31,7 @@ function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { } } -interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; -} - +interface Dependencies extends LensProtocolRouterDependencies {} export class LensProtocolRouterRenderer extends proto.LensProtocolRouter { constructor(protected dependencies: Dependencies) { diff --git a/src/renderer/utils/channel/ipc-renderer.global-override-for-injectable.ts b/src/renderer/utils/channel/ipc-renderer.global-override-for-injectable.ts new file mode 100644 index 0000000000..28043f1299 --- /dev/null +++ b/src/renderer/utils/channel/ipc-renderer.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 type { IpcRenderer } from "electron"; +import ipcRendererInjectable from "./ipc-renderer.injectable"; +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; + +export default getGlobalOverride(ipcRendererInjectable, () => ({ + invoke: () => {}, + on: () => {}, +}) as unknown as IpcRenderer);