Skip to content

Commit edfe8fa

Browse files
authored
Add manifest to slack [] (#10222)
* Update Node.js version in Dockerfile and serverless.yml to 18.x * updating controller to have response * wip: new manifest for slack app * updating backend packages * updating fe deps * adding script * refactoring * Updating test * Fixing old tests * Removing unused dep * Revert "Update Node.js version in Dockerfile and serverless.yml to 18.x" This reverts commit f7ca3bc. * Updating instructions * Revert "updating fe deps" This reverts commit 8f3796f. * Revert "updating backend packages" This reverts commit 8756b0e. * Moving everything to the back * Avoiding updating lambda deps and making script simpler * Removing unnecessary export
1 parent 9d42fb7 commit edfe8fa

File tree

13 files changed

+417
-24
lines changed

13 files changed

+417
-24
lines changed

apps/slack/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ This sections explains how to run the Slack app locally.
9999

100100
### Lambda
101101

102+
- Change Dockerfile node version to 18. It should look like: `FROM node:18-alpine AS base`
103+
- Change serverless.yml runtime node version to 18. It should look like `runtime: nodejs18.x`
102104
- Copy `lambda/config/serverless.dev.yml.example` to `lambda/config/serverless.dev.yml`
103105
- Create a new Contentful app, if this has not been done already.
104106
- Set frontend URL to `http://localhost:1234`
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"actions": [
3+
{
4+
"id": "sendMessage",
5+
"name": "Send Slack Message",
6+
"category": "Custom",
7+
"description": "Sends a message to a specified Slack channel or user",
8+
"type": "endpoint",
9+
"url": "/messages",
10+
"allowNetworks": [],
11+
"parametersSchema": {
12+
"type": "object",
13+
"properties": {
14+
"message": {
15+
"type": "string",
16+
"title": "Message Content",
17+
"description": "The content of the message to be sent to Slack",
18+
"minLength": 1,
19+
"maxLength": 4000
20+
},
21+
"channelId": {
22+
"type": "string",
23+
"title": "Slack Channel ID",
24+
"description": "The Slack channel ID to send the message to",
25+
"minLength": 1
26+
},
27+
"workspaceId": {
28+
"type": "string",
29+
"title": "Slack Workspace ID",
30+
"description": "The Slack workspace ID where the message should be sent",
31+
"minLength": 1
32+
}
33+
},
34+
"required": [
35+
"message",
36+
"channelId"
37+
],
38+
"additionalProperties": false
39+
},
40+
"resultSchema": {
41+
"type": "object",
42+
"properties": {
43+
"ok": {
44+
"type": "boolean",
45+
"description": "Indicates whether the message was sent successfully"
46+
},
47+
"channel": {
48+
"type": "string",
49+
"description": "The channel ID where the message was sent"
50+
},
51+
"ts": {
52+
"type": "string",
53+
"description": "The timestamp of the message"
54+
},
55+
"message": {
56+
"type": "object",
57+
"description": "The message object that was sent",
58+
"properties": {
59+
"type": {
60+
"type": "string",
61+
"description": "The type of message"
62+
},
63+
"text": {
64+
"type": "string",
65+
"description": "The text content of the message"
66+
},
67+
"user": {
68+
"type": "string",
69+
"description": "The user ID who sent the message"
70+
},
71+
"ts": {
72+
"type": "string",
73+
"description": "The timestamp of the message"
74+
},
75+
"team": {
76+
"type": "string",
77+
"description": "The team ID"
78+
},
79+
"bot_id": {
80+
"type": "string",
81+
"description": "The bot ID that sent the message"
82+
}
83+
}
84+
},
85+
"error": {
86+
"type": "string",
87+
"description": "Error message when operation fails (present when ok: false)"
88+
},
89+
"errors": {
90+
"type": "array",
91+
"items": {
92+
"type": "string"
93+
},
94+
"description": "Array of error messages (present when ok: false)"
95+
},
96+
"response_metadata": {
97+
"type": "object",
98+
"description": "Additional metadata about the response",
99+
"properties": {
100+
"warnings": {
101+
"type": "array",
102+
"items": {
103+
"type": "string"
104+
}
105+
}
106+
}
107+
}
108+
},
109+
"required": [
110+
"ok"
111+
],
112+
"additionalProperties": false
113+
}
114+
}
115+
]
116+
}

apps/slack/lambda/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ SLACK_CLIENT_ID=from-slack_app
44
SLACK_CLIENT_SECRET=from-slack-app
55
FRONTEND_URL=https://something.ngrok.io
66
BACKEND_URL=https://some-other-thing.ngrok.io/api
7+
8+
## This are only needed to run the updateAppAction script
9+
CONTENTFUL_ACCESS_TOKEN=<your-contentful-access-token>
10+
CONTENTFUL_ORG_ID=<your-contentful-organization-id>
11+
CONTENTFUL_APP_DEF_ID=<your-contentful-app-definition-id>
12+
#Optional
13+
APP_ACTION_ID=<your-app-action-id>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { errorMiddleware } from './middleware';
22
export { NotFoundException } from './not-found';
33
export { UnprocessableEntityException } from './unprocessable-entity';
4+
export { ConflictException } from './conflict';
45
export { SlackError } from './slack-error';

apps/slack/lambda/lib/routes/auth-token/controller.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ describe('AuthTokenController', () => {
5050

5151
const error = next.getCall(0).args[0];
5252

53-
assert.include(error.details[0], {
53+
assert.include(error.details?.error[0], {
5454
schemaPath: '#/required',
5555
keyword: 'required',
5656
});
57-
assert.include(error.details[1], {
57+
assert.include(error.details?.error[1], {
5858
schemaPath: '#/properties/code/type',
5959
keyword: 'type',
6060
});
61-
assert.include(error.details[2], {
61+
assert.include(error.details?.error[2], {
6262
schemaPath: '#/properties/spaceId/type',
6363
keyword: 'type',
6464
});
@@ -146,7 +146,7 @@ describe('AuthTokenController', () => {
146146

147147
const error = next.getCall(0).args[0];
148148

149-
assert.include(error.details[0], {
149+
assert.include(error.details?.error[0], {
150150
schemaPath: '#/properties/refreshToken/type',
151151
keyword: 'type',
152152
});

apps/slack/lambda/lib/routes/auth-token/repository.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
SinonStubbedInstance,
55
useFakeTimers,
66
stub,
7+
restore,
78
} from 'sinon';
89

910
import { AuthTokenRepository } from './repository';
@@ -51,6 +52,10 @@ describe('AuthTokenRepository', () => {
5152
clock.restore();
5253
});
5354

55+
after(() => {
56+
restore();
57+
});
58+
5459
describe('#validate', () => {
5560
it('returns access token, refresh token and workspace', async () => {
5661
slackClient.getAuthToken.resolves({

apps/slack/lambda/lib/routes/events/service.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventsService } from './service';
33
import { ACCEPTED_EVENTS } from './constants';
44
import { assert } from '../../../test/utils';
55
import { NotFoundException } from '../../errors';
6-
import { createStubInstance, SinonStubbedInstance, stub } from 'sinon';
6+
import { createStubInstance, SinonStubbedInstance, stub, restore } from 'sinon';
77
import { PlainClientAPI } from 'contentful-management';
88
import {
99
EventEntity,
@@ -26,6 +26,10 @@ describe('EventsService', () => {
2626
stub(helpers, 'getInstallationParametersFromCma').resolves(expectedParams);
2727
});
2828

29+
after(() => {
30+
restore();
31+
});
32+
2933
beforeEach(() => {
3034
messagesRepository = createStubInstance(MessagesRepository);
3135
authTokenRepository = createStubInstance(AuthTokenRepository);
Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ChatPostMessageResponse } from '@slack/web-api';
22
import { createStubInstance, SinonStubbedInstance, stub } from 'sinon';
33
import { assert, mockRequest, mockResponse, runHandler } from '../../../test/utils';
4-
import { UnprocessableEntityException } from '../../errors';
4+
import { UnprocessableEntityException, ConflictException } from '../../errors';
55
import { AuthToken } from '../../interfaces';
66
import { AuthTokenRepository } from '../auth-token';
77
import { MessagesController } from './controller';
@@ -20,42 +20,75 @@ describe('MessagesController', () => {
2020
});
2121

2222
describe('#post', () => {
23-
it('throws a UnprocessableEntityException for invalid body', async () => {
24-
oAuthRepository.get.resolves({ token: 'token' } as AuthToken);
25-
messagesRepository.create.resolves({
26-
result: 'ok',
27-
} as unknown as ChatPostMessageResponse);
23+
it('throws UnprocessableEntityException for invalid body', async () => {
2824
const request = mockRequest({
2925
body: {
3026
not: 'the',
3127
correct: 'body',
3228
values: '.',
3329
},
30+
headers: {
31+
['x-contentful-space-id']: 'space123',
32+
['x-contentful-environment-id']: 'env123',
33+
['x-contentful-crn']: 'crn:contentful:space:space123',
34+
},
3435
});
3536
const next = stub();
36-
await runHandler(instance.post(request, mockResponse(), next));
37+
const response = mockResponse();
38+
39+
await runHandler(instance.post(request, response, next));
3740

3841
const error = next.getCall(0).args[0];
3942
assert.instanceOf(error, UnprocessableEntityException);
4043
});
4144

42-
it('returns correct status', async () => {
43-
const result = { ok: true };
44-
messagesRepository.create.resolves(result);
45-
oAuthRepository.get.resolves({ token: 'token' } as AuthToken);
45+
it('throws ConflictException when required headers are missing', async () => {
46+
const request = mockRequest({
47+
body: { workspaceId: 'workspace123', message: 'Hello World!', channelId: 'channel123' },
48+
headers: {},
49+
});
50+
const next = stub();
51+
const response = mockResponse();
52+
53+
await runHandler(instance.post(request, response, next));
54+
55+
const error = next.getCall(0).args[0];
56+
assert.instanceOf(error, ConflictException);
57+
assert.include(error.details?.errMessage, 'EnvironmentId or spaceId not found in headers');
58+
});
59+
60+
it('returns correct response on successful message creation', async () => {
61+
const slackResponse = {
62+
ok: true,
63+
channel: 'channel123',
64+
ts: '1234567890.123456',
65+
message: { text: 'Hello World!' },
66+
} as ChatPostMessageResponse;
67+
68+
oAuthRepository.get.resolves({ token: 'slack-token-123' } as AuthToken);
69+
messagesRepository.create.resolves(slackResponse);
4670

4771
const request = mockRequest({
48-
body: { workspaceId: 'lol', message: 'message', channelId: 'channel' },
72+
body: { workspaceId: 'workspace123', message: 'Hello World!', channelId: 'channel123' },
4973
headers: {
50-
['x-contentful-space-id']: 'space',
51-
['x-contentful-environment-id']: 'env',
74+
['x-contentful-space-id']: 'space123',
75+
['x-contentful-environment-id']: 'env123',
76+
['x-contentful-crn']: 'crn:contentful:space:space123',
5277
},
5378
});
5479
const next = stub();
55-
const response = mockResponse();
80+
const statusStub = stub().returnsThis();
81+
const jsonStub = stub().returnsThis();
82+
const response = mockResponse({
83+
status: statusStub,
84+
json: jsonStub,
85+
});
86+
5687
await runHandler(instance.post(request, response, next));
5788

58-
assert.calledWith(response.sendStatus, 204);
89+
assert.calledWith(statusStub, 200);
90+
assert.calledWith(jsonStub, slackResponse);
91+
assert.notCalled(next);
5992
});
6093
});
6194
});

apps/slack/lambda/lib/routes/messages/controller.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MessagesRepository } from './repository';
66
import { PostMessageBody, postMessageWorkspacesBodySchema } from './validation';
77
import { getWorkspaceId } from '../../helpers/getWorkspaceId';
88
import { getHost } from '../../helpers/getHost';
9+
import { ChatPostMessageResponse } from '@slack/web-api';
910

1011
const extractFromSignedHeaders = (request: Request) => {
1112
const spaceId = request.header('x-contentful-space-id');
@@ -54,8 +55,12 @@ export class MessagesController {
5455
host
5556
);
5657

57-
await this.messagesRepository.create(token, channelId, { text: message });
58+
const slackResponse: ChatPostMessageResponse = await this.messagesRepository.create(
59+
token,
60+
channelId,
61+
{ text: message }
62+
);
5863

59-
response.sendStatus(204);
64+
response.status(200).json(slackResponse);
6065
});
6166
}

apps/slack/lambda/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"deploy:test": "sls deploy --stage test",
1515
"dev": "node ./scripts/dev.js",
1616
"create_table": "node ./scripts/create_table",
17-
"docs": "node ./scripts/openapi.js"
17+
"docs": "node ./scripts/openapi.js",
18+
"upsert-app-action": "node ./scripts/upsertAppAction.js"
1819
},
1920
"keywords": [],
2021
"author": "",

0 commit comments

Comments
 (0)