@getanthill/datastore
Version:
Event-Sourced Datastore
398 lines (341 loc) • 9.7 kB
text/typescript
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,
);
});
});
});