Skip to content

Commit

Permalink
Add in block_suggestion interactivity handler support (#1645)
Browse files Browse the repository at this point in the history
* Add in new Block Suggestion type
  • Loading branch information
hello-ashleyintech authored Nov 11, 2022
1 parent f83ac89 commit cd432d6
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 9 deletions.
7 changes: 7 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
WorkflowStepEdit,
SubscriptionInteraction,
FunctionExecutedEvent,
SlackBlockSuggestion,
} from './types';
import { IncomingEventType, getTypeAndConversation, assertNever } from './helpers';
import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors';
Expand Down Expand Up @@ -169,6 +170,12 @@ export interface ViewConstraints {
type?: 'view_closed' | 'view_submission';
}

export interface BlockSuggestionConstraints<B extends SlackBlockSuggestion = SlackBlockSuggestion> {
type?: B['type'];
block_id?: B extends SlackBlockSuggestion ? string | RegExp : never;
action_id?: B extends SlackBlockSuggestion ? string | RegExp : never;
}

// Passed internally to the handleError method
interface AllErrorHandlerArgs {
error: Error; // Error is not necessarily a CodedError
Expand Down
112 changes: 108 additions & 4 deletions src/SlackFunction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {

import { FunctionExecutionContext } from './types/functions';
import { SlackFunctionInitializationError } from './errors';
import { ActionConstraints, ViewConstraints } from './App';
import { ActionConstraints, BlockSuggestionConstraints, ViewConstraints } from './App';

export default async function importSlackFunctionModule(overrides: Override = {}): Promise<typeof import('./SlackFunction')> {
return rewiremock.module(() => import('./SlackFunction'), overrides);
Expand Down Expand Up @@ -162,8 +162,46 @@ describe('SlackFunction module', () => {
assert.doesNotThrow(shouldNotThrow);
});
});
describe('app.function.blockSuggestion() adds a handler to interactivity handlers', () => {
it('should not error when valid handler constraints supplied', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
const goodConstraints: BlockSuggestionConstraints = {
action_id: '',
};
const shouldNotThrow = () => testFunc.blockSuggestion(goodConstraints, async () => {});
assert.doesNotThrow(shouldNotThrow, SlackFunctionInitializationError);
});
it('should error when invalid handler constraints supplied', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
const badConstraints = {
bad_id: '',
action_id: '',
} as BlockSuggestionConstraints;
const shouldThrow = () => testFunc.blockSuggestion(badConstraints, async () => {});
assert.throws(shouldThrow, SlackFunctionInitializationError);
});
it('should return the instance of slackfunction', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
const goodConstraints: BlockSuggestionConstraints = {
action_id: '',
};
const mockHandler = async () => {};
// expect that the return value of blockSuggestion is a Slack function
assert.instanceOf(testFunc.blockSuggestion(goodConstraints, mockHandler), SlackFunction);
// chained valid handlers should not error
const shouldNotThrow = () => testFunc.blockSuggestion(goodConstraints, mockHandler)
.blockSuggestion(goodConstraints, mockHandler);
assert.doesNotThrow(shouldNotThrow, SlackFunctionInitializationError);
});
});
describe('runInteractivityHandlers', () => {
it('should execute all provided callbacks', async () => {
it('app.function.action() should execute all provided callbacks', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
Expand Down Expand Up @@ -196,7 +234,7 @@ describe('SlackFunction module', () => {
assert(spy.calledOnce);
assert(spy2.calledOnce);
});
it('should error if a promise rejects', async () => {
it('app.function.action() should error if a promise rejects', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
Expand All @@ -223,7 +261,73 @@ describe('SlackFunction module', () => {
client: {} as WebClient,
} as unknown as AnyMiddlewareArgs & AllMiddlewareArgs;

// ensure handlers are not
// ensure handler call rejects

const shouldReject = async () => testFunc.runInteractivityHandlers(fakeArgs);
assertNode.rejects(shouldReject);
});
it('app.function.blockSuggestion() should execute all provided callbacks', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
const goodConstraints: BlockSuggestionConstraints = {
action_id: 'my-action',
};
const mockHandler = async () => Promise.resolve();
const spy = sinon.spy(mockHandler);
const spy2 = sinon.spy(mockHandler);
// add blockSuggestion handlers
testFunc.blockSuggestion(goodConstraints, spy).blockSuggestion(goodConstraints, spy2);

// set up event args
const fakeArgs = {
next: () => {},
payload: {
action_id: 'my-action',
},
body: {
function_data: {
execution_id: 'asdasdas',
},
},
client: {} as WebClient,
} as unknown as AnyMiddlewareArgs & AllMiddlewareArgs;

// ensure handlers are both called

await testFunc.runInteractivityHandlers(fakeArgs);
assert(spy.calledOnce);
assert(spy2.calledOnce);
});
it('app.function.blockSuggestion() should error if a promise rejects', async () => {
const mockFunctionCallbackId = 'reverse_approval';
const { SlackFunction } = await importSlackFunctionModule(withMockValidManifestUtil(mockFunctionCallbackId));
const testFunc = new SlackFunction(mockFunctionCallbackId, async () => {});
const action_id = 'my-action';
const goodConstraints: BlockSuggestionConstraints = {
action_id,
};
const mockHandler = async () => Promise.reject();
const spy = sinon.spy(mockHandler);
// add a blockSuggestion handler
testFunc.blockSuggestion(goodConstraints, spy);

// set up event args
const fakeArgs = {
next: () => {},
payload: {
action_id,
},
body: {
function_data: {
execution_id: 'asdasdas',
},
},
client: {} as WebClient,
} as unknown as AnyMiddlewareArgs & AllMiddlewareArgs;

// ensure handler call rejects

const shouldReject = async () => testFunc.runInteractivityHandlers(fakeArgs);
assertNode.rejects(shouldReject);
});
Expand Down
50 changes: 46 additions & 4 deletions src/SlackFunction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
SlackEventMiddlewareArgs,
SlackViewAction,
SlackViewMiddlewareArgs,
SlackBlockSuggestion,
SlackBlockSuggestionsMiddlewareArgs,
} from './types';

import {
ActionConstraints,
ViewConstraints,
BlockSuggestionConstraints,
} from './App';

import {
Expand Down Expand Up @@ -45,17 +48,21 @@ export interface CompleteFunctionArgs {
export type AllSlackFunctionExecutedMiddlewareArgs =
SlackFunctionExecutedMiddlewareArgs &
SlackActionMiddlewareArgs &
SlackBlockSuggestionsMiddlewareArgs &
AllMiddlewareArgs;

interface FunctionInteractivityMiddleware {
constraints: FunctionInteractivityConstraints,
handler: Middleware<SlackActionMiddlewareArgs> | Middleware<SlackViewMiddlewareArgs>
handler: Middleware<SlackActionMiddlewareArgs> |
Middleware<SlackViewMiddlewareArgs> |
Middleware<SlackBlockSuggestionsMiddlewareArgs>;
}

type FunctionInteractivityConstraints = ActionConstraints | ViewConstraints;
type FunctionInteractivityConstraints = ActionConstraints | ViewConstraints | BlockSuggestionConstraints;
// an array of Action constraints keys as strings
type ActionConstraintsKeys = Extract<(keyof ActionConstraints), string>[];
type ViewConstraintsKeys = Extract<(keyof ViewConstraints), string>[];
type BlockSuggestionConstraintsKeys = Extract<(keyof BlockSuggestionConstraints), string>[];

interface SlackFnValidateResult { pass: boolean, msg?: string }
export interface ManifestDefinitionResult {
Expand Down Expand Up @@ -194,6 +201,39 @@ export class SlackFunction {
return this;
}

/**
* Attach a block_suggestion interactivity handler to your SlackFunction
*
* ```
* @param handler Provide a handler function
* @returns SlackFunction instance
*/
public blockSuggestion<
BlockSuggestion extends SlackBlockSuggestion = SlackBlockSuggestion,
Constraints extends BlockSuggestionConstraints<BlockSuggestion> = BlockSuggestionConstraints<BlockSuggestion>,
>(
actionIdOrConstraints: string | RegExp | Constraints,
handler: Middleware<SlackBlockSuggestionsMiddlewareArgs>,
): this {
// normalize constraints
const constraints: BlockSuggestionConstraints = (
typeof actionIdOrConstraints === 'string' ||
util.types.isRegExp(actionIdOrConstraints)
) ?
{ action_id: actionIdOrConstraints } :
actionIdOrConstraints;

// declare our valid constraints keys
const validConstraintsKeys: BlockSuggestionConstraintsKeys = ['action_id', 'block_id', 'type'];
// cast to string array for convenience
const validConstraintsKeysAsStrings = validConstraintsKeys as string[];

errorIfInvalidConstraintKeys(constraints, validConstraintsKeysAsStrings, handler);

this.interactivityHandlers.push({ constraints, handler });
return this;
}

/**
* Attach a view_submission or view_closed interactivity handler
* to your SlackFunction
Expand Down Expand Up @@ -387,7 +427,7 @@ export function isFunctionExecutedEvent(args: AnyMiddlewareArgs): boolean {

export function isFunctionInteractivityEvent(args: AnyMiddlewareArgs & AllMiddlewareArgs): boolean {
const allowedInteractivityTypes = [
'block_actions', 'view_submission', 'view_closed'];
'block_actions', 'view_submission', 'view_closed', 'block_suggestion'];
if (args.body === undefined || args.body === null) return false;
return (
allowedInteractivityTypes.includes(args.body.type) &&
Expand Down Expand Up @@ -466,7 +506,9 @@ export function validate(callbackId: string, handler: Middleware<SlackEventMiddl
export function errorIfInvalidConstraintKeys(
constraints: FunctionInteractivityConstraints,
validKeys: string[],
handler: Middleware<SlackActionMiddlewareArgs> | Middleware<SlackViewMiddlewareArgs<SlackViewAction>>,
handler: Middleware<SlackActionMiddlewareArgs> |
Middleware<SlackViewMiddlewareArgs<SlackViewAction>> |
Middleware<SlackBlockSuggestionsMiddlewareArgs>,
): void {
const invalidKeys = Object.keys(constraints).filter(
(key) => !validKeys.includes(key),
Expand Down
Loading

0 comments on commit cd432d6

Please sign in to comment.