Skip to content

Commit

Permalink
frontend: Add unit tests for api/v2
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Dubenko <[email protected]>
  • Loading branch information
sniok committed Sep 17, 2024
1 parent 3cbc3e5 commit 9854644
Show file tree
Hide file tree
Showing 4 changed files with 316 additions and 0 deletions.
130 changes: 130 additions & 0 deletions frontend/src/lib/k8s/api/v2/KubeList.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, expect, it, vi } from 'vitest';
import { KubeObjectClass, KubeObjectInterface } from '../../cluster';
import { KubeList, KubeListUpdateEvent } from './KubeList';

class MockKubeObject implements KubeObjectInterface {
apiVersion = 'v1';
kind = 'MockKubeObject';
metadata: any = {
uid: 'mock-uid',
resourceVersion: '1',
};

constructor(data: Partial<KubeObjectInterface>) {
Object.assign(this, data);
}
}

describe('KubeList.applyUpdate', () => {
const itemClass = MockKubeObject as unknown as KubeObjectClass;
const initialList = {
kind: 'MockKubeList',
apiVersion: 'v1',
items: [
{ apiVersion: 'v1', kind: 'MockKubeObject', metadata: { uid: '1', resourceVersion: '1' } },
],
metadata: {
resourceVersion: '1',
},
};

it('should add a new item on ADDED event', () => {
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'ADDED',
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '2', resourceVersion: '2' },
},
};

const updatedList = KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(updatedList.items).toHaveLength(2);
expect(updatedList.items[1].metadata.uid).toBe('2');
expect(updatedList.items[1] instanceof MockKubeObject).toBe(true);
});

it('should modify an existing item on MODIFIED event', () => {
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'MODIFIED',
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '1', resourceVersion: '2' },
},
};

const updatedList = KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(updatedList.items).toHaveLength(1);
expect(updatedList.items[0].metadata.resourceVersion).toBe('2');
expect(updatedList.items[0] instanceof MockKubeObject).toBe(true);
});

it('should add a new item on MODIFIED event', () => {
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'MODIFIED',
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '3', resourceVersion: '3' },
},
};

const updatedList = KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(updatedList.items).toHaveLength(2);
expect(updatedList.items[1].metadata.uid).toBe('3');
expect(updatedList.items[1] instanceof MockKubeObject).toBe(true);
});

it('should delete an existing item on DELETED event', () => {
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'DELETED',
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '1', resourceVersion: '2' },
},
};

const updatedList = KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(updatedList.items).toHaveLength(0);
});

it('should log an error on ERROR event', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'ERROR',
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '1', resourceVersion: '2' },
},
};

KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(consoleErrorSpy).toHaveBeenCalledWith('Error in update', updateEvent);
consoleErrorSpy.mockRestore();
});

it('should log an error on unknown event type', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const updateEvent: KubeListUpdateEvent<MockKubeObject> = {
type: 'UNKNOWN' as any,
object: {
apiVersion: 'v1',
kind: 'MockKubeObject',
metadata: { uid: '1', resourceVersion: '2' },
},
};

KubeList.applyUpdate(initialList, updateEvent, itemClass);

expect(consoleErrorSpy).toHaveBeenCalledWith('Unknown update type', updateEvent);
consoleErrorSpy.mockRestore();
});
});
42 changes: 42 additions & 0 deletions frontend/src/lib/k8s/api/v2/KubeObjectEndpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { KubeObjectEndpoint } from './KubeObjectEndpoint';

describe('KubeObjectEndpoint', () => {
describe('toUrl', () => {
it('should generate URL for core resources without namespace', () => {
const endpoint = { version: 'v1', resource: 'pods' };
const url = KubeObjectEndpoint.toUrl(endpoint);
expect(url).toBe('api/v1/pods');
});

it('should generate URL for core resources with namespace', () => {
const endpoint = { version: 'v1', resource: 'pods' };
const url = KubeObjectEndpoint.toUrl(endpoint, 'default');
expect(url).toBe('api/v1/namespaces/default/pods');
});

it('should generate URL for custom resources without namespace', () => {
const endpoint = { group: 'apps', version: 'v1', resource: 'deployments' };
const url = KubeObjectEndpoint.toUrl(endpoint);
expect(url).toBe('apis/apps/v1/deployments');
});

it('should generate URL for custom resources with namespace', () => {
const endpoint = { group: 'apps', version: 'v1', resource: 'deployments' };
const url = KubeObjectEndpoint.toUrl(endpoint, 'default');
expect(url).toBe('apis/apps/v1/namespaces/default/deployments');
});

it('should generate URL for custom resources with empty group', () => {
const endpoint = { group: '', version: 'v1', resource: 'services' };
const url = KubeObjectEndpoint.toUrl(endpoint);
expect(url).toBe('api/v1/services');
});

it('should generate URL for custom resources with empty group and namespace', () => {
const endpoint = { group: '', version: 'v1', resource: 'services' };
const url = KubeObjectEndpoint.toUrl(endpoint, 'default');
expect(url).toBe('api/v1/namespaces/default/services');
});
});
});
90 changes: 90 additions & 0 deletions frontend/src/lib/k8s/api/v2/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import nock from 'nock';
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { findKubeconfigByClusterName, getUserIdFromLocalStorage } from '../../../../stateless';
import { getToken, setToken } from '../../../auth';
import { getClusterAuthType } from '../v1/clusterRequests';
import { BASE_HTTP_URL, clusterFetch } from './fetch';

vi.mock('../../../auth', () => ({
getToken: vi.fn(),
setToken: vi.fn(),
}));

vi.mock('../../../../stateless', () => ({
findKubeconfigByClusterName: vi.fn(),
getUserIdFromLocalStorage: vi.fn(),
}));

vi.mock('../v1/clusterRequests', () => ({
getClusterAuthType: vi.fn(),
}));

vi.mock('../v1/tokenApi', () => ({
refreshToken: vi.fn(),
}));

describe('clusterFetch', () => {
const clusterName = 'test-cluster';
const testUrl = '/test/url';
const mockResponse = { message: 'mock response' };
const token = 'test-token';
const newToken = 'new-token';
const kubeconfig = 'mock-kubeconfig';
const userID = 'mock-user-id';

beforeEach(() => {
vi.resetAllMocks();
(getToken as Mock).mockReturnValue(token);
(findKubeconfigByClusterName as Mock).mockResolvedValue(kubeconfig);
(getUserIdFromLocalStorage as Mock).mockReturnValue(userID);
(getClusterAuthType as Mock).mockReturnValue('serviceAccount');
});

afterEach(() => {
nock.cleanAll();
});

it('Successfully makes a request', async () => {
nock(BASE_HTTP_URL).get(`/clusters/${clusterName}${testUrl}`).reply(200, mockResponse);

const response = await clusterFetch(testUrl, { cluster: clusterName });
const responseBody = await response.json();

expect(responseBody).toEqual(mockResponse);
});

it('Sets Authorization header with token', async () => {
nock(BASE_HTTP_URL)
.get(`/clusters/${clusterName}${testUrl}`)
.matchHeader('Authorization', `Bearer ${token}`)
.reply(200, mockResponse);

await clusterFetch(testUrl, { cluster: clusterName });
});

it('Sets KUBECONFIG and X-HEADLAMP-USER-ID headers if kubeconfig exists', async () => {
nock(BASE_HTTP_URL)
.get(`/clusters/${clusterName}${testUrl}`)
.matchHeader('KUBECONFIG', kubeconfig)
.matchHeader('X-HEADLAMP-USER-ID', userID)
.reply(200, mockResponse);

await clusterFetch(testUrl, { cluster: clusterName });
});

it('Sets new token if X-Authorization header is present in response', async () => {
nock(BASE_HTTP_URL)
.get(`/clusters/${clusterName}${testUrl}`)
.reply(200, mockResponse, { 'X-Authorization': newToken });

await clusterFetch(testUrl, { cluster: clusterName });

expect(setToken).toHaveBeenCalledWith(clusterName, newToken);
});

it('Throws an error if response is not ok', async () => {
nock(BASE_HTTP_URL).get(`/clusters/${clusterName}${testUrl}`).reply(500);

await expect(clusterFetch(testUrl, { cluster: clusterName })).rejects.toThrow('Unreachable');
});
});
54 changes: 54 additions & 0 deletions frontend/src/lib/k8s/api/v2/makeUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { makeUrl } from './makeUrl';

describe('makeUrl', () => {
it('should create a URL from parts without query parameters', () => {
const urlParts = ['http://example.com', 'path', 'to', 'resource'];
const result = makeUrl(urlParts);
expect(result).toBe('http://example.com/path/to/resource');
});

it('should create a URL from parts with query parameters', () => {
const urlParts = ['http://example.com', 'path', 'to', 'resource'];
const query = { key1: 'value1', key2: 'value2' };
const result = makeUrl(urlParts, query);
expect(result).toBe('http://example.com/path/to/resource?key1=value1&key2=value2');
});

it('should handle empty urlParts', () => {
const urlParts: any[] = [];
const result = makeUrl(urlParts);
expect(result).toBe('');
});

it('should handle empty query parameters', () => {
const urlParts = ['http://example.com', 'path', 'to', 'resource'];
const query = {};
const result = makeUrl(urlParts, query);
expect(result).toBe('http://example.com/path/to/resource');
});

it('should replace multiple slashes with a single one', () => {
const urlParts = ['http://example.com/', '/path/', '/to/', '/resource'];
const result = makeUrl(urlParts);
expect(result).toBe('http://example.com/path/to/resource');
});

it('should handle special characters in query parameters', () => {
const urlParts = ['http://example.com', 'path', 'to', 'resource'];
const query = {
'key with spaces': 'value with spaces',
'key&with&special&chars': 'value&with&special&chars',
};
const result = makeUrl(urlParts, query);
expect(result).toBe(
'http://example.com/path/to/resource?key+with+spaces=value+with+spaces&key%26with%26special%26chars=value%26with%26special%26chars'
);
});

it('should handle numeric and boolean values in urlParts', () => {
const urlParts = ['http://example.com', 123, true, 'resource'];
const result = makeUrl(urlParts);
expect(result).toBe('http://example.com/123/true/resource');
});
});

0 comments on commit 9854644

Please sign in to comment.