Skip to content

Commit

Permalink
Cover static and dynamic plugin SDKs with unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtechszocs committed Oct 13, 2020
1 parent 7cb5073 commit aa74938
Show file tree
Hide file tree
Showing 22 changed files with 1,694 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import * as _ from 'lodash';
import { Extension } from '@console/plugin-sdk/src/typings/base';
import {
isEncodedCodeRef,
isExecutableCodeRef,
filterEncodedCodeRefProperties,
filterExecutableCodeRefProperties,
parseEncodedCodeRefValue,
loadReferencedObject,
resolveEncodedCodeRefs,
resolveCodeRefProperties,
} from '../coderef-resolver';
import { EncodedCodeRef } from '../../types';
import {
getExecutableCodeRefMock,
getEntryModuleMocks,
ModuleFactoryMock,
RemoteEntryModuleMock,
} from '../../utils/test-utils';

describe('isEncodedCodeRef', () => {
it('returns true if obj is structured as { $codeRef: string }', () => {
expect(isEncodedCodeRef({})).toBe(false);
expect(isEncodedCodeRef({ $codeRef: true })).toBe(false);
expect(isEncodedCodeRef({ $codeRef: 'foo' })).toBe(true);
expect(isEncodedCodeRef({ $codeRef: 'foo', bar: true })).toBe(false);
});
});

describe('isExecutableCodeRef', () => {
it('returns true if obj is a function marked with CodeRef symbol', () => {
expect(isExecutableCodeRef(() => {})).toBe(false);
expect(isExecutableCodeRef(getExecutableCodeRefMock('qux'))).toBe(true);
});
});

describe('filterEncodedCodeRefProperties', () => {
it('picks properties whose values match isEncodedCodeRef predicate', () => {
expect(
filterEncodedCodeRefProperties({
foo: { $codeRef: 'foo' },
bar: ['test'],
baz: () => {},
qux: getExecutableCodeRefMock('qux'),
}),
).toEqual({
foo: { $codeRef: 'foo' },
});
});
});

describe('filterExecutableCodeRefProperties', () => {
it('picks properties whose values match isExecutableCodeRef predicate', () => {
const ref = getExecutableCodeRefMock('qux');

expect(
filterExecutableCodeRefProperties({
foo: { $codeRef: 'foo' },
bar: ['test'],
baz: () => {},
qux: ref,
}),
).toEqual({
qux: ref,
});
});
});

describe('parseEncodedCodeRefValue', () => {
it('returns [moduleName, exportName] tuple if value has the right format', () => {
expect(parseEncodedCodeRefValue('foo.bar')).toEqual(['foo', 'bar']);
expect(parseEncodedCodeRefValue('foo')).toEqual(['foo', 'default']);
});

it('returns an empty array if value does not have the expected format', () => {
expect(parseEncodedCodeRefValue('')).toEqual([]);
expect(parseEncodedCodeRefValue('.')).toEqual([]);
expect(parseEncodedCodeRefValue('.bar')).toEqual([]);
expect(parseEncodedCodeRefValue('.bar.')).toEqual([]);
});
});

describe('loadReferencedObject', () => {
const testResult = async (
ref: EncodedCodeRef,
requestedModule: {},
beforeResult: (entryModule: RemoteEntryModuleMock, moduleFactory: ModuleFactoryMock) => void,
afterResult: (
result: any,
errorCallback: jest.Mock<void>,
entryModule: RemoteEntryModuleMock,
moduleFactory: ModuleFactoryMock,
) => void,
) => {
const errorCallback = jest.fn<void>();
const [moduleFactory, entryModule] = getEntryModuleMocks(requestedModule);
beforeResult(entryModule, moduleFactory);

const result = await loadReferencedObject(ref, entryModule, 'Test@1.2.3', errorCallback);
afterResult(result, errorCallback, entryModule, moduleFactory);
};

it('returns the referenced object via remote entry module', async () => {
await testResult(
{ $codeRef: 'foo.bar' },
{ bar: 'value1', default: 'value2' },
_.noop,
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe('value1');
expect(errorCallback).not.toHaveBeenCalled();
expect(entryModule.get).toHaveBeenCalledWith('foo');
expect(moduleFactory).toHaveBeenCalledWith();
},
);

await testResult(
{ $codeRef: 'foo' },
{ bar: 'value1', default: 'value2' },
_.noop,
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe('value2');
expect(errorCallback).not.toHaveBeenCalled();
expect(entryModule.get).toHaveBeenCalledWith('foo');
expect(moduleFactory).toHaveBeenCalledWith();
},
);
});

it('fails on malformed code reference', async () => {
await testResult(
{ $codeRef: '' },
{ bar: 'value1', default: 'value2' },
_.noop,
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe(null);
expect(errorCallback).toHaveBeenCalledWith();
expect(entryModule.get).not.toHaveBeenCalled();
expect(moduleFactory).not.toHaveBeenCalled();
},
);
});

it('fails when requested module resolution throws an error', async () => {
await testResult(
{ $codeRef: 'foo.bar' },
{ bar: 'value1', default: 'value2' },
(entryModule, moduleFactory) => {
entryModule.get.mockImplementation(() => {
throw new Error('boom');
});
},
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe(null);
expect(errorCallback).toHaveBeenCalledWith();
expect(entryModule.get).toHaveBeenCalledWith('foo');
expect(moduleFactory).not.toHaveBeenCalled();
},
);

await testResult(
{ $codeRef: 'foo.bar' },
{ bar: 'value1', default: 'value2' },
(entryModule, moduleFactory) => {
moduleFactory.mockImplementation(() => {
throw new Error('boom');
});
},
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe(null);
expect(errorCallback).toHaveBeenCalledWith();
expect(entryModule.get).toHaveBeenCalledWith('foo');
expect(moduleFactory).toHaveBeenCalledWith();
},
);
});

it('fails on missing module export', async () => {
await testResult(
{ $codeRef: 'foo.bar' },
{ default: 'value2' },
_.noop,
(result, errorCallback, entryModule, moduleFactory) => {
expect(result).toBe(null);
expect(errorCallback).toHaveBeenCalledWith();
expect(entryModule.get).toHaveBeenCalledWith('foo');
expect(moduleFactory).toHaveBeenCalledWith();
},
);
});
});

describe('resolveEncodedCodeRefs', () => {
it('replaces encoded code references with executable CodeRef functions', () => {
const extensions: Extension[] = [
{
type: 'Foo',
properties: { test: true },
},
{
type: 'Bar',
properties: { baz: 1, qux: { $codeRef: 'a.b' } },
},
];

const errorCallback = jest.fn();
const [moduleFactory, entryModule] = getEntryModuleMocks({ b: 'value' });

const resolvedExtensions = resolveEncodedCodeRefs(
extensions,
entryModule,
'Test@1.2.3',
errorCallback,
);

expect(resolvedExtensions.length).toBe(extensions.length);
expect(resolvedExtensions[0]).toEqual(extensions[0]);

expect(_.omit(resolvedExtensions[1], 'properties.qux')).toEqual(
_.omit(extensions[1], 'properties.qux'),
);

expect(isExecutableCodeRef(resolvedExtensions[1].properties.qux)).toBe(true);
});
});

describe('resolveCodeRefProperties', () => {
it('replaces executable CodeRef functions with corresponding objects', async () => {
const extensions: Extension[] = [
{
type: 'Foo',
properties: { test: true },
},
{
type: 'Bar',
properties: { baz: 1, qux: getExecutableCodeRefMock('value') },
},
];

expect(await resolveCodeRefProperties(extensions[0])).toEqual({});
expect(await resolveCodeRefProperties(extensions[1])).toEqual({ qux: 'value' });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { executeReferencedFunction } from '../coderef-utils';

describe('executeReferencedFunction', () => {
it('executes the referenced function with given args and returns its result', async () => {
const func = jest.fn(() => 'value');
const args = ['foo', true, { bar: [1, 'qux'] }];
const ref = jest.fn(() => Promise.resolve(func));

const result = await executeReferencedFunction(ref, ...args);

expect(ref).toHaveBeenCalledWith();
expect(func).toHaveBeenCalledWith(...args);
expect(result).toBe('value');
});

it('returns null when the referenced object is not a function', async () => {
const args = ['foo', true, { bar: [1, 'qux'] }];
const ref = jest.fn(() => Promise.resolve('value'));

const result = await executeReferencedFunction(ref, ...args);

expect(ref).toHaveBeenCalledWith();
expect(result).toBe(null);
});

it('returns null when the referenced function throws an error', async () => {
const func = jest.fn(() => {
throw new Error('boom');
});
const args = ['foo', true, { bar: [1, 'qux'] }];
const ref = jest.fn(() => Promise.resolve(func));

const result = await executeReferencedFunction(ref, ...args);

expect(ref).toHaveBeenCalledWith();
expect(func).toHaveBeenCalledWith(...args);
expect(result).toBe(null);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import * as _ from 'lodash';
import { Extension } from '@console/plugin-sdk/src/typings/base';
import { SupportedExtension } from '../schema/console-extensions';
import {
RemoteEntryModule,
EncodedCodeRef,
Expand All @@ -11,14 +10,14 @@ import {
ExtensionProperties,
} from '../types';

const codeRefSymbol = Symbol('CodeRef');
export const codeRefSymbol = Symbol('CodeRef');

const isEncodedCodeRef = (obj): obj is EncodedCodeRef =>
export const isEncodedCodeRef = (obj): obj is EncodedCodeRef =>
_.isPlainObject(obj) &&
_.isEqual(Object.getOwnPropertyNames(obj), ['$codeRef']) &&
typeof (obj as EncodedCodeRef).$codeRef === 'string';

const isExecutableCodeRef = (obj): obj is CodeRef =>
export const isExecutableCodeRef = (obj): obj is CodeRef =>
_.isFunction(obj) &&
_.isEqual(Object.getOwnPropertySymbols(obj), [codeRefSymbol]) &&
obj[codeRefSymbol] === true;
Expand Down Expand Up @@ -46,19 +45,21 @@ export const parseEncodedCodeRefValue = (value: string): [string, string] | [] =
*
* _Does not throw errors by design._
*/
const loadReferencedObject = async <TExport = any>(
export const loadReferencedObject = async <TExport = any>(
ref: EncodedCodeRef,
entryModule: RemoteEntryModule,
pluginID: string,
errorCallback: VoidFunction,
): Promise<TExport> => {
if (process.env.NODE_ENV !== 'production') {
console.info(`Loading object '${ref.$codeRef}' of plugin ${pluginID}`);
}

const [moduleName, exportName] = parseEncodedCodeRefValue(ref.$codeRef);
let requestedModule: object;

if (!moduleName) {
console.error(`Malformed code reference '${ref.$codeRef}' of plugin ${pluginID}`);
errorCallback();
return null;
}

try {
const moduleFactory = await entryModule.get(moduleName);
requestedModule = moduleFactory();
Expand All @@ -83,7 +84,7 @@ const loadReferencedObject = async <TExport = any>(
* _Does not execute `CodeRef` functions to load the referenced objects._
*/
export const resolveEncodedCodeRefs = (
extensions: SupportedExtension[],
extensions: Extension[],
entryModule: RemoteEntryModule,
pluginID: string,
errorCallback: VoidFunction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { CodeRef } from '../types';
*
* _Does not throw errors by design._
*/
export const executeReferencedFunction = async <T extends (...args: any) => any>(
export const executeReferencedFunction = async <T extends (...args: any[]) => any>(
ref: CodeRef<T>,
...args: Parameters<T>
): Promise<ReturnType<T>> => {
try {
const func = await ref();
return func(args);
return func(...args);
} catch (error) {
console.error('Failed to execute referenced function', error);
return null;
Expand Down

0 comments on commit aa74938

Please sign in to comment.