UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

398 lines (341 loc) 9.7 kB
import type { Services } from '../../typings'; import PostgreSQLClient from '../../services/pg'; import config from '../../config'; import { build } from '../../services'; import * as handlers from '.'; describe('src/data', () => { let services: Services; let handlerConfig; beforeEach(() => { services = build({ ...config, datastores: [ { name: 'datastore', config: {}, }, ], }); services.telemetry = { // @ts-ignore logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, }; jest .spyOn(services.datastores.get('datastore')!, 'getModels') .mockImplementation(() => ({ data: { profiles: {}, }, })); jest .spyOn(services.datastores.get('datastore')!, 'count') .mockImplementation(() => 1); }); afterEach(() => { handlerConfig && handlerConfig.stop(); jest.resetAllMocks(); }); describe('#syncPostgreSQL', () => { it('returns the services object when calling start', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/projections#syncPostgreSQL', 'ant://handlers'), services, ); const connectMock = jest .spyOn(services.pg, 'connect') .mockImplementation(() => null); // ACT const startServices = await handlerConfig.start(); // ASSERT expect(startServices).toMatchObject(services); }); it('builds a new services object when calling start with no services parameter', async () => { let error; try { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/projections#syncPostgreSQL', 'ant://handlers'), ); // ACT const startServices = await handlerConfig.start(); // ASSERT expect(startServices).not.toMatchObject(services); } catch (err) { error = err; } expect(error.message).toEqual('Unknown datastore'); }); it('logs info about the ending of the process', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/projections#syncPostgreSQL', 'ant://handlers'), services, ); // ACT await handlerConfig.stop(); // ASSERT expect(services.telemetry?.logger.info.mock.calls[1][0]).toEqual( '[projections#syncPostgreSQL] Ending', ); }); it('returns triggers aligned with available models', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/projections#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'profiles', query: {}, source: 'events', }, ]); }); it('allows to define the datastore to sync', async () => { // ARRANGE services = build({ ...config, datastores: [ { name: 'source', config: {}, }, ], }); jest .spyOn(services.datastores.get('source')!, 'getModels') .mockImplementation(() => ({ data: { profiles: {}, }, })); jest .spyOn(services.datastores.get('source')!, 'count') .mockImplementation(() => 1); handlerConfig = await handlers.syncPostgreSQL( new URL('/data?datastore=source#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'source', model: 'profiles', query: {}, source: 'events', }, ]); }); it('filters models not present in the `only` parameter if present', async () => { // ARRANGE jest .spyOn(services.datastores.get('datastore')!, 'getModels') .mockImplementation(() => ({ data: { modelA: {}, modelB: {}, modelC: {}, }, })); handlerConfig = await handlers.syncPostgreSQL( new URL('/data?only=modelA,modelC#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'modelA', query: {}, source: 'events', }, { datastore: 'datastore', model: 'modelC', query: {}, source: 'events', }, ]); }); it('excludes models present in the `skip` parameter if present', async () => { // ARRANGE jest .spyOn(services.datastores.get('datastore')!, 'getModels') .mockImplementation(() => ({ data: { modelA: {}, modelB: {}, modelC: {}, }, })); handlerConfig = await handlers.syncPostgreSQL( new URL('/data?skip=modelB#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'modelA', query: {}, source: 'events', }, { datastore: 'datastore', model: 'modelC', query: {}, source: 'events', }, ]); }); it('allows to define a custom query', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?query={"version":1}#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'profiles', query: { version: 1, }, source: 'events', }, ]); }); it('allows to define the entities source', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?source=entities#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'profiles', query: {}, source: 'entities', }, ]); }); it('returns triggers aligned with defined URL for entities', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?source=entities#syncPostgreSQL', 'ant://handlers'), services, ); // ASSERT expect(handlerConfig.triggers).toEqual([ { datastore: 'datastore', model: 'profiles', query: {}, source: 'entities', }, ]); }); it('initializes database schema if `init=1`', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?init=1#syncPostgreSQL', 'ant://handlers'), services, ); const initMock = jest .spyOn(PostgreSQLClient, 'init') .mockImplementation(() => null); const connectMock = jest .spyOn(services.pg, 'connect') .mockImplementation(() => null); const queryAllMock = jest .spyOn(services.pg, 'queryAll') .mockImplementation(() => null); // ASSERT await handlerConfig.start(); expect(initMock).toHaveBeenCalledTimes(1); expect(queryAllMock).toHaveBeenCalledTimes(1); }); it('disconnects PostgreSQL client on stop', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?init=1#syncPostgreSQL', 'ant://handlers'), services, ); const disconnectMock = jest .spyOn(services.pg, 'disconnect') .mockImplementation(() => null); // ASSERT await handlerConfig.stop(); expect(disconnectMock).toHaveBeenCalledTimes(1); }); it('inserts received data into PostgreSQL', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?init=1#syncPostgreSQL', 'ant://handlers'), services, ); const insertMock = jest .spyOn(services.pg, 'insert') .mockImplementation(() => null); // ASSERT await handlerConfig.handler( { profile_id: 'profile_id', }, { datastore: 'datastore', model: 'profiles', source: 'events', }, ); expect(insertMock).toHaveBeenCalledWith( {}, 'events', { profile_id: 'profile_id', }, { with_encrypted_data: false }, ); }); it('logs an error in case of insertion failure', async () => { // ARRANGE handlerConfig = await handlers.syncPostgreSQL( new URL('/data?init=1#syncPostgreSQL', 'ant://handlers'), services, ); const error = new Error('Ooops'); const insertMock = jest .spyOn(services.pg, 'insert') .mockImplementation(() => { throw error; }); // ASSERT await handlerConfig.handler( { profile_id: 'profile_id', }, { datastore: 'datastore', model: 'profiles', source: 'events', }, ); expect(services.telemetry.logger.error).toHaveBeenCalledWith( '[projections#syncPostgreSQL] Error', error, ); }); }); });