UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

1,284 lines (984 loc) 33.5 kB
import type { Services } from '../../typings'; import { MongoDbConnector } from '@getanthill/mongodb-connector'; import setup from '../../../test/setup'; import { stream, serverSentEvents, writeJSON } from './controllers'; import fixtureUsers from '../../../test/fixtures/users'; import { EventEmitter } from 'events'; describe('controllers/stream', () => { let app; let services: Services; let mongodb: MongoDbConnector; let models; class Req extends EventEmitter { query: {}; params: {}; body: []; headers: {}; constructor({ query, params, body, headers }) { super(); this.query = query; this.params = params; this.body = body; this.headers = headers; } header(h) { return this.headers[h]; } } class Res extends EventEmitter {} function mockPartsToObject(fn) { if (fn.mock.calls[fn.mock.calls.length - 1][0] !== ']') { fn.mock.calls.push([']']); } return JSON.parse(fn.mock.calls.map((call) => call[0]).join('')); } function mockPartsToSSEObject(fn) { return JSON.parse( '[' + fn.mock.calls .map((call) => call[0]) .filter((m) => m !== ':\n\n' && m !== 'data: ' && m !== '\n\n') .join(',') + ']', ); } function getWaiters(res, count: number, timeout = 30_000) { let _resolve0; const startsPromise = new Promise((resolve) => (_resolve0 = resolve)); let _resolve; const eventsPromise = new Promise((resolve) => (_resolve = resolve)); let _resolve2; const endPromise = new Promise((resolve2) => (_resolve2 = resolve2)); let _count = 0; res.write.mockImplementation((a) => { if (a === '[') { _resolve0(); return; } if (a === ']' || a === 'data: ' || a === '\n\n') { return; } _count += 1; if (_count === count) { _resolve(); } }); res.end.mockImplementation(_resolve2); return { starts: startsPromise, events: eventsPromise, end: endPromise, }; } beforeAll(async () => { app = await setup.build(); services = app.services; mongodb = services.mongodb; }); beforeEach(async () => { services.config.features.api.stream.maxWaitOnReconnectInMilliseconds = 30_000; models = await setup.initModels(services, [ fixtureUsers, { ...fixtureUsers, name: 'guests', }, ]); const Users = models.getModel(fixtureUsers.name); await Promise.all([ Users.getStatesCollection(Users.db(mongodb)).deleteMany({}), Users.getEventsCollection(Users.db(mongodb)).deleteMany({}), Users.getSnapshotsCollection(Users.db(mongodb)).deleteMany({}), ]); }); afterAll(async () => { await setup.teardownDb(mongodb); }); describe('#writeJSON', () => { it('skips writing if no data is provided', () => { const res = { write: jest.fn(), }; // @ts-ignore writeJSON({}, res, null); expect(res.write).toHaveBeenCalledTimes(0); }); it('writes the data in JSON if available', () => { const res = { write: jest.fn(), }; // @ts-ignore writeJSON({}, res, { a: 1 }); expect(res.write).toHaveBeenCalledWith(JSON.stringify({ a: 1 })); }); }); describe('#stream', () => { let error; let req; let res; let next; let waiters; beforeEach(() => { error = null; next = jest.fn().mockImplementation((err) => (error = err)); req = new Req({ query: {}, params: {}, body: [], headers: {}, }); res = new Res(); res.writeHead = jest.fn(); res.write = jest.fn(); res.end = jest.fn(); waiters = getWaiters(res, 1); }); afterEach(() => { jest.restoreAllMocks(); }); it('opens a stream and returns the inserted element if created', async () => { const controller = stream({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(res.write.mock.calls[0][0]).toEqual('['); expect(res.write.mock.calls[1][0].startsWith('{')).toEqual(true); const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { firstname: 'Alice', }, ]); }); it('opens a stream with empty pipeline if no body provided', async () => { const controller = stream({ ...services, models }); delete req.body; req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(res.write.mock.calls[0][0]).toEqual('['); expect(res.write.mock.calls[1][0].startsWith('{')).toEqual(true); const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { firstname: 'Alice', }, ]); }); it('opens a stream and returns the inserted element as an entity', async () => { const controller = stream({ ...services, models }); req.params.model = 'users'; req.headers.output = 'entity'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(res.write.mock.calls[0][0]).toEqual('['); expect(res.write.mock.calls[1][0].startsWith('{')).toEqual(true); const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { firstname: 'Alice', }, ]); }); it('opens a stream and returns the raw inserted element change document', async () => { const controller = stream({ ...services, models }); req.params.model = 'users'; req.headers.output = 'raw'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(res.write.mock.calls[0][0]).toEqual('['); expect(res.write.mock.calls[1][0].startsWith('{')).toEqual(true); const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { fullDocument: { firstname: 'Alice', }, ns: { coll: 'users', }, }, ]); }); it('opens a stream and returns multiple inserted elements', async () => { waiters = getWaiters(res, 2); const controller = stream({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; res.emit('close'); await waiters.end; const response = mockPartsToObject(res.write); expect( response.sort((a, b) => a.firstname.localeCompare(b.firstname)), ).toMatchObject([ { firstname: 'Alice', }, { firstname: 'Bernard', }, ]); }); it('opens a stream and returns inserted and updated elements', async () => { waiters = getWaiters(res, 2); const controller = stream({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { firstname: 'Alice', version: 0, }, { firstname: 'Alizzz', version: 1, }, ]); }); it('opens a stream and returns events associated to a model', async () => { waiters = getWaiters(res, 2); const controller = stream({ ...services, models }); req.params.model = 'users'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { firstname: 'Alice', type: 'CREATED', version: 0, }, { firstname: 'Alizzz', type: 'UPDATED', version: 1, }, ]); }); it('opens a stream on all models', async () => { waiters = getWaiters(res, 2); const controller = stream({ ...services, models }); req.params.model = 'all'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', version: 0, }, }, { model: 'users', entity: { firstname: 'Alizzz', version: 1, }, }, ]); }); it('opens a stream on all models events', async () => { waiters = getWaiters(res, 2); const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; const response = mockPartsToObject(res.write); expect(response).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Alizzz', type: 'UPDATED', version: 1, }, }, ]); }); it('throws an exception in case of error', async () => { const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; const error = new Error('Ooops'); res.write = jest.fn().mockImplementation(() => { throw error; }); await controller(req, res, next); expect(next).toHaveBeenCalledWith(error); }); it('logs an error in case of invalid request', async () => { const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; req.query.pipeline = JSON.stringify([{ is: 'invalid' }]); await controller(req, res, next); expect(services.telemetry.logger.error.mock.calls[0][0]).toEqual( '[api/stream] Stream error', ); }); it('keeps the stream live even after a mongodb connection loss', async () => { waiters = getWaiters(res, 3); const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await services.mongodb.disconnect(); // Required because the reconnect of the stream failure occurs after the // connection heartbeat delay const dbConnectionHeartbeat = 1_000; await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 10)); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Bernard', type: 'CREATED', version: 0, }, }, ]); }); it('keeps the stream live even after a second mongodb connection loss', async () => { waiters = getWaiters(res, 3); const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await services.mongodb.disconnect(); // Required because the reconnect of the stream failure occurs after the // connection heartbeat delay const dbConnectionHeartbeat = 1_000; await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 100)); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); // Lost the connection a second time... await services.mongodb.disconnect(); await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 100)); const gerard = models.factory('users'); await gerard.create({ firstname: 'Gerard' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Bernard', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Gerard', type: 'CREATED', version: 0, }, }, ]); }); it('fails if the max reconnect delay is reached', async () => { waiters = getWaiters(res, 1); const controller = stream({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); services.config.features.api.stream.maxWaitOnReconnectInMilliseconds = 0; const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await services.mongodb.disconnect(); // Required because the reconnect of the stream failure occurs after the // connection heartbeat delay const dbConnectionHeartbeat = 1_000; await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 10)); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; // No need to wait because the connection closes on error await waiters.end; expect(mockPartsToObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, ]); }); }); describe('#stream/sse', () => { let error; let req; let res; let next; let waiters; beforeEach(() => { error = null; next = jest.fn().mockImplementation((err) => (error = err)); req = new Req({ query: { // pipeline: [], }, params: {}, body: [], headers: {}, }); res = new Res(); res.writeHead = jest.fn(); res.write = jest.fn(); res.end = jest.fn(); waiters = getWaiters(res, 2); }); afterEach(() => { jest.restoreAllMocks(); }); it('opens a stream and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ firstname: 'Alice', }); }); it('opens a stream without any query and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); const uuid = setup.uuid(); req.params.model = 'users'; req.query.q = JSON.stringify({}); await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: `Alice ${uuid}` }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ firstname: `Alice ${uuid}`, }); }); it('opens a stream with a specific query and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); const uuid = setup.uuid(); req.params.model = 'users'; req.query.q = JSON.stringify({ firstname: `Alice ${uuid}`, }); await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: `Alice ${uuid}` }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ firstname: `Alice ${uuid}`, }); }); it('opens a stream with a specific pipeline and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); const uuid = setup.uuid(); req.params.model = 'users'; req.body = [ { $match: { 'fullDocument.firstname': `Alice ${uuid}`, }, }, ]; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: `Alice ${uuid}` }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ firstname: `Alice ${uuid}`, }); }); it('opens a stream with a mix between query and pipeline and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); const uuid = setup.uuid(); req.params.model = 'users'; req.body = [ { $match: { 'fullDocument.is_enabled': false, }, }, ]; req.query.q = JSON.stringify({ firstname: `Alice ${uuid}`, }); await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: `Alice ${uuid}`, is_enabled: false }); const alice2 = models.factory('users'); await alice2.create({ firstname: `Alice ${uuid}`, is_enabled: true }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)).toMatchObject([ { firstname: `Alice ${uuid}`, is_enabled: false, }, ]); }); it('opens a stream with a mix between query and pipeline from query and returns the inserted element if created', async () => { const controller = serverSentEvents({ ...services, models }); const uuid = setup.uuid(); req.params.model = 'users'; req.query.pipeline = JSON.stringify([ { $match: { 'fullDocument.is_enabled': false, }, }, ]); req.query.q = JSON.stringify({ firstname: `Alice ${uuid}`, }); await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: `Alice ${uuid}`, is_enabled: false }); const alice2 = models.factory('users'); await alice2.create({ firstname: `Alice ${uuid}`, is_enabled: true }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)).toMatchObject([ { firstname: `Alice ${uuid}`, is_enabled: false, }, ]); }); it('opens a stream and returns the inserted element as an entity', async () => { const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; req.headers.output = 'entity'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ firstname: 'Alice', }); }); it('opens a stream and returns the raw inserted element change document', async () => { const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; req.headers.output = 'raw'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await waiters.events; res.emit('close'); await waiters.end; expect(res.end).toHaveBeenCalledTimes(1); expect(mockPartsToSSEObject(res.write)[0]).toMatchObject({ fullDocument: { firstname: 'Alice', }, ns: { coll: 'users', }, }); }); it('opens a stream and returns multiple inserted elements', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { firstname: 'Alice', }, { firstname: 'Bernard', }, ]); }); it('opens a stream and returns inserted and updated elements', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { firstname: 'Alice', version: 0, }, { firstname: 'Alizzz', version: 1, }, ]); }); it('opens a stream and returns events associated to a model', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'users'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { firstname: 'Alice', type: 'CREATED', version: 0, }, { firstname: 'Alizzz', type: 'UPDATED', version: 1, }, ]); }); it('opens a stream on all models', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', version: 0, }, }, { model: 'users', entity: { firstname: 'Alizzz', version: 1, }, }, ]); }); it('opens a stream on some specific models only', async () => { waiters = getWaiters(res, 2); const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.headers['only-models'] = 'guests'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); const guest = models.factory('guests'); await guest.create({ firstname: 'Guest' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { model: 'guests', entity: { firstname: 'Guest', version: 0, }, }, ]); }); it('opens a stream on all models events', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await alice.update({ firstname: 'Alizzz' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Alizzz', type: 'UPDATED', version: 1, }, }, ]); }); it('throws an exception in case of error', async () => { const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; const error = new Error('Ooops'); res.writeHead = jest.fn().mockImplementation(() => { throw error; }); await controller(req, res, next); expect(next).toHaveBeenCalledWith(error); }); it('logs an error in case of invalid request', async () => { const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; req.query.pipeline = JSON.stringify([{ is: 'invalid' }]); await controller(req, res, next); expect(services.telemetry.logger.error.mock.calls[0][0]).toEqual( '[api/stream] Stream error', ); }); it('keeps the stream live even after a mongodb connection loss', async () => { waiters = getWaiters(res, 3); const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await services.mongodb.disconnect(); // Required because the reconnect of the stream failure occurs after the // connection heartbeat delay const dbConnectionHeartbeat = 1_000; await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 10)); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; res.emit('close'); await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, { model: 'users', entity: { firstname: 'Bernard', type: 'CREATED', version: 0, }, }, ]); }); it('fails if the max reconnect delay is reached', async () => { waiters = getWaiters(res, 2); const controller = serverSentEvents({ ...services, models }); req.params.model = 'all'; req.params.source = 'events'; await controller(req, res, next); expect(next).toHaveBeenCalledTimes(0); services.config.features.api.stream.maxWaitOnReconnectInMilliseconds = 0; const alice = models.factory('users'); await alice.create({ firstname: 'Alice' }); await services.mongodb.disconnect(); // Required because the reconnect of the stream failure occurs after the // connection heartbeat delay const dbConnectionHeartbeat = 1_000; await new Promise((resolve) => setTimeout(resolve, dbConnectionHeartbeat), ); await services.mongodb.connect(); // Required because the change stream does not reconnect in a milliseconds await new Promise((resolve) => setTimeout(resolve, 10)); const bernard = models.factory('users'); await bernard.create({ firstname: 'Bernard' }); await waiters.events; // No need to wait because the connection closes on error await waiters.end; expect(mockPartsToSSEObject(res.write)).toMatchObject([ { model: 'users', entity: { firstname: 'Alice', type: 'CREATED', version: 0, }, }, ]); }); }); });