UNPKG

@mapbox/cloudfriend

Version:

Helper functions for assembling CloudFormation templates in JavaScript

1,489 lines (1,332 loc) 61.9 kB
'use strict'; const cp = require('child_process'); const path = require('path'); const fs = require('fs'); const cf = require('..'); const fixtures = require('./fixtures/shortcuts'); const util = require('util'); const sleep = util.promisify(setTimeout); const update = !!process.env.UPDATE; const noUndefined = (template) => JSON.parse(JSON.stringify(template)); describe('[shortcuts] fixture validation', () => { // Runs cfn-lint, ignoring "warnings". Install via pip or Homebrew to run these // tests locally. const cfnLint = (filepath, filename) => new Promise((resolve, reject) => { // Ignore E3003 (missing TableInput) and E3002 (unexpected properties) for Iceberg tables only // cfn-lint doesn't yet support OpenTableFormatInput (Iceberg table format) const isIcebergTable = filename.includes('glue-iceberg-table'); const ignoreChecks = isIcebergTable ? 'W,E3003,E3002' : 'W'; cp.execFile('cfn-lint', [filepath, '--ignore-checks', ignoreChecks], (err, stdout) => { if (err) return reject(new Error(stdout)); return resolve(); }); }); const toValidate = fs .readdirSync(path.join(__dirname, 'fixtures', 'shortcuts')) .filter((filename) => path.extname(filename) === '.json'); test.each(toValidate)('%s fixture passes validation', async (filename) => { await Promise.all([ cfnLint(path.join(__dirname, 'fixtures', 'shortcuts', filename), filename), sleep(1000) ]); }); }); describe('[shortcuts] lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.Lambda()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.Lambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws for RoleArn and Statements both provided', () => { expect(() => new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, RoleArn: 'custom', Statement: [{ Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::my-bucket/*' }] })).toThrow(/You cannot specify both Statements and a RoleArn/); }); test('expected resources generated using all default values', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } }); const template = cf.merge(lambda); if (update) fixtures.update('lambda-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('lambda-defaults')); }); test('LogPolicyDeletionPolicy=Retain sets DeletionPolicy on IAM Policy resource', () => { const lambdaWithRetain = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, LogPolicyDeletionPolicy: 'Retain' }); const templateWithRetain = cf.merge(lambdaWithRetain); expect(templateWithRetain.Resources.MyLambdaLogPolicy.DeletionPolicy).toBe('Retain'); }); test('expected resources generated using all default values and a docker image', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { ImageUri: 'MyImage' } }); const template = cf.merge(lambda); if (update) fixtures.update('lambda-docker', template); expect(noUndefined(template)).toEqual(fixtures.get('lambda-docker')); }); test('expected resources generated using all default values and inline code', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { ZipFile: 'fake code' } }); const template = cf.merge(lambda); if (update) fixtures.update('lambda-zipfile', template); expect(noUndefined(template)).toEqual(fixtures.get('lambda-zipfile')); }); test('expected resources generated if RoleArn provided', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, RoleArn: cf.getAtt('CustomLambdaRole', 'Arn') }); const template = cf.merge(lambda, { Resources: { 'CustomLambdaRole': { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: {} } } } }); if (update) fixtures.update('lambda-provided-role', template); expect(noUndefined(template)).toEqual(fixtures.get('lambda-provided-role')); }); test('expected resources generated using no default values', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, Architectures: ['arm64'], DeadLetterConfig: { TargetArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake' }, Description: 'my description', Environment: { Variables: { MyCoolEnv: 'a' } }, FunctionName: 'my-function', Handler: 'index.something', KmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/fake', Layers: ['arn:aws:lambda:us-east-2:590474943231:layer:AWS-Parameters-and-Secrets-Lambda-Extension:4'], MemorySize: 512, ReservedConcurrentExecutions: 10, Runtime: 'nodejs22.x', Tags: [{ Key: 'a', Value: 'b' }], Timeout: 30, TracingConfig: { Mode: 'Active' }, VpcConfig: { SecurityGroupIds: ['sg-12345678'], SubnetIds: ['fake'] }, Condition: 'Always', DependsOn: 'AnotherThing', Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], AlarmName: 'my-alarm', AlarmDescription: 'some alarm', AlarmActions: ['devnull@mapbox.com'], Period: 120, EvaluationPeriods: 2, Statistic: 'Minimum', Threshold: 10, ComparisonOperator: 'LessThanThreshold', TreatMissingData: 'breaching', EvaluateLowSampleCountPercentile: 'ignore', ExtendedStatistics: 'p100', OKActions: ['devnull@mapbox.com'], LogRetentionInDays: 30 }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, lambda ); if (update) fixtures.update('lambda-full', template); expect(noUndefined(template)).toEqual(fixtures.get('lambda-full')); }); test('LogPolicyName parameter correctly overrides the default policy name', () => { const lambda = new cf.shortcuts.Lambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, LogPolicyName: 'CustomLogPolicyName' }); const template = cf.merge(lambda); expect(template.Resources.MyLambdaLogPolicy.Properties.PolicyName).toBe('CustomLogPolicyName'); }); }); describe('[shortcuts] queue-lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.QueueLambda()).toThrow('Options required'); }); test('throws without basic lambda required parameters', () => { expect(() => new cf.shortcuts.QueueLambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws without queue-lambda required parameters', () => { expect(() => new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } })).toThrow(/You must provide an EventSourceArn and ReservedConcurrentExecutions/); }); test('throws when ReservedConcurrentExecutions is a negative number', () => { expect(() => new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: -1 })).toThrow(/ReservedConcurrentExecutions must be greater than or equal to 0/); }); test('expected resources generated with zero concurrency', () => { const zeroLambda = new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: 0 }); const zeroTemplate = cf.merge(zeroLambda); if (update) fixtures.update('queue-lambda-zero', zeroTemplate); expect(noUndefined(zeroTemplate)).toEqual(fixtures.get('queue-lambda-zero')); }); test('expected resources generated', () => { const lambda = new cf.shortcuts.QueueLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:queue/fake', ReservedConcurrentExecutions: 10 }); const template = cf.merge(lambda); if (update) fixtures.update('queue-lambda', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-lambda')); }); }); describe('[shortcuts] scheduled-lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.ScheduledLambda()).toThrow('Options required'); }); test('throws without basic lambda required parameters', () => { expect(() => new cf.shortcuts.ScheduledLambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws without scheduled-lambda required parameters', () => { expect(() => new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } })).toThrow(/You must provide a ScheduleExpression/); }); test('expected resources generated with defaults', () => { const lambda = new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, ScheduleExpression: 'rate(1 hour)' }); const template = cf.merge(lambda); if (update) fixtures.update('scheduled-lambda-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('scheduled-lambda-defaults')); }); test('expected resources generated without defaults', () => { const lambda = new cf.shortcuts.ScheduledLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, ScheduleRoleArn: 'arn:aws:iam::012345678901:role/MyCoolRole', ScheduleGroupName: 'my-cool-stack', ScheduleExpression: 'rate(1 hour)', State: 'DISABLED' }); const template = cf.merge(lambda); if (update) fixtures.update('scheduled-lambda-full', template); expect(noUndefined(template)).toEqual(fixtures.get('scheduled-lambda-full')); }); }); describe('[shortcuts] event-lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.EventLambda()).toThrow('Options required'); }); test('throws without basic lambda required parameters', () => { expect(() => new cf.shortcuts.EventLambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws without event-lambda required parameters', () => { expect(() => new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } })).toThrow(/You must provide an EventPattern/); }); test('expected resources generated with defaults', () => { const lambda = new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventPattern: { source: ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], detail: { state: ['running'] } } }); const template = cf.merge(lambda); if (update) fixtures.update('event-lambda-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('event-lambda-defaults')); }); test('expected resources generated without defaults', () => { const lambda = new cf.shortcuts.EventLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventPattern: { source: ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification'], detail: { state: ['running'] } }, State: 'DISABLED' }); const template = cf.merge(lambda); if (update) fixtures.update('event-lambda-full', template); expect(noUndefined(template)).toEqual(fixtures.get('event-lambda-full')); }); }); describe('[shortcuts] stream-lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.StreamLambda()).toThrow('Options required'); }); test('throws without basic lambda required parameters', () => { expect(() => new cf.shortcuts.StreamLambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws without stream-lambda required parameters', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } })).toThrow(/You must provide an EventSourceArn/); }); test('expected resources generated via defaults', () => { const lambda = new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake' }); const template = cf.merge(lambda); if (update) fixtures.update('stream-lambda-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('stream-lambda-defaults')); }); test('expected resources generated without defaults', () => { const lambda = new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) } ] }, BatchSize: 10000, MaximumBatchingWindowInSeconds: 300, Enabled: false, StartingPosition: 'TRIM_HORIZON' }); const template = cf.merge(lambda); if (update) fixtures.update('stream-lambda-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('stream-lambda-no-defaults')); }); }); describe('[shortcuts] StreamLambda FilterCriteria', () => { test('FilterCriteria must be a JSON-like object', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: ['test'] })).toThrow(); }); test('FilterCriteria must contain property Filter of type array', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: {} })).toThrow(); }); test('FilterCriteria.Filter must be an array', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filter: 613 } })).toThrow(); }); test('FilterCriteria.Filter objects must have Pattern property', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { NotPattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) }, { Pattern: JSON.stringify({ eventName: ['INSERT', 'MODIFY'] }) } ] } })).toThrow(); }); test('FilterCriteria.Filter Pattern must be JSON parseable string', () => { expect(() => new cf.shortcuts.StreamLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, EventSourceArn: 'arn:aws:kinesis:us-east-1:123456789012:stream/fake', FilterCriteria: { Filters: [ { Pattern: '{"eventName":["INSERT","MODIFY"]}' }, { Pattern: { eventName: ['INSERT', 'MODIFY'] } } ] } })).toThrow(); }); }); describe('[shortcuts] log-subscription-lambda', () => { test('throws without options', () => { expect(() => new cf.shortcuts.LogSubscriptionLambda()).toThrow('Options required'); }); test('throws without basic lambda required parameters', () => { expect(() => new cf.shortcuts.LogSubscriptionLambda({})).toThrow(/You must provide a LogicalName, and Code/); }); test('throws without log-subscription-lambda required parameters', () => { expect(() => new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' } })).toThrow(/You must provide a LogGroupName/); }); test('expected resources generated via defaults', () => { const lambda = new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, LogGroupName: 'my-log-group' }); const template = cf.merge(lambda); if (update) fixtures.update('log-subscription-lambda-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('log-subscription-lambda-defaults')); }); test('expected resources generated without defaults', () => { const lambda = new cf.shortcuts.LogSubscriptionLambda({ LogicalName: 'MyLambda', Code: { S3Bucket: 'my-code-bucket', S3Key: 'path/to/code.zip' }, FilterPattern: '{ $.errorCode = 400 }', LogGroupName: 'my-log-group' }); const template = cf.merge(lambda); if (update) fixtures.update('log-subscription-lambda-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('log-subscription-lambda-no-defaults')); }); }); describe('[shortcuts] queue', () => { test('throws without options', () => { expect(() => new cf.shortcuts.Queue()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.Queue({})).toThrow(/You must provide a LogicalName/); }); test('expected resources generated for full defaults', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue' }); const template = cf.merge(queue); if (update) fixtures.update('queue-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-defaults')); }); test('expected resources generated no defaults', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', VisibilityTimeout: 60, maxReceiveCount: 100, DelaySeconds: 60, KmsMasterKeyId: 'alias/my-key', KmsDataKeyReusePeriondSeconds: 86400, MaximumMessageSize: 1024, MessageRetentionPeriod: 60, QueueName: 'my-queue', ReceiveMessageWaitTimeSeconds: 20, Condition: 'Always', DependsOn: 'AnotherThing', TopicName: 'my-topic', DisplayName: 'topic-display-name', DeadLetterVisibilityTimeout: 60 }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, queue ); if (update) fixtures.update('queue-full', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-full')); }); test('expected resources generated for external topic', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', ExistingTopicArn: 'arn:aws:sns:us-east-1:111122223333:MyTopic' }); const template = cf.merge(queue); if (update) fixtures.update('queue-external-topic', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-external-topic')); }); test('expected resources generated for external topic identified by ref', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyQueue', ExistingTopicArn: { Ref: 'TopicForOtherThing' } }); const template = cf.merge( { Resources: { TopicForOtherThing: { Type: 'AWS::SNS::Topic' } } }, queue ); if (update) fixtures.update('queue-external-topic-ref', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-external-topic-ref')); }); test('expected resources generated for FIFO queue', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoQueue', FifoQueue: true }); const template = cf.merge(queue); if (update) fixtures.update('queue-fifo', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-fifo')); }); test('expected resources generated for FIFO queue with specified QueueName', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoQueue', QueueName: 'custom-and-fancy', FifoQueue: true }); const template = cf.merge(queue); if (update) fixtures.update('queue-fifo-queuename', template); expect(noUndefined(template)).toEqual(fixtures.get('queue-fifo-queuename')); }); test('the FifoQueue value false is converted to undefined, to pass CFN validation', () => { const queue = new cf.shortcuts.Queue({ LogicalName: 'MyFifoFalseQueue', FifoQueue: false }); const template = cf.merge(queue); expect(template.Resources.MyFifoFalseQueue.Properties.FifoQueue).toBeUndefined(); }); }); describe('[shortcuts] s3 kinesis firehose', () => { test('throws without options', () => { expect(() => new cf.shortcuts.S3KinesisFirehose()).toThrow('Options required'); }); test('throws without required LogicalName parameter', () => { expect(() => new cf.shortcuts.S3KinesisFirehose({})).toThrow(/You must provide a LogicalName/); }); test('throws without required DestinationBucket parameter', () => { expect(() => new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose' })).toThrow(/You must provide a DestinationBucket/); }); test('expected resources generated for full defaults', () => { const firehose = new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose', DestinationBucket: 'mah-bukkit' }); const template = cf.merge(firehose); if (update) fixtures.update('firehose-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('firehose-defaults')); }); test('expected resources generated with stream', () => { const firehose = new cf.shortcuts.S3KinesisFirehose({ LogicalName: 'MyKinesisFirehose', DestinationBucket: 'mah-bukkit', KinesisStreamARN: 'arn:aws:kinesis:us-east-1:111122223333:stream/my-stream' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, firehose ); if (update) fixtures.update('firehose-with-stream', template); expect(noUndefined(template)).toEqual(fixtures.get('firehose-with-stream')); }); }); describe('[shortcuts] role', () => { test('throws without options', () => { expect(() => new cf.shortcuts.Role()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.Role({})).toThrow(/You must provide a LogicalName and AssumeRolePrincipals/); }); test('expected resources generated with defaults', () => { const role = new cf.shortcuts.Role({ LogicalName: 'MyRole', AssumeRolePrincipals: [{ Service: 'ec2.amazonaws.com' }] }); const template = cf.merge(role); if (update) fixtures.update('role-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('role-defaults')); }); test('expected resources generated without defaults', () => { const role = new cf.shortcuts.Role({ LogicalName: 'MyRole', AssumeRolePrincipals: [{ Service: 'ec2.amazonaws.com' }], Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Tags: [{ Key: 'pipeline-name', Value: 'test' }], Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('role-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('role-no-defaults')); }); }); describe('[shortcuts] cross-account role', () => { test('throws without options', () => { expect(() => new cf.shortcuts.CrossAccountRole()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.CrossAccountRole({})).toThrow(/You must provide a LogicalName and Accounts/); }); test('expected resources generated with defaults', () => { const role = new cf.shortcuts.CrossAccountRole({ LogicalName: 'MyRole', Accounts: [ '123456789012', 'arn:aws:iam::123456789012:root', { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' } ] }); const template = cf.merge(role); if (update) fixtures.update('cross-account-role-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('cross-account-role-defaults')); }); test('expected resources generated without defaults', () => { const role = new cf.shortcuts.CrossAccountRole({ LogicalName: 'MyRole', Accounts: [ '123456789012', 'arn:aws:iam::123456789012:root', { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' } ], Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('cross-account-role-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('cross-account-role-no-defaults')); }); }); describe('[shortcuts] service role', () => { test('throws without options', () => { expect(() => new cf.shortcuts.ServiceRole()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.ServiceRole({})).toThrow(/You must provide a LogicalName and Service/); }); test('expected resources generated with defaults', () => { const role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda' }); const template = cf.merge(role); if (update) fixtures.update('service-role-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('service-role-defaults')); }); test('expected resources generated, service for which AWS::URLSuffix is invalid', () => { const role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda.amazonaws.com' }); const template = cf.merge(role); if (update) fixtures.update('service-role-no-url-suffix', template); expect(noUndefined(template)).toEqual(fixtures.get('service-role-no-url-suffix')); }); test('expected resources generated, service for which AWS::URLSuffix is valid', () => { const role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'ec2' }); const template = cf.merge(role); if (update) fixtures.update('service-role-url-suffix', template); expect(noUndefined(template)).toEqual(fixtures.get('service-role-url-suffix')); }); test('expected resources generated, service for which AWS::URLSuffix is invalid specified with a suffix', () => { const role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'ec2.amazonaws.com' }); const template = cf.merge(role); if (update) fixtures.update('service-role-url-suffix-with-replacement', template); expect(noUndefined(template)).toEqual(fixtures.get('service-role-url-suffix-with-replacement')); }); test('expected resources generated without defaults', () => { const role = new cf.shortcuts.ServiceRole({ LogicalName: 'MyRole', Service: 'lambda.amazonaws.com', Statement: [ { Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::fake/data' } ], ManagedPolicyArns: ['arn:aws:iam::123456789012:policy/fake'], MaxSessionDuration: 3600, Path: '/fake/', RoleName: 'my-role', Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, role ); if (update) fixtures.update('service-role-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('service-role-no-defaults')); }); }); describe('[shortcuts] glue database', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueDatabase()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueDatabase({})).toThrow(/You must provide a LogicalName and Name/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueDatabase({ LogicalName: 'MyDatabase', Name: 'my_database' }); const template = cf.merge(db); if (update) fixtures.update('glue-database-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-database-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueDatabase({ LogicalName: 'MyDatabase', Name: 'my_database', CatalogId: '123456', Description: 'my_database description', LocationUri: 'fakeuri', Parameters: { thing: 'a' }, Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-database-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-database-no-defaults')); }); }); describe('[shortcuts] glue table', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueTable()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueTable({})).toThrow(/You must provide a LogicalName, Name, DatabaseName, and Columns/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ] }); const template = cf.merge(db); if (update) fixtures.update('glue-table-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-table-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, InputFormat: 'fake.input.format', Location: 's3://fake/location', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-table-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-table-no-defaults')); }); }); describe('[shortcuts] glue json table', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueJsonTable()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueJsonTable({})).toThrow(/You must provide a Location/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueJsonTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); const template = cf.merge(db); if (update) fixtures.update('glue-json-table-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-json-table-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueJsonTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-json-table-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-json-table-no-defaults')); }); }); describe('[shortcuts] glue orc table', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueOrcTable()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueOrcTable({})).toThrow(/You must provide a Location/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueOrcTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); const template = cf.merge(db); if (update) fixtures.update('glue-orc-table-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-orc-table-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueOrcTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-orc-table-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-orc-table-no-defaults')); }); }); describe('[shortcuts] glue parquet table', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueParquetTable()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueParquetTable({})).toThrow(/You must provide a Location/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueParquetTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], Location: 's3://fake/location' }); const template = cf.merge(db); if (update) fixtures.update('glue-parquet-table-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-parquet-table-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueParquetTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Columns: [ { Name: 'column', Type: 'string' } ], CatalogId: '1234', Owner: 'Team', Parameters: { table: 'params' }, Description: 'my_table description', Retention: 12, TableType: 'EXTERNAL_TABLE', ViewExpandedText: '/* Presto View */', ViewOriginalText: '/* Presto View: abc123= */', BucketColumns: ['column'], Compressed: true, Location: 's3://fake/location', InputFormat: 'fake.input.format', OutputFormat: 'fake.output.format', StorageParameters: { storage: 'parameters' }, SerdeInfo: { SerializationLibrary: 'fake.serde' }, SkewedColumns: { SkewedColumnNames: ['column'], SkewedColumnValueLocationMap: { fake: 'map' }, SkewedColumnValues: ['value'] }, SortColumns: [ { Column: 'column', SortOrder: 0 } ], StoredAsSubdirectory: true, Condition: 'Always', DependsOn: 'AnotherThing' }); const template = cf.merge( { Conditions: { Always: cf.equals('1', '1') } }, { Resources: { AnotherThing: { Type: 'AWS::SNS::Topic' } } }, db ); if (update) fixtures.update('glue-parquet-table-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-parquet-table-no-defaults')); }); }); describe('[shortcuts] glue iceberg table', () => { test('throws without options', () => { expect(() => new cf.shortcuts.GlueIcebergTable()).toThrow('Options required'); }); test('throws without required parameters', () => { expect(() => new cf.shortcuts.GlueIcebergTable({})).toThrow(/You must provide a LogicalName, Name, DatabaseName, Location, and Schema/); }); test('expected resources generated with defaults', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-defaults')); }); test('expected resources generated without defaults', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, CatalogId: '1234', Location: 's3://fake/location', IcebergVersion: '2' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-no-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-no-defaults')); }); test('throws when EnableOptimizer is true but OptimizerRoleArn is missing', () => { expect(() => new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOptimizer: true })).toThrow(/You must provide an OptimizerRoleArn when EnableOptimizer is true/); }); test('expected resources generated with optimizer using default retention settings', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOptimizer: true, OptimizerRoleArn: 'arn:aws:iam::123456789012:role/OptimizerRole' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-with-optimizer-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-optimizer-defaults')); }); test('expected resources generated with optimizer using custom retention settings', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOptimizer: true, OptimizerRoleArn: cf.getAtt('OptimizerRole', 'Arn'), SnapshotRetentionPeriodInDays: 7, NumberOfSnapshotsToRetain: 3, CleanExpiredFiles: false }); const template = cf.merge( { Resources: { OptimizerRole: { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: {} } } } }, db ); if (update) fixtures.update('glue-iceberg-table-with-optimizer-custom', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-optimizer-custom')); }); test('throws when EnableCompaction is true but CompactionRoleArn is missing', () => { expect(() => new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableCompaction: true })).toThrow(/You must provide a CompactionRoleArn when EnableCompaction is true/); }); test('expected resources generated with compaction using default settings', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableCompaction: true, CompactionRoleArn: 'arn:aws:iam::123456789012:role/CompactionRole' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-with-compaction-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-compaction-defaults')); }); test('expected resources generated with compaction using custom settings', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableCompaction: true, CompactionRoleArn: cf.getAtt('CompactionRole', 'Arn') }); const template = cf.merge( { Resources: { CompactionRole: { Type: 'AWS::IAM::Role', Properties: { AssumeRolePolicyDocument: {} } } } }, db ); if (update) fixtures.update('glue-iceberg-table-with-compaction-custom', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-compaction-custom')); }); test('expected resources generated with both retention and compaction optimizers', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOptimizer: true, OptimizerRoleArn: 'arn:aws:iam::123456789012:role/RetentionRole', EnableCompaction: true, CompactionRoleArn: 'arn:aws:iam::123456789012:role/CompactionRole' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-with-both-optimizers', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-both-optimizers')); }); test('throws when EnableOrphanFileDeletion is true but OrphanFileDeletionRoleArn is missing', () => { expect(() => new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOrphanFileDeletion: true })).toThrow(/You must provide an OrphanFileDeletionRoleArn when EnableOrphanFileDeletion is true/); }); test('expected resources generated with orphan file deletion using default settings', () => { const db = new cf.shortcuts.GlueIcebergTable({ LogicalName: 'MyTable', DatabaseName: 'my_database', Name: 'my_table', Schema: { Type: 'struct', Fields: [ { Name: 'column', Type: 'string', Id: 1, Required: true } ] }, Location: 's3://fake/location', EnableOrphanFileDeletion: true, OrphanFileDeletionRoleArn: 'arn:aws:iam::123456789012:role/OrphanFileDeletionRole' }); const template = cf.merge(db); if (update) fixtures.update('glue-iceberg-table-with-orphan-deletion-defaults', template); expect(noUndefined(template)).toEqual(fixtures.get('glue-iceberg-table-with-orphan-deletion-defaults')); }); test('expected resources generated with orphan file deletion using custom settings', ()