@shipfox/api-integration-slack 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +24 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +3 -1
- package/dist/api/client.js.map +1 -1
- package/dist/core/webhook.d.ts +5 -3
- package/dist/core/webhook.d.ts.map +1 -1
- package/dist/core/webhook.js +101 -2
- package/dist/core/webhook.js.map +1 -1
- package/dist/db/db.d.ts +34 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/installations.d.ts +2 -0
- package/dist/db/installations.d.ts.map +1 -1
- package/dist/db/installations.js +4 -2
- package/dist/db/installations.js.map +1 -1
- package/dist/db/schema/installations.d.ts +17 -0
- package/dist/db/schema/installations.d.ts.map +1 -1
- package/dist/db/schema/installations.js +3 -1
- package/dist/db/schema/installations.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/webhooks.d.ts +2 -1
- package/dist/presentation/routes/webhooks.d.ts.map +1 -1
- package/dist/presentation/routes/webhooks.js +1 -0
- package/dist/presentation/routes/webhooks.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_dark_tyrannus.sql +1 -0
- package/drizzle/meta/0001_snapshot.json +139 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +18 -28
- package/src/api/client.test.ts +24 -0
- package/src/api/client.ts +3 -1
- package/src/core/disconnect.test.ts +49 -0
- package/src/core/webhook.test.ts +131 -2
- package/src/core/webhook.ts +143 -6
- package/src/db/installations.test.ts +15 -0
- package/src/db/installations.ts +11 -3
- package/src/db/schema/installations.ts +3 -1
- package/src/index.test.ts +11 -0
- package/src/index.ts +11 -0
- package/src/presentation/routes/webhooks.test.ts +28 -1
- package/src/presentation/routes/webhooks.ts +3 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/turbo.json +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE "integrations_slack_installations" ADD COLUMN "generation" integer DEFAULT 1 NOT NULL;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "5acf9796-7da0-4134-b7f8-000a1a5a7de0",
|
|
3
|
+
"prevId": "62801d4c-2399-4306-8d37-9773d10cbb02",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.integrations_slack_installations": {
|
|
8
|
+
"name": "integrations_slack_installations",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "uuid",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true,
|
|
16
|
+
"default": "uuidv7()"
|
|
17
|
+
},
|
|
18
|
+
"connection_id": {
|
|
19
|
+
"name": "connection_id",
|
|
20
|
+
"type": "uuid",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"team_id": {
|
|
25
|
+
"name": "team_id",
|
|
26
|
+
"type": "text",
|
|
27
|
+
"primaryKey": false,
|
|
28
|
+
"notNull": true
|
|
29
|
+
},
|
|
30
|
+
"team_name": {
|
|
31
|
+
"name": "team_name",
|
|
32
|
+
"type": "text",
|
|
33
|
+
"primaryKey": false,
|
|
34
|
+
"notNull": true
|
|
35
|
+
},
|
|
36
|
+
"app_id": {
|
|
37
|
+
"name": "app_id",
|
|
38
|
+
"type": "text",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": true
|
|
41
|
+
},
|
|
42
|
+
"bot_user_id": {
|
|
43
|
+
"name": "bot_user_id",
|
|
44
|
+
"type": "text",
|
|
45
|
+
"primaryKey": false,
|
|
46
|
+
"notNull": true
|
|
47
|
+
},
|
|
48
|
+
"scopes": {
|
|
49
|
+
"name": "scopes",
|
|
50
|
+
"type": "jsonb",
|
|
51
|
+
"primaryKey": false,
|
|
52
|
+
"notNull": true
|
|
53
|
+
},
|
|
54
|
+
"status": {
|
|
55
|
+
"name": "status",
|
|
56
|
+
"type": "text",
|
|
57
|
+
"primaryKey": false,
|
|
58
|
+
"notNull": true
|
|
59
|
+
},
|
|
60
|
+
"generation": {
|
|
61
|
+
"name": "generation",
|
|
62
|
+
"type": "integer",
|
|
63
|
+
"primaryKey": false,
|
|
64
|
+
"notNull": true,
|
|
65
|
+
"default": 1
|
|
66
|
+
},
|
|
67
|
+
"token_expires_at": {
|
|
68
|
+
"name": "token_expires_at",
|
|
69
|
+
"type": "timestamp with time zone",
|
|
70
|
+
"primaryKey": false,
|
|
71
|
+
"notNull": false
|
|
72
|
+
},
|
|
73
|
+
"created_at": {
|
|
74
|
+
"name": "created_at",
|
|
75
|
+
"type": "timestamp with time zone",
|
|
76
|
+
"primaryKey": false,
|
|
77
|
+
"notNull": true,
|
|
78
|
+
"default": "now()"
|
|
79
|
+
},
|
|
80
|
+
"updated_at": {
|
|
81
|
+
"name": "updated_at",
|
|
82
|
+
"type": "timestamp with time zone",
|
|
83
|
+
"primaryKey": false,
|
|
84
|
+
"notNull": true,
|
|
85
|
+
"default": "now()"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"indexes": {
|
|
89
|
+
"integrations_slack_installations_connection_unique": {
|
|
90
|
+
"name": "integrations_slack_installations_connection_unique",
|
|
91
|
+
"columns": [
|
|
92
|
+
{
|
|
93
|
+
"expression": "connection_id",
|
|
94
|
+
"isExpression": false,
|
|
95
|
+
"asc": true,
|
|
96
|
+
"nulls": "last"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"isUnique": true,
|
|
100
|
+
"concurrently": false,
|
|
101
|
+
"method": "btree",
|
|
102
|
+
"with": {}
|
|
103
|
+
},
|
|
104
|
+
"integrations_slack_installations_team_unique": {
|
|
105
|
+
"name": "integrations_slack_installations_team_unique",
|
|
106
|
+
"columns": [
|
|
107
|
+
{
|
|
108
|
+
"expression": "team_id",
|
|
109
|
+
"isExpression": false,
|
|
110
|
+
"asc": true,
|
|
111
|
+
"nulls": "last"
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"isUnique": true,
|
|
115
|
+
"concurrently": false,
|
|
116
|
+
"method": "btree",
|
|
117
|
+
"with": {}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"foreignKeys": {},
|
|
121
|
+
"compositePrimaryKeys": {},
|
|
122
|
+
"uniqueConstraints": {},
|
|
123
|
+
"policies": {},
|
|
124
|
+
"checkConstraints": {},
|
|
125
|
+
"isRLSEnabled": false
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"enums": {},
|
|
129
|
+
"schemas": {},
|
|
130
|
+
"sequences": {},
|
|
131
|
+
"roles": {},
|
|
132
|
+
"policies": {},
|
|
133
|
+
"views": {},
|
|
134
|
+
"_meta": {
|
|
135
|
+
"columns": {},
|
|
136
|
+
"schemas": {},
|
|
137
|
+
"tables": {}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-slack",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -13,51 +13,41 @@
|
|
|
13
13
|
"types": "dist/index.d.ts",
|
|
14
14
|
"imports": {
|
|
15
15
|
"#test/*": "./test/*",
|
|
16
|
-
"#*":
|
|
17
|
-
"workspace-source": "./src/*",
|
|
18
|
-
"development": "./src/*",
|
|
19
|
-
"default": "./dist/*"
|
|
20
|
-
}
|
|
16
|
+
"#*": "./dist/*"
|
|
21
17
|
},
|
|
22
18
|
"exports": {
|
|
23
19
|
".": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"default": "./src/index.ts"
|
|
27
|
-
},
|
|
28
|
-
"default": {
|
|
29
|
-
"types": "./dist/index.d.ts",
|
|
30
|
-
"default": "./dist/index.js"
|
|
31
|
-
}
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
32
22
|
}
|
|
33
23
|
},
|
|
34
24
|
"dependencies": {
|
|
35
25
|
"drizzle-orm": "^0.45.2",
|
|
36
26
|
"ky": "^2.0.0",
|
|
37
27
|
"zod": "^4.4.3",
|
|
38
|
-
"@shipfox/api-auth-context": "
|
|
39
|
-
"@shipfox/api-integration-core-dto": "
|
|
40
|
-
"@shipfox/
|
|
41
|
-
"@shipfox/api-
|
|
42
|
-
"@shipfox/
|
|
43
|
-
"@shipfox/node-drizzle": "0.3.
|
|
44
|
-
"@shipfox/node-fastify": "0.2.
|
|
45
|
-
"@shipfox/node-opentelemetry": "0.5.
|
|
46
|
-
"@shipfox/node-postgres": "0.4.
|
|
28
|
+
"@shipfox/api-auth-context": "5.0.0",
|
|
29
|
+
"@shipfox/api-integration-core-dto": "5.0.0",
|
|
30
|
+
"@shipfox/api-integration-slack-dto": "5.0.0",
|
|
31
|
+
"@shipfox/api-workspaces": "5.0.0",
|
|
32
|
+
"@shipfox/config": "1.2.2",
|
|
33
|
+
"@shipfox/node-drizzle": "0.3.1",
|
|
34
|
+
"@shipfox/node-fastify": "0.2.3",
|
|
35
|
+
"@shipfox/node-opentelemetry": "0.5.2",
|
|
36
|
+
"@shipfox/node-postgres": "0.4.2"
|
|
47
37
|
},
|
|
48
38
|
"peerDependencies": {
|
|
49
|
-
"@shipfox/api-secrets": "
|
|
39
|
+
"@shipfox/api-secrets": "5.0.0"
|
|
50
40
|
},
|
|
51
41
|
"devDependencies": {
|
|
52
42
|
"@types/pg": "^8.15.5",
|
|
53
43
|
"drizzle-kit": "^0.31.10",
|
|
54
44
|
"fastify": "^5.3.3",
|
|
55
45
|
"fishery": "^2.4.0",
|
|
56
|
-
"@shipfox/
|
|
57
|
-
"@shipfox/
|
|
46
|
+
"@shipfox/biome": "1.8.2",
|
|
47
|
+
"@shipfox/swc": "1.2.6",
|
|
58
48
|
"@shipfox/ts-config": "1.3.8",
|
|
59
|
-
"@shipfox/typescript": "1.1.
|
|
60
|
-
"@shipfox/vitest": "1.2.
|
|
49
|
+
"@shipfox/typescript": "1.1.7",
|
|
50
|
+
"@shipfox/vitest": "1.2.3"
|
|
61
51
|
},
|
|
62
52
|
"scripts": {
|
|
63
53
|
"build": "shipfox-swc",
|
package/src/api/client.test.ts
CHANGED
|
@@ -217,6 +217,30 @@ describe('createSlackApiClient', () => {
|
|
|
217
217
|
expect(options.body.has('latest')).toBe(false);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it('normalizes a trailing slash in the Slack API base URL', async () => {
|
|
221
|
+
vi.stubEnv('SLACK_API_BASE_URL', 'http://127.0.0.1:0/');
|
|
222
|
+
vi.resetModules();
|
|
223
|
+
mocks.post.mockReturnValue(resolves({ok: true}));
|
|
224
|
+
try {
|
|
225
|
+
const {createSlackApiClient: createClient} = await import('./client.js');
|
|
226
|
+
|
|
227
|
+
await createClient().callMethod({
|
|
228
|
+
method: 'conversations.replies',
|
|
229
|
+
token: 'xoxb-secret',
|
|
230
|
+
arguments: {channel: 'C123', ts: '1721300000.000001'},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(mocks.post).toHaveBeenCalledWith('http://127.0.0.1:0/conversations.replies', {
|
|
234
|
+
headers: {authorization: 'Bearer xoxb-secret'},
|
|
235
|
+
body: new URLSearchParams({channel: 'C123', ts: '1721300000.000001'}),
|
|
236
|
+
timeout: 10_000,
|
|
237
|
+
});
|
|
238
|
+
} finally {
|
|
239
|
+
vi.unstubAllEnvs();
|
|
240
|
+
vi.resetModules();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
220
244
|
it.each([
|
|
221
245
|
[httpError(429, {'retry-after': '17'}), 'rate-limited', 17],
|
|
222
246
|
[httpError(413), 'content-too-large', undefined],
|
package/src/api/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '#core/errors.js';
|
|
9
9
|
|
|
10
10
|
const SLACK_API_TIMEOUT_MS = 10_000;
|
|
11
|
+
const trailingSlashesPattern = /\/+$/;
|
|
11
12
|
|
|
12
13
|
export interface SlackAuthorization {
|
|
13
14
|
accessToken: string;
|
|
@@ -232,7 +233,8 @@ function slackMethodArguments(input: Record<string, unknown>): URLSearchParams {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
function slackApiUrl(path: string): string {
|
|
235
|
-
|
|
236
|
+
const baseUrl = config.SLACK_API_BASE_URL.replace(trailingSlashesPattern, '');
|
|
237
|
+
return new URL(path, `${baseUrl}/`).toString();
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
function retryAfterSeconds(headers: Headers): number | undefined {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {disconnectSlackInstallation} from './disconnect.js';
|
|
2
|
+
|
|
3
|
+
vi.mock('#db/installations.js', () => ({
|
|
4
|
+
deleteSlackInstallationByConnectionId: vi.fn(() => Promise.resolve(true)),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
const {deleteSlackInstallationByConnectionId} = await import('#db/installations.js');
|
|
8
|
+
const deleteSlackInstallationByConnectionIdMock = vi.mocked(deleteSlackInstallationByConnectionId);
|
|
9
|
+
|
|
10
|
+
describe('disconnectSlackInstallation', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
deleteSlackInstallationByConnectionIdMock.mockClear();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('deletes stored tokens before deleting connection records', async () => {
|
|
16
|
+
const tx = Symbol('tx');
|
|
17
|
+
const calls: string[] = [];
|
|
18
|
+
const deleteSecrets = vi.fn(() => {
|
|
19
|
+
calls.push('secrets');
|
|
20
|
+
return Promise.resolve(1);
|
|
21
|
+
});
|
|
22
|
+
const deleteConnection = vi.fn(() => {
|
|
23
|
+
calls.push('connection');
|
|
24
|
+
return Promise.resolve(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await disconnectSlackInstallation({
|
|
28
|
+
connectionId: 'connection-1',
|
|
29
|
+
getConnection: vi.fn(() => Promise.resolve({workspaceId: 'workspace-1'})),
|
|
30
|
+
deleteSecrets,
|
|
31
|
+
transaction: async (fn) => await fn(tx),
|
|
32
|
+
deleteConnection,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(deleteSecrets).toHaveBeenCalledWith({
|
|
36
|
+
workspaceId: 'workspace-1',
|
|
37
|
+
namespace: 'system/integrations/slack/connection-1',
|
|
38
|
+
});
|
|
39
|
+
expect(deleteSlackInstallationByConnectionIdMock).toHaveBeenCalledWith('connection-1', {tx});
|
|
40
|
+
expect(deleteConnection).toHaveBeenCalledWith({connectionId: 'connection-1'}, {tx});
|
|
41
|
+
expect(calls).toEqual(['secrets', 'connection']);
|
|
42
|
+
expect(deleteSecrets.mock.invocationCallOrder[0]).toBeLessThan(
|
|
43
|
+
deleteSlackInstallationByConnectionIdMock.mock.invocationCallOrder[0] ?? 0,
|
|
44
|
+
);
|
|
45
|
+
expect(deleteSlackInstallationByConnectionIdMock.mock.invocationCallOrder[0]).toBeLessThan(
|
|
46
|
+
deleteConnection.mock.invocationCallOrder[0] ?? 0,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
package/src/core/webhook.test.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
IntegrationConnection,
|
|
5
5
|
} from '@shipfox/api-integration-core-dto';
|
|
6
6
|
import {db} from '#db/db.js';
|
|
7
|
-
import {upsertSlackInstallation} from '#db/installations.js';
|
|
7
|
+
import {getSlackInstallationByTeamId, upsertSlackInstallation} from '#db/installations.js';
|
|
8
8
|
import {slackInstallations} from '#db/schema/installations.js';
|
|
9
9
|
import {handleSlackCommand, handleSlackEvent} from './webhook.js';
|
|
10
10
|
|
|
@@ -30,7 +30,7 @@ function eventEnvelope(overrides: Record<string, unknown> = {}) {
|
|
|
30
30
|
api_app_id: 'A1',
|
|
31
31
|
event: {type: 'app_mention', channel: 'C1', user: 'U1', text: 'hello', ts: '1.0'},
|
|
32
32
|
event_id: 'Ev1',
|
|
33
|
-
event_time:
|
|
33
|
+
event_time: Math.floor(Date.now() / 1000) + 60,
|
|
34
34
|
...overrides,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
@@ -69,6 +69,7 @@ function handlers(connection = fakeConnection()) {
|
|
|
69
69
|
connection,
|
|
70
70
|
publishIntegrationEventReceived: vi.fn(() => Promise.resolve({published: true})),
|
|
71
71
|
recordDeliveryOnly: vi.fn(() => Promise.resolve()),
|
|
72
|
+
claimWebhookDelivery: vi.fn(() => Promise.resolve({claimed: true})),
|
|
72
73
|
getIntegrationConnectionById: vi.fn<GetIntegrationConnectionByIdFn>(() =>
|
|
73
74
|
Promise.resolve(connection),
|
|
74
75
|
),
|
|
@@ -126,6 +127,134 @@ describe('Slack webhook handlers', () => {
|
|
|
126
127
|
);
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
it('revokes an installation when Slack uninstalls the app', async () => {
|
|
131
|
+
const deps = handlers();
|
|
132
|
+
await seedInstallation(deps.connection);
|
|
133
|
+
|
|
134
|
+
const result = await handleSlackEvent({
|
|
135
|
+
tx: db(),
|
|
136
|
+
deliveryId: 'Ev-uninstalled',
|
|
137
|
+
envelope: eventEnvelope({event: {type: 'app_uninstalled'}}),
|
|
138
|
+
...deps,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const installation = await getSlackInstallationByTeamId('T1');
|
|
142
|
+
expect(result.outcome).toBe('revoked');
|
|
143
|
+
expect(installation?.status).toBe('revoked');
|
|
144
|
+
expect(deps.claimWebhookDelivery).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(deps.publishIntegrationEventReceived).not.toHaveBeenCalled();
|
|
146
|
+
expect(deps.getIntegrationConnectionById).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('revokes an installation when its bot token is revoked', async () => {
|
|
150
|
+
const deps = handlers();
|
|
151
|
+
await seedInstallation(deps.connection);
|
|
152
|
+
|
|
153
|
+
const result = await handleSlackEvent({
|
|
154
|
+
tx: db(),
|
|
155
|
+
deliveryId: 'Ev-bot-revoked',
|
|
156
|
+
envelope: eventEnvelope({event: {type: 'tokens_revoked', tokens: {bot: ['UBOT']}}}),
|
|
157
|
+
...deps,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const installation = await getSlackInstallationByTeamId('T1');
|
|
161
|
+
expect(result.outcome).toBe('revoked');
|
|
162
|
+
expect(installation?.status).toBe('revoked');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it.each([
|
|
166
|
+
['an OAuth token is revoked', {oauth: ['UOAUTH']}],
|
|
167
|
+
['the revoked token set is missing', undefined],
|
|
168
|
+
['the revoked bot token set is empty', {bot: []}],
|
|
169
|
+
])('keeps an installation active when %s', async (_description, tokens) => {
|
|
170
|
+
const deps = handlers();
|
|
171
|
+
await seedInstallation(deps.connection);
|
|
172
|
+
|
|
173
|
+
const result = await handleSlackEvent({
|
|
174
|
+
tx: db(),
|
|
175
|
+
deliveryId: 'Ev-oauth-revoked',
|
|
176
|
+
envelope: eventEnvelope({event: {type: 'tokens_revoked', tokens}}),
|
|
177
|
+
...deps,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const installation = await getSlackInstallationByTeamId('T1');
|
|
181
|
+
expect(result.outcome).toBe('unaffected-revocation');
|
|
182
|
+
expect(installation?.status).toBe('installed');
|
|
183
|
+
expect(deps.publishIntegrationEventReceived).not.toHaveBeenCalled();
|
|
184
|
+
expect(deps.claimWebhookDelivery).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('records an unknown lifecycle-event team without resolving a connection', async () => {
|
|
188
|
+
const deps = handlers();
|
|
189
|
+
|
|
190
|
+
const result = await handleSlackEvent({
|
|
191
|
+
tx: db(),
|
|
192
|
+
deliveryId: 'Ev-uninstalled-missing',
|
|
193
|
+
envelope: eventEnvelope({
|
|
194
|
+
team_id: 'T-missing',
|
|
195
|
+
event: {type: 'app_uninstalled'},
|
|
196
|
+
}),
|
|
197
|
+
...deps,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result.outcome).toBe('unknown-team');
|
|
201
|
+
expect(deps.getIntegrationConnectionById).not.toHaveBeenCalled();
|
|
202
|
+
expect(deps.claimWebhookDelivery).toHaveBeenCalledTimes(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('records an already revoked lifecycle-event installation without throwing', async () => {
|
|
206
|
+
const deps = handlers();
|
|
207
|
+
await seedInstallation(deps.connection, 'revoked');
|
|
208
|
+
|
|
209
|
+
const result = await handleSlackEvent({
|
|
210
|
+
tx: db(),
|
|
211
|
+
deliveryId: 'Ev-uninstalled-replay',
|
|
212
|
+
envelope: eventEnvelope({event: {type: 'app_uninstalled'}}),
|
|
213
|
+
...deps,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.outcome).toBe('revoked-installation');
|
|
217
|
+
expect(deps.claimWebhookDelivery).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('does not revoke an installation after a duplicate lifecycle delivery', async () => {
|
|
221
|
+
const deps = handlers();
|
|
222
|
+
deps.claimWebhookDelivery.mockResolvedValue({claimed: false});
|
|
223
|
+
await seedInstallation(deps.connection);
|
|
224
|
+
|
|
225
|
+
const result = await handleSlackEvent({
|
|
226
|
+
tx: db(),
|
|
227
|
+
deliveryId: 'Ev-uninstalled-duplicate',
|
|
228
|
+
envelope: eventEnvelope({event: {type: 'app_uninstalled'}}),
|
|
229
|
+
...deps,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const installation = await getSlackInstallationByTeamId('T1');
|
|
233
|
+
expect(result.outcome).toBe('duplicate');
|
|
234
|
+
expect(installation?.status).toBe('installed');
|
|
235
|
+
expect(deps.getIntegrationConnectionById).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not revoke a reconnected installation for a delayed lifecycle event', async () => {
|
|
239
|
+
const deps = handlers();
|
|
240
|
+
await seedInstallation(deps.connection);
|
|
241
|
+
await seedInstallation(deps.connection);
|
|
242
|
+
|
|
243
|
+
const result = await handleSlackEvent({
|
|
244
|
+
tx: db(),
|
|
245
|
+
deliveryId: 'Ev-uninstalled-stale',
|
|
246
|
+
envelope: eventEnvelope({
|
|
247
|
+
event: {type: 'app_uninstalled'},
|
|
248
|
+
event_time: Math.floor(Date.now() / 1000) - 60,
|
|
249
|
+
}),
|
|
250
|
+
...deps,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const installation = await getSlackInstallationByTeamId('T1');
|
|
254
|
+
expect(result.outcome).toBe('stale-lifecycle-event');
|
|
255
|
+
expect(installation?.status).toBe('installed');
|
|
256
|
+
});
|
|
257
|
+
|
|
129
258
|
it.each([
|
|
130
259
|
['a bot-authored event', {event: {type: 'message', bot_id: 'B1'}}],
|
|
131
260
|
['an event from this installation bot', {event: {type: 'reaction_added', user: 'UBOT'}}],
|