/** * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ import "@testing-library/jest-dom/extend-expect"; import { KubeConfig } from "@kubernetes/client-node"; import { fireEvent } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react"; import mockFs from "mock-fs"; import * as selectEvent from "react-select-event"; import type { CreateCluster } from "../../../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token"; import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable"; import type { OpenDeleteClusterDialog } from "../open.injectable"; import openDeleteClusterDialogInjectable from "../open.injectable"; import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; import createKubeconfigManagerInjectable from "../../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; import type { ApplicationBuilder } from "../../test-utils/get-application-builder"; import { getApplicationBuilder } from "../../test-utils/get-application-builder"; import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; import { EventEmitter } from "../../../../common/event-emitter"; import type { AppEvent } from "../../../../common/app-event-bus/event-bus"; 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(), }, })); const currentClusterServerUrl = "https://localhost"; const nonCurrentClusterServerUrl = "http://localhost"; const multiClusterConfig = ` apiVersion: v1 clusters: - cluster: server: ${currentClusterServerUrl} name: some-current-context-cluster - cluster: server: ${nonCurrentClusterServerUrl} name: some-non-current-context-cluster contexts: - context: cluster: some-current-context-cluster user: some-user name: some-current-context - context: cluster: some-non-current-context-cluster user: some-user name: some-non-current-context current-context: some-current-context kind: Config preferences: {} users: - name: some-user user: token: kubeconfig-user-q4lm4:xxxyyyy `; const singleClusterServerUrl = "http://localhost"; const singleClusterConfig = ` apiVersion: v1 clusters: - cluster: server: ${singleClusterServerUrl} name: some-cluster contexts: - context: cluster: some-cluster user: some-user name: some-context current-context: some-context kind: Config preferences: {} users: - name: some-user user: token: kubeconfig-user-q4lm4:xxxyyyy `; let config: KubeConfig; describe("", () => { let applicationBuilder: ApplicationBuilder; let createCluster: CreateCluster; let openDeleteClusterDialog: OpenDeleteClusterDialog; beforeEach(async () => { applicationBuilder = getApplicationBuilder(); applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { mainDi.override(createContextHandlerInjectable, () => () => undefined as never); mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never); mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); mainDi.override(normalizedPlatformInjectable, () => "darwin"); rendererDi.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>()); rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); }); mockFs(); applicationBuilder.beforeRender(({ rendererDi }) => { openDeleteClusterDialog = rendererDi.inject(openDeleteClusterDialogInjectable); createCluster = rendererDi.inject(createClusterInjectionToken); }); }); afterEach(() => { mockFs.restore(); }); it("shows context switcher when deleting current cluster", async () => { const mockOpts = { "temp-kube-config": multiClusterConfig, }; mockFs(mockOpts); config = new KubeConfig(); config.loadFromString(multiClusterConfig); applicationBuilder.beforeRender(({ rendererDi }) => { const createCluster = rendererDi.inject(createClusterInjectionToken); const cluster = createCluster({ id: "some-current-context-cluster", contextName: "some-current-context", preferences: { clusterName: "some-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: currentClusterServerUrl, }); openDeleteClusterDialog({ cluster, config }); }); const rendered = await applicationBuilder.render(); const { getByText } = rendered; const menu = getByText("Select new context..."); expect(menu).toBeInTheDocument(); selectEvent.openMenu(menu); expect(getByText("some-current-context")).toBeInTheDocument(); expect(getByText("some-non-current-context")).toBeInTheDocument(); }); describe("Kubeconfig with different clusters", () => { let rendered: RenderResult; beforeEach(async () => { const mockOpts = { "temp-kube-config": multiClusterConfig, }; mockFs(mockOpts); config = new KubeConfig(); config.loadFromString(multiClusterConfig); rendered = await applicationBuilder.render(); }); it("renders w/o errors", () => { expect(rendered.container).toBeInstanceOf(HTMLElement); }); it("shows warning when deleting non-current-context cluster", () => { const cluster = createCluster({ id: "some-non-current-context-cluster", contextName: "some-non-current-context", preferences: { clusterName: "minikube", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: nonCurrentClusterServerUrl, }); openDeleteClusterDialog({ cluster, config }); const message = "The contents of kubeconfig file will be changed!"; expect(rendered.getByText(message)).toBeInstanceOf(HTMLElement); }); it("shows warning when deleting current-context cluster", () => { const cluster = createCluster({ id: "some-current-context-cluster", contextName: "some-current-context", preferences: { clusterName: "some-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: currentClusterServerUrl, }); openDeleteClusterDialog({ cluster, config }); expect(rendered.getByTestId("current-context-warning")).toBeInstanceOf(HTMLElement); }); it("shows context switcher after checkbox click", () => { const cluster = createCluster({ id: "some-current-context-cluster", contextName: "some-current-context", preferences: { clusterName: "some-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: currentClusterServerUrl, }); openDeleteClusterDialog({ cluster, config }); const { getByText, getByTestId } = rendered; const link = getByTestId("context-switch"); expect(link).toBeInstanceOf(HTMLElement); fireEvent.click(link); const menu = getByText("Select new context..."); expect(menu).toBeInTheDocument(); selectEvent.openMenu(menu); expect(getByText("some-current-context")).toBeInTheDocument(); expect(getByText("some-non-current-context")).toBeInTheDocument(); }); it("given cluster in internal kubeconfig, when deleting cluster outside of current context, shows warning for internal kubeconfig cluster", () => { const cluster = createCluster({ id: "some-non-current-context-cluster", contextName: "some-non-current-context", preferences: { clusterName: "some-non-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: nonCurrentClusterServerUrl, }); const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true); openDeleteClusterDialog({ cluster, config }); expect(rendered.getByTestId("internal-kubeconfig-warning")).toBeInstanceOf(HTMLElement); spy.mockRestore(); }); }); describe("Kubeconfig with single cluster", () => { let rendered: RenderResult; beforeEach(async () => { const mockOpts = { "temp-kube-config": singleClusterConfig, }; mockFs(mockOpts); config = new KubeConfig(); config.loadFromString(singleClusterConfig); rendered = await applicationBuilder.render(); }); it("shows warning if no other contexts left", () => { const cluster = createCluster({ id: "some-cluster", contextName: "some-context", preferences: { clusterName: "some-cluster", }, kubeConfigPath: "./temp-kube-config", }, { clusterServerUrl: singleClusterServerUrl, }); openDeleteClusterDialog({ cluster, config }); expect(rendered.getByTestId("no-more-contexts-warning")).toBeInstanceOf(HTMLElement); }); }); });