From 93c18e036860274a2f832abf5717c942285d4d71 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 13 Jan 2023 11:53:17 -0500 Subject: [PATCH] Fix technical tests for KubeconfigManager - And start using ApplicationBuilder for them Signed-off-by: Sebastian Malton --- .../fs/copy.global-override-for-injectable.ts | 11 - .../lstat.global-override-for-injectable.ts | 11 - ...irectory.global-override-for-injectable.ts | 11 - .../remove.global-override-for-injectable.ts | 11 - ...ite-file.global-override-for-injectable.ts | 11 - .../cluster/proxy-config/techincal.test.ts | 157 +++++++++ src/main/__test__/kubeconfig-manager.test.ts | 324 ------------------ .../kubeconfig-manager/kubeconfig-manager.ts | 8 +- 8 files changed, 163 insertions(+), 381 deletions(-) delete mode 100644 src/common/fs/copy.global-override-for-injectable.ts delete mode 100644 src/common/fs/lstat.global-override-for-injectable.ts delete mode 100644 src/common/fs/read-directory.global-override-for-injectable.ts delete mode 100644 src/common/fs/remove.global-override-for-injectable.ts delete mode 100644 src/common/fs/write-file.global-override-for-injectable.ts create mode 100644 src/features/cluster/proxy-config/techincal.test.ts delete mode 100644 src/main/__test__/kubeconfig-manager.test.ts diff --git a/src/common/fs/copy.global-override-for-injectable.ts b/src/common/fs/copy.global-override-for-injectable.ts deleted file mode 100644 index b6d899d2c4..0000000000 --- a/src/common/fs/copy.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 copyInjectable from "./copy.injectable"; - -export default getGlobalOverride(copyInjectable, () => async () => { - throw new Error("tried to copy filepaths without override"); -}); diff --git a/src/common/fs/lstat.global-override-for-injectable.ts b/src/common/fs/lstat.global-override-for-injectable.ts deleted file mode 100644 index 9c9f3d4933..0000000000 --- a/src/common/fs/lstat.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 lstatInjectable from "./lstat.injectable"; - -export default getGlobalOverride(lstatInjectable, () => async () => { - throw new Error("tried to lstat a filepath without override"); -}); diff --git a/src/common/fs/read-directory.global-override-for-injectable.ts b/src/common/fs/read-directory.global-override-for-injectable.ts deleted file mode 100644 index 57c83ceffb..0000000000 --- a/src/common/fs/read-directory.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 readDirectoryInjectable from "./read-directory.injectable"; - -export default getGlobalOverride(readDirectoryInjectable, () => async () => { - throw new Error("tried to read a directory's content without override"); -}); diff --git a/src/common/fs/remove.global-override-for-injectable.ts b/src/common/fs/remove.global-override-for-injectable.ts deleted file mode 100644 index 4b92353344..0000000000 --- a/src/common/fs/remove.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 removePathInjectable from "./remove.injectable"; - -export default getGlobalOverride(removePathInjectable, () => async () => { - throw new Error("tried to remove path without override"); -}); diff --git a/src/common/fs/write-file.global-override-for-injectable.ts b/src/common/fs/write-file.global-override-for-injectable.ts deleted file mode 100644 index c8b7ef8e45..0000000000 --- a/src/common/fs/write-file.global-override-for-injectable.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 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 writeFileInjectable from "./write-file.injectable"; - -export default getGlobalOverride(writeFileInjectable, () => async () => { - throw new Error("tried to write file without override"); -}); diff --git a/src/features/cluster/proxy-config/techincal.test.ts b/src/features/cluster/proxy-config/techincal.test.ts new file mode 100644 index 0000000000..4d56df1eb6 --- /dev/null +++ b/src/features/cluster/proxy-config/techincal.test.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { KubeconfigManager } from "../../../main/kubeconfig-manager/kubeconfig-manager"; +import createKubeconfigManagerInjectable from "../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; +import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; +import createContextHandlerInjectable from "../../../main/context-handler/create-context-handler.injectable"; +import { parse } from "url"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; +import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; +import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; +import removePathInjectable from "../../../common/fs/remove.injectable"; + +const kubeConfigPath = "/minikube-config.yml"; +const clusterServerUrl = "https://192.168.64.3:8443"; + +describe("kubeconfig manager technical tests", () => { + let builder: ApplicationBuilder; + let ensureServerMock: AsyncFnMock<() => Promise>; + let kubeconfigManager: KubeconfigManager; + + beforeEach(async () => { + builder = getApplicationBuilder(); + + ensureServerMock = asyncFn(); + + builder.mainDi.override(createContextHandlerInjectable, () => (cluster) => ({ + restartServer: jest.fn(), + stopServer: jest.fn(), + clusterUrl: parse(cluster.apiUrl), + getApiTarget: jest.fn(), + getPrometheusDetails: jest.fn(), + resolveAuthProxyCa: jest.fn(), + resolveAuthProxyUrl: jest.fn(), + setupPrometheus: jest.fn(), + ensureServer: ensureServerMock, + })); + + await builder.render(); + + const writeJsonFile = builder.mainDi.inject(writeJsonFileInjectable); + + await writeJsonFile(kubeConfigPath, { + apiVersion: "v1", + clusters: [{ + name: "minikube", + cluster: { + server: clusterServerUrl, + }, + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", + }], + users: [{ + name: "minikube", + }], + kind: "Config", + preferences: {}, + }); + + const createCluster = builder.mainDi.inject(createClusterInjectionToken); + const createKubeconfigManager = builder.mainDi.inject(createKubeconfigManagerInjectable); + + const clusterFake = createCluster({ + id: "foo", + contextName: "minikube", + kubeConfigPath, + }, { + clusterServerUrl, + }); + + kubeconfigManager = createKubeconfigManager(clusterFake); + }); + + describe("when calling clear", () => { + it("should resolve immediately", async () => { + await kubeconfigManager.clear(); + }); + + it("being called several times shouldn't throw", async () => { + await kubeconfigManager.clear(); + await kubeconfigManager.clear(); + await kubeconfigManager.clear(); + }); + }); + + describe("when getPath() is called initially", () => { + let getPathPromise: Promise; + + beforeEach(async () => { + getPathPromise = kubeconfigManager.getPath(); + }); + + it("should call ensureServer on the cluster context", () => { + expect(ensureServerMock).toBeCalledTimes(1); + }); + + describe("when ensureServer resolves", () => { + beforeEach(async () => { + await ensureServerMock.resolve(); + + // clear state of calls + ensureServerMock.mock.calls.length = 0; + }); + + it("should allow getPath to resolve with the path to the kubeconfig", async () => { + expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo"); + }); + + it("when calling clear, should remove file from filesystem", async () => { + const pathExists = builder.mainDi.inject(pathExistsInjectable); + + await kubeconfigManager.clear(); + + expect(await pathExists("/some-directory-for-temp/kubeconfig-foo")).toBe(false); + }); + + it("when calling getPath a second time, should resolve with same path", async () => { + expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo"); + }); + + describe("if file is removed, and getPath is called agin", () => { + let getPathPromise2: Promise; + + beforeEach(async () => { + const removePath = builder.mainDi.inject(removePathInjectable); + + await removePath("/some-directory-for-temp/kubeconfig-foo"); + + getPathPromise2 = kubeconfigManager.getPath(); + }); + + it("should call ensureServer on the cluster context", () => { + expect(ensureServerMock).toBeCalledTimes(1); + }); + + describe("when ensureServer resolves", () => { + beforeEach(async () => { + await ensureServerMock.resolve(); + }); + + it("should allow getPath to resolve with the path to the kubeconfig", async () => { + expect(await getPathPromise2).toBe("/some-directory-for-temp/kubeconfig-foo"); + }); + }); + }); + }); + }); +}); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts deleted file mode 100644 index 689ef13ce8..0000000000 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; -import type { Cluster } from "../../common/cluster/cluster"; -import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; -import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; -import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; -import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; -import type { DiContainer } from "@ogre-tools/injectable"; -import { parse } from "url"; -import loggerInjectable from "../../common/logger.injectable"; -import type { Logger } from "../../common/logger"; -import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; -import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; -import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; -import type { ReadFile } from "../../common/fs/read-file.injectable"; -import readFileInjectable from "../../common/fs/read-file.injectable"; -import type { AsyncFnMock } from "@async-fn/jest"; -import asyncFn from "@async-fn/jest"; -import type { WriteFile } from "../../common/fs/write-file.injectable"; -import writeFileInjectable from "../../common/fs/write-file.injectable"; -import type { PathExists } from "../../common/fs/path-exists.injectable"; -import pathExistsInjectable from "../../common/fs/path-exists.injectable"; -import type { RemovePath } from "../../common/fs/remove.injectable"; -import removePathInjectable from "../../common/fs/remove.injectable"; -import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable"; -import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable"; -import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"; - -const clusterServerUrl = "https://192.168.64.3:8443"; - -describe("kubeconfig manager tests", () => { - let clusterFake: Cluster; - let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; - let di: DiContainer; - let loggerMock: jest.Mocked; - let readFileMock: AsyncFnMock; - let deleteFileMock: AsyncFnMock; - let writeFileMock: AsyncFnMock; - let pathExistsMock: AsyncFnMock; - let kubeConfManager: KubeconfigManager; - let ensureServerMock: AsyncFnMock<() => Promise>; - - beforeEach(async () => { - di = getDiForUnitTesting({ doGeneralOverrides: true }); - - di.override(directoryForTempInjectable, () => "/some-directory-for-temp"); - di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); - di.override(kubectlBinaryNameInjectable, () => "kubectl"); - di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); - di.override(normalizedPlatformInjectable, () => "darwin"); - di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); - di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); - di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); - - readFileMock = asyncFn(); - di.override(readFileInjectable, () => readFileMock); - writeFileMock = asyncFn(); - di.override(writeFileInjectable, () => writeFileMock); - pathExistsMock = asyncFn(); - di.override(pathExistsInjectable, () => pathExistsMock); - deleteFileMock = asyncFn(); - di.override(removePathInjectable, () => deleteFileMock); - - loggerMock = { - warn: jest.fn(), - debug: jest.fn(), - error: jest.fn(), - info: jest.fn(), - silly: jest.fn(), - }; - - di.override(loggerInjectable, () => loggerMock); - - ensureServerMock = asyncFn(); - - di.override(createContextHandlerInjectable, () => (cluster) => ({ - restartServer: jest.fn(), - stopServer: jest.fn(), - clusterUrl: parse(cluster.apiUrl), - getApiTarget: jest.fn(), - getPrometheusDetails: jest.fn(), - resolveAuthProxyCa: jest.fn(), - resolveAuthProxyUrl: jest.fn(), - setupPrometheus: jest.fn(), - ensureServer: ensureServerMock, - })); - - const createCluster = di.inject(createClusterInjectionToken); - - createKubeconfigManager = di.inject(createKubeconfigManagerInjectable); - - clusterFake = createCluster({ - id: "foo", - contextName: "minikube", - kubeConfigPath: "/minikube-config.yml", - }, { - clusterServerUrl, - }); - - jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("https://127.0.0.1:9191/foo"); - - kubeConfManager = createKubeconfigManager(clusterFake); - }); - - describe("when calling clear", () => { - it("should resolve immediately", async () => { - await kubeConfManager.clear(); - }); - - it("being called several times shouldn't throw", async () => { - await kubeConfManager.clear(); - await kubeConfManager.clear(); - await kubeConfManager.clear(); - }); - }); - - describe("when getPath() is called initially", () => { - let getPathPromise: Promise; - - beforeEach(async () => { - getPathPromise = kubeConfManager.getPath(); - }); - - it("should not call pathExists()", () => { - expect(pathExistsMock).not.toBeCalled(); - }); - - it("should call ensureServer on the cluster context", () => { - expect(ensureServerMock).toBeCalledTimes(1); - }); - - describe("when ensureServer resolves", () => { - beforeEach(async () => { - await ensureServerMock.resolve(); - - // clear state of calls - ensureServerMock.mock.calls.length = 0; - }); - - describe("when reading cluster's kubeconfig resolves", () => { - beforeEach(async () => { - await readFileMock.resolveSpecific( - ["/minikube-config.yml"], - JSON.stringify({ - apiVersion: "v1", - clusters: [{ - name: "minikube", - cluster: { - server: clusterServerUrl, - }, - }], - contexts: [{ - context: { - cluster: "minikube", - user: "minikube", - }, - name: "minikube", - }], - users: [{ - name: "minikube", - }], - kind: "Config", - preferences: {}, - }), - ); - }); - - describe("when writing out new proxy kubeconfig resolves", () => { - beforeEach(async () => { - await writeFileMock.resolveSpecific( - [ - "/some-directory-for-temp/kubeconfig-foo", - "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", - ], - ); - }); - - it("should allow getPath to resolve with the path to the kubeconfig", async () => { - expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo"); - }); - - describe("when calling clear", () => { - let clearPromise: Promise; - - beforeEach(() => { - clearPromise = kubeConfManager.clear(); - }); - - it("should call deleteFile", () => { - expect(deleteFileMock).toBeCalledTimes(1); - }); - - describe("when deleteFile resolves", () => { - beforeEach(async () => { - await deleteFileMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo"], - ); - }); - - it("should allow clear to resolve", async () => { - await clearPromise; - }); - }); - - describe("when deleteFile rejects with ENOENT", () => { - beforeEach(async () => { - await deleteFileMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo"], - Promise.reject(Object.assign(new Error("file not found"), { - code: "ENOENT", - })), - ); - }); - - it("should allow clear to resolve", async () => { - await clearPromise; - }); - }); - - it("when deleteFile rejects with some other error; clear should also reject", async () => { - const expectPromise = expect(clearPromise).rejects.toBeDefined(); - - await deleteFileMock.reject(new Error("some other error")); - await expectPromise; - }); - }); - - describe("when calling getPath a second time", () => { - let getPathPromise: Promise; - - beforeEach(async () => { - getPathPromise = kubeConfManager.getPath(); - }); - - it("should call pathExists", () => { - expect(pathExistsMock).toBeCalledTimes(1); - }); - - describe("when pathExists resoves to true", () => { - beforeEach(async () => { - await pathExistsMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo"], - true, - ); - }); - - it("always getPath to resolve with path", async () => { - expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo"); - }); - }); - - describe("when pathExists resoves to false", () => { - beforeEach(async () => { - await pathExistsMock.resolveSpecific( - ["/some-directory-for-temp/kubeconfig-foo"], - false, - ); - }); - - it("should call ensureServer on the cluster context", () => { - expect(ensureServerMock).toBeCalledTimes(1); - }); - - describe("when ensureServer resolves", () => { - beforeEach(async () => { - await ensureServerMock.resolve(); - }); - - describe("when reading cluster's kubeconfig resolves", () => { - beforeEach(async () => { - await readFileMock.resolveSpecific( - ["/minikube-config.yml"], - JSON.stringify({ - apiVersion: "v1", - clusters: [{ - name: "minikube", - cluster: { - server: clusterServerUrl, - }, - }], - contexts: [{ - context: { - cluster: "minikube", - user: "minikube", - }, - name: "minikube", - }], - users: [{ - name: "minikube", - }], - kind: "Config", - preferences: {}, - }), - ); - }); - - describe("when writing out new proxy kubeconfig resolves", () => { - beforeEach(async () => { - await writeFileMock.resolveSpecific( - [ - "/some-directory-for-temp/kubeconfig-foo", - "apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n", - ], - ); - }); - - it("should allow getPath to resolve with the path to the kubeconfig", async () => { - expect(await getPathPromise).toBe("/some-directory-for-temp/kubeconfig-foo"); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); -}); diff --git a/src/main/kubeconfig-manager/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts index 41548698fb..0c2fcd7310 100644 --- a/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -50,11 +50,15 @@ export class KubeconfigManager { * @returns The path to the temporary kubeconfig */ async getPath(): Promise { - if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) { + if (!this.tempFilePath) { return await this.ensureFile(); } - return this.tempFilePath; + if (await this.dependencies.pathExists(this.tempFilePath)) { + return this.tempFilePath; + } + + return await this.ensureFile(); } /**