diff --git a/src/Mercury.test.ts b/src/Mercury.test.ts index 6f29918..267258a 100644 --- a/src/Mercury.test.ts +++ b/src/Mercury.test.ts @@ -3,6 +3,7 @@ import BaseTest, { test, assert } from '@sprucelabs/test' import MercuryMock from './MercuryMock' import { IMercuryEventContract } from './types/mercuryEvents' import { MercurySubscriptionScope } from './types/subscriptions' +// import { MercurySubscriptionScope } from './types/subscriptions' export const SpruceEvents = { core: { @@ -59,13 +60,15 @@ export default class MercuryTest extends BaseTest { // console.log(body) // } // ) - this.mercury.setEmitResponse({ + this.mercury.setMockResponse({ eventName: SpruceEvents.core.DidEnter, payload: { somethingElse: 'blah' } }) + let numCallbacks = 0 + this.mercury.on( { // eventName: 'did-enter', @@ -75,17 +78,20 @@ export default class MercuryTest extends BaseTest { }, options => { console.log(options.payload.somethingElse) + numCallbacks += 1 } ) const result = await this.mercury.emit({ - eventName: 'did-leave', + eventName: 'did-enter', payload: { - somethingElse: '' + userId: 'asdf', + enteredAt: 'asdf' } }) - assert.isOk(result.responses[0].payload.userId) + assert.isOk(result.responses[0].payload.somethingElse) + assert.equal(numCallbacks, 1) // mercury.emit(SpruceEvents.core.didEnter) diff --git a/src/Mercury.ts b/src/Mercury.ts index 6a9c95f..96239b3 100644 --- a/src/Mercury.ts +++ b/src/Mercury.ts @@ -139,7 +139,6 @@ export default class Mercury { options: IMercuryEmitOptions, handler?: OnHandler ): Promise<{ - // TODO: Better typing on this response responses: IOnData[] }> { await this.awaitConnection() diff --git a/src/MercuryMock.ts b/src/MercuryMock.ts new file mode 100644 index 0000000..b34cd97 --- /dev/null +++ b/src/MercuryMock.ts @@ -0,0 +1,194 @@ +import { faker } from '@sprucelabs/test' +import MercuryAdapterMock from './adapters/MercuryAdapterMock' +import log from './lib/log' +import Mercury, { MercuryAdapterKind } from './Mercury' +import { + IMercuryEventContract, + IOnData, + ISkillOnData, + IMercuryEmitOptions, + OnHandler +} from './types/mercuryEvents' +import { MercurySubscriptionScope } from './types/subscriptions' + +interface IMockEmitResponses { + [eventName: string]: { + [skillId: string]: Omit, 'eventId'> + } +} + +export default class MercuryMock< + EventContract extends IMercuryEventContract +> extends Mercury { + private mockEmitResponses: IMockEmitResponses = {} + + /** Emit an event and set handler for responses */ + public async emit< + EventName extends keyof EventContract, + EventSpace extends EventContract[EventName] + >( + options: IMercuryEmitOptions, + handler?: OnHandler + ): Promise<{ + responses: IOnData[] + }> { + const { eventName } = options + + await this.awaitConnection() + + if (!this.adapter) { + log.warn('Mercury: Unable to emit because adapter is not set.') + // @ts-ignore + return + } + + const eventId = options.eventId ?? this.uuid() + if (!this.eventHandlers[eventId]) { + this.eventHandlers[eventId] = { + onFinished: [], + onError: [], + onResponse: [] + } + } + if (handler) { + this.eventHandlers[eventId].onResponse = [handler] + } + this.adapter.emit({ + ...options, + eventId, + credentials: this.credentials + }) + + process.nextTick(async () => { + try { + const responses = this.getMockResponsesForEvent({ + eventName, + eventId: eventId ?? this.uuid() + }) + + if (responses.length > 0) { + const promises = responses.map(r => { + return this.handleEvent({ + ...r, + // TODO: set / control subscription scope + scope: MercurySubscriptionScope.Location, + eventId, + responses + }) + }) + + // Execute individual event handlers for each + await Promise.all(promises) + + // Execute the final callback + await this.handleEvent({ + eventName, + // TODO: set / control subscription scope + scope: MercurySubscriptionScope.Location, + eventId, + organizationId: responses[0].organizationId, + locationId: responses[0].locationId, + userId: responses[0].userId, + payload: responses[0].payload, + responses + }) + } + } catch (e) { + log.warn(e) + } + }) + + const finishedHandler = this.emitOnFinishedCallback(eventId) + + return finishedHandler + } + + /** Sets the response to an emit */ + public setMockResponse< + EventName extends keyof EventContract, + ResponsePayload extends EventContract[EventName]['responsePayload'] + >(options: { + eventName: EventName + payload: ResponsePayload + skill?: ISkillOnData + }) { + const { eventName, payload } = options + const eventNameStr = eventName as string + + if (!this.mockEmitResponses[eventNameStr]) { + this.mockEmitResponses[eventNameStr] = {} + } + + const skillData = options.skill ?? { + id: this.uuid(), + name: faker.name.title(), + slug: faker.name.title() + } + + this.mockEmitResponses[eventNameStr][skillData.id] = { + eventName, + skill: skillData, + payload + } + } + + /** Clear the mock data for an event */ + public clearMockResponse< + EventName extends keyof EventContract, + ResponsePayload extends EventContract[EventName]['responsePayload'] + >(options: { eventName?: EventName }) { + const { eventName } = options + const eventNameStr = eventName as string + delete this.mockEmitResponses[eventNameStr] + } + + /** Clear all mock data */ + public clearMocks() { + this.mockEmitResponses = {} + } + + protected setAdapter(options: { + adapter: MercuryAdapterKind + connectionOptions: Record + }): boolean { + const { connectionOptions } = options + log.debug('setAdapter', { options }) + + if (this.adapter) { + this.adapter.disconnect() + this.adapter = undefined + } + + this.adapter = new MercuryAdapterMock() + this.adapter.init( + connectionOptions, + this.handleEvent.bind(this), + this.handleError.bind(this), + this.onConnect.bind(this), + this.onDisconnect.bind(this) + ) + + return true + } + + private getMockResponsesForEvent< + EventName extends keyof EventContract, + EventSpace extends EventContract[EventName] + >(options: { + eventName: EventName + eventId: string + }): IOnData[] { + const { eventId, eventName } = options + let responses: IOnData[] = [] + + const mockResponses = this.mockEmitResponses[eventName as string] + if (mockResponses) { + responses = Object.values(mockResponses).map(r => ({ + ...r, + eventId + })) + } + + return responses + } +} diff --git a/src/types/mercuryEvents.ts b/src/types/mercuryEvents.ts index 2f0d1bb..dcf2ac4 100644 --- a/src/types/mercuryEvents.ts +++ b/src/types/mercuryEvents.ts @@ -81,6 +81,15 @@ export interface IOnData< /** The event name that is being triggered */ eventName: EventName + /** The organization id where the event is triggered */ + organizationId?: string | null + + /** The location id where the event is triggered. If passed, organizationId should also be set. */ + locationId?: string | null + + /** The user id who is triggering this event */ + userId?: string | null + /** The unique id for this event */ eventId: string diff --git a/tests/BaseMercuryTest.ts b/tests/BaseMercuryTest.ts new file mode 100644 index 0000000..802feb1 --- /dev/null +++ b/tests/BaseMercuryTest.ts @@ -0,0 +1,3 @@ +import BaseTest from '@sprucelabs/test' + +export default class BaseMercuryTest extends BaseTest {}