Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions frontend/public/module/k8s/__tests__/swagger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type { SwaggerAPISpec, SwaggerDefinitions } from '../swagger';

const mockCoFetch = jest.fn();
jest.mock('@console/shared/src/utils/console-fetch', () => ({
coFetch: mockCoFetch,
}));

const mockDefinitions: SwaggerDefinitions = {
'io.k8s.api.core.v1.Pod': {
description: 'Pod is a collection of containers.',
type: 'object',
properties: {
metadata: { $ref: '#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta' },
},
},
};

const createMockResponse = (definitions: SwaggerDefinitions, etag?: string): Response =>
(({
status: 200,
ok: true,
headers: new Headers(etag ? { ETag: etag } : {}),
json: jest.fn().mockResolvedValue({
swagger: '2.0',
info: { title: 'Kubernetes', version: 'v1' },
paths: {},
definitions,
} as SwaggerAPISpec),
} as unknown) as Response);

const create304Response = (): Response =>
(({
status: 304,
ok: true,
headers: new Headers(),
json: jest.fn(),
} as unknown) as Response);
Comment thread
rhamilto marked this conversation as resolved.

describe('fetchSwagger', () => {
let fetchSwagger: () => Promise<SwaggerDefinitions>;
let getSwaggerDefinitions: () => SwaggerDefinitions;

beforeEach(() => {
jest.isolateModules(() => {
// Fresh module to reset cachedETag and swaggerDefinitions
const swagger = require('../swagger');
fetchSwagger = swagger.fetchSwagger;
getSwaggerDefinitions = swagger.getSwaggerDefinitions;
});
mockCoFetch.mockReset();
jest.spyOn(window, 'dispatchEvent').mockImplementation(() => true);
});

afterEach(() => {
jest.restoreAllMocks();
});

it('should fetch and return swagger definitions on first call', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));

const result = await fetchSwagger();

expect(result).toEqual(mockDefinitions);
expect(mockCoFetch).toHaveBeenCalledWith('api/kubernetes/openapi/v2', {
headers: { Accept: 'application/json' },
});
});

it('should send If-None-Match header on subsequent requests', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));
await fetchSwagger();

mockCoFetch.mockResolvedValue(create304Response());
await fetchSwagger();

expect(mockCoFetch).toHaveBeenCalledTimes(2);
expect(mockCoFetch.mock.calls[1]).toEqual([
'api/kubernetes/openapi/v2',
{ headers: { Accept: 'application/json', 'If-None-Match': '"abc123"' } },
]);
});

it('should return cached definitions on 304', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));
await fetchSwagger();

const notModifiedResponse = create304Response();
mockCoFetch.mockResolvedValue(notModifiedResponse);
const result = await fetchSwagger();

expect(result).toEqual(mockDefinitions);
expect(notModifiedResponse.json).not.toHaveBeenCalled();
});

it('should update definitions when server returns new data', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));
await fetchSwagger();

const updatedDefinitions: SwaggerDefinitions = {
...mockDefinitions,
'io.k8s.api.apps.v1.Deployment': { description: 'A Deployment.', type: 'object' },
};
mockCoFetch.mockResolvedValue(createMockResponse(updatedDefinitions, '"def456"'));
const result = await fetchSwagger();

expect(result).toEqual(updatedDefinitions);
expect(getSwaggerDefinitions()).toEqual(updatedDefinitions);
});

it('should dispatch console_swagger_refresh on successful fetch', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));
await fetchSwagger();

expect(window.dispatchEvent).toHaveBeenCalledWith(new Event('console_swagger_refresh'));
});

it('should not dispatch console_swagger_refresh on 304', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions, '"abc123"'));
await fetchSwagger();
(window.dispatchEvent as jest.Mock).mockClear();

mockCoFetch.mockResolvedValue(create304Response());
await fetchSwagger();

expect(window.dispatchEvent).not.toHaveBeenCalled();
});

it('should return null when definitions are missing from response', async () => {
const response = ({
status: 200,
ok: true,
headers: new Headers(),
json: jest.fn().mockResolvedValue({ swagger: '2.0', paths: {} }),
} as unknown) as Response;
mockCoFetch.mockResolvedValue(response);

const result = await fetchSwagger();

expect(result).toBeNull();
});

it('should return null on fetch error', async () => {
mockCoFetch.mockRejectedValue(new Error('Network error'));

const result = await fetchSwagger();

expect(result).toBeNull();
});

it('should work without ETag header from server', async () => {
mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions));
const result = await fetchSwagger();

expect(result).toEqual(mockDefinitions);

mockCoFetch.mockResolvedValue(createMockResponse(mockDefinitions));
await fetchSwagger();

expect(mockCoFetch.mock.calls[1]).toEqual([
'api/kubernetes/openapi/v2',
{ headers: { Accept: 'application/json' } },
]);
});
});
26 changes: 16 additions & 10 deletions frontend/public/module/k8s/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import * as _ from 'lodash';

import { STORAGE_PREFIX } from '@console/shared/src/constants/common';
import { coFetchJSON } from '@console/shared/src/utils/console-fetch';
import { coFetch } from '@console/shared/src/utils/console-fetch';
import { K8sKind } from '@console/dynamic-plugin-sdk/src/api/common-types';
import { referenceForModel } from '@console/internal/module/k8s/k8s';

const SWAGGER_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/swagger-definitions`;

export const getDefinitionKey = _.memoize(
(model: K8sKind, definitions: SwaggerDefinitions): string => {
return _.findKey(definitions, (def: SwaggerDefinition) => {
Expand All @@ -25,18 +22,27 @@ export const getDefinitionKey = _.memoize(
let swaggerDefinitions: SwaggerDefinitions;
export const getSwaggerDefinitions = (): SwaggerDefinitions => swaggerDefinitions;

let cachedETag: string;
Comment thread
rhamilto marked this conversation as resolved.

export const fetchSwagger = async (): Promise<SwaggerDefinitions> => {
// Remove any old definitions from `localSotrage`. We rely on the browser cache now.
// TODO: We should be able to remove this in a future release.
localStorage.removeItem(SWAGGER_LOCAL_STORAGE_KEY);
try {
const response: SwaggerAPISpec = await coFetchJSON('api/kubernetes/openapi/v2');
if (!response.definitions) {
const response = await coFetch('api/kubernetes/openapi/v2', {
headers: {
Accept: 'application/json',
...(cachedETag && { 'If-None-Match': cachedETag }),
},
});
if (response.status === 304) {
return swaggerDefinitions;
}
const data: SwaggerAPISpec = await response.json();
if (!data.definitions) {
// eslint-disable-next-line no-console
console.error('Definitions missing in OpenAPI response.');
return null;
}
swaggerDefinitions = response.definitions;
cachedETag = response.headers.get('ETag');
swaggerDefinitions = data.definitions;
window.dispatchEvent(new Event('console_swagger_refresh'));
return swaggerDefinitions;
} catch (e) {
Expand Down