@shipfox/api-integration-github 2.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.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +138 -0
  5. package/LICENSE +21 -0
  6. package/dist/api/client.d.ts +90 -0
  7. package/dist/api/client.d.ts.map +1 -0
  8. package/dist/api/client.js +306 -0
  9. package/dist/api/client.js.map +1 -0
  10. package/dist/api/installation-token-envelope.d.ts +36 -0
  11. package/dist/api/installation-token-envelope.d.ts.map +1 -0
  12. package/dist/api/installation-token-envelope.js +123 -0
  13. package/dist/api/installation-token-envelope.js.map +1 -0
  14. package/dist/api/installation-token-provider.d.ts +23 -0
  15. package/dist/api/installation-token-provider.d.ts.map +1 -0
  16. package/dist/api/installation-token-provider.js +130 -0
  17. package/dist/api/installation-token-provider.js.map +1 -0
  18. package/dist/api/shared-installation-token-cache.d.ts +43 -0
  19. package/dist/api/shared-installation-token-cache.d.ts.map +1 -0
  20. package/dist/api/shared-installation-token-cache.js +224 -0
  21. package/dist/api/shared-installation-token-cache.js.map +1 -0
  22. package/dist/config.d.ts +15 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +44 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/core/actions-logs.d.ts +47 -0
  27. package/dist/core/actions-logs.d.ts.map +1 -0
  28. package/dist/core/actions-logs.js +134 -0
  29. package/dist/core/actions-logs.js.map +1 -0
  30. package/dist/core/agent-tool-selection.d.ts +4 -0
  31. package/dist/core/agent-tool-selection.d.ts.map +1 -0
  32. package/dist/core/agent-tool-selection.js +38 -0
  33. package/dist/core/agent-tool-selection.js.map +1 -0
  34. package/dist/core/agent-tools.d.ts +50 -0
  35. package/dist/core/agent-tools.d.ts.map +1 -0
  36. package/dist/core/agent-tools.js +1122 -0
  37. package/dist/core/agent-tools.js.map +1 -0
  38. package/dist/core/errors.d.ts +15 -0
  39. package/dist/core/errors.d.ts.map +1 -0
  40. package/dist/core/errors.js +22 -0
  41. package/dist/core/errors.js.map +1 -0
  42. package/dist/core/install.d.ts +38 -0
  43. package/dist/core/install.d.ts.map +1 -0
  44. package/dist/core/install.js +64 -0
  45. package/dist/core/install.js.map +1 -0
  46. package/dist/core/source-control.d.ts +15 -0
  47. package/dist/core/source-control.d.ts.map +1 -0
  48. package/dist/core/source-control.js +175 -0
  49. package/dist/core/source-control.js.map +1 -0
  50. package/dist/core/state.d.ts +12 -0
  51. package/dist/core/state.d.ts.map +1 -0
  52. package/dist/core/state.js +54 -0
  53. package/dist/core/state.js.map +1 -0
  54. package/dist/core/webhook.d.ts +20 -0
  55. package/dist/core/webhook.d.ts.map +1 -0
  56. package/dist/core/webhook.js +216 -0
  57. package/dist/core/webhook.js.map +1 -0
  58. package/dist/db/db.d.ts +433 -0
  59. package/dist/db/db.d.ts.map +1 -0
  60. package/dist/db/db.js +18 -0
  61. package/dist/db/db.js.map +1 -0
  62. package/dist/db/installation-token-lock.d.ts +8 -0
  63. package/dist/db/installation-token-lock.d.ts.map +1 -0
  64. package/dist/db/installation-token-lock.js +37 -0
  65. package/dist/db/installation-token-lock.js.map +1 -0
  66. package/dist/db/installations.d.ts +33 -0
  67. package/dist/db/installations.d.ts.map +1 -0
  68. package/dist/db/installations.js +56 -0
  69. package/dist/db/installations.js.map +1 -0
  70. package/dist/db/migrations.d.ts +2 -0
  71. package/dist/db/migrations.d.ts.map +1 -0
  72. package/dist/db/migrations.js +5 -0
  73. package/dist/db/migrations.js.map +1 -0
  74. package/dist/db/schema/common.d.ts +2 -0
  75. package/dist/db/schema/common.d.ts.map +1 -0
  76. package/dist/db/schema/common.js +4 -0
  77. package/dist/db/schema/common.js.map +1 -0
  78. package/dist/db/schema/installations.d.ts +218 -0
  79. package/dist/db/schema/installations.d.ts.map +1 -0
  80. package/dist/db/schema/installations.js +46 -0
  81. package/dist/db/schema/installations.js.map +1 -0
  82. package/dist/index.d.ts +53 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +66 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/metrics/index.d.ts +2 -0
  87. package/dist/metrics/index.d.ts.map +1 -0
  88. package/dist/metrics/index.js +3 -0
  89. package/dist/metrics/index.js.map +1 -0
  90. package/dist/metrics/instance.d.ts +14 -0
  91. package/dist/metrics/instance.d.ts.map +1 -0
  92. package/dist/metrics/instance.js +75 -0
  93. package/dist/metrics/instance.js.map +1 -0
  94. package/dist/presentation/dto/integrations.d.ts +15 -0
  95. package/dist/presentation/dto/integrations.d.ts.map +1 -0
  96. package/dist/presentation/dto/integrations.js +10 -0
  97. package/dist/presentation/dto/integrations.js.map +1 -0
  98. package/dist/presentation/e2eRoutes/create-connection.d.ts +11 -0
  99. package/dist/presentation/e2eRoutes/create-connection.d.ts.map +1 -0
  100. package/dist/presentation/e2eRoutes/create-connection.js +57 -0
  101. package/dist/presentation/e2eRoutes/create-connection.js.map +1 -0
  102. package/dist/presentation/e2eRoutes/index.d.ts +5 -0
  103. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  104. package/dist/presentation/e2eRoutes/index.js +11 -0
  105. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  106. package/dist/presentation/routes/errors.d.ts +2 -0
  107. package/dist/presentation/routes/errors.d.ts.map +1 -0
  108. package/dist/presentation/routes/errors.js +46 -0
  109. package/dist/presentation/routes/errors.js.map +1 -0
  110. package/dist/presentation/routes/install.d.ts +13 -0
  111. package/dist/presentation/routes/install.d.ts.map +1 -0
  112. package/dist/presentation/routes/install.js +77 -0
  113. package/dist/presentation/routes/install.js.map +1 -0
  114. package/dist/presentation/routes/webhooks.d.ts +16 -0
  115. package/dist/presentation/routes/webhooks.d.ts.map +1 -0
  116. package/dist/presentation/routes/webhooks.js +110 -0
  117. package/dist/presentation/routes/webhooks.js.map +1 -0
  118. package/dist/tsconfig.test.tsbuildinfo +1 -0
  119. package/drizzle/0000_initial.sql +17 -0
  120. package/drizzle/meta/_journal.json +13 -0
  121. package/drizzle.config.ts +7 -0
  122. package/package.json +71 -0
  123. package/src/api/client.test.ts +94 -0
  124. package/src/api/client.ts +525 -0
  125. package/src/api/installation-token-envelope.ts +171 -0
  126. package/src/api/installation-token-provider.test.ts +241 -0
  127. package/src/api/installation-token-provider.ts +224 -0
  128. package/src/api/shared-installation-token-cache.test.ts +294 -0
  129. package/src/api/shared-installation-token-cache.ts +346 -0
  130. package/src/config.test.ts +14 -0
  131. package/src/config.ts +47 -0
  132. package/src/connection-external-url.test.ts +78 -0
  133. package/src/core/actions-logs.test.ts +156 -0
  134. package/src/core/actions-logs.ts +186 -0
  135. package/src/core/agent-tool-selection.test.ts +41 -0
  136. package/src/core/agent-tool-selection.ts +42 -0
  137. package/src/core/agent-tools.test.ts +539 -0
  138. package/src/core/agent-tools.ts +1269 -0
  139. package/src/core/errors.ts +23 -0
  140. package/src/core/install.test.ts +238 -0
  141. package/src/core/install.ts +119 -0
  142. package/src/core/source-control.test.ts +327 -0
  143. package/src/core/source-control.ts +223 -0
  144. package/src/core/state.test.ts +43 -0
  145. package/src/core/state.ts +85 -0
  146. package/src/core/webhook.test.ts +522 -0
  147. package/src/core/webhook.ts +296 -0
  148. package/src/db/db.ts +18 -0
  149. package/src/db/installation-token-lock.test.ts +68 -0
  150. package/src/db/installation-token-lock.ts +39 -0
  151. package/src/db/installations.test.ts +67 -0
  152. package/src/db/installations.ts +108 -0
  153. package/src/db/migrations.ts +4 -0
  154. package/src/db/schema/common.ts +3 -0
  155. package/src/db/schema/installations.ts +46 -0
  156. package/src/index.ts +124 -0
  157. package/src/metrics/index.ts +1 -0
  158. package/src/metrics/instance.ts +82 -0
  159. package/src/presentation/dto/integrations.ts +8 -0
  160. package/src/presentation/e2eRoutes/create-connection.ts +70 -0
  161. package/src/presentation/e2eRoutes/index.test.ts +126 -0
  162. package/src/presentation/e2eRoutes/index.ts +14 -0
  163. package/src/presentation/routes/errors.ts +43 -0
  164. package/src/presentation/routes/install.test.ts +279 -0
  165. package/src/presentation/routes/install.ts +93 -0
  166. package/src/presentation/routes/webhooks.test.ts +560 -0
  167. package/src/presentation/routes/webhooks.ts +115 -0
  168. package/test/env.ts +16 -0
  169. package/test/factories/github-installation.ts +35 -0
  170. package/test/factories/index.ts +1 -0
  171. package/test/fixtures/github-webhook.ts +20 -0
  172. package/test/globalSetup.ts +14 -0
  173. package/test/index.ts +2 -0
  174. package/test/setup.ts +17 -0
  175. package/tsconfig.build.json +9 -0
  176. package/tsconfig.build.tsbuildinfo +1 -0
  177. package/tsconfig.json +3 -0
  178. package/tsconfig.test.json +9 -0
  179. package/vitest.config.ts +12 -0
@@ -0,0 +1,522 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import type {
3
+ GetIntegrationConnectionByIdFn,
4
+ IntegrationConnection,
5
+ PublishIntegrationEventReceivedFn,
6
+ PublishSourcePushFn,
7
+ RecordDeliveryOnlyFn,
8
+ } from '@shipfox/api-integration-core-dto';
9
+ import {db} from '#db/db.js';
10
+ import {githubInstallations} from '#db/schema/installations.js';
11
+ import {githubInstallationFactory, githubPushPayload} from '#test/index.js';
12
+ import {handleGithubEvent} from './webhook.js';
13
+
14
+ function fakeConnection(overrides: Partial<IntegrationConnection> = {}): IntegrationConnection {
15
+ return {
16
+ id: randomUUID(),
17
+ workspaceId: randomUUID(),
18
+ provider: 'github',
19
+ externalAccountId: '123',
20
+ slug: 'github_shipfox',
21
+ displayName: 'GitHub shipfox',
22
+ lifecycleStatus: 'active',
23
+ createdAt: new Date(),
24
+ updatedAt: new Date(),
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ function deps(
30
+ options: {
31
+ connection?: IntegrationConnection | undefined;
32
+ publishIntegrationEventReceivedResult?: {published: boolean};
33
+ publishSourcePushResult?: {published: boolean};
34
+ } = {},
35
+ ) {
36
+ return {
37
+ publishIntegrationEventReceived: vi.fn<PublishIntegrationEventReceivedFn>(() =>
38
+ Promise.resolve(options.publishIntegrationEventReceivedResult ?? {published: true}),
39
+ ),
40
+ publishSourcePush: vi.fn<PublishSourcePushFn>(() =>
41
+ Promise.resolve(options.publishSourcePushResult ?? {published: true}),
42
+ ),
43
+ recordDeliveryOnly: vi.fn<RecordDeliveryOnlyFn>(() => Promise.resolve()),
44
+ getIntegrationConnectionById: vi.fn<GetIntegrationConnectionByIdFn>(() =>
45
+ Promise.resolve(options.connection ?? fakeConnection()),
46
+ ),
47
+ };
48
+ }
49
+
50
+ function firstPublishSourcePushCall(publishSourcePush: {
51
+ mock: {calls: Array<Parameters<PublishSourcePushFn>>};
52
+ }): Parameters<PublishSourcePushFn>[0] {
53
+ const [call] = publishSourcePush.mock.calls;
54
+ if (!call) {
55
+ throw new Error('Expected publishSourcePush to be called');
56
+ }
57
+
58
+ return call[0];
59
+ }
60
+
61
+ function firstPublishIntegrationEventReceivedCall(publishIntegrationEventReceived: {
62
+ mock: {calls: Array<Parameters<PublishIntegrationEventReceivedFn>>};
63
+ }): Parameters<PublishIntegrationEventReceivedFn>[0] {
64
+ const [call] = publishIntegrationEventReceived.mock.calls;
65
+ if (!call) {
66
+ throw new Error('Expected publishIntegrationEventReceived to be called');
67
+ }
68
+
69
+ return call[0];
70
+ }
71
+
72
+ function firstRecordDeliveryOnlyCall(recordDeliveryOnly: {
73
+ mock: {calls: Array<Parameters<RecordDeliveryOnlyFn>>};
74
+ }): Parameters<RecordDeliveryOnlyFn>[0] {
75
+ const [call] = recordDeliveryOnly.mock.calls;
76
+ if (!call) {
77
+ throw new Error('Expected recordDeliveryOnly to be called');
78
+ }
79
+
80
+ return call[0];
81
+ }
82
+
83
+ async function seedInstallation(installationId: number, connectionId?: string): Promise<void> {
84
+ await githubInstallationFactory.create({
85
+ installationId: String(installationId),
86
+ ...(connectionId !== undefined && {connectionId}),
87
+ });
88
+ }
89
+
90
+ describe('handleGithubEvent', () => {
91
+ beforeEach(async () => {
92
+ await db().delete(githubInstallations);
93
+ });
94
+
95
+ it('publishes a mapped event for a valid push from a known installation', async () => {
96
+ const installationId = 7777;
97
+ const connection = fakeConnection();
98
+ await seedInstallation(installationId, connection.id);
99
+ const handlers = deps({connection});
100
+ const deliveryId = randomUUID();
101
+ const payload = githubPushPayload({
102
+ installationId,
103
+ repositoryId: 42,
104
+ ref: 'refs/heads/main',
105
+ defaultBranch: 'main',
106
+ sha: 'abc123',
107
+ });
108
+
109
+ const result = await handleGithubEvent({
110
+ tx: db(),
111
+ deliveryId,
112
+ event: 'push',
113
+ payload,
114
+ ...handlers,
115
+ });
116
+
117
+ expect(result.outcome).toBe('published');
118
+ expect(handlers.recordDeliveryOnly).not.toHaveBeenCalled();
119
+ expect(handlers.publishIntegrationEventReceived).not.toHaveBeenCalled();
120
+ expect(handlers.getIntegrationConnectionById).toHaveBeenCalledWith(
121
+ connection.id,
122
+ expect.objectContaining({tx: expect.anything()}),
123
+ );
124
+ expect(firstPublishSourcePushCall(handlers.publishSourcePush)).toMatchObject({
125
+ provider: 'github',
126
+ source: connection.slug,
127
+ deliveryId,
128
+ workspaceId: connection.workspaceId,
129
+ connectionId: connection.id,
130
+ connectionName: connection.displayName,
131
+ rawPayload: payload,
132
+ push: {
133
+ externalRepositoryId: 'github:42',
134
+ ref: 'main',
135
+ headCommitSha: 'abc123',
136
+ isDefaultBranch: true,
137
+ },
138
+ });
139
+ });
140
+
141
+ it('returns duplicate when a push delivery was already published', async () => {
142
+ const installationId = 7778;
143
+ const connection = fakeConnection();
144
+ await seedInstallation(installationId, connection.id);
145
+ const handlers = deps({connection, publishSourcePushResult: {published: false}});
146
+
147
+ const result = await handleGithubEvent({
148
+ tx: db(),
149
+ deliveryId: randomUUID(),
150
+ event: 'push',
151
+ payload: githubPushPayload({
152
+ installationId,
153
+ repositoryId: 42,
154
+ ref: 'refs/heads/main',
155
+ defaultBranch: 'main',
156
+ sha: 'abc123',
157
+ }),
158
+ ...handlers,
159
+ });
160
+
161
+ expect(result.outcome).toBe('duplicate');
162
+ expect(handlers.publishSourcePush).toHaveBeenCalledTimes(1);
163
+ expect(handlers.publishIntegrationEventReceived).not.toHaveBeenCalled();
164
+ });
165
+
166
+ it('publishes a generic envelope for a branch deletion', async () => {
167
+ const installationId = 7779;
168
+ const connection = fakeConnection();
169
+ await seedInstallation(installationId, connection.id);
170
+ const handlers = deps({connection});
171
+ const deliveryId = randomUUID();
172
+ const payload = githubPushPayload({
173
+ installationId,
174
+ repositoryId: 42,
175
+ ref: 'refs/heads/main',
176
+ defaultBranch: 'main',
177
+ sha: '0000000000000000000000000000000000000000',
178
+ });
179
+
180
+ const result = await handleGithubEvent({
181
+ tx: db(),
182
+ deliveryId,
183
+ event: 'push',
184
+ payload,
185
+ ...handlers,
186
+ });
187
+
188
+ expect(result.outcome).toBe('published-push-envelope-only');
189
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
190
+ expect(
191
+ firstPublishIntegrationEventReceivedCall(handlers.publishIntegrationEventReceived),
192
+ ).toMatchObject({
193
+ event: {
194
+ source: connection.slug,
195
+ event: 'push',
196
+ workspaceId: connection.workspaceId,
197
+ connectionId: connection.id,
198
+ connectionName: connection.displayName,
199
+ deliveryId,
200
+ payload,
201
+ },
202
+ });
203
+ });
204
+
205
+ it('returns duplicate-push-envelope-only when a branch deletion was already published', async () => {
206
+ const installationId = 7780;
207
+ await seedInstallation(installationId);
208
+ const handlers = deps({publishIntegrationEventReceivedResult: {published: false}});
209
+
210
+ const result = await handleGithubEvent({
211
+ tx: db(),
212
+ deliveryId: randomUUID(),
213
+ event: 'push',
214
+ payload: githubPushPayload({
215
+ installationId,
216
+ repositoryId: 42,
217
+ ref: 'refs/heads/main',
218
+ defaultBranch: 'main',
219
+ sha: '0000000000000000000000000000000000000000',
220
+ }),
221
+ ...handlers,
222
+ });
223
+
224
+ expect(result.outcome).toBe('duplicate-push-envelope-only');
225
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
226
+ expect(handlers.publishIntegrationEventReceived).toHaveBeenCalledTimes(1);
227
+ });
228
+
229
+ it('publishes a generic envelope when a push payload fails schema validation', async () => {
230
+ const installationId = 7786;
231
+ const connection = fakeConnection();
232
+ await seedInstallation(installationId, connection.id);
233
+ const handlers = deps({connection});
234
+ const deliveryId = randomUUID();
235
+ const payload = {
236
+ installation: {id: installationId},
237
+ ref: 'refs/heads/main',
238
+ };
239
+
240
+ const result = await handleGithubEvent({
241
+ tx: db(),
242
+ deliveryId,
243
+ event: 'push',
244
+ payload,
245
+ ...handlers,
246
+ });
247
+
248
+ expect(result.outcome).toBe('published-envelope');
249
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
250
+ expect(
251
+ firstPublishIntegrationEventReceivedCall(handlers.publishIntegrationEventReceived),
252
+ ).toMatchObject({
253
+ event: {
254
+ source: connection.slug,
255
+ event: 'push',
256
+ workspaceId: connection.workspaceId,
257
+ connectionId: connection.id,
258
+ connectionName: connection.displayName,
259
+ deliveryId,
260
+ payload,
261
+ },
262
+ });
263
+ expect(handlers.recordDeliveryOnly).not.toHaveBeenCalled();
264
+ });
265
+
266
+ it('publishes a generic envelope for a non-push event with an action', async () => {
267
+ const installationId = 7781;
268
+ const connection = fakeConnection();
269
+ await seedInstallation(installationId, connection.id);
270
+ const handlers = deps({connection});
271
+ const deliveryId = randomUUID();
272
+ const payload = {
273
+ action: 'opened',
274
+ installation: {id: installationId},
275
+ pull_request: {number: 17},
276
+ };
277
+
278
+ const result = await handleGithubEvent({
279
+ tx: db(),
280
+ deliveryId,
281
+ event: 'pull_request',
282
+ payload,
283
+ ...handlers,
284
+ });
285
+
286
+ expect(result.outcome).toBe('published-envelope');
287
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
288
+ expect(
289
+ firstPublishIntegrationEventReceivedCall(handlers.publishIntegrationEventReceived),
290
+ ).toMatchObject({
291
+ event: {
292
+ source: connection.slug,
293
+ event: 'pull_request.opened',
294
+ workspaceId: connection.workspaceId,
295
+ connectionId: connection.id,
296
+ deliveryId,
297
+ payload,
298
+ },
299
+ });
300
+ });
301
+
302
+ it('requests cached installation token cleanup when the installation is deleted', async () => {
303
+ const installationId = 7791;
304
+ const connection = fakeConnection();
305
+ await seedInstallation(installationId, connection.id);
306
+ const handlers = deps({connection});
307
+ const deleteInstallationTokenSecret = vi.fn(() => Promise.resolve());
308
+
309
+ const result = await handleGithubEvent({
310
+ tx: db(),
311
+ deliveryId: randomUUID(),
312
+ event: 'installation',
313
+ payload: {action: 'deleted', installation: {id: installationId}},
314
+ deleteInstallationTokenSecret,
315
+ ...handlers,
316
+ });
317
+
318
+ expect(result.outcome).toBe('published-envelope');
319
+ expect(deleteInstallationTokenSecret).toHaveBeenCalledWith({
320
+ workspaceId: connection.workspaceId,
321
+ installationId,
322
+ });
323
+ });
324
+
325
+ it('does not clean up cached installation tokens for duplicate lifecycle deliveries', async () => {
326
+ const installationId = 7792;
327
+ const connection = fakeConnection();
328
+ await seedInstallation(installationId, connection.id);
329
+ const handlers = deps({connection, publishIntegrationEventReceivedResult: {published: false}});
330
+ const deleteInstallationTokenSecret = vi.fn(() => Promise.resolve());
331
+
332
+ const result = await handleGithubEvent({
333
+ tx: db(),
334
+ deliveryId: randomUUID(),
335
+ event: 'installation',
336
+ payload: {action: 'suspend', installation: {id: installationId}},
337
+ deleteInstallationTokenSecret,
338
+ ...handlers,
339
+ });
340
+
341
+ expect(result.outcome).toBe('duplicate-envelope');
342
+ expect(deleteInstallationTokenSecret).not.toHaveBeenCalled();
343
+ });
344
+
345
+ it('does not fail lifecycle webhook handling when cached token cleanup fails', async () => {
346
+ const installationId = 7793;
347
+ const connection = fakeConnection();
348
+ await seedInstallation(installationId, connection.id);
349
+ const handlers = deps({connection});
350
+ const deleteInstallationTokenSecret = vi.fn(() => Promise.reject(new Error('store down')));
351
+
352
+ const result = await handleGithubEvent({
353
+ tx: db(),
354
+ deliveryId: randomUUID(),
355
+ event: 'installation',
356
+ payload: {action: 'deleted', installation: {id: installationId}},
357
+ deleteInstallationTokenSecret,
358
+ ...handlers,
359
+ });
360
+
361
+ expect(result.outcome).toBe('published-envelope');
362
+ expect(deleteInstallationTokenSecret).toHaveBeenCalledWith({
363
+ workspaceId: connection.workspaceId,
364
+ installationId,
365
+ });
366
+ });
367
+
368
+ it('publishes a bare resource envelope when action is malformed', async () => {
369
+ const installationId = 7782;
370
+ const connection = fakeConnection();
371
+ await seedInstallation(installationId, connection.id);
372
+ const handlers = deps({connection});
373
+ const deliveryId = randomUUID();
374
+ const payload = {
375
+ action: null,
376
+ installation: {id: installationId},
377
+ pull_request: {number: 17},
378
+ };
379
+
380
+ const result = await handleGithubEvent({
381
+ tx: db(),
382
+ deliveryId,
383
+ event: 'pull_request',
384
+ payload,
385
+ ...handlers,
386
+ });
387
+
388
+ expect(result.outcome).toBe('published-envelope');
389
+ expect(
390
+ firstPublishIntegrationEventReceivedCall(handlers.publishIntegrationEventReceived),
391
+ ).toMatchObject({
392
+ event: {
393
+ source: connection.slug,
394
+ event: 'pull_request',
395
+ workspaceId: connection.workspaceId,
396
+ connectionId: connection.id,
397
+ connectionName: connection.displayName,
398
+ deliveryId,
399
+ payload,
400
+ },
401
+ });
402
+ expect(handlers.recordDeliveryOnly).not.toHaveBeenCalled();
403
+ });
404
+
405
+ it('returns duplicate-envelope when a generic envelope delivery was already published', async () => {
406
+ const installationId = 7783;
407
+ await seedInstallation(installationId);
408
+ const handlers = deps({publishIntegrationEventReceivedResult: {published: false}});
409
+
410
+ const result = await handleGithubEvent({
411
+ tx: db(),
412
+ deliveryId: randomUUID(),
413
+ event: 'fork',
414
+ payload: {installation: {id: installationId}},
415
+ ...handlers,
416
+ });
417
+
418
+ expect(result.outcome).toBe('duplicate-envelope');
419
+ expect(handlers.publishIntegrationEventReceived).toHaveBeenCalledTimes(1);
420
+ });
421
+
422
+ it('records the delivery only when there is no installation id', async () => {
423
+ const handlers = deps();
424
+ const deliveryId = randomUUID();
425
+
426
+ const result = await handleGithubEvent({
427
+ tx: db(),
428
+ deliveryId,
429
+ event: 'push',
430
+ payload: {ref: 'refs/heads/main'},
431
+ ...handlers,
432
+ });
433
+
434
+ expect(result.outcome).toBe('no-installation-id');
435
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
436
+ expect(handlers.publishIntegrationEventReceived).not.toHaveBeenCalled();
437
+ expect(firstRecordDeliveryOnlyCall(handlers.recordDeliveryOnly)).toMatchObject({
438
+ provider: 'github',
439
+ deliveryId,
440
+ });
441
+ });
442
+
443
+ it('records the delivery only for an unknown installation', async () => {
444
+ const handlers = deps();
445
+ const deliveryId = randomUUID();
446
+
447
+ const result = await handleGithubEvent({
448
+ tx: db(),
449
+ deliveryId,
450
+ event: 'push',
451
+ payload: githubPushPayload({
452
+ installationId: 999999,
453
+ repositoryId: 42,
454
+ ref: 'refs/heads/main',
455
+ defaultBranch: 'main',
456
+ sha: 'abc123',
457
+ }),
458
+ ...handlers,
459
+ });
460
+
461
+ expect(result.outcome).toBe('unknown-installation');
462
+ expect(handlers.getIntegrationConnectionById).not.toHaveBeenCalled();
463
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
464
+ expect(firstRecordDeliveryOnlyCall(handlers.recordDeliveryOnly)).toMatchObject({
465
+ provider: 'github',
466
+ deliveryId,
467
+ });
468
+ });
469
+
470
+ it('records the delivery only when the installation has no connection', async () => {
471
+ const installationId = 7784;
472
+ await seedInstallation(installationId);
473
+ const handlers = deps();
474
+ handlers.getIntegrationConnectionById.mockResolvedValue(undefined);
475
+
476
+ const result = await handleGithubEvent({
477
+ tx: db(),
478
+ deliveryId: randomUUID(),
479
+ event: 'push',
480
+ payload: githubPushPayload({
481
+ installationId,
482
+ repositoryId: 42,
483
+ ref: 'refs/heads/main',
484
+ defaultBranch: 'main',
485
+ sha: 'abc123',
486
+ }),
487
+ ...handlers,
488
+ });
489
+
490
+ expect(result.outcome).toBe('missing-connection');
491
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
492
+ expect(handlers.recordDeliveryOnly).toHaveBeenCalledTimes(1);
493
+ });
494
+
495
+ it.each([
496
+ 'disabled',
497
+ 'error',
498
+ ] as const)('records the delivery only when the connection is %s', async (lifecycleStatus) => {
499
+ const installationId = 7785;
500
+ const connection = fakeConnection({lifecycleStatus});
501
+ await seedInstallation(installationId, connection.id);
502
+ const handlers = deps({connection});
503
+
504
+ const result = await handleGithubEvent({
505
+ tx: db(),
506
+ deliveryId: randomUUID(),
507
+ event: 'push',
508
+ payload: githubPushPayload({
509
+ installationId,
510
+ repositoryId: 42,
511
+ ref: 'refs/heads/main',
512
+ defaultBranch: 'main',
513
+ sha: 'abc123',
514
+ }),
515
+ ...handlers,
516
+ });
517
+
518
+ expect(result.outcome).toBe('inactive-connection');
519
+ expect(handlers.publishSourcePush).not.toHaveBeenCalled();
520
+ expect(handlers.recordDeliveryOnly).toHaveBeenCalledTimes(1);
521
+ });
522
+ });