Add tests for Flows operations (#16580)

* Add tests for Flows operations

* fix notifications test env

* tweaks

* fix env mock
This commit is contained in:
Azri Kahar
2023-01-12 05:12:16 +08:00
committed by GitHub
parent 102b965abe
commit d2a6621153
9 changed files with 548 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import { afterEach, expect, test, vi } from 'vitest';
vi.mock('../../services', () => {
const ItemsService = vi.fn();
ItemsService.prototype.createMany = vi.fn();
return { ItemsService };
});
vi.mock('../../utils/get-accountability-for-role', () => ({
getAccountabilityForRole: vi.fn((role: string | null, _context) => Promise.resolve(role)),
}));
import { ItemsService } from '../../services';
import config from './index';
const testCollection = 'test';
const testId = '00000000-0000-0000-0000-000000000000';
const testAccountability = { user: testId, role: testId };
const getSchema = vi.fn().mockResolvedValue({});
afterEach(() => {
vi.clearAllMocks();
});
test.each([
{ permissions: undefined, expected: testAccountability },
{ permissions: '$trigger', expected: testAccountability },
{ permissions: '$full', expected: 'system' },
{ permissions: '$public', expected: null },
{ permissions: 'test', expected: 'test' },
])('accountability for permissions "$permissions" should be $expected', async ({ permissions, expected }) => {
await config.handler(
{ collection: testCollection, permissions } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService)).toHaveBeenCalledWith(
testCollection,
expect.objectContaining({ schema: {}, accountability: expected, knex: undefined })
);
});
test.each([
{ payload: null, expected: null },
{ payload: { test: 'test' }, expected: [{ test: 'test' }] },
])('payload $payload should be passed as $expected', async ({ payload, expected }) => {
await config.handler(
{ collection: testCollection, payload } as any,
{ accountability: testAccountability, getSchema } as any
);
if (expected) {
expect(vi.mocked(ItemsService).prototype.createMany).toHaveBeenCalledWith(expected, expect.anything());
} else {
expect(vi.mocked(ItemsService).prototype.createMany).not.toHaveBeenCalled();
}
});
test('should emit events when true', async () => {
await config.handler(
{ collection: testCollection, payload: {}, emitEvents: true } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.createMany).toHaveBeenCalledWith([{}], { emitEvents: true });
});
test.each([undefined, false])('should not emit events when %s', async (emitEvents) => {
await config.handler(
{ collection: testCollection, payload: {}, emitEvents } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.createMany).toHaveBeenCalledWith([{}], { emitEvents: false });
});

View File

@@ -0,0 +1,157 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
vi.mock('../../services', () => {
const ItemsService = vi.fn();
ItemsService.prototype.updateByQuery = vi.fn();
ItemsService.prototype.updateOne = vi.fn();
ItemsService.prototype.updateMany = vi.fn();
return { ItemsService };
});
vi.mock('../../utils/get-accountability-for-role', () => ({
getAccountabilityForRole: vi.fn((role: string | null, _context) => Promise.resolve(role)),
}));
import { ItemsService } from '../../services';
import config from './index';
const testCollection = 'test';
const testPayload = {};
const testId = '00000000-0000-0000-0000-000000000000';
const testAccountability = { user: testId, role: testId };
const getSchema = vi.fn().mockResolvedValue({});
describe('Operations / Item Update', () => {
afterEach(() => {
vi.clearAllMocks();
});
test.each([
{ permissions: undefined, expected: testAccountability },
{ permissions: '$trigger', expected: testAccountability },
{ permissions: '$full', expected: 'system' },
{ permissions: '$public', expected: null },
{ permissions: 'test', expected: 'test' },
])('accountability for permissions "$permissions" should be $expected', async ({ permissions, expected }) => {
await config.handler(
{ collection: testCollection, payload: testPayload, permissions } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService)).toHaveBeenCalledWith(
testCollection,
expect.objectContaining({ schema: {}, accountability: expected, knex: undefined })
);
});
test('should return null when payload is not defined', async () => {
const result = await config.handler(
{ collection: testCollection } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(result).toBe(null);
expect(vi.mocked(ItemsService).prototype.updateByQuery).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateOne).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateMany).not.toHaveBeenCalled();
});
test.each([undefined, []])('should call updateByQuery with correct query when key is $payload', async (key) => {
const query = { limit: -1 };
await config.handler(
{ collection: testCollection, payload: testPayload, query, key } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateByQuery).toHaveBeenCalledWith(query, testPayload, expect.anything());
expect(vi.mocked(ItemsService).prototype.updateOne).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateMany).not.toHaveBeenCalled();
});
test('should emit events for updateByQuery when true', async () => {
const query = { limit: -1 };
await config.handler(
{ collection: testCollection, payload: testPayload, query, emitEvents: true } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateByQuery).toHaveBeenCalledWith(query, testPayload, {
emitEvents: true,
});
});
test.each([undefined, false])('should not emit events for updateByQuery when %s', async (emitEvents) => {
const query = { limit: -1 };
await config.handler(
{ collection: testCollection, payload: testPayload, query, emitEvents } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateByQuery).toHaveBeenCalledWith(query, testPayload, {
emitEvents: false,
});
});
test.each([1, [1]])('should call updateOne when key is $payload', async (key) => {
await config.handler(
{ collection: testCollection, payload: testPayload, key } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateByQuery).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateOne).toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateMany).not.toHaveBeenCalled();
});
test('should emit events for updateOne when true', async () => {
const key = 1;
await config.handler(
{ collection: testCollection, payload: testPayload, key, emitEvents: true } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateOne).toHaveBeenCalledWith(key, testPayload, { emitEvents: true });
});
test.each([undefined, false])('should not emit events for updateOne when %s', async (emitEvents) => {
const key = 1;
await config.handler(
{ collection: testCollection, payload: testPayload, key: key, emitEvents } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateOne).toHaveBeenCalledWith(key, testPayload, { emitEvents: false });
});
test('should call updateMany when key is an array with more than one item', async () => {
await config.handler(
{ collection: testCollection, payload: testPayload, key: [1, 2, 3] } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateByQuery).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateOne).not.toHaveBeenCalled();
expect(vi.mocked(ItemsService).prototype.updateMany).toHaveBeenCalled();
});
test('should emit events for updateMany when true', async () => {
const keys = [1, 2, 3];
await config.handler(
{ collection: testCollection, payload: testPayload, key: keys, emitEvents: true } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateMany).toHaveBeenCalledWith(keys, testPayload, { emitEvents: true });
});
test.each([undefined, false])('should not emit events for updateMany when %s', async (emitEvents) => {
const keys = [1, 2, 3];
await config.handler(
{ collection: testCollection, payload: testPayload, key: keys, emitEvents } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(ItemsService).prototype.updateMany).toHaveBeenCalledWith(keys, testPayload, { emitEvents: false });
});
});

View File

@@ -0,0 +1,31 @@
import { afterEach, expect, test, vi } from 'vitest';
const loggerInfo = vi.fn();
vi.mock('../../logger', () => ({
default: {
info: loggerInfo,
},
}));
import config from './index';
afterEach(() => {
vi.clearAllMocks();
});
test('logs number message as string', () => {
const message = 1;
config.handler({ message }, {} as any);
expect(loggerInfo).toHaveBeenCalledWith(String(1));
});
test('logs json message as stringified json', () => {
const message = { test: 'message' };
config.handler({ message }, {} as any);
expect(loggerInfo).toHaveBeenCalledWith(JSON.stringify(message));
});

View File

@@ -0,0 +1,54 @@
import { afterEach, expect, test, vi } from 'vitest';
vi.mock('../../services', () => {
const NotificationsService = vi.fn();
NotificationsService.prototype.createMany = vi.fn();
return { NotificationsService };
});
vi.mock('../../utils/get-accountability-for-role', () => ({
getAccountabilityForRole: vi.fn((role: string | null, _context) => Promise.resolve(role)),
}));
import { NotificationsService } from '../../services';
import config from './index';
const testId = '00000000-0000-0000-0000-000000000000';
const testAccountability = { user: testId, role: testId };
const testRecipient = [testId];
const getSchema = vi.fn().mockResolvedValue({});
afterEach(() => {
vi.clearAllMocks();
});
test.each([
{ permissions: undefined, expected: testAccountability },
{ permissions: '$trigger', expected: testAccountability },
{ permissions: '$full', expected: null },
{ permissions: '$public', expected: null },
{ permissions: 'test', expected: 'test' },
])('accountability for permissions "$permissions" should be $expected', async ({ permissions, expected }) => {
await config.handler({ permissions } as any, { accountability: testAccountability, getSchema } as any);
expect(vi.mocked(NotificationsService)).toHaveBeenCalledWith(expect.objectContaining({ accountability: expected }));
});
test.each([
{ message: null, expected: null },
{ message: 123, expected: '123' },
{ message: { test: 'test' }, expected: '{"test":"test"}' },
])('message $message should be sent as string $expected', async ({ message, expected }) => {
await config.handler(
{ recipient: testRecipient, message } as any,
{ accountability: testAccountability, getSchema } as any
);
expect(vi.mocked(NotificationsService).prototype.createMany).toHaveBeenCalledWith(
expect.arrayContaining([expect.objectContaining({ message: expected })])
);
});

View File

@@ -0,0 +1,109 @@
import { afterEach, expect, test, vi } from 'vitest';
const axiosDefault = vi.fn();
vi.mock('axios', () => ({
default: axiosDefault.mockResolvedValue({
status: 200,
statusText: 'OK',
headers: {},
data: {},
}),
}));
const url = '/';
const method = 'POST';
import config from './index';
afterEach(() => {
vi.clearAllMocks();
});
test('no headers configured', async () => {
const body = 'body';
const headers = undefined;
await config.handler({ url, method, body, headers }, {} as any);
expect(axiosDefault).toHaveBeenCalledWith(
expect.objectContaining({
url,
method,
data: body,
headers: {},
})
);
});
test('headers array is converted to object', async () => {
const body = 'body';
const headers = [
{ header: 'header1', value: 'value1' },
{ header: 'header2', value: 'value2' },
];
await config.handler({ url, method, body, headers }, {} as any);
expect(axiosDefault).toHaveBeenCalledWith(
expect.objectContaining({
url,
method,
data: body,
headers: expect.objectContaining({
header1: 'value1',
header2: 'value2',
}),
})
);
});
test('should not automatically set Content-Type header when it is already defined', async () => {
const body = 'body';
const headers = [{ header: 'Content-Type', value: 'application/octet-stream' }];
await config.handler({ url, method, body, headers }, {} as any);
expect(axiosDefault).toHaveBeenCalledWith(
expect.objectContaining({
url,
method,
data: body,
headers: expect.objectContaining({
'Content-Type': expect.not.stringContaining('application/json'),
}),
})
);
});
test('should not automatically set Content-Type header to "application/json" when the body is not a valid JSON string', async () => {
const body = '"a": "b"';
const headers = [{ header: 'header1', value: 'value1' }];
await config.handler({ url, method, body, headers }, {} as any);
expect(axiosDefault).toHaveBeenCalledWith(
expect.objectContaining({
url,
method,
data: body,
headers: expect.not.objectContaining({
'Content-Type': 'application/json',
}),
})
);
});
test('should automatically set Content-Type header to "application/json" when the body is a valid JSON string', async () => {
const body = '{ "a": "b" }';
const headers = [{ header: 'header1', value: 'value1' }];
await config.handler({ url, method, body, headers }, {} as any);
expect(axiosDefault).toHaveBeenCalledWith(
expect.objectContaining({
url,
method,
data: body,
headers: expect.objectContaining({
header1: 'value1',
'Content-Type': 'application/json',
}),
})
);
});

View File

@@ -0,0 +1,47 @@
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import config from './index';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test('promise resolves after the configured duration in milliseconds', () => {
const milliseconds = 1000;
// asserts there is no timer (setTimeout) running yet
expect(vi.getTimerCount()).toBe(0);
// intentionally don't await to assert the timer
config.handler({ milliseconds }, {} as any);
// asserts there is 1 timer (setTimeout) running now
expect(vi.getTimerCount()).toBe(1);
vi.advanceTimersByTime(milliseconds);
// asserts there is no longer any timer (setTimeout) running
expect(vi.getTimerCount()).toBe(0);
});
test('casts string input for milliseconds to number', () => {
const milliseconds = '1000';
// asserts there is no timer (setTimeout) running yet
expect(vi.getTimerCount()).toBe(0);
// intentionally don't await to assert the timer
config.handler({ milliseconds }, {} as any);
// asserts there is 1 timer (setTimeout) running now
expect(vi.getTimerCount()).toBe(1);
vi.advanceTimersByTime(Number(milliseconds));
// asserts there is no longer any timer (setTimeout) running
expect(vi.getTimerCount()).toBe(0);
});

View File

@@ -0,0 +1,19 @@
import { expect, test } from 'vitest';
import config from './index';
test('runs the same object as the input', async () => {
const json = { test: 'item' };
const result = await config.handler({ json }, {} as any);
expect(result).toEqual(json);
});
test('runs parsed JSON for stringified JSON input', async () => {
const json = '{"test":"item"}';
const result = await config.handler({ json }, {} as any);
expect(result).toEqual({ test: 'item' });
});

View File

@@ -0,0 +1,43 @@
import { afterEach, expect, test, vi } from 'vitest';
const runOperationFlow = vi.fn();
vi.mock('../../flows', () => ({
getFlowManager: vi.fn().mockReturnValue({
runOperationFlow,
}),
}));
import config from './index';
const testFlowId = '00000000-0000-0000-0000-000000000000';
afterEach(() => {
vi.clearAllMocks();
});
test('runs the target flow one time for payload', async () => {
const payload = { test: 'payload' };
await config.handler({ flow: testFlowId, payload }, {} as any);
expect(runOperationFlow).toHaveBeenCalledOnce();
});
test('runs the target flow N times for number of items in payload array', async () => {
const payload = [1, 2, 3, 4, 5];
await config.handler({ flow: testFlowId, payload }, {} as any);
expect(runOperationFlow).toHaveBeenCalledTimes(payload.length);
});
test.each([
{ payload: null, expected: null },
{ payload: { test: 'test' }, expected: { test: 'test' } },
{ payload: '{ "test": "test" }', expected: { test: 'test' } },
])('payload $payload should be sent as $expected', async ({ payload, expected }) => {
await config.handler({ flow: testFlowId, payload }, {} as any);
expect(runOperationFlow).toHaveBeenCalledWith(testFlowId, expected, expect.anything());
});

View File

@@ -1,6 +1,18 @@
import { afterEach, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest';
import { ItemsService, NotificationsService } from '.';
vi.mock('../env', async () => {
const actual = (await vi.importActual('../env')) as { default: Record<string, any> };
const MOCK_ENV = {
...actual.default,
PUBLIC_URL: '/',
};
return {
default: MOCK_ENV,
getEnv: () => MOCK_ENV,
};
});
vi.mock('../../src/database/index', () => ({
default: vi.fn(),
getDatabaseClient: vi.fn().mockReturnValue('postgres'),