Skip to content

Commit

Permalink
Fixes and polish for stable release (#2128)
Browse files Browse the repository at this point in the history
Co-authored-by: Fil Maj <[email protected]>
  • Loading branch information
misscoded and filmaj authored Aug 12, 2024
1 parent 87730ef commit bea900d
Show file tree
Hide file tree
Showing 14 changed files with 222 additions and 88 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"lint": "eslint --fix --ext .ts src",
"mocha": "TS_NODE_PROJECT=tsconfig.json nyc mocha --config .mocharc.json \"src/**/*.spec.ts\"",
"test": "npm run lint && npm run mocha && npm run test:types",
"test": "npm run build && npm run lint && npm run mocha && npm run test:types",
"test:coverage": "npm run mocha && nyc report --reporter=text",
"test:types": "tsd",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
},
Expand Down
22 changes: 17 additions & 5 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
SlashCommand,
WorkflowStepEdit,
SlackOptions,
FunctionInputs,
} from './types';
import { IncomingEventType, getTypeAndConversation, assertNever, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize } from './helpers';
import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors';
Expand Down Expand Up @@ -502,6 +503,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}
}

public get webClientOptions(): WebClientOptions {
return this.clientOptions;
}

/**
* Register a new middleware, processed in the order registered.
*
Expand Down Expand Up @@ -529,7 +534,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
* Register CustomFunction middleware
*/
public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this {
const fn = new CustomFunction(callbackId, listeners);
const fn = new CustomFunction(callbackId, listeners, this.webClientOptions);
const m = fn.getMiddleware();
this.middleware.push(m);
return this;
Expand Down Expand Up @@ -964,9 +969,12 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
retryReason: event.retryReason,
};

// Extract function-related information and augment to context
const { functionExecutionId, functionBotAccessToken } = extractFunctionContext(body);
if (functionExecutionId) { context.functionExecutionId = functionExecutionId; }
// Extract function-related information and augment context
const { functionExecutionId, functionBotAccessToken, functionInputs } = extractFunctionContext(body);
if (functionExecutionId) {
context.functionExecutionId = functionExecutionId;
if (functionInputs) { context.functionInputs = functionInputs; }
}

if (this.attachFunctionToken) {
if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
Expand Down Expand Up @@ -1029,6 +1037,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
ack?: AckFn<any>;
complete?: FunctionCompleteFn;
fail?: FunctionFailFn;
inputs?: FunctionInputs;
} = {
body: bodyArg,
payload,
Expand Down Expand Up @@ -1088,6 +1097,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
listenerArgs.complete = CustomFunction.createFunctionComplete(context, client);
listenerArgs.fail = CustomFunction.createFunctionFail(context, client);
listenerArgs.inputs = context.functionInputs;
}

if (token !== undefined) {
Expand Down Expand Up @@ -1599,6 +1609,7 @@ function escapeHtml(input: string | undefined | null): string {
function extractFunctionContext(body: StringIndexed) {
let functionExecutionId;
let functionBotAccessToken;
let functionInputs;

// function_executed event
if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
Expand All @@ -1610,9 +1621,10 @@ function extractFunctionContext(body: StringIndexed) {
if (body.function_data) {
functionExecutionId = body.function_data.execution_id;
functionBotAccessToken = body.bot_access_token;
functionInputs = body.function_data.inputs;
}

return { functionExecutionId, functionBotAccessToken };
return { functionExecutionId, functionBotAccessToken, functionInputs };
}

// ----------------------------
Expand Down
144 changes: 104 additions & 40 deletions src/CustomFunction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import 'mocha';
import { assert } from 'chai';
import sinon from 'sinon';
import rewiremock from 'rewiremock';
import { WebClient } from '@slack/web-api';
import {
CustomFunction,
SlackCustomFunctionMiddlewareArgs,
AllCustomFunctionMiddlewareArgs,
CustomFunctionMiddleware,
CustomFunctionExecuteMiddlewareArgs,
} from './CustomFunction';
import { Override } from './test-helpers';
import { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware } from './types';
import { createFakeLogger, Override } from './test-helpers';
import { AllMiddlewareArgs, Middleware } from './types';
import { CustomFunctionInitializationError } from './errors';

async function importCustomFunction(overrides: Override = {}): Promise<typeof import('./CustomFunction')> {
Expand All @@ -26,36 +27,35 @@ const MOCK_MIDDLEWARE_MULTIPLE = [MOCK_FN, MOCK_FN_2];
describe('CustomFunction class', () => {
describe('constructor', () => {
it('should accept single function as middleware', async () => {
const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE);
const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
assert.isNotNull(fn);
});

it('should accept multiple functions as middleware', async () => {
const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE);
const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, {});
assert.isNotNull(fn);
});
});

describe('getMiddleware', () => {
it('should not call next if a function_executed event', async () => {
const fn = new CustomFunction('test_executed_callback_id', MOCK_MIDDLEWARE_SINGLE);
const cbId = 'test_executed_callback_id';
const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, {});
const middleware = fn.getMiddleware();
const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
const fakeEditArgs = createFakeFunctionExecutedEvent(cbId);

const fakeNext = sinon.spy();
fakeEditArgs.next = fakeNext;

await middleware(fakeEditArgs);

assert(fakeNext.notCalled);
assert(fakeNext.notCalled, 'next called!');
});

it('should call next if valid custom function but mismatched callback_id', async () => {
const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE);
const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
const middleware = fn.getMiddleware();
const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
const fakeEditArgs = createFakeFunctionExecutedEvent();

const fakeNext = sinon.spy();
fakeEditArgs.next = fakeNext;
Expand All @@ -65,8 +65,8 @@ describe('CustomFunction class', () => {
assert(fakeNext.called);
});

it('should call next if not a workflow step event', async () => {
const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE);
it('should call next if not a function executed event', async () => {
const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
const middleware = fn.getMiddleware();
const fakeViewArgs = createFakeViewEvent() as unknown as
SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
Expand Down Expand Up @@ -120,8 +120,7 @@ describe('CustomFunction class', () => {

describe('isFunctionEvent', () => {
it('should return true if recognized function_executed payload type', async () => {
const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as SlackCustomFunctionMiddlewareArgs
& AllMiddlewareArgs;
const fakeExecuteArgs = createFakeFunctionExecutedEvent();

const { isFunctionEvent } = await importCustomFunction();
const eventIsFunctionExcuted = isFunctionEvent(fakeExecuteArgs);
Expand All @@ -130,7 +129,8 @@ describe('CustomFunction class', () => {
});

it('should return false if not a function_executed payload type', async () => {
const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
const fakeExecutedEvent = createFakeFunctionExecutedEvent();
// @ts-expect-error expected invalid payload type
fakeExecutedEvent.payload.type = 'invalid_type';

const { isFunctionEvent } = await importCustomFunction();
Expand All @@ -142,10 +142,10 @@ describe('CustomFunction class', () => {

describe('enrichFunctionArgs', () => {
it('should remove next() from all original event args', async () => {
const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
const fakeExecutedEvent = createFakeFunctionExecutedEvent();

const { enrichFunctionArgs } = await importCustomFunction();
const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent);
const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent, {});

assert.notExists(executeFunctionArgs.next);
});
Expand All @@ -154,7 +154,7 @@ describe('CustomFunction class', () => {
const fakeArgs = createFakeFunctionExecutedEvent();

const { enrichFunctionArgs } = await importCustomFunction();
const functionArgs = enrichFunctionArgs(fakeArgs);
const functionArgs = enrichFunctionArgs(fakeArgs, {});

assert.exists(functionArgs.inputs);
assert.exists(functionArgs.complete);
Expand All @@ -163,59 +163,123 @@ describe('CustomFunction class', () => {
});

describe('custom function utility functions', () => {
it('complete should call functions.completeSuccess', async () => {
// TODO
describe('`complete` factory function', () => {
it('complete should call functions.completeSuccess', async () => {
const client = new WebClient('sometoken');
const completeMock = sinon.stub(client.functions, 'completeSuccess').resolves();
const complete = CustomFunction.createFunctionComplete({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client);
await complete();
assert(completeMock.called, 'client.functions.completeSuccess not called!');
});
it('should throw if no functionExecutionId present on context', () => {
const client = new WebClient('sometoken');
assert.throws(() => {
CustomFunction.createFunctionComplete({ isEnterpriseInstall: false }, client);
});
});
});

it('fail should call functions.completeError', async () => {
// TODO
describe('`fail` factory function', () => {
it('fail should call functions.completeError', async () => {
const client = new WebClient('sometoken');
const completeMock = sinon.stub(client.functions, 'completeError').resolves();
const complete = CustomFunction.createFunctionFail({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client);
await complete({ error: 'boom' });
assert(completeMock.called, 'client.functions.completeError not called!');
});
it('should throw if no functionExecutionId present on context', () => {
const client = new WebClient('sometoken');
assert.throws(() => {
CustomFunction.createFunctionFail({ isEnterpriseInstall: false }, client);
});
});
});

it('inputs should map to function payload inputs', async () => {
const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
const fakeExecuteArgs = createFakeFunctionExecutedEvent();

const { enrichFunctionArgs } = await importCustomFunction();
const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs);
const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs, {});

assert.isTrue(enrichedArgs.inputs === fakeExecuteArgs.event.inputs);
});
});

describe('processFunctionMiddleware', () => {
it('should call each callback in user-provided middleware', async () => {
const { ...fakeArgs } = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
const { ...fakeArgs } = createFakeFunctionExecutedEvent();
const { processFunctionMiddleware } = await importCustomFunction();

const fn1 = sinon.spy((async ({ next: continuation }) => {
await continuation();
}) as Middleware<CustomFunctionExecuteMiddlewareArgs>);
const fn2 = sinon.spy(async () => {});
const fn2 = sinon.spy(async () => {
});
const fakeMiddleware = [fn1, fn2] as CustomFunctionMiddleware;

await processFunctionMiddleware(fakeArgs, fakeMiddleware);

assert(fn1.called);
assert(fn2.called);
assert(fn1.called, 'first user-provided middleware not called!');
assert(fn2.called, 'second user-provided middleware not called!');
});
});
});

function createFakeFunctionExecutedEvent() {
function createFakeFunctionExecutedEvent(callbackId?: string): AllCustomFunctionMiddlewareArgs {
const func = {
type: 'function',
id: 'somefunc',
callback_id: callbackId || 'callback_id',
title: 'My dope function',
input_parameters: [],
output_parameters: [],
app_id: 'A1234',
date_created: 123456,
date_deleted: 0,
date_updated: 123456,
};
const base = {
bot_access_token: 'xoxb-abcd-1234',
event_ts: '123456.789',
function_execution_id: 'Fx1234',
workflow_execution_id: 'Wf1234',
type: 'function_executed',
} as const;
const inputs = { message: 'test123', recipient: 'U012345' };
const event = {
function: func,
inputs,
...base,
} as const;
return {
event: {
inputs: { message: 'test123', recipient: 'U012345' },
},
payload: {
type: 'function_executed',
function: {
callback_id: 'test_executed_callback_id',
},
inputs: { message: 'test123', recipient: 'U012345' },
bot_access_token: 'xwfp-123',
body: {
api_app_id: 'A1234',
event,
event_id: 'E1234',
event_time: 123456,
team_id: 'T1234',
token: 'xoxb-1234',
type: 'event_callback',
},
client: new WebClient('faketoken'),
complete: () => Promise.resolve({ ok: true }),
context: {
functionBotAccessToken: 'xwfp-123',
functionExecutionId: 'test_executed_callback_id',
isEnterpriseInstall: false,
},
event,
fail: () => Promise.resolve({ ok: true }),
inputs,
logger: createFakeLogger(),
message: undefined,
next: () => Promise.resolve(),
payload: {
function: func,
inputs: { message: 'test123', recipient: 'U012345' },
...base,
},
say: undefined,
};
}

Expand Down
Loading

0 comments on commit bea900d

Please sign in to comment.