Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in block_suggestion interactivity handler support #1645

Merged
merged 9 commits into from Nov 11, 2022
7 changes: 7 additions & 0 deletions src/App.ts
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
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 () => {
hello-ashleyintech marked this conversation as resolved.
Show resolved Hide resolved
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
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