diff --git a/src/receivers/AwsLambdaReceiver.ts b/src/receivers/AwsLambdaReceiver.ts index 7671ca2a8..2836de9ea 100644 --- a/src/receivers/AwsLambdaReceiver.ts +++ b/src/receivers/AwsLambdaReceiver.ts @@ -1,5 +1,4 @@ import crypto from 'crypto'; -/* eslint-disable @typescript-eslint/no-explicit-any */ import querystring from 'querystring'; import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; import tsscmp from 'tsscmp'; @@ -257,7 +256,6 @@ export default class AwsLambdaReceiver implements Receiver { }; } - // eslint-disable-next-line class-methods-use-this private getRawBody(awsEvent: AwsEvent): string { if (typeof awsEvent.body === 'undefined' || awsEvent.body == null) { return ''; @@ -268,7 +266,6 @@ export default class AwsLambdaReceiver implements Receiver { return awsEvent.body; } - // eslint-disable-next-line class-methods-use-this private parseRequestBody(stringBody: string, contentType: string | undefined, logger: Logger): any { if (contentType === 'application/x-www-form-urlencoded') { const parsedBody = querystring.parse(stringBody); diff --git a/test/unit/.mocharc.json b/test/unit/.mocharc.json index 3958af49e..f98b6dc91 100644 --- a/test/unit/.mocharc.json +++ b/test/unit/.mocharc.json @@ -1,5 +1,10 @@ { - "require": ["ts-node/register", "source-map-support/register"], - "spec": ["test/unit/**/*.spec.ts"], + "require": [ + "ts-node/register", + "source-map-support/register" + ], + "spec": [ + "test/unit/**/*.spec.ts" + ], "timeout": 3000 } diff --git a/test/unit/helpers/receivers.ts b/test/unit/helpers/receivers.ts index adf16207c..f9ad1d02f 100644 --- a/test/unit/helpers/receivers.ts +++ b/test/unit/helpers/receivers.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'node:events'; +import crypto from 'node:crypto'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import type { Receiver, ReceiverEvent } from '../../../src/types'; @@ -66,3 +67,35 @@ export function withHttpsCreateServer(spy: SinonSpy): Override { }, }; } + +export function createDummyAWSPayload( + // biome-ignore lint/suspicious/noExplicitAny: HTTP request bodies can be anything + body: any, + timestamp: number = Math.floor(Date.now() / 1000), + headers?: Record, + isBase64Encoded = false, +) { + const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); + const realBody = isBase64Encoded ? Buffer.from(body).toString('base64') : body; + return { + resource: '/slack/events', + path: '/slack/events', + httpMethod: 'POST', + headers: headers || { + Accept: 'application/json,*/*', + 'Content-Type': 'application/json', + Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', + 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', + 'X-Slack-Request-Timestamp': `${timestamp}`, + 'X-Slack-Signature': `v0=${signature}`, + }, + multiValueHeaders: {}, + queryStringParameters: null, + multiValueQueryStringParameters: null, + pathParameters: null, + stageVariables: null, + requestContext: {}, + body: realBody, + isBase64Encoded, + }; +} diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index e34beaec3..876181837 100644 --- a/test/unit/receivers/AwsLambdaReceiver.spec.ts +++ b/test/unit/receivers/AwsLambdaReceiver.spec.ts @@ -3,6 +3,8 @@ import { assert } from 'chai'; import sinon from 'sinon'; import AwsLambdaReceiver from '../../../src/receivers/AwsLambdaReceiver'; import { + createDummyAppMentionEventMiddlewareArgs, + createDummyAWSPayload, createFakeLogger, importApp, mergeOverrides, @@ -56,61 +58,10 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const timestamp = Math.floor(Date.now() / 1000); - // TODO: use event test helpers instead - const body = JSON.stringify({ - token: 'fixed-value', - team_id: 'T111', - enterprise_id: 'E111', - api_app_id: 'A111', - event: { - client_msg_id: '977a7fa8-c9b3-4b51-a0b6-3b6c647e2165', - type: 'app_mention', - text: '<@U222> test', - user: 'W111', - ts: '1612879521.002100', - team: 'T111', - channel: 'C111', - event_ts: '1612879521.002100', - }, - type: 'event_callback', - event_id: 'Ev111', - event_time: 1612879521, - authorizations: [ - { - enterprise_id: 'E111', - team_id: 'T111', - user_id: 'W111', - is_bot: true, - is_enterprise_install: false, - }, - ], - is_ext_shared_channel: false, - event_context: '1-app_mention-T111-C111', - }); - const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); - // TODO: create an aws event factory test helper thing? - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/json', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body, - isBase64Encoded: false, - }; - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const args = createDummyAppMentionEventMiddlewareArgs(); + const body = JSON.stringify(args.body); + const awsEvent = createDummyAWSPayload(body, timestamp); + const response1 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response1.statusCode, 404); const App = await importApp(appOverrides); const app = new App({ @@ -118,7 +69,7 @@ describe('AwsLambdaReceiver', () => { receiver: awsReceiver, }); app.event('app_mention', noopVoid); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response2.statusCode, 200); }); @@ -129,61 +80,18 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const timestamp = Math.floor(Date.now() / 1000); - // TODO: use test helper - const body = JSON.stringify({ - token: 'fixed-value', - team_id: 'T111', - enterprise_id: 'E111', - api_app_id: 'A111', - event: { - client_msg_id: '977a7fa8-c9b3-4b51-a0b6-3b6c647e2165', - type: 'app_mention', - text: '<@U222> test', - user: 'W111', - ts: '1612879521.002100', - team: 'T111', - channel: 'C111', - event_ts: '1612879521.002100', - }, - type: 'event_callback', - event_id: 'Ev111', - event_time: 1612879521, - authorizations: [ - { - enterprise_id: 'E111', - team_id: 'T111', - user_id: 'W111', - is_bot: true, - is_enterprise_install: false, - }, - ], - is_ext_shared_channel: false, - event_context: '1-app_mention-T111-C111', - }); + const args = createDummyAppMentionEventMiddlewareArgs(); + const body = JSON.stringify(args.body); const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); - // TODO: factor out into helper - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - accept: 'application/json,*/*', - 'content-type': 'application/json', - host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'user-agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'x-slack-request-timestamp': `${timestamp}`, - 'x-slack-signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body, - isBase64Encoded: false, - }; - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(body, timestamp, { + accept: 'application/json,*/*', + 'content-type': 'application/json', + host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', + 'user-agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', + 'x-slack-request-timestamp': `${timestamp}`, + 'x-slack-signature': `v0=${signature}`, + }); + const response1 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response1.statusCode, 404); const App = await importApp(appOverrides); const app = new App({ @@ -191,11 +99,11 @@ describe('AwsLambdaReceiver', () => { receiver: awsReceiver, }); app.event('app_mention', noopVoid); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response2.statusCode, 200); }); - it('should accept interactivity requests', async () => { + it('should accept interactivity requests as form-encoded payload', async () => { const awsReceiver = new AwsLambdaReceiver({ signingSecret: 'my-secret', logger: noopLogger, @@ -205,28 +113,15 @@ describe('AwsLambdaReceiver', () => { const body = 'payload=%7B%22type%22%3A%22shortcut%22%2C%22token%22%3A%22fixed-value%22%2C%22action_ts%22%3A%221612879511.716075%22%2C%22team%22%3A%7B%22id%22%3A%22T111%22%2C%22domain%22%3A%22domain-value%22%2C%22enterprise_id%22%3A%22E111%22%2C%22enterprise_name%22%3A%22Sandbox+Org%22%7D%2C%22user%22%3A%7B%22id%22%3A%22W111%22%2C%22username%22%3A%22primary-owner%22%2C%22team_id%22%3A%22T111%22%7D%2C%22is_enterprise_install%22%3Afalse%2C%22enterprise%22%3A%7B%22id%22%3A%22E111%22%2C%22name%22%3A%22Kaz+SDK+Sandbox+Org%22%7D%2C%22callback_id%22%3A%22bolt-js-aws-lambda-shortcut%22%2C%22trigger_id%22%3A%22111.222.xxx%22%7D'; const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/x-www-form-urlencoded', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body, - isBase64Encoded: false, - }; - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(body, timestamp, { + Accept: 'application/json,*/*', + 'Content-Type': 'application/x-www-form-urlencoded', + Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', + 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', + 'X-Slack-Request-Timestamp': `${timestamp}`, + 'X-Slack-Signature': `v0=${signature}`, + }); + const response1 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response1.statusCode, 404); const App = await importApp(appOverrides); const app = new App({ @@ -236,11 +131,11 @@ describe('AwsLambdaReceiver', () => { app.shortcut('bolt-js-aws-lambda-shortcut', async ({ ack }) => { await ack(); }); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response2.statusCode, 200); }); - it('should accept slash commands', async () => { + it('should accept slash commands with form-encoded body', async () => { const awsReceiver = new AwsLambdaReceiver({ signingSecret: 'my-secret', logger: noopLogger, @@ -250,28 +145,15 @@ describe('AwsLambdaReceiver', () => { const body = 'token=fixed-value&team_id=T111&team_domain=domain-value&channel_id=C111&channel_name=random&user_id=W111&user_name=primary-owner&command=%2Fhello-bolt-js&text=&api_app_id=A111&is_enterprise_install=false&enterprise_id=E111&enterprise_name=Sandbox+Org&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxx&trigger_id=111.222.xxx'; const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/x-www-form-urlencoded', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body, - isBase64Encoded: false, - }; - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(body, timestamp, { + Accept: 'application/json,*/*', + 'Content-Type': 'application/x-www-form-urlencoded', + Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', + 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', + 'X-Slack-Request-Timestamp': `${timestamp}`, + 'X-Slack-Signature': `v0=${signature}`, + }); + const response1 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response1.statusCode, 404); const App = await importApp(appOverrides); const app = new App({ @@ -281,7 +163,7 @@ describe('AwsLambdaReceiver', () => { app.command('/hello-bolt-js', async ({ ack }) => { await ack(); }); - const response2 = await handler(awsEvent, {}, (_error, _result) => {}); + const response2 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response2.statusCode, 200); }); @@ -292,59 +174,10 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const timestamp = Math.floor(Date.now() / 1000); - const body = JSON.stringify({ - token: 'fixed-value', - team_id: 'T111', - enterprise_id: 'E111', - api_app_id: 'A111', - event: { - client_msg_id: '977a7fa8-c9b3-4b51-a0b6-3b6c647e2165', - type: 'app_mention', - text: '<@U222> test', - user: 'W111', - ts: '1612879521.002100', - team: 'T111', - channel: 'C111', - event_ts: '1612879521.002100', - }, - type: 'event_callback', - event_id: 'Ev111', - event_time: 1612879521, - authorizations: [ - { - enterprise_id: 'E111', - team_id: 'T111', - user_id: 'W111', - is_bot: true, - is_enterprise_install: false, - }, - ], - is_ext_shared_channel: false, - event_context: '1-app_mention-T111-C111', - }); - const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/json', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body: Buffer.from(body).toString('base64'), - isBase64Encoded: true, - }; - const response1 = await handler(awsEvent, {}, (_error, _result) => {}); + const args = createDummyAppMentionEventMiddlewareArgs(); + const body = JSON.stringify(args.body); + const awsEvent = createDummyAWSPayload(body, timestamp, undefined, true); + const response1 = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response1.statusCode, 404); }); @@ -355,26 +188,17 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const body = 'ssl_check=1&token=legacy-fixed-token'; - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/x-www-form-urlencoded', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body, - isBase64Encoded: false, - }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const timestamp = Math.floor(Date.now() / 1000); + const signature = crypto.createHmac('sha256', 'my-secret').update(`v0:${timestamp}:${body}`).digest('hex'); + const awsEvent = createDummyAWSPayload(body, timestamp, { + Accept: 'application/json,*/*', + 'Content-Type': 'application/x-www-form-urlencoded', + Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', + 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', + 'X-Slack-Request-Timestamp': `${timestamp}`, + 'X-Slack-Signature': `v0=${signature}`, + }); + const response = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response.statusCode, 200); }); @@ -391,32 +215,8 @@ describe('AwsLambdaReceiver', () => { logger: noopLogger, }); const handler = awsReceiver.toHandler(); - const signature = crypto - .createHmac('sha256', 'my-secret') - .update(`v0:${timestamp}:${urlVerificationBody}`) - .digest('hex'); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/json', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body: urlVerificationBody, - isBase64Encoded: false, - }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(urlVerificationBody, timestamp); + const response = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response.statusCode, 200); }); @@ -454,7 +254,7 @@ describe('AwsLambdaReceiver', () => { body: urlVerificationBody, isBase64Encoded: false, }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const response = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response.statusCode, 401); assert(spy.calledOnce); }); @@ -466,32 +266,8 @@ describe('AwsLambdaReceiver', () => { }); const handler = awsReceiver.toHandler(); const timestamp = Math.floor(Date.now() / 1000) - 600; // 10 minutes ago - const signature = crypto - .createHmac('sha256', 'my-secret') - .update(`v0:${timestamp}:${urlVerificationBody}`) - .digest('hex'); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/json', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': `${timestamp}`, - 'X-Slack-Signature': `v0=${signature}`, - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body: urlVerificationBody, - isBase64Encoded: false, - }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(urlVerificationBody, timestamp); + const response = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response.statusCode, 401); }); @@ -502,28 +278,8 @@ describe('AwsLambdaReceiver', () => { logger: noopLogger, }); const handler = awsReceiver.toHandler(); - const awsEvent = { - resource: '/slack/events', - path: '/slack/events', - httpMethod: 'POST', - headers: { - Accept: 'application/json,*/*', - 'Content-Type': 'application/json', - Host: 'xxx.execute-api.ap-northeast-1.amazonaws.com', - 'User-Agent': 'Slackbot 1.0 (+https://api.slack.com/robots)', - 'X-Slack-Request-Timestamp': 'far back dude', - 'X-Slack-Signature': 'very much invalid', - }, - multiValueHeaders: {}, - queryStringParameters: null, - multiValueQueryStringParameters: null, - pathParameters: null, - stageVariables: null, - requestContext: {}, - body: urlVerificationBody, - isBase64Encoded: false, - }; - const response = await handler(awsEvent, {}, (_error, _result) => {}); + const awsEvent = createDummyAWSPayload(urlVerificationBody); + const response = await handler(awsEvent, {}, (_error, _result) => { }); assert.equal(response.statusCode, 200); }); });