Skip to content

Commit

Permalink
Extend app.function to register action + view handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
srajiang committed Aug 26, 2022
1 parent d7ac7c4 commit 265a89f
Show file tree
Hide file tree
Showing 16 changed files with 1,077 additions and 165 deletions.
103 changes: 103 additions & 0 deletions src/App-slack-function.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'mocha';
import sinon from 'sinon';
import { assert } from 'chai';
import rewiremock from 'rewiremock';
import { Override, mergeOverrides } from './test-helpers';
import {
Receiver,
ReceiverEvent,
} from './types';
import App from './App';
import importSlackFunctionModule from './SlackFunction.spec';

// Fakes
class FakeReceiver implements Receiver {
private bolt: App | undefined;

public init = (bolt: App) => {
this.bolt = bolt;
};

public start = sinon.fake((...params: any[]): Promise<unknown> => Promise.resolve([...params]));

public stop = sinon.fake((...params: any[]): Promise<unknown> => Promise.resolve([...params]));

public async sendEvent(event: ReceiverEvent): Promise<void> {
return this.bolt?.processEvent(event);
}
}

describe('App SlackFunction middleware', () => {
let fakeReceiver: FakeReceiver;
let dummyAuthorizationResult: { botToken: string; botId: string };

beforeEach(() => {
fakeReceiver = new FakeReceiver();
dummyAuthorizationResult = { botToken: '', botId: '' };
});

let app: App;

beforeEach(async () => {
const MockAppNoOverrides = await importApp();
app = new MockAppNoOverrides({
receiver: fakeReceiver,
authorize: sinon.fake.resolves(dummyAuthorizationResult),
});
});

it('should add a middleware for each SlackFunction passed to app.function', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const slackFn = new SlackFunction(mockFunctionCallbackId, () => new Promise((resolve) => resolve()));

/* middleware is a private property on App. Since app.function relies on app.use,
and app.use is fully tested above, we're opting just to ensure that the step listener
is added to the global middleware array, rather than repeating the same tests. */
const { middleware } = (app as any);

assert.equal(middleware.length, 2);

app.function(slackFn);

assert.equal(middleware.length, 3);
});
});

/* Testing Harness */

// Loading the system under test using overrides
async function importApp(
overrides: Override = mergeOverrides(
withNoopAppMetadata(),
withNoopWebClient(),
),
): Promise<typeof import('./App').default> {
return (await rewiremock.module(() => import('./App'), overrides)).default;
}

// Composable overrides
function withNoopWebClient(): Override {
return {
'@slack/web-api': {
WebClient: class {},
},
};
}

function withNoopAppMetadata(): Override {
return {
'@slack/web-api': {
addAppMetadata: sinon.fake(),
},
};
}

export default function withMockValidManifestUtil(functionCallbackId: string): Override {
const mockManifestOutput = JSON.parse(`{"functions": {"${functionCallbackId}": {}}}`);
return {
'./cli/hook-utils/manifest': {
getManifestData: () => mockManifestOutput,
},
};
}
82 changes: 45 additions & 37 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import processMiddleware from './middleware/process';
import { ConversationStore, conversationContext, MemoryStore } from './conversation-store';
import { WorkflowStep } from './WorkflowStep';
import { Subscription, SubscriptionOptions } from './Subscription';
import { SlackFunction } from './SlackFunction';
import {
CompleteFunction,
SlackFunction,
} from './SlackFunction';
import {
Middleware,
AnyMiddlewareArgs,
Expand Down Expand Up @@ -540,17 +543,17 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>

/**
* Register WorkflowStep middleware
*
*
* Not to be confused with next-gen platform Workflows + Functions
*
* @param workflowStep global workflow step middleware function
*/
public step(workflowStep: WorkflowStep): this {
public step(workflowStep: WorkflowStep): this {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Process a subscription event
*
Expand Down Expand Up @@ -617,27 +620,22 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

/**
* Register a Slack Function handler
*
* and other function-scoped
* interactivity handlers
* (block_actions, view interaction payloads)
*
*
* Register a Slack Function
*
* @param callbackId the id of the function as defined in manifest
*
* @param fn a function to register
*
* @param slackFn a main function to register
*
* */
public function(callbackId: string, fn: Middleware<SlackEventMiddlewareArgs>): this {
const slackFn = new SlackFunction(callbackId, fn);

public function(slackFn: SlackFunction): void {
const m = slackFn.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Process a message event
*
* Process a message event
*
* @param listeners Middlewares that process and react to a message event
*/
public message<
Expand Down Expand Up @@ -713,7 +711,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
] as Middleware<AnyMiddlewareArgs>[]);
}

/**
/**
* Process a shortcut event
*/
public shortcut<
Expand Down Expand Up @@ -761,10 +759,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
] as Middleware<AnyMiddlewareArgs>[]);
}

/**
/**
* Process a block_action event
* https://api.slack.com/reference/interaction-payloads/block-actions
*
*
*/
// NOTE: this is what's called a convenience generic, so that types flow more easily without casting.
// https://web.archive.org/web/20210629110615/https://basarat.gitbook.io/typescript/type-system/generics#motivation-and-samples
Expand Down Expand Up @@ -989,8 +987,15 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}
}

// set bot access token if it exists
let botAccessToken = body.bot_access_token ?? body.event.bot_access_token;
/**
* Set the Bot Access Token if it exists in event payload
* Sometimes the bot_access_token is located in the event
* Sometimes it is located directly in the body.
*/
let botAccessToken = body.bot_access_token;
if (botAccessToken === undefined && 'event' in body) {
botAccessToken = body.event.bot_access_token;
}

// Declare the event context
const context: Context = {
Expand Down Expand Up @@ -1049,17 +1054,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
break;
}

// NOTE: the following doesn't work because... distributive?
// const listenerArgs: Partial<AnyMiddlewareArgs> = {
const listenerArgs: Pick<AnyMiddlewareArgs, 'body' | 'payload'> & {
/** Say function might be set below */
say?: SayFn;
/** Respond function might be set below */
respond?: RespondFn;
/** Ack function might be set below */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ack?: AckFn<any>;
} = {
const listenerArgs: ListenerArgs = {
body: bodyArg,
payload,
};
Expand Down Expand Up @@ -1110,7 +1105,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
await ack();
}

// Get the client arg
// Get or create the client
let { client } = this;
const token = selectToken(context);

Expand Down Expand Up @@ -1616,11 +1611,11 @@ function isBlockActionOrInteractiveMessageBody(
return (body as SlackActionMiddlewareArgs<BlockAction | InteractiveMessage>['body']).actions !== undefined;
}

/**
/**
* Returns a bot token, bot access token or user token for client, say()
* */
function selectToken(context: Context): string | undefined {
return context.botAccessToken ?? context.botToken ?? context.userToken;
return context.botAccessToken ?? context.botToken ?? context.userToken;
}

function buildRespondFn(
Expand All @@ -1644,3 +1639,16 @@ function isEventTypeToSkipAuthorize(eventType: string) {
// Instrumentation
// Don't change the position of the following code
addAppMetadata({ name: packageJson.name, version: packageJson.version });

// types
export type ListenerArgs = Pick<AnyMiddlewareArgs, 'body' | 'payload'> & {
/** Say function might be set below */
say?: SayFn;
/** Respond function might be set below */
respond?: RespondFn;
/** Ack function might be set below */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ack?: AckFn<any>;
/* Complete might be set below */
complete?: CompleteFunction;
};
2 changes: 2 additions & 0 deletions src/Manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Schema,
ManifestSchema,
DefineOAuth2Provider,
DefineDatastore,
} from '@slack/deno-slack-sdk';

export const Manifest = (definition: SlackManifestType): ManifestSchema => {
Expand All @@ -22,6 +23,7 @@ export {
Schema,
SlackManifest,
DefineType,
DefineDatastore,
};

export type {
Expand Down

0 comments on commit 265a89f

Please sign in to comment.