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

Remote functions: Some processEvent unit tests #2203

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions src/App-custom-step.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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';

// 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 function() and function_executed and function-scoped interactivity event processing', () => {
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 listener to middleware for each function listener passed to app.function', async () => {
/* middleware is a private property on App, so ensure that the step listener
is added to the global middleware array. */
const { middleware } = (app as any);
const noop = () => Promise.resolve();

assert.equal(middleware.length, 2);

app.function('callback_id', noop, noop);

// Bundles two listeners into one middleware, thus 3 here:
assert.equal(middleware.length, 3);
});
it('should enrich custom step context and arguments with function_executed properties like execution ID and inputs', async () => {
const noop = () => Promise.resolve();
const processStub = sinon.fake.resolves({});
const MockAppNoOverrides = await importApp(mergeOverrides(withNoopAppMetadata(), withNoopWebClient(), {
'./middleware/process': processStub,
}));
app = new MockAppNoOverrides({
receiver: fakeReceiver,
authorize: sinon.fake.resolves(dummyAuthorizationResult),
});
await fakeReceiver.sendEvent({
body: {
event: {
type: 'function_executed',
function_execution_id: 'Fx1234',
bot_access_token: 'xowfp',
},
function_data: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to double-check, but AFAIK, function_data only is associated with block_actions payloads – not function_executed.

{
  type: 'function_executed',
  function: {
    id: 'Fn0695V91LGN',
    callback_id: 'sample_function',
    title: 'Sample function 1',
    description: 'Runs sample function',
    type: 'app',
    input_parameters: [ [Object] ],
    output_parameters: [ [Object] ],
    app_id: 'A06A1C8C5FS',
    date_created: 1702069571,
    date_released: 0,
    date_updated: 1723658242,
    date_deleted: 0,
    form_enabled: false
  },
  inputs: { user_id: 'U019GD47LJH' },
  function_execution_id: 'Fx07HMB67Y3S',
  workflow_execution_id: 'Wx07GVLL7ZTP',
  event_ts: '1723658328.841355',
  bot_access_token: 'xwfp-444'
}

versus

{
  type: 'block_actions',
  team: { id: 'T019KFN8A2W', domain: 'misscoded' },
  enterprise: null,
  user: { id: 'U019GD47LJH', name: 'arenz', team_id: 'T019KFN8A2W' },
  channel: { id: 'D0699LNQ3HT', name: 'directmessage' },
  message: {
    user: 'U069CFDKS03',
    type: 'message',
    ts: '1723658484.749309',
    bot_id: 'B069F2HQSFN',
    app_id: 'A06A1C8C5FS',
    text: 'Click the button to signal the function has completed',
    team: 'T019KFN8A2W',
    blocks: [ [Object] ],
    assistant_app_thread: { title: '', title_blocks: [] }
  },
  container: {
    type: 'message',
    message_ts: '1723658484.749309',
    channel_id: 'D0699LNQ3HT',
    is_ephemeral: false
  },
  actions: [
    {
      block_id: 'I9kjf',
      action_id: 'sample_button',
      type: 'button',
      text: [Object],
      action_ts: '1723658488.667103'
    }
  ],
  api_app_id: 'A06A1C8C5FS',
  state: { values: {} },
  bot_access_token: 'xwfp-444',
  function_data: {
    execution_id: 'Fx07GVMA73NH',
    function: { callback_id: 'sample_function' },
    inputs: { user_id: 'U019GD47LJH' }
  },
  interactivity: {
    interactor: {
      secret: 'xxx',
      id: 'U019GD47LJH'
    },
    interactivity_pointer: '7576612985154.1325532282098.90f3e0899eaced8770aaf84abe894dcd'
  }
}

inputs: 'yo',
},
},
ack: noop,
});
const context = processStub.lastCall.args[2];
assert.equal(context.functionBotAccessToken, 'xowfp');
assert.equal(context.functionExecutionId, 'Fx1234');
assert.equal(context.functionInputs, 'yo');
});
it('should not enrich custom step middleware arguments with JIT workflow token if attachFunctionToken set to false', async () => {
const noop = () => Promise.resolve();
const processStub = sinon.fake.resolves({});
const MockAppNoOverrides = await importApp(mergeOverrides(withNoopAppMetadata(), withNoopWebClient(), {
'./middleware/process': processStub,
}));
app = new MockAppNoOverrides({
receiver: fakeReceiver,
authorize: sinon.fake.resolves(dummyAuthorizationResult),
attachFunctionToken: false,
});
await fakeReceiver.sendEvent({
body: {
event: {
type: 'function_executed',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about payloads above.

function_execution_id: 'Fx1234',
bot_access_token: 'xowfp',
},
function_data: {
inputs: 'yo',
},
},
ack: noop,
});
const context = processStub.lastCall.args[2];
assert.notEqual(context.functionBotAccessToken, 'xowfp');
});
it('should enrich action middleware arguments with function_executed properties like execution ID, workflow JIT token and inputs if action event is function-scoped', async () => {
const noop = () => Promise.resolve();
const processStub = sinon.fake.resolves({});
const MockAppNoOverrides = await importApp(mergeOverrides(withNoopAppMetadata(), withNoopWebClient(), {
'./middleware/process': processStub,
}));
app = new MockAppNoOverrides({
receiver: fakeReceiver,
authorize: sinon.fake.resolves(dummyAuthorizationResult),
});
await fakeReceiver.sendEvent({
body: {
actions: [],
bot_access_token: 'xowfp',
channel: { id: 'C1234' },
function_data: {
execution_id: 'Fx1234',
inputs: 'yo',
},
user: {
team_id: 'T1244',
},
},
ack: noop,
});
const args = processStub.lastCall.args[1];
assert(args.complete);
assert(args.fail);
assert(args.inputs);
const context = processStub.lastCall.args[2];
assert.equal(context.functionBotAccessToken, 'xowfp');
assert.equal(context.functionExecutionId, 'Fx1234');
assert.equal(context.functionInputs, 'yo');
});
});

/* 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(),
},
};
}
8 changes: 4 additions & 4 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,8 +977,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
}

// Attach and make available the JIT/function-related token on context
if (this.attachFunctionToken) {
if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
if (this.attachFunctionToken && functionBotAccessToken) {
context.functionBotAccessToken = functionBotAccessToken;
}

// Factory for say() utility
Expand Down Expand Up @@ -1624,8 +1624,8 @@ function extractFunctionContext(body: StringIndexed) {

// interactivity (block_actions)
if (body.function_data) {
functionExecutionId = body.function_data.execution_id;
functionBotAccessToken = body.bot_access_token;
functionExecutionId = body.function_data.execution_id || functionExecutionId;
Copy link
Contributor

@misscoded misscoded Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow the logic here: the two conditions never would happen in one go? body.event.type === 'function_executed' will never be true if body.function_data is also true, AFAIK, so the fallback would never be anything other than undefined.

functionBotAccessToken = body.bot_access_token || functionBotAccessToken;
functionInputs = body.function_data.inputs;
}

Expand Down