fixed all src/** tests, moved from spec/src to source code locations

This commit is contained in:
Roman committed 2020-06-25 15:21:28 +03:00
1 parent a772ef6b94
commit 43b3453e24
20 files changed
+167 -160

No files matched your search

+333
View File
@@ -0,0 +1,333 @@
import mockFs from "mock-fs"
import yaml from "js-yaml"
import { ClusterStore } from "./cluster-store";
import { Cluster } from "../main/cluster";
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log("");
describe("for an empty config", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("allows to store and retrieve a cluster", async () => {
const cluster = new Cluster({
id: 'foo',
kubeConfig: 'kubeconfig string',
preferences: {
terminalCWD: '/tmp',
icon: 'path to icon'
}
})
const clusterStore = ClusterStore.getInstance()
clusterStore.storeCluster(cluster);
const storedCluster = clusterStore.getCluster(cluster.id);
expect(storedCluster.kubeConfig).toBe(cluster.kubeConfig)
expect(storedCluster.preferences.icon).toBe(cluster.preferences.icon)
expect(storedCluster.preferences.terminalCWD).toBe(cluster.preferences.terminalCWD)
expect(storedCluster.id).toBe(cluster.id)
})
it("allows to delete a cluster", async () => {
const cluster = new Cluster({
id: 'foofoo',
kubeConfig: 'kubeconfig string',
preferences: {
terminalCWD: '/tmp'
}
})
const clusterStore = ClusterStore.getInstance()
clusterStore.storeCluster(cluster);
const storedCluster = clusterStore.getCluster(cluster.id);
expect(storedCluster.id).toBe(cluster.id)
clusterStore.removeCluster(cluster.id);
expect(clusterStore.getCluster(cluster.id)).toBe(null)
})
})
describe("for a config with existing clusters", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "99.99.99"
}
},
clusters: [
{
id: 'cluster1',
kubeConfig: 'foo',
preferences: { terminalCWD: '/foo' }
},
{
id: 'cluster2',
kubeConfig: 'foo2',
preferences: { terminalCWD: '/foo2' }
}
]
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("allows to retrieve a cluster", async () => {
const clusterStore = ClusterStore.getInstance()
const storedCluster = clusterStore.getCluster('cluster1')
expect(storedCluster.kubeConfig).toBe('foo')
expect(storedCluster.preferences.terminalCWD).toBe('/foo')
expect(storedCluster.id).toBe('cluster1')
const storedCluster2 = clusterStore.getCluster('cluster2')
expect(storedCluster2.kubeConfig).toBe('foo2')
expect(storedCluster2.preferences.terminalCWD).toBe('/foo2')
expect(storedCluster2.id).toBe('cluster2')
})
it("allows to delete a cluster", async () => {
const clusterStore = ClusterStore.getInstance()
clusterStore.removeCluster('cluster2')
// Verify the other cluster still exists:
const storedCluster = clusterStore.getCluster('cluster1')
expect(storedCluster.id).toBe('cluster1')
const storedCluster2 = clusterStore.getCluster('cluster2')
expect(storedCluster2).toBe(null)
})
it("allows to reload a cluster in-place", async () => {
const cluster = new Cluster({
id: 'cluster1',
kubeConfig: 'kubeconfig string',
preferences: {
terminalCWD: '/tmp'
}
})
const clusterStore = ClusterStore.getInstance()
clusterStore.reloadCluster(cluster)
expect(cluster.kubeConfig).toBe('foo')
expect(cluster.preferences.terminalCWD).toBe('/foo')
expect(cluster.id).toBe('cluster1')
})
it("allows getting all the clusters", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusters = clusterStore.getAllClusters()
expect(storedClusters[0].id).toBe('cluster1')
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
expect(storedClusters[0].kubeConfig).toBe('foo')
expect(storedClusters[1].id).toBe('cluster2')
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
expect(storedClusters[1].kubeConfig).toBe('foo2')
})
it("allows storing the clusters in a different order", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusters = clusterStore.getAllClusters()
const reorderedClusters = [storedClusters[1], storedClusters[0]]
clusterStore.storeClusters(reorderedClusters)
const storedClusters2 = clusterStore.getAllClusters()
expect(storedClusters2[0].id).toBe('cluster2')
expect(storedClusters2[1].id).toBe('cluster1')
})
})
describe("for a pre 2.0 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "1.0.0"
}
},
cluster1: 'kubeconfig content'
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("migrates to modern format with kubeconfig under a key", async () => {
const clusterStore = ClusterStore.getInstance()
const storedCluster = clusterStore.store.get('clusters')[0]
expect(storedCluster.kubeConfig).toBe('kubeconfig content')
})
})
describe("for a pre 2.4.1 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "2.0.0-beta.2"
}
},
cluster1: {
kubeConfig: 'foo',
online: true,
accessible: false,
failureReason: 'user error'
},
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("migrates to modern format throwing out the state related data", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusterData = clusterStore.store.get('clusters')[0]
expect(storedClusterData.hasOwnProperty('online')).toBe(false)
expect(storedClusterData.hasOwnProperty('accessible')).toBe(false)
expect(storedClusterData.hasOwnProperty('failureReason')).toBe(false)
})
})
describe("for a pre 2.6.0 config with a cluster that has arrays in auth config", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "2.4.1"
}
},
cluster1: {
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
},
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("replaces array format access token and expiry into string", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusterData = clusterStore.store.get('clusters')[0]
const kc = yaml.safeLoad(storedClusterData.kubeConfig)
expect(kc.users[0].user['auth-provider'].config['access-token']).toBe("should be string")
expect(kc.users[0].user['auth-provider'].config['expiry']).toBe("should be string")
})
})
describe("for a pre 2.6.0 config with a cluster icon", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "2.4.1"
}
},
cluster1: {
kubeConfig: "foo",
icon: "icon path",
preferences: {
terminalCWD: "/tmp"
}
},
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("moves the icon into preferences", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusterData = clusterStore.store.get('clusters')[0]
expect(storedClusterData.hasOwnProperty('icon')).toBe(false)
expect(storedClusterData.preferences.hasOwnProperty('icon')).toBe(true)
expect(storedClusterData.preferences.icon).toBe("icon path")
})
})
describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
beforeEach(() => {
ClusterStore.resetInstance()
const mockOpts = {
'tmp': {
'lens-cluster-store.json': JSON.stringify({
__internal__: {
migrations: {
version: "2.6.6"
}
},
cluster1: {
kubeConfig: "foo",
icon: "icon path",
preferences: {
terminalCWD: "/tmp"
}
},
})
}
}
mockFs(mockOpts)
})
afterEach(() => {
mockFs.restore()
})
it("adds cluster to default workspace", async () => {
const clusterStore = ClusterStore.getInstance()
const storedClusterData = clusterStore.store.get("clusters")[0]
expect(storedClusterData.workspace).toBe('default')
})
})
+77
View File
@@ -0,0 +1,77 @@
import mockFs from "mock-fs"
import { userStore, UserStore } from "./user-store"
jest.mock("electron")
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log("");
describe("for an empty config", () => {
beforeEach(() => {
UserStore.resetInstance()
const mockOpts = {
'tmp': {
'config.json': JSON.stringify({})
}
}
mockFs(mockOpts)
const userStore = UserStore.getInstance()
})
afterEach(() => {
mockFs.restore()
})
it("allows setting and retrieving lastSeenAppVersion", async () => {
userStore.setLastSeenAppVersion("1.2.3");
expect(userStore.lastSeenAppVersion()).toBe("1.2.3");
})
it("allows adding and listing seen contexts", async () => {
userStore.storeSeenContext(['foo'])
expect(userStore.getSeenContexts().length).toBe(1)
userStore.storeSeenContext(['foo', 'bar'])
const seenContexts = userStore.getSeenContexts()
expect(seenContexts.length).toBe(2) // check 'foo' isn't added twice
expect(seenContexts[0]).toBe('foo')
expect(seenContexts[1]).toBe('bar')
})
it("allows setting and getting preferences", async () => {
userStore.setPreferences({
httpsProxy: 'abcd://defg',
})
const storedPreferences = userStore.getPreferences()
expect(storedPreferences.httpsProxy).toBe('abcd://defg')
expect(storedPreferences.colorTheme).toBe('dark') // defaults to dark
userStore.setPreferences({
colorTheme: 'light'
})
expect(userStore.getPreferences().colorTheme).toBe('light')
})
})
describe("migrations", () => {
beforeEach(() => {
UserStore.resetInstance()
const mockOpts = {
'tmp': {
'config.json': JSON.stringify({
user: { username: 'foobar' },
preferences: { colorTheme: 'light' },
lastSeenAppVersion: '1.2.3'
})
}
}
mockFs(mockOpts)
const userStore = UserStore.getInstance()
})
afterEach(() => {
mockFs.restore()
})
it("sets last seen app version to 0.0.0", async () => {
expect(userStore.lastSeenAppVersion()).toBe('0.0.0')
})
})
@@ -1,4 +1,4 @@
import { splitArray } from "../splitArray";
import { splitArray } from "./splitArray";
describe("split array on element tests", () => {
test("empty array", () => {
+1
View File
@@ -12,6 +12,7 @@ export const isDebugging = process.env.DEBUG === "true";
export const isProduction = process.env.NODE_ENV === "production"
export const isDevelopment = isDebugging || !isProduction;
export const buildVersion = process.env.BUILD_VERSION;
export const isTestEnv = !!process.env.JEST_WORKER_ID;
// Paths
export const contextDir = process.cwd();
+18
View File
@@ -0,0 +1,18 @@
import packageInfo from "../../package.json"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
jest.mock("electron")
jest.mock("../common/user-store")
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
})
+33
View File
@@ -0,0 +1,33 @@
import { EventEmitter } from 'events'
import { getFreePort } from "./port"
import net from "net"
jest.mock("net");
class MockServer extends EventEmitter {
listen = jest.fn((obj) => {
this.emit('listening', {})
return this
})
address = () => {
return { port: 12345 }
}
unref = jest.fn()
close = jest.fn(cb => cb())
}
describe.only("getFreePort", () => {
beforeEach(() => {
// @ts-ignore
// fixme: find a better way to support types for mocked module
net.createServer.mockReturnValue(new MockServer)
})
afterEach(() => {
jest.clearAllMocks()
})
it("finds the next free port", async () => {
return expect(getFreePort()).resolves.toEqual(expect.any(Number))
})
})
+5 -1
View File
@@ -1,8 +1,12 @@
/* Early store format had the kubeconfig directly under context name, this moves
it under the kubeConfig key */
import { isTestEnv } from "../../common/vars";
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.0.0-beta.2");
if(!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.0.0-beta.2");
}
for (const value of store) {
const contextName = value[0];
// Looping all the keys gives out the store internal stuff too...
+6 -2
View File
@@ -1,9 +1,13 @@
// Cleans up a store that had the state related data stored
import { isTestEnv } from "../../common/vars";
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.4.1");
if (!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.4.1");
}
for (const value of store) {
const contextName = value[0];
if(contextName === "__internal__") continue;
if (contextName === "__internal__") continue;
const cluster = value[1];
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
+5 -1
View File
@@ -1,6 +1,10 @@
// Move cluster icon from root to preferences
import { isTestEnv } from "../../common/vars";
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.2");
if(!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.2");
}
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
+4 -1
View File
@@ -1,8 +1,11 @@
import * as yaml from "js-yaml"
import { isTestEnv } from "../../common/vars";
// Convert access token and expiry from arrays into strings
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.3");
if(!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.3");
}
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
+5 -1
View File
@@ -1,6 +1,10 @@
// Add existing clusters to "default" workspace
import { isTestEnv } from "../../common/vars";
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.0");
if(!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.0");
}
for (const value of store) {
const clusterKey = value[0];
if(clusterKey === "__internal__") continue
+4 -1
View File
@@ -1,8 +1,11 @@
// add id for clusters and store them to array
import { v4 as uuid } from "uuid"
import { isTestEnv } from "../../common/vars";
export function migration(store: any) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.1");
if(!isTestEnv) {
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.1");
}
const clusters: any[] = []
for (const value of store) {
const clusterKey = value[0];
+103
View File
@@ -0,0 +1,103 @@
// Parse kube-api path and get api-version, group, etc.
import { splitArray } from "../../common/utils";
export interface IKubeApiLinkRef {
apiPrefix?: string;
apiVersion: string;
resource: string;
name: string;
namespace?: string;
}
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
apiBase: string;
apiGroup: string;
apiVersionWithGroup: string;
}
export function parseApi(path: string): IKubeApiLinkBase {
path = new URL(path, location.origin).pathname;
const [, prefix, ...parts] = path.split("/");
const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = splitArray(parts, "namespaces");
let apiGroup, apiVersion, namespace, resource, name;
if (namespaced) {
switch (right.length) {
case 1:
name = right[0];
// fallthrough
case 0:
resource = "namespaces"; // special case this due to `split` removing namespaces
break;
default:
[namespace, resource, name] = right;
break;
}
apiVersion = left.pop();
apiGroup = left.join("/");
} else {
switch (left.length) {
case 2:
resource = left.pop();
// fallthrough
case 1:
apiVersion = left.pop();
apiGroup = "";
break;
default:
/**
* Given that
* - `apiVersion` is `GROUP/VERSION` and
* - `VERSION` is `DNS_LABEL` which is /^[a-z0-9]((-[a-z0-9])|[a-z0-9])*$/i
* where length <= 63
* - `GROUP` is /^D(\.D)*$/ where D is `DNS_LABEL` and length <= 253
*
* There is no well defined selection from an array of items that were
* seperated by '/'
*
* Solution is to create a huristic. Namely:
* 1. if '.' in left[0] then apiGroup <- left[0]
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
* 3. otherwise assume apiVersion <- left[0]
* 4. always resource, name <- left[(0 or 1)+1..]
*/
if (left[0].includes('.') || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left;
resource = left.slice(2).join("/")
} else {
apiGroup = "";
apiVersion = left[0];
[resource, name] = left.slice(1)
}
break;
}
}
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) {
throw new Error(`invalid apiPath: ${path}`)
}
return {
apiBase,
apiPrefix, apiGroup,
apiVersion, apiVersionWithGroup,
namespace, resource, name,
};
}
export function createApiLink(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/")
}
@@ -1,11 +1,11 @@
import { IKubeApiLinkBase, KubeApi } from "../kube-api";
import { IKubeApiLinkBase, parseApi } from "./kube-api-parse";
interface ParseAPITest {
interface KubeApi_Parse_Test {
url: string;
expected: Required<IKubeApiLinkBase>;
}
const tests: ParseAPITest[] = [
const tests: KubeApi_Parse_Test[] = [
{
url: "/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27",
expected: {
@@ -112,12 +112,11 @@ const tests: ParseAPITest[] = [
},
];
jest.mock('../kube-watch-api.ts', () => 'KubeWatchApi');
describe("parseApi unit tests", () => {
describe.only("parseApi unit tests", () => {
for (const i in tests) {
const { url: tUrl, expected:tExpect} = tests[i];
test(`test #${parseInt(i)+1}`, () => {
expect(KubeApi.parseApi(tUrl)).toStrictEqual(tExpect);
expect(parseApi(tUrl)).toStrictEqual(tExpect);
});
}
});
+3 -101
View File
@@ -7,7 +7,7 @@ import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } fro
import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager";
import { splitArray } from "../../common/utils";
import { createApiLink, parseApi } from "./kube-api-parse";
export interface IKubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace"
@@ -25,107 +25,9 @@ export interface IKubeApiQueryParams {
continue?: string; // might be used with ?limit from second request
}
export interface IKubeApiLinkRef {
apiPrefix?: string;
apiVersion: string;
resource: string;
name: string;
namespace?: string;
}
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
apiBase: string;
apiGroup: string;
apiVersionWithGroup: string;
}
export class KubeApi<T extends KubeObject = any> {
static parseApi(apiPath = ""): IKubeApiLinkBase {
apiPath = new URL(apiPath, location.origin).pathname;
const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = splitArray(parts, "namespaces");
let apiGroup, apiVersion, namespace, resource, name;
if (namespaced) {
switch (right.length) {
case 1:
name = right[0];
// fallthrough
case 0:
resource = "namespaces"; // special case this due to `split` removing namespaces
break;
default:
[namespace, resource, name] = right;
break;
}
apiVersion = left.pop();
apiGroup = left.join("/");
} else {
switch (left.length) {
case 2:
resource = left.pop();
// fallthrough
case 1:
apiVersion = left.pop();
apiGroup = "";
break;
default:
/**
* Given that
* - `apiVersion` is `GROUP/VERSION` and
* - `VERSION` is `DNS_LABEL` which is /^[a-z0-9]((-[a-z0-9])|[a-z0-9])*$/i
* where length <= 63
* - `GROUP` is /^D(\.D)*$/ where D is `DNS_LABEL` and length <= 253
*
* There is no well defined selection from an array of items that were
* seperated by '/'
*
* Solution is to create a huristic. Namely:
* 1. if '.' in left[0] then apiGroup <- left[0]
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
* 3. otherwise assume apiVersion <- left[0]
* 4. always resource, name <- left[(0 or 1)+1..]
*/
if (left[0].includes('.') || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left;
resource = left.slice(2).join("/")
} else {
apiGroup = "";
apiVersion = left[0];
[resource, name] = left.slice(1)
}
break;
}
}
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`)
}
return {
apiBase,
apiPrefix, apiGroup,
apiVersion, apiVersionWithGroup,
namespace, resource, name,
};
}
static createLink(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
if (namespace) {
namespace = `namespaces/${namespace}`
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/")
}
static parseApi = parseApi;
static createLink = createApiLink;
static watchAll(...apis: KubeApi[]) {
const disposers = apis.map(api => api.watch());
Whitespace-only changes.