diff --git a/src/App.ts b/src/App.ts index 4839c9a49..30fb01a26 100644 --- a/src/App.ts +++ b/src/App.ts @@ -184,7 +184,7 @@ export interface ShortcutConstraints { type?: S['type']; callback_id?: string | RegExp; } - +// TODO: add a type parameter here, just like the above constraint interfaces have. export interface ViewConstraints { callback_id?: string | RegExp; type?: 'view_closed' | 'view_submission'; diff --git a/test/unit/App-routes.spec.ts b/test/unit/App-routes.spec.ts index 158e5a1e8..de3d3a1ba 100644 --- a/test/unit/App-routes.spec.ts +++ b/test/unit/App-routes.spec.ts @@ -470,7 +470,6 @@ describe('App event routing', () => { it('should acknowledge any of possible events', async () => { // Arrange - const viewFn = sinon.fake.resolves({}); const optionsFn = sinon.fake.resolves({}); overrides = buildOverrides([withNoopWebClient()]); const MockApp = await importApp(overrides); @@ -484,12 +483,6 @@ describe('App event routing', () => { authorize: sinon.fake.resolves(dummyAuthorizationResult), }); - app.view('view_callback_id', async () => { - await viewFn(); - }); - app.view({ callback_id: 'view_callback_id', type: 'view_closed' }, async () => { - await viewFn(); - }); app.options('external_select_action_id', async () => { await optionsFn(); }); @@ -518,22 +511,10 @@ describe('App event routing', () => { app.message('hello', noop); app.command('/echo', noop); app.command(/\/e.*/, noop); - - assert.isTrue(fakeLogger.error.called); - fakeLogger.error.reset(); - - assert.isTrue(fakeLogger.error.called); - - app.error(fakeErrorHandler); await Promise.all(dummyReceiverEvents.map((event) => fakeReceiver.sendEvent(event))); // Assert - assert.equal(actionFn.callCount, 3); - assert.equal(shortcutFn.callCount, 4); - assert.equal(viewFn.callCount, 5); assert.equal(optionsFn.callCount, 4); - assert.equal(ackFn.callCount, dummyReceiverEvents.length); - assert(fakeErrorHandler.notCalled); }); // This test confirms authorize is being used for org events diff --git a/test/unit/App/middleware.spec.ts b/test/unit/App/middleware.spec.ts index 3da1338ca..a659c093b 100644 --- a/test/unit/App/middleware.spec.ts +++ b/test/unit/App/middleware.spec.ts @@ -567,6 +567,11 @@ describe('App middleware processing', () => { app.error(fakeErrorHandler); await fakeReceiver.sendEvent( createDummyViewSubmissionMiddlewareArgs( + { + id: 'V111', + type: 'modal', + callback_id: 'view-id', + }, { response_urls: [ { @@ -577,11 +582,6 @@ describe('App middleware processing', () => { }, ], }, - { - id: 'V111', - type: 'modal', - callback_id: 'view-id', - }, ), ); @@ -1069,13 +1069,10 @@ describe('App middleware processing', () => { app.error(fakeErrorHandler); await fakeReceiver.sendEvent( - createDummyViewSubmissionMiddlewareArgs( - {}, - { - callback_id, - app_installed_team_id, - }, - ), + createDummyViewSubmissionMiddlewareArgs({ + callback_id, + app_installed_team_id, + }), ); assert.isTrue(ackCalled); diff --git a/test/unit/App/routing-view.spec.ts b/test/unit/App/routing-view.spec.ts new file mode 100644 index 000000000..0a0e2627e --- /dev/null +++ b/test/unit/App/routing-view.spec.ts @@ -0,0 +1,101 @@ +import sinon, { type SinonSpy } from 'sinon'; +import { + FakeReceiver, + type Override, + createFakeLogger, + createDummyViewSubmissionMiddlewareArgs, + createDummyViewClosedMiddlewareArgs, + importApp, + mergeOverrides, + noopMiddleware, + withConversationContext, + withMemoryStore, + withNoopAppMetadata, + withNoopWebClient, +} from '../helpers'; +import type App from '../../../src/App'; + +function buildOverrides(secondOverrides: Override[]): Override { + return mergeOverrides( + withNoopAppMetadata(), + withNoopWebClient(), + ...secondOverrides, + withMemoryStore(sinon.fake()), + withConversationContext(sinon.fake.returns(noopMiddleware)), + ); +} + +describe('App view() routing', () => { + let fakeReceiver: FakeReceiver; + let fakeHandler: SinonSpy; + const fakeLogger = createFakeLogger(); + let dummyAuthorizationResult: { botToken: string; botId: string }; + let MockApp: Awaited>; + let app: App; + + beforeEach(async () => { + fakeLogger.error.reset(); + fakeReceiver = new FakeReceiver(); + fakeHandler = sinon.fake(); + dummyAuthorizationResult = { botToken: '', botId: '' }; + MockApp = await importApp(buildOverrides([])); + app = new MockApp({ + logger: fakeLogger, + receiver: fakeReceiver, + authorize: sinon.fake.resolves(dummyAuthorizationResult), + }); + }); + + it('should throw if provided a constraint with unknown view constraint keys', async () => { + // @ts-ignore providing known invalid view constraint parameter + app.view({ id: 'boom' }, fakeHandler); + sinon.assert.calledWithMatch(fakeLogger.error, 'unknown constraint keys'); + }); + describe('for view submission events', () => { + it('should route a view submission event to a handler registered with `view(string)` that matches the callback ID', async () => { + app.view('my_id', fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewSubmissionMiddlewareArgs({ callback_id: 'my_id' }), + }); + sinon.assert.called(fakeHandler); + }); + it('should route a view submission event to a handler registered with `view(RegExp)` that matches the callback ID', async () => { + app.view(/my_action/, fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewSubmissionMiddlewareArgs({ callback_id: 'my_action' }), + }); + sinon.assert.called(fakeHandler); + }); + it('should route a view submission event to a handler registered with `view({callback_id})` that matches callback ID', async () => { + app.view({ callback_id: 'my_id' }, fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewSubmissionMiddlewareArgs({ callback_id: 'my_id' }), + }); + sinon.assert.called(fakeHandler); + }); + it('should route a view submission event to a handler registered with `view({type:view_submission})`', async () => { + app.view({ type: 'view_submission' }, fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewSubmissionMiddlewareArgs(), + }); + sinon.assert.called(fakeHandler); + }); + }); + + describe('for view closed events', () => { + it('should route a view closed event to a handler registered with `view({callback_id, type:view_closed})` that matches callback ID', async () => { + app.view({ callback_id: 'my_id', type: 'view_closed' }, fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewClosedMiddlewareArgs({ callback_id: 'my_id', type: 'view_closed' }), + }); + sinon.assert.called(fakeHandler); + }); + it('should route a view closed event to a handler registered with `view({type:view_closed})`', async () => { + app.view({ type: 'view_closed' }, fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyViewClosedMiddlewareArgs(), + }); + sinon.assert.called(fakeHandler); + }); + }); +}); diff --git a/test/unit/helpers/events.ts b/test/unit/helpers/events.ts index f2c1fe194..a09311fff 100644 --- a/test/unit/helpers/events.ts +++ b/test/unit/helpers/events.ts @@ -14,6 +14,7 @@ import type { SlackEventMiddlewareArgs, SlackShortcutMiddlewareArgs, SlackViewMiddlewareArgs, + ViewClosedAction, ViewSubmitAction, ViewOutput, } from '../../../src/types'; @@ -110,13 +111,8 @@ export function createDummyBlockActionEventMiddlewareArgs( }; } -export function createDummyViewSubmissionMiddlewareArgs( - // biome-ignore lint/suspicious/noExplicitAny: allow mocking tools to provide any override - bodyOverrides?: Record, - viewOverrides?: Partial, - viewEvent?: ViewSubmitAction, -): SlackViewMiddlewareArgs { - const payload: ViewOutput = { +function createDummyViewOutput(viewOverrides?: Partial): ViewOutput { + return { type: 'view', id: 'V1234', callback_id: 'Cb1234', @@ -136,12 +132,18 @@ export function createDummyViewSubmissionMiddlewareArgs( notify_on_close: false, ...viewOverrides, }; +} + +export function createDummyViewSubmissionMiddlewareArgs( + viewOverrides?: Partial, + // biome-ignore lint/suspicious/noExplicitAny: allow mocking tools to provide any override + bodyOverrides?: Record, +): SlackViewMiddlewareArgs { + const payload = createDummyViewOutput(viewOverrides); const event: ViewSubmitAction = { - ...(viewEvent || { - type: 'view_submission', - team: { id: team, domain: 'slack.com' }, - user: { id: user, name: 'filmaj' }, - }), + type: 'view_submission', + team: { id: team, domain: 'slack.com' }, + user: { id: user, name: 'filmaj' }, view: payload, api_app_id: app_id, token, @@ -157,6 +159,31 @@ export function createDummyViewSubmissionMiddlewareArgs( }; } +export function createDummyViewClosedMiddlewareArgs( + viewOverrides?: Partial, + // biome-ignore lint/suspicious/noExplicitAny: allow mocking tools to provide any override + bodyOverrides?: Record, +): SlackViewMiddlewareArgs { + const payload = createDummyViewOutput(viewOverrides); + const event: ViewClosedAction = { + type: 'view_closed', + team: { id: team, domain: 'slack.com' }, + user: { id: user, name: 'filmaj' }, + view: payload, + api_app_id: app_id, + token, + is_cleared: false, + ...bodyOverrides, + }; + return { + payload, + view: payload, + body: event, + respond, + ack: () => Promise.resolve(), + }; +} + export function createDummyMessageShortcutMiddlewareArgs( callback_id = 'Cb1234', shortcut?: MessageShortcut,