@friggframework/devtools 2.0.0--canary.606.7d3473f.0 → 2.0.0--canary.608.ba60ba6.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/frigg-cli/package.json +2 -2
- package/infrastructure/domains/integration/integration-builder.js +115 -0
- package/infrastructure/domains/integration/integration-builder.test.js +217 -100
- package/infrastructure/domains/security/iam-generator.js +14 -5
- package/infrastructure/domains/security/iam-generator.test.js +38 -19
- package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +399 -384
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +105 -55
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +79 -27
- package/package.json +7 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Integration Builder
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tests integration-specific Lambda functions and SQS queues
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -17,9 +17,7 @@ describe('IntegrationBuilder', () => {
|
|
|
17
17
|
describe('shouldExecute()', () => {
|
|
18
18
|
it('should return true when integrations array has items', () => {
|
|
19
19
|
const appDefinition = {
|
|
20
|
-
integrations: [
|
|
21
|
-
{ Definition: { name: 'test' } },
|
|
22
|
-
],
|
|
20
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
23
21
|
};
|
|
24
22
|
|
|
25
23
|
expect(integrationBuilder.shouldExecute(appDefinition)).toBe(true);
|
|
@@ -85,9 +83,7 @@ describe('IntegrationBuilder', () => {
|
|
|
85
83
|
|
|
86
84
|
it('should error when integration is missing Definition', () => {
|
|
87
85
|
const appDefinition = {
|
|
88
|
-
integrations: [
|
|
89
|
-
{ someOtherField: 'value' },
|
|
90
|
-
],
|
|
86
|
+
integrations: [{ someOtherField: 'value' }],
|
|
91
87
|
};
|
|
92
88
|
|
|
93
89
|
const result = integrationBuilder.validate(appDefinition);
|
|
@@ -100,9 +96,7 @@ describe('IntegrationBuilder', () => {
|
|
|
100
96
|
|
|
101
97
|
it('should error when integration Definition is missing name', () => {
|
|
102
98
|
const appDefinition = {
|
|
103
|
-
integrations: [
|
|
104
|
-
{ Definition: {} },
|
|
105
|
-
],
|
|
99
|
+
integrations: [{ Definition: {} }],
|
|
106
100
|
};
|
|
107
101
|
|
|
108
102
|
const result = integrationBuilder.validate(appDefinition);
|
|
@@ -132,9 +126,7 @@ describe('IntegrationBuilder', () => {
|
|
|
132
126
|
describe('build()', () => {
|
|
133
127
|
it('should create HTTP handler for integration', async () => {
|
|
134
128
|
const appDefinition = {
|
|
135
|
-
integrations: [
|
|
136
|
-
{ Definition: { name: 'hubspot' } },
|
|
137
|
-
],
|
|
129
|
+
integrations: [{ Definition: { name: 'hubspot' } }],
|
|
138
130
|
};
|
|
139
131
|
|
|
140
132
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -147,9 +139,7 @@ describe('IntegrationBuilder', () => {
|
|
|
147
139
|
|
|
148
140
|
it('should configure HTTP API event for integration', async () => {
|
|
149
141
|
const appDefinition = {
|
|
150
|
-
integrations: [
|
|
151
|
-
{ Definition: { name: 'salesforce' } },
|
|
152
|
-
],
|
|
142
|
+
integrations: [{ Definition: { name: 'salesforce' } }],
|
|
153
143
|
};
|
|
154
144
|
|
|
155
145
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -166,9 +156,7 @@ describe('IntegrationBuilder', () => {
|
|
|
166
156
|
|
|
167
157
|
it('should create SQS queue for integration', async () => {
|
|
168
158
|
const appDefinition = {
|
|
169
|
-
integrations: [
|
|
170
|
-
{ Definition: { name: 'slack' } },
|
|
171
|
-
],
|
|
159
|
+
integrations: [{ Definition: { name: 'slack' } }],
|
|
172
160
|
};
|
|
173
161
|
|
|
174
162
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -179,39 +167,39 @@ describe('IntegrationBuilder', () => {
|
|
|
179
167
|
|
|
180
168
|
it('should configure queue with correct retention and visibility timeout', async () => {
|
|
181
169
|
const appDefinition = {
|
|
182
|
-
integrations: [
|
|
183
|
-
{ Definition: { name: 'test' } },
|
|
184
|
-
],
|
|
170
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
185
171
|
};
|
|
186
172
|
|
|
187
173
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
188
174
|
|
|
189
|
-
expect(
|
|
190
|
-
|
|
175
|
+
expect(
|
|
176
|
+
result.resources.TestQueue.Properties.MessageRetentionPeriod
|
|
177
|
+
).toBe(345600);
|
|
178
|
+
expect(
|
|
179
|
+
result.resources.TestQueue.Properties.VisibilityTimeout
|
|
180
|
+
).toBe(1800);
|
|
191
181
|
});
|
|
192
182
|
|
|
193
183
|
it('should configure redrive policy to internal error queue', async () => {
|
|
194
184
|
const appDefinition = {
|
|
195
|
-
integrations: [
|
|
196
|
-
{ Definition: { name: 'test' } },
|
|
197
|
-
],
|
|
185
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
198
186
|
};
|
|
199
187
|
|
|
200
188
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
201
189
|
|
|
202
|
-
expect(result.resources.TestQueue.Properties.RedrivePolicy).toEqual(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
190
|
+
expect(result.resources.TestQueue.Properties.RedrivePolicy).toEqual(
|
|
191
|
+
{
|
|
192
|
+
maxReceiveCount: 3,
|
|
193
|
+
deadLetterTargetArn: {
|
|
194
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
);
|
|
208
198
|
});
|
|
209
199
|
|
|
210
200
|
it('should create queue worker function', async () => {
|
|
211
201
|
const appDefinition = {
|
|
212
|
-
integrations: [
|
|
213
|
-
{ Definition: { name: 'hubspot' } },
|
|
214
|
-
],
|
|
202
|
+
integrations: [{ Definition: { name: 'hubspot' } }],
|
|
215
203
|
};
|
|
216
204
|
|
|
217
205
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -221,9 +209,7 @@ describe('IntegrationBuilder', () => {
|
|
|
221
209
|
|
|
222
210
|
it('should configure queue worker with SQS event', async () => {
|
|
223
211
|
const appDefinition = {
|
|
224
|
-
integrations: [
|
|
225
|
-
{ Definition: { name: 'test' } },
|
|
226
|
-
],
|
|
212
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
227
213
|
};
|
|
228
214
|
|
|
229
215
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -241,9 +227,7 @@ describe('IntegrationBuilder', () => {
|
|
|
241
227
|
|
|
242
228
|
it('should set queue worker timeout to 600 seconds', async () => {
|
|
243
229
|
const appDefinition = {
|
|
244
|
-
integrations: [
|
|
245
|
-
{ Definition: { name: 'test' } },
|
|
246
|
-
],
|
|
230
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
247
231
|
};
|
|
248
232
|
|
|
249
233
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -253,21 +237,19 @@ describe('IntegrationBuilder', () => {
|
|
|
253
237
|
|
|
254
238
|
it('should set queue worker reserved concurrency', async () => {
|
|
255
239
|
const appDefinition = {
|
|
256
|
-
integrations: [
|
|
257
|
-
{ Definition: { name: 'test' } },
|
|
258
|
-
],
|
|
240
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
259
241
|
};
|
|
260
242
|
|
|
261
243
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
262
244
|
|
|
263
|
-
expect(result.functions.testQueueWorker.reservedConcurrency).toBe(
|
|
245
|
+
expect(result.functions.testQueueWorker.reservedConcurrency).toBe(
|
|
246
|
+
20
|
|
247
|
+
);
|
|
264
248
|
});
|
|
265
249
|
|
|
266
250
|
it('should add queue URL to environment variables', async () => {
|
|
267
251
|
const appDefinition = {
|
|
268
|
-
integrations: [
|
|
269
|
-
{ Definition: { name: 'slack' } },
|
|
270
|
-
],
|
|
252
|
+
integrations: [{ Definition: { name: 'slack' } }],
|
|
271
253
|
};
|
|
272
254
|
|
|
273
255
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -279,14 +261,14 @@ describe('IntegrationBuilder', () => {
|
|
|
279
261
|
|
|
280
262
|
it('should add queue name to custom variables', async () => {
|
|
281
263
|
const appDefinition = {
|
|
282
|
-
integrations: [
|
|
283
|
-
{ Definition: { name: 'stripe' } },
|
|
284
|
-
],
|
|
264
|
+
integrations: [{ Definition: { name: 'stripe' } }],
|
|
285
265
|
};
|
|
286
266
|
|
|
287
267
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
288
268
|
|
|
289
|
-
expect(result.custom.StripeQueue).toBe(
|
|
269
|
+
expect(result.custom.StripeQueue).toBe(
|
|
270
|
+
'${self:service}--${self:provider.stage}-StripeQueue'
|
|
271
|
+
);
|
|
290
272
|
});
|
|
291
273
|
|
|
292
274
|
it('should handle multiple integrations', async () => {
|
|
@@ -321,9 +303,7 @@ describe('IntegrationBuilder', () => {
|
|
|
321
303
|
|
|
322
304
|
it('should capitalize integration name for queue reference', async () => {
|
|
323
305
|
const appDefinition = {
|
|
324
|
-
integrations: [
|
|
325
|
-
{ Definition: { name: 'myIntegration' } },
|
|
326
|
-
],
|
|
306
|
+
integrations: [{ Definition: { name: 'myIntegration' } }],
|
|
327
307
|
};
|
|
328
308
|
|
|
329
309
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -334,9 +314,7 @@ describe('IntegrationBuilder', () => {
|
|
|
334
314
|
|
|
335
315
|
it('should handle integration names with hyphens', async () => {
|
|
336
316
|
const appDefinition = {
|
|
337
|
-
integrations: [
|
|
338
|
-
{ Definition: { name: 'my-integration' } },
|
|
339
|
-
],
|
|
317
|
+
integrations: [{ Definition: { name: 'my-integration' } }],
|
|
340
318
|
};
|
|
341
319
|
|
|
342
320
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
@@ -361,7 +339,8 @@ describe('IntegrationBuilder', () => {
|
|
|
361
339
|
};
|
|
362
340
|
|
|
363
341
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
364
|
-
const retention =
|
|
342
|
+
const retention =
|
|
343
|
+
result.resources.TestQueue.Properties.MessageRetentionPeriod;
|
|
365
344
|
|
|
366
345
|
// The max SQS DelaySeconds is 900. Retention must comfortably
|
|
367
346
|
// exceed this to ensure delayed messages are never silently lost.
|
|
@@ -377,7 +356,9 @@ describe('IntegrationBuilder', () => {
|
|
|
377
356
|
};
|
|
378
357
|
|
|
379
358
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
380
|
-
const maxReceiveCount =
|
|
359
|
+
const maxReceiveCount =
|
|
360
|
+
result.resources.TestQueue.Properties.RedrivePolicy
|
|
361
|
+
.maxReceiveCount;
|
|
381
362
|
|
|
382
363
|
// Should allow at least 2 retries (maxReceiveCount >= 3)
|
|
383
364
|
expect(maxReceiveCount).toBeGreaterThanOrEqual(3);
|
|
@@ -393,7 +374,9 @@ describe('IntegrationBuilder', () => {
|
|
|
393
374
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
394
375
|
const sqsEvent = result.functions.testQueueWorker.events[0].sqs;
|
|
395
376
|
|
|
396
|
-
expect(sqsEvent.functionResponseType).toBe(
|
|
377
|
+
expect(sqsEvent.functionResponseType).toBe(
|
|
378
|
+
'ReportBatchItemFailures'
|
|
379
|
+
);
|
|
397
380
|
});
|
|
398
381
|
});
|
|
399
382
|
|
|
@@ -406,10 +389,18 @@ describe('IntegrationBuilder', () => {
|
|
|
406
389
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
407
390
|
|
|
408
391
|
expect(result.resources.DLQMessageAlarm).toBeDefined();
|
|
409
|
-
expect(result.resources.DLQMessageAlarm.Type).toBe(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
expect(result.resources.DLQMessageAlarm.Properties.
|
|
392
|
+
expect(result.resources.DLQMessageAlarm.Type).toBe(
|
|
393
|
+
'AWS::CloudWatch::Alarm'
|
|
394
|
+
);
|
|
395
|
+
expect(result.resources.DLQMessageAlarm.Properties.MetricName).toBe(
|
|
396
|
+
'ApproximateNumberOfMessagesVisible'
|
|
397
|
+
);
|
|
398
|
+
expect(
|
|
399
|
+
result.resources.DLQMessageAlarm.Properties.ComparisonOperator
|
|
400
|
+
).toBe('GreaterThanThreshold');
|
|
401
|
+
expect(result.resources.DLQMessageAlarm.Properties.Threshold).toBe(
|
|
402
|
+
500
|
|
403
|
+
);
|
|
413
404
|
});
|
|
414
405
|
|
|
415
406
|
it('should wire alarm to InternalErrorBridgeTopic for notifications', async () => {
|
|
@@ -419,9 +410,9 @@ describe('IntegrationBuilder', () => {
|
|
|
419
410
|
|
|
420
411
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
421
412
|
|
|
422
|
-
expect(
|
|
423
|
-
|
|
424
|
-
]);
|
|
413
|
+
expect(
|
|
414
|
+
result.resources.DLQMessageAlarm.Properties.AlarmActions
|
|
415
|
+
).toEqual([{ Ref: 'InternalErrorBridgeTopic' }]);
|
|
425
416
|
});
|
|
426
417
|
|
|
427
418
|
it('should create a DLQ processor Lambda triggered by InternalErrorQueue', async () => {
|
|
@@ -435,7 +426,9 @@ describe('IntegrationBuilder', () => {
|
|
|
435
426
|
expect(result.functions.dlqProcessor.events[0].sqs.arn).toEqual({
|
|
436
427
|
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
437
428
|
});
|
|
438
|
-
expect(
|
|
429
|
+
expect(
|
|
430
|
+
result.functions.dlqProcessor.events[0].sqs.functionResponseType
|
|
431
|
+
).toBe('ReportBatchItemFailures');
|
|
439
432
|
});
|
|
440
433
|
|
|
441
434
|
it('DLQ processor should have skipEsbuild, short timeout, and low concurrency', async () => {
|
|
@@ -447,7 +440,9 @@ describe('IntegrationBuilder', () => {
|
|
|
447
440
|
|
|
448
441
|
expect(result.functions.dlqProcessor.skipEsbuild).toBe(true);
|
|
449
442
|
expect(result.functions.dlqProcessor.package).toBeDefined();
|
|
450
|
-
expect(result.functions.dlqProcessor.timeout).toBeLessThanOrEqual(
|
|
443
|
+
expect(result.functions.dlqProcessor.timeout).toBeLessThanOrEqual(
|
|
444
|
+
60
|
|
445
|
+
);
|
|
451
446
|
expect(result.functions.dlqProcessor.reservedConcurrency).toBe(1);
|
|
452
447
|
});
|
|
453
448
|
});
|
|
@@ -474,7 +469,7 @@ describe('IntegrationBuilder', () => {
|
|
|
474
469
|
Definition: {
|
|
475
470
|
name: 'hubspot',
|
|
476
471
|
webhooks: true,
|
|
477
|
-
}
|
|
472
|
+
},
|
|
478
473
|
},
|
|
479
474
|
],
|
|
480
475
|
};
|
|
@@ -494,7 +489,7 @@ describe('IntegrationBuilder', () => {
|
|
|
494
489
|
Definition: {
|
|
495
490
|
name: 'salesforce',
|
|
496
491
|
webhooks: { enabled: true },
|
|
497
|
-
}
|
|
492
|
+
},
|
|
498
493
|
},
|
|
499
494
|
],
|
|
500
495
|
};
|
|
@@ -511,7 +506,7 @@ describe('IntegrationBuilder', () => {
|
|
|
511
506
|
Definition: {
|
|
512
507
|
name: 'slack',
|
|
513
508
|
webhooks: false,
|
|
514
|
-
}
|
|
509
|
+
},
|
|
515
510
|
},
|
|
516
511
|
],
|
|
517
512
|
};
|
|
@@ -528,7 +523,7 @@ describe('IntegrationBuilder', () => {
|
|
|
528
523
|
Definition: {
|
|
529
524
|
name: 'test',
|
|
530
525
|
webhooks: { enabled: false },
|
|
531
|
-
}
|
|
526
|
+
},
|
|
532
527
|
},
|
|
533
528
|
],
|
|
534
529
|
};
|
|
@@ -545,7 +540,7 @@ describe('IntegrationBuilder', () => {
|
|
|
545
540
|
Definition: {
|
|
546
541
|
name: 'stripe',
|
|
547
542
|
webhooks: true,
|
|
548
|
-
}
|
|
543
|
+
},
|
|
549
544
|
},
|
|
550
545
|
],
|
|
551
546
|
};
|
|
@@ -716,7 +711,7 @@ describe('IntegrationBuilder', () => {
|
|
|
716
711
|
Definition: {
|
|
717
712
|
name: 'asana',
|
|
718
713
|
webhooks: true,
|
|
719
|
-
}
|
|
714
|
+
},
|
|
720
715
|
},
|
|
721
716
|
],
|
|
722
717
|
};
|
|
@@ -742,7 +737,7 @@ describe('IntegrationBuilder', () => {
|
|
|
742
737
|
Definition: {
|
|
743
738
|
name: 'test',
|
|
744
739
|
webhooks: true,
|
|
745
|
-
}
|
|
740
|
+
},
|
|
746
741
|
},
|
|
747
742
|
],
|
|
748
743
|
};
|
|
@@ -751,9 +746,12 @@ describe('IntegrationBuilder', () => {
|
|
|
751
746
|
|
|
752
747
|
const functionKeys = Object.keys(result.functions);
|
|
753
748
|
|
|
754
|
-
// Expected order: dlqProcessor
|
|
749
|
+
// Expected order: dlqProcessor + userActionQueueWorker (both
|
|
750
|
+
// app-level, created before the integration loop), then webhook,
|
|
751
|
+
// integration, queueWorker.
|
|
755
752
|
expect(functionKeys).toEqual([
|
|
756
753
|
'dlqProcessor',
|
|
754
|
+
'userActionQueueWorker',
|
|
757
755
|
'testWebhook',
|
|
758
756
|
'test',
|
|
759
757
|
'testQueueWorker',
|
|
@@ -767,19 +765,19 @@ describe('IntegrationBuilder', () => {
|
|
|
767
765
|
Definition: {
|
|
768
766
|
name: 'hubspot',
|
|
769
767
|
webhooks: true,
|
|
770
|
-
}
|
|
768
|
+
},
|
|
771
769
|
},
|
|
772
770
|
{
|
|
773
771
|
Definition: {
|
|
774
772
|
name: 'salesforce',
|
|
775
773
|
webhooks: false,
|
|
776
|
-
}
|
|
774
|
+
},
|
|
777
775
|
},
|
|
778
776
|
{
|
|
779
777
|
Definition: {
|
|
780
778
|
name: 'slack',
|
|
781
779
|
webhooks: { enabled: true },
|
|
782
|
-
}
|
|
780
|
+
},
|
|
783
781
|
},
|
|
784
782
|
],
|
|
785
783
|
};
|
|
@@ -809,7 +807,7 @@ describe('IntegrationBuilder', () => {
|
|
|
809
807
|
Definition: {
|
|
810
808
|
name: 'test',
|
|
811
809
|
webhooks: true,
|
|
812
|
-
}
|
|
810
|
+
},
|
|
813
811
|
},
|
|
814
812
|
],
|
|
815
813
|
};
|
|
@@ -826,7 +824,7 @@ describe('IntegrationBuilder', () => {
|
|
|
826
824
|
Definition: {
|
|
827
825
|
name: 'test',
|
|
828
826
|
webhooks: true,
|
|
829
|
-
}
|
|
827
|
+
},
|
|
830
828
|
},
|
|
831
829
|
],
|
|
832
830
|
};
|
|
@@ -834,24 +832,26 @@ describe('IntegrationBuilder', () => {
|
|
|
834
832
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
835
833
|
|
|
836
834
|
expect(result.functions.testWebhook.package).toBeDefined();
|
|
837
|
-
expect(result.functions.testWebhook.package.exclude).toContain(
|
|
838
|
-
|
|
835
|
+
expect(result.functions.testWebhook.package.exclude).toContain(
|
|
836
|
+
'node_modules/aws-sdk/**'
|
|
837
|
+
);
|
|
838
|
+
expect(result.functions.testWebhook.package.exclude).toContain(
|
|
839
|
+
'node_modules/@prisma/**'
|
|
840
|
+
);
|
|
839
841
|
});
|
|
840
842
|
});
|
|
841
843
|
|
|
842
844
|
describe('Prisma Layer Configuration', () => {
|
|
843
845
|
it('should attach Prisma Lambda layer to queue worker functions', async () => {
|
|
844
846
|
const appDefinition = {
|
|
845
|
-
integrations: [
|
|
846
|
-
{ Definition: { name: 'hubspot' } },
|
|
847
|
-
],
|
|
847
|
+
integrations: [{ Definition: { name: 'hubspot' } }],
|
|
848
848
|
};
|
|
849
849
|
|
|
850
850
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
851
851
|
|
|
852
852
|
// Queue workers need Prisma layer for database operations
|
|
853
853
|
expect(result.functions.hubspotQueueWorker.layers).toEqual([
|
|
854
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
854
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
855
855
|
]);
|
|
856
856
|
});
|
|
857
857
|
|
|
@@ -867,28 +867,26 @@ describe('IntegrationBuilder', () => {
|
|
|
867
867
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
868
868
|
|
|
869
869
|
expect(result.functions.hubspotQueueWorker.layers).toEqual([
|
|
870
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
870
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
871
871
|
]);
|
|
872
872
|
expect(result.functions.salesforceQueueWorker.layers).toEqual([
|
|
873
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
873
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
874
874
|
]);
|
|
875
875
|
expect(result.functions.slackQueueWorker.layers).toEqual([
|
|
876
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
876
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
877
877
|
]);
|
|
878
878
|
});
|
|
879
879
|
|
|
880
880
|
it('should attach Prisma layer to HTTP handlers for database access', async () => {
|
|
881
881
|
const appDefinition = {
|
|
882
|
-
integrations: [
|
|
883
|
-
{ Definition: { name: 'stripe' } },
|
|
884
|
-
],
|
|
882
|
+
integrations: [{ Definition: { name: 'stripe' } }],
|
|
885
883
|
};
|
|
886
884
|
|
|
887
885
|
const result = await integrationBuilder.build(appDefinition, {});
|
|
888
886
|
|
|
889
887
|
// HTTP handlers also need Prisma for integration queries
|
|
890
888
|
expect(result.functions.stripe.layers).toEqual([
|
|
891
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
889
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
892
890
|
]);
|
|
893
891
|
});
|
|
894
892
|
|
|
@@ -899,7 +897,7 @@ describe('IntegrationBuilder', () => {
|
|
|
899
897
|
Definition: {
|
|
900
898
|
name: 'hubspot',
|
|
901
899
|
webhooks: true,
|
|
902
|
-
}
|
|
900
|
+
},
|
|
903
901
|
},
|
|
904
902
|
],
|
|
905
903
|
};
|
|
@@ -908,7 +906,7 @@ describe('IntegrationBuilder', () => {
|
|
|
908
906
|
|
|
909
907
|
// Webhook handlers need Prisma for credential lookups
|
|
910
908
|
expect(result.functions.hubspotWebhook.layers).toEqual([
|
|
911
|
-
{ Ref: 'PrismaLambdaLayer' }
|
|
909
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
912
910
|
]);
|
|
913
911
|
});
|
|
914
912
|
|
|
@@ -935,5 +933,124 @@ describe('IntegrationBuilder', () => {
|
|
|
935
933
|
);
|
|
936
934
|
});
|
|
937
935
|
});
|
|
938
|
-
});
|
|
939
936
|
|
|
937
|
+
describe('User-Action FIFO queue', () => {
|
|
938
|
+
const appDefinition = {
|
|
939
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
it('creates a FIFO queue without ContentBasedDeduplication', async () => {
|
|
943
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
944
|
+
|
|
945
|
+
const q = result.resources.FriggUserActionQueue;
|
|
946
|
+
expect(q).toBeDefined();
|
|
947
|
+
expect(q.Type).toBe('AWS::SQS::Queue');
|
|
948
|
+
expect(q.Properties.FifoQueue).toBe(true);
|
|
949
|
+
expect(q.Properties.ContentBasedDeduplication).toBeUndefined();
|
|
950
|
+
expect(q.Properties.VisibilityTimeout).toBe(1800);
|
|
951
|
+
expect(q.Properties.MessageRetentionPeriod).toBe(345600);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it('redrives to the FIFO DLQ with maxReceiveCount 2', async () => {
|
|
955
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
956
|
+
|
|
957
|
+
expect(
|
|
958
|
+
result.resources.FriggUserActionQueue.Properties.RedrivePolicy
|
|
959
|
+
).toEqual({
|
|
960
|
+
maxReceiveCount: 2,
|
|
961
|
+
deadLetterTargetArn: {
|
|
962
|
+
'Fn::GetAtt': ['FriggUserActionDLQ', 'Arn'],
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('creates a FIFO DLQ with 14-day retention', async () => {
|
|
968
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
969
|
+
|
|
970
|
+
const dlq = result.resources.FriggUserActionDLQ;
|
|
971
|
+
expect(dlq).toBeDefined();
|
|
972
|
+
expect(dlq.Properties.FifoQueue).toBe(true);
|
|
973
|
+
expect(dlq.Properties.MessageRetentionPeriod).toBe(1209600);
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('creates the worker bound to the FIFO queue ARN with no reservedConcurrency', async () => {
|
|
977
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
978
|
+
|
|
979
|
+
const worker = result.functions.userActionQueueWorker;
|
|
980
|
+
expect(worker).toBeDefined();
|
|
981
|
+
expect(worker.handler).toBe(
|
|
982
|
+
'node_modules/@friggframework/core/handlers/workers/user-action-worker.userActionQueueWorker'
|
|
983
|
+
);
|
|
984
|
+
expect(worker.timeout).toBe(900);
|
|
985
|
+
expect(worker.reservedConcurrency).toBeUndefined();
|
|
986
|
+
expect(worker.events).toEqual([
|
|
987
|
+
{
|
|
988
|
+
sqs: {
|
|
989
|
+
arn: { 'Fn::GetAtt': ['FriggUserActionQueue', 'Arn'] },
|
|
990
|
+
batchSize: 1,
|
|
991
|
+
functionResponseType: 'ReportBatchItemFailures',
|
|
992
|
+
},
|
|
993
|
+
},
|
|
994
|
+
]);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('exposes USER_ACTION_QUEUE_URL to all Lambdas', async () => {
|
|
998
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
999
|
+
|
|
1000
|
+
expect(result.environment.USER_ACTION_QUEUE_URL).toEqual({
|
|
1001
|
+
Ref: 'FriggUserActionQueue',
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
it('alarms on the FIFO DLQ via the InternalErrorBridgeTopic', async () => {
|
|
1006
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
1007
|
+
|
|
1008
|
+
const alarm = result.resources.FriggUserActionDLQAlarm;
|
|
1009
|
+
expect(alarm).toBeDefined();
|
|
1010
|
+
expect(alarm.Properties.AlarmActions).toEqual([
|
|
1011
|
+
{ Ref: 'InternalErrorBridgeTopic' },
|
|
1012
|
+
]);
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it('creates exactly one FIFO queue + worker regardless of integration count', async () => {
|
|
1016
|
+
const result = await integrationBuilder.build(
|
|
1017
|
+
{
|
|
1018
|
+
integrations: [
|
|
1019
|
+
{ Definition: { name: 'hubspot' } },
|
|
1020
|
+
{ Definition: { name: 'salesforce' } },
|
|
1021
|
+
{ Definition: { name: 'slack' } },
|
|
1022
|
+
],
|
|
1023
|
+
},
|
|
1024
|
+
{}
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const fifoQueues = Object.keys(result.resources).filter(
|
|
1028
|
+
(k) => k === 'FriggUserActionQueue'
|
|
1029
|
+
);
|
|
1030
|
+
expect(fifoQueues).toHaveLength(1);
|
|
1031
|
+
expect(result.functions.userActionQueueWorker).toBeDefined();
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('does not make per-integration queues FIFO', async () => {
|
|
1035
|
+
const result = await integrationBuilder.build(appDefinition, {});
|
|
1036
|
+
|
|
1037
|
+
expect(
|
|
1038
|
+
result.resources.TestQueue.Properties.FifoQueue
|
|
1039
|
+
).toBeUndefined();
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it('omits the Prisma layer on the worker when usePrismaLambdaLayer=false', async () => {
|
|
1043
|
+
const result = await integrationBuilder.build(
|
|
1044
|
+
{
|
|
1045
|
+
usePrismaLambdaLayer: false,
|
|
1046
|
+
integrations: [{ Definition: { name: 'test' } }],
|
|
1047
|
+
},
|
|
1048
|
+
{}
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
expect(
|
|
1052
|
+
result.functions.userActionQueueWorker.layers
|
|
1053
|
+
).toBeUndefined();
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
@@ -14,7 +14,7 @@ function generateIAMCloudFormation(options = {}) {
|
|
|
14
14
|
appName = 'Frigg',
|
|
15
15
|
features = {},
|
|
16
16
|
userPrefix = 'frigg-deployment-user',
|
|
17
|
-
stackName = 'frigg-deployment-iam'
|
|
17
|
+
stackName = 'frigg-deployment-iam',
|
|
18
18
|
} = options;
|
|
19
19
|
|
|
20
20
|
const deploymentUserName = userPrefix;
|
|
@@ -426,6 +426,10 @@ function generateIAMCloudFormation(options = {}) {
|
|
|
426
426
|
],
|
|
427
427
|
Resource: [
|
|
428
428
|
{ 'Fn::Sub': 'arn:aws:sqs:*:${AWS::AccountId}:*frigg*' },
|
|
429
|
+
// IAM wildcard matching is case-sensitive; the app-level FIFO
|
|
430
|
+
// queue is named "...-FriggUserActionQueue.fifo", which the
|
|
431
|
+
// lowercase "*frigg*" glob would not match.
|
|
432
|
+
{ 'Fn::Sub': 'arn:aws:sqs:*:${AWS::AccountId}:*Frigg*' },
|
|
429
433
|
{
|
|
430
434
|
'Fn::Sub':
|
|
431
435
|
'arn:aws:sqs:*:${AWS::AccountId}:internal-error-queue-*',
|
|
@@ -761,8 +765,7 @@ function getFeatureSummary(appDefinition) {
|
|
|
761
765
|
const features = {
|
|
762
766
|
core: true, // Always enabled
|
|
763
767
|
vpc: appDefinition.vpc?.enable === true,
|
|
764
|
-
kms:
|
|
765
|
-
appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms',
|
|
768
|
+
kms: appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms',
|
|
766
769
|
ssm: appDefinition.ssm?.enable === true,
|
|
767
770
|
websockets: appDefinition.websockets?.enable === true,
|
|
768
771
|
};
|
|
@@ -781,7 +784,10 @@ function getFeatureSummary(appDefinition) {
|
|
|
781
784
|
* @returns {Object} Basic IAM policy document
|
|
782
785
|
*/
|
|
783
786
|
function generateBasicIAMPolicy() {
|
|
784
|
-
const basicPolicyPath = path.join(
|
|
787
|
+
const basicPolicyPath = path.join(
|
|
788
|
+
__dirname,
|
|
789
|
+
'templates/iam-policy-basic.json'
|
|
790
|
+
);
|
|
785
791
|
return require(basicPolicyPath);
|
|
786
792
|
}
|
|
787
793
|
|
|
@@ -790,7 +796,10 @@ function generateBasicIAMPolicy() {
|
|
|
790
796
|
* @returns {Object} Full IAM policy document
|
|
791
797
|
*/
|
|
792
798
|
function generateFullIAMPolicy() {
|
|
793
|
-
const fullPolicyPath = path.join(
|
|
799
|
+
const fullPolicyPath = path.join(
|
|
800
|
+
__dirname,
|
|
801
|
+
'templates/iam-policy-full.json'
|
|
802
|
+
);
|
|
794
803
|
return require(fullPolicyPath);
|
|
795
804
|
}
|
|
796
805
|
|