@friggframework/devtools 2.0.0-next.36 → 2.0.0-next.38

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.
@@ -118,16 +118,9 @@ describe('composeServerlessDefinition', () => {
118
118
 
119
119
  const result = await composeServerlessDefinition(appDefinition);
120
120
 
121
- expect(result.provider.vpc).toBe('${self:custom.vpc.${self:provider.stage}}');
122
- expect(result.custom.vpc).toEqual({
123
- '${self:provider.stage}': {
124
- securityGroupIds: ['${env:AWS_DISCOVERY_SECURITY_GROUP_ID}'],
125
- subnetIds: [
126
- '${env:AWS_DISCOVERY_SUBNET_ID_1}',
127
- '${env:AWS_DISCOVERY_SUBNET_ID_2}'
128
- ]
129
- }
130
- });
121
+ expect(result.provider.vpc).toBeDefined();
122
+ expect(result.provider.vpc.securityGroupIds).toEqual(['sg-123456']);
123
+ expect(result.provider.vpc.subnetIds).toEqual(['subnet-123456', 'subnet-789012']);
131
124
  });
132
125
 
133
126
  it('should add VPC endpoint for S3 when VPC is enabled', async () => {
@@ -138,15 +131,9 @@ describe('composeServerlessDefinition', () => {
138
131
 
139
132
  const result = await composeServerlessDefinition(appDefinition);
140
133
 
141
- expect(result.resources.Resources.VPCEndpointS3).toEqual({
142
- Type: 'AWS::EC2::VPCEndpoint',
143
- Properties: {
144
- VpcId: '${env:AWS_DISCOVERY_VPC_ID}',
145
- ServiceName: 'com.amazonaws.${self:provider.region}.s3',
146
- VpcEndpointType: 'Gateway',
147
- RouteTableIds: ['${env:AWS_DISCOVERY_ROUTE_TABLE_ID}']
148
- }
149
- });
134
+ expect(result.resources.Resources.VPCEndpointS3).toBeDefined();
135
+ expect(result.resources.Resources.VPCEndpointS3.Type).toBe('AWS::EC2::VPCEndpoint');
136
+ expect(result.resources.Resources.VPCEndpointS3.Properties.VpcId).toBe('vpc-123456');
150
137
  });
151
138
 
152
139
  it('should not add VPC configuration when vpc.enable is false', async () => {
@@ -158,7 +145,6 @@ describe('composeServerlessDefinition', () => {
158
145
  const result = await composeServerlessDefinition(appDefinition);
159
146
 
160
147
  expect(result.provider.vpc).toBeUndefined();
161
- expect(result.custom.vpc).toBeUndefined();
162
148
  expect(result.resources.Resources.VPCEndpointS3).toBeUndefined();
163
149
  });
164
150
 
@@ -170,14 +156,13 @@ describe('composeServerlessDefinition', () => {
170
156
  const result = await composeServerlessDefinition(appDefinition);
171
157
 
172
158
  expect(result.provider.vpc).toBeUndefined();
173
- expect(result.custom.vpc).toBeUndefined();
174
159
  });
175
160
  });
176
161
 
177
162
  describe('KMS Configuration', () => {
178
- it('should add KMS configuration when encryption is enabled', async () => {
163
+ it('should add KMS configuration when encryption is enabled and key is found', async () => {
179
164
  const appDefinition = {
180
- encryption: { useDefaultKMSForFieldLevelEncryption: true },
165
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
181
166
  integrations: []
182
167
  };
183
168
 
@@ -193,24 +178,188 @@ describe('composeServerlessDefinition', () => {
193
178
  'kms:GenerateDataKey',
194
179
  'kms:Decrypt'
195
180
  ],
196
- Resource: ['${self:custom.kmsGrants.kmsKeyId}']
181
+ Resource: ['arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012']
197
182
  });
198
183
 
199
184
  // Check environment variable
200
- expect(result.provider.environment.KMS_KEY_ARN).toBe('${self:custom.kmsGrants.kmsKeyId}');
185
+ expect(result.provider.environment.KMS_KEY_ARN).toBe('arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012');
201
186
 
202
187
  // Check plugin
203
188
  expect(result.plugins).toContain('serverless-kms-grants');
204
189
 
205
190
  // Check custom configuration
206
191
  expect(result.custom.kmsGrants).toEqual({
207
- kmsKeyId: '${env:AWS_DISCOVERY_KMS_KEY_ID}'
192
+ kmsKeyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
193
+ });
194
+ });
195
+
196
+ it('should create new KMS key when encryption is enabled, no key found, and createResourceIfNoneFound is true', async () => {
197
+ // Mock AWS discovery to return no KMS key
198
+ const { AWSDiscovery } = require('./aws-discovery');
199
+ const mockDiscoverResources = jest.fn().mockResolvedValue({
200
+ defaultVpcId: 'vpc-123456',
201
+ defaultSecurityGroupId: 'sg-123456',
202
+ privateSubnetId1: 'subnet-123456',
203
+ privateSubnetId2: 'subnet-789012',
204
+ publicSubnetId: 'subnet-public',
205
+ defaultRouteTableId: 'rtb-123456',
206
+ defaultKmsKeyId: null // No KMS key found
207
+ });
208
+ AWSDiscovery.mockImplementation(() => ({
209
+ discoverResources: mockDiscoverResources
210
+ }));
211
+
212
+ const appDefinition = {
213
+ encryption: {
214
+ fieldLevelEncryptionMethod: 'kms',
215
+ createResourceIfNoneFound: true
216
+ },
217
+ integrations: []
218
+ };
219
+
220
+ const result = await composeServerlessDefinition(appDefinition);
221
+
222
+ // Check that KMS key resource was created
223
+ expect(result.resources.Resources.FriggKMSKey).toEqual({
224
+ Type: 'AWS::KMS::Key',
225
+ Properties: {
226
+ EnableKeyRotation: true,
227
+ Description: 'Frigg KMS key for field-level encryption',
228
+ KeyPolicy: {
229
+ Version: '2012-10-17',
230
+ Statement: [
231
+ {
232
+ Sid: 'AllowRootAccountAdmin',
233
+ Effect: 'Allow',
234
+ Principal: {
235
+ AWS: {
236
+ 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root'
237
+ }
238
+ },
239
+ Action: 'kms:*',
240
+ Resource: '*'
241
+ },
242
+ {
243
+ Sid: 'AllowLambdaService',
244
+ Effect: 'Allow',
245
+ Principal: {
246
+ Service: 'lambda.amazonaws.com'
247
+ },
248
+ Action: [
249
+ 'kms:GenerateDataKey',
250
+ 'kms:Decrypt',
251
+ 'kms:DescribeKey'
252
+ ],
253
+ Resource: '*',
254
+ Condition: {
255
+ StringEquals: {
256
+ 'kms:ViaService': 'lambda.us-east-1.amazonaws.com'
257
+ }
258
+ }
259
+ }
260
+ ]
261
+ },
262
+ Tags: [
263
+ {
264
+ Key: 'Name',
265
+ Value: '${self:service}-${self:provider.stage}-frigg-kms-key'
266
+ },
267
+ {
268
+ Key: 'Purpose',
269
+ Value: 'Field-level encryption for Frigg application'
270
+ }
271
+ ]
272
+ }
273
+ });
274
+
275
+ // Check IAM permissions for the new key
276
+ const kmsPermission = result.provider.iamRoleStatements.find(
277
+ statement => statement.Action.includes('kms:GenerateDataKey')
278
+ );
279
+ expect(kmsPermission).toEqual({
280
+ Effect: 'Allow',
281
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
282
+ Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }]
283
+ });
284
+
285
+ // Check environment variable
286
+ expect(result.provider.environment.KMS_KEY_ARN).toEqual({
287
+ 'Fn::GetAtt': ['FriggKMSKey', 'Arn']
288
+ });
289
+
290
+ // Check plugin
291
+ expect(result.plugins).toContain('serverless-kms-grants');
292
+
293
+ // Check custom configuration
294
+ // When creating a new key, it should reference the CloudFormation resource
295
+ expect(result.custom.kmsGrants).toEqual({
296
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
297
+ });
298
+ });
299
+
300
+ it('should throw error when encryption is enabled, no key found, and createResourceIfNoneFound is false', async () => {
301
+ // Mock AWS discovery to return no KMS key
302
+ const { AWSDiscovery } = require('./aws-discovery');
303
+ const mockDiscoverResources = jest.fn().mockResolvedValue({
304
+ defaultVpcId: 'vpc-123456',
305
+ defaultSecurityGroupId: 'sg-123456',
306
+ privateSubnetId1: 'subnet-123456',
307
+ privateSubnetId2: 'subnet-789012',
308
+ publicSubnetId: 'subnet-public',
309
+ defaultRouteTableId: 'rtb-123456',
310
+ defaultKmsKeyId: null // No KMS key found
311
+ });
312
+ AWSDiscovery.mockImplementation(() => ({
313
+ discoverResources: mockDiscoverResources
314
+ }));
315
+
316
+ const appDefinition = {
317
+ encryption: {
318
+ fieldLevelEncryptionMethod: 'kms',
319
+ createResourceIfNoneFound: false
320
+ },
321
+ integrations: []
322
+ };
323
+
324
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
325
+ 'KMS field-level encryption is enabled but no KMS key was found. ' +
326
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
327
+ );
328
+ });
329
+
330
+ it('should throw error when encryption is enabled, no key found, and createResourceIfNoneFound is not specified', async () => {
331
+ // Mock AWS discovery to return no KMS key
332
+ const { AWSDiscovery } = require('./aws-discovery');
333
+ const mockDiscoverResources = jest.fn().mockResolvedValue({
334
+ defaultVpcId: 'vpc-123456',
335
+ defaultSecurityGroupId: 'sg-123456',
336
+ privateSubnetId1: 'subnet-123456',
337
+ privateSubnetId2: 'subnet-789012',
338
+ publicSubnetId: 'subnet-public',
339
+ defaultRouteTableId: 'rtb-123456',
340
+ defaultKmsKeyId: null // No KMS key found
208
341
  });
342
+ AWSDiscovery.mockImplementation(() => ({
343
+ discoverResources: mockDiscoverResources
344
+ }));
345
+
346
+ const appDefinition = {
347
+ encryption: {
348
+ fieldLevelEncryptionMethod: 'kms'
349
+ // createResourceIfNoneFound not specified, defaults to false
350
+ },
351
+ integrations: []
352
+ };
353
+
354
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow(
355
+ 'KMS field-level encryption is enabled but no KMS key was found. ' +
356
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
357
+ );
209
358
  });
210
359
 
211
360
  it('should not add KMS configuration when encryption is disabled', async () => {
212
361
  const appDefinition = {
213
- encryption: { useDefaultKMSForFieldLevelEncryption: false },
362
+ encryption: { fieldLevelEncryptionMethod: 'aes' },
214
363
  integrations: []
215
364
  };
216
365
 
@@ -315,10 +464,9 @@ describe('composeServerlessDefinition', () => {
315
464
  expect(result.functions.testIntegration).toEqual({
316
465
  handler: 'node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.testIntegration.handler',
317
466
  events: [{
318
- http: {
467
+ httpApi: {
319
468
  path: '/api/testIntegration-integration/{proxy+}',
320
- method: 'ANY',
321
- cors: true
469
+ method: 'ANY'
322
470
  }
323
471
  }]
324
472
  });
@@ -389,7 +537,7 @@ describe('composeServerlessDefinition', () => {
389
537
  it('should combine VPC, KMS, and SSM configurations', async () => {
390
538
  const appDefinition = {
391
539
  vpc: { enable: true },
392
- encryption: { useDefaultKMSForFieldLevelEncryption: true },
540
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
393
541
  ssm: { enable: true },
394
542
  integrations: [mockIntegration]
395
543
  };
@@ -398,7 +546,7 @@ describe('composeServerlessDefinition', () => {
398
546
 
399
547
  // VPC
400
548
  expect(result.provider.vpc).toBeDefined();
401
- expect(result.custom.vpc).toBeDefined();
549
+ // custom.vpc doesn't exist in the serverless template
402
550
  expect(result.resources.Resources.VPCEndpointS3).toBeDefined();
403
551
 
404
552
  // KMS
@@ -428,7 +576,7 @@ describe('composeServerlessDefinition', () => {
428
576
  it('should handle partial configuration combinations', async () => {
429
577
  const appDefinition = {
430
578
  vpc: { enable: true },
431
- encryption: { useDefaultKMSForFieldLevelEncryption: true },
579
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
432
580
  integrations: []
433
581
  };
434
582
 
@@ -459,9 +607,9 @@ describe('composeServerlessDefinition', () => {
459
607
  expect(result.resources.Resources.ApiGatewayAlarm5xx).toBeDefined();
460
608
 
461
609
  // Check default functions
462
- expect(result.functions.defaultWebsocket).toBeDefined();
463
610
  expect(result.functions.auth).toBeDefined();
464
611
  expect(result.functions.user).toBeDefined();
612
+ expect(result.functions.health).toBeDefined();
465
613
 
466
614
  // Check default plugins
467
615
  expect(result.plugins).toContain('serverless-jetpack');
@@ -496,11 +644,64 @@ describe('composeServerlessDefinition', () => {
496
644
 
497
645
  const result = await composeServerlessDefinition(appDefinition);
498
646
 
499
- expect(result.provider.environment.STAGE).toBe('${opt:stage}');
647
+ expect(result.provider.environment.STAGE).toBe('${opt:stage, "dev"}');
500
648
  expect(result.provider.environment.AWS_NODEJS_CONNECTION_REUSE_ENABLED).toBe(1);
501
649
  });
502
650
  });
503
651
 
652
+ describe('WebSocket Configuration', () => {
653
+ it('should add websocket function when websockets.enable is true', async () => {
654
+ const appDefinition = {
655
+ websockets: { enable: true },
656
+ integrations: []
657
+ };
658
+
659
+ const result = await composeServerlessDefinition(appDefinition);
660
+
661
+ expect(result.functions.defaultWebsocket).toEqual({
662
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
663
+ events: [
664
+ {
665
+ websocket: {
666
+ route: '$connect',
667
+ },
668
+ },
669
+ {
670
+ websocket: {
671
+ route: '$default',
672
+ },
673
+ },
674
+ {
675
+ websocket: {
676
+ route: '$disconnect',
677
+ },
678
+ },
679
+ ],
680
+ });
681
+ });
682
+
683
+ it('should not add websocket function when websockets.enable is false', async () => {
684
+ const appDefinition = {
685
+ websockets: { enable: false },
686
+ integrations: []
687
+ };
688
+
689
+ const result = await composeServerlessDefinition(appDefinition);
690
+
691
+ expect(result.functions.defaultWebsocket).toBeUndefined();
692
+ });
693
+
694
+ it('should not add websocket function when websockets is not defined', async () => {
695
+ const appDefinition = {
696
+ integrations: []
697
+ };
698
+
699
+ const result = await composeServerlessDefinition(appDefinition);
700
+
701
+ expect(result.functions.defaultWebsocket).toBeUndefined();
702
+ });
703
+ });
704
+
504
705
  describe('Edge Cases', () => {
505
706
  it('should handle empty app definition', async () => {
506
707
  const appDefinition = {};
@@ -515,7 +716,9 @@ describe('composeServerlessDefinition', () => {
515
716
  integrations: null
516
717
  };
517
718
 
518
- await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
719
+ // Should not throw, just ignore invalid integrations
720
+ const result = await composeServerlessDefinition(appDefinition);
721
+ expect(result).toBeDefined();
519
722
  });
520
723
 
521
724
  it('should handle integration with missing Definition', async () => {
@@ -524,7 +727,7 @@ describe('composeServerlessDefinition', () => {
524
727
  integrations: [invalidIntegration]
525
728
  };
526
729
 
527
- await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
730
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow('Invalid integration: missing Definition or name');
528
731
  });
529
732
 
530
733
  it('should handle integration with missing name', async () => {
@@ -535,7 +738,7 @@ describe('composeServerlessDefinition', () => {
535
738
  integrations: [invalidIntegration]
536
739
  };
537
740
 
538
- await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow();
741
+ await expect(composeServerlessDefinition(appDefinition)).rejects.toThrow('Invalid integration: missing Definition or name');
539
742
  });
540
743
  });
541
744
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/devtools",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.36",
4
+ "version": "2.0.0-next.38",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-ec2": "^3.835.0",
7
7
  "@aws-sdk/client-kms": "^3.835.0",
@@ -9,8 +9,8 @@
9
9
  "@babel/eslint-parser": "^7.18.9",
10
10
  "@babel/parser": "^7.25.3",
11
11
  "@babel/traverse": "^7.25.3",
12
- "@friggframework/schemas": "2.0.0-next.36",
13
- "@friggframework/test": "2.0.0-next.36",
12
+ "@friggframework/schemas": "2.0.0-next.38",
13
+ "@friggframework/test": "2.0.0-next.38",
14
14
  "@hapi/boom": "^10.0.1",
15
15
  "@inquirer/prompts": "^5.3.8",
16
16
  "axios": "^1.7.2",
@@ -32,8 +32,9 @@
32
32
  "serverless-http": "^2.7.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@friggframework/eslint-config": "2.0.0-next.36",
36
- "@friggframework/prettier-config": "2.0.0-next.36",
35
+ "@friggframework/eslint-config": "2.0.0-next.38",
36
+ "@friggframework/prettier-config": "2.0.0-next.38",
37
+ "jest": "^30.1.3",
37
38
  "prettier": "^2.7.1",
38
39
  "serverless": "3.39.0",
39
40
  "serverless-dotenv-plugin": "^6.0.0",
@@ -65,5 +66,5 @@
65
66
  "publishConfig": {
66
67
  "access": "public"
67
68
  },
68
- "gitHead": "6cca53cb3091d6bd11ae37f6c459679f1b5b19a6"
69
+ "gitHead": "a9c9c28fd9abdc8c96a38b8ab0fbb3cbf7d89960"
69
70
  }