AWS-CDK How to test props with Fine-grained assertions - amazon-web-services

How can I test all the props passed to the s3.Bucket?
I would like to test all the props passed to the s3.Bucket (no Snapshot).
The test is giving me an error on WebsiteConfiguration ...
To check how to write the prop obj inside the toHaveResource fn i used this doc
Anyone could help me?
import * as cdk from "#aws-cdk/core";
import * as s3 from "#aws-cdk/aws-s3";
export class S3CdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.Bucket(this, "ReactGitHubActionBucket", {
versioned: true,
publicReadAccess: true,
websiteIndexDocument: "index.html",
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
}
}
import { expect as expectCDK, haveResource } from '#aws-cdk/assert';
import * as cdk from '#aws-cdk/core';
import * as Stacks from '../lib/s3-cdk-stack';
test('First test', () => {
const app = new cdk.App();
const stack = new Stacks.S3CdkStack(app, 'S3CdkTestStack');
expectCDK(stack).to(haveResource("AWS::S3::Bucket",{
VersioningConfiguration: {
Status: "Enabled"
},
WebsiteConfiguration: {
IndexDocument: "index.html"
}
}))
});
$ jest
FAIL test/stacks.test.ts
✕ First test (68 ms)
● First test
None of 1 resources matches resource 'AWS::S3::Bucket' with {
"$objectLike": {
"VersioningConfiguration": {
"Status": "Enabled"
},
"WebsiteConfiguration": {
"IndexDocument": "index.html"
}
}
}.
- Field WebsiteConfiguration missing in:
{
"Type": "AWS::S3::Bucket",
"Properties": {
"VersioningConfiguration": {
"Status": "Enabled"
}
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain"
}

It may have been updated since you wrote your question, but as of CDK 1.113.0 the following example works for me.
Note that my imports are slightly different to yours, and that I've used .toHaveResource() instead of .to(haveResource()).
// CDK version: 1.113.0
import '#aws-cdk/assert/jest';
import * as cdk from '#aws-cdk/core';
import * as s3 from '#aws-cdk/aws-s3';
// Included for completeness, but you'd import this:
class S3CdkStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'ReactGitHubActionBucket', {
versioned: true,
publicReadAccess: true,
websiteIndexDocument: 'index.html',
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
}
}
// This test should pass:
test('Test complete S3 bucket configuration', () => {
const app = new cdk.App();
const stack = new S3CdkStack(app, 'S3CdkTestStack');
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
expect(stack).toHaveResource('AWS::S3::Bucket', {
VersioningConfiguration: {
Status: 'Enabled',
},
WebsiteConfiguration: {
IndexDocument: 'index.html',
},
});
});
Bonus tip: .toHaveResourceLike()
.toHaveResource() works well when the resource property value you're checking is simple (ie. a scalar value like a string or a number), but if it's more complex you should look into .toHaveResourceLike() which unfortunately isn't very well documented (link to the v1.113.0 source).
For example, the CloudFormation template you've created via the S3CdkStack() class above contains a Lambda function with the following Description property:
{
"Resources": {
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Description": {
"Fn::Join": [
"",
[
"Lambda function for auto-deleting objects in ",
{
"Ref": "ReactGitHubActionBucket1CE57800"
},
" S3 bucket."
]
]
}
}
}
}
}
Testing for the existence of "auto-deleting" in that description (just as an expository example) with .toHaveResource() would require including the whole description object, which is fragile and contains what looks like a hash...
In this case it's much better to use .toHaveResourceLike() and check only the part of the object you're interested in testing:
test('Test a subset of the Lambda description', () => {
const app = new cdk.App();
const stack = new S3CdkStack(app, 'S3CdkTestStack');
// ↓↓↓↓
expect(stack).toHaveResourceLike('AWS::Lambda::Function', {
// Only need to include the subset of
// the value object you're testing:
Description: {
'Fn::Join': [
'',
['Lambda function for auto-deleting objects in '],
],
},
});
});

Related

CDKpipeline - Cannot set lambda layer in a stack called from multiple stages in a pipeline

I want to set multiple stages with the same stack in a cdk pipeline. But I am getting the following error when bootstrapping my cdk project
C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\code.ts:185
throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` +
^
Error: Asset is already associated with another stack 'msm-customer'. Create a new Code instance for every stack.
at AssetCode.bind (C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\code.ts:185:13)
at new LayerVersion (C:\dev\aws-cdk\node_modules\aws-cdk-lib\aws-lambda\lib\layers.ts:124:29)
at new CustomerStack (C:\dev\aws-cdk\lib\CustomerStack.ts:22:17)
After debugging the code I found out that it is the layer declaration in the "CustomerStack" that is causing the issue. If I comment the layer section or if I keep only one stage in my pipeline then the bootstrap cmd works successfully. .
Pipelinestack.ts
// Creates a CodeCommit repository called 'CodeRepo'
const repo = new codecommit.Repository(this, 'CodeRepo', {
repositoryName: "CodeRepo"
});
const pipeline = new CodePipeline(this, 'Pipeline-dev', {
pipelineName: 'Pipeline-dev',
synth: new CodeBuildStep('SynthStep-dev', {
//role: role,
input: CodePipelineSource.codeCommit(repo, 'master'),
installCommands: [
'npm install -g aws-cdk'
],
commands: [
'npm ci',
'npm run build',
'npx cdk synth'
],
})
});
pipeline.addStage(new PipelineStage(this, 'dev'));
pipeline.addStage(new PipelineStage(this, 'uat'));
pipeline.addStage(new PipelineStage(this, 'prod'));
PipelineStage.ts
export class PipelineStage extends Stage {
constructor(scope: Construct, id: string, props?: StageProps) {
super(scope, id, props);
new CustomerStack(this, 'msm-customer-' + id, {
stackName: 'msm-customer'
env: {
account: process.env.ACCOUNT,
region: process.env.REGION,
},
});
}
}
CustomerStack.ts
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import 'dotenv/config';
export class CustomerStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
//define existing role
const role = iam.Role.fromRoleArn(this, 'Role',
`arn:aws:iam::${Stack.of(this).account}:role/` + process.env.IAM_ROLE,
{ mutable: false },
);
//define layer
const layer = new lambda.LayerVersion(this, 'msm-layer', {
code: lambda.Code.fromAsset('resources/layer/customer'),
description: 'Frontend common resources',
compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
removalPolicy: cdk.RemovalPolicy.DESTROY
});
const lambdaDefault = new lambda.Function(this, 'default', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('resources/lambda/customer/default'),
handler: 'index.handler',
role: role,
timeout: Duration.seconds(20),
memorySize: 256,
layers: [layer],
allowPublicSubnet: true,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }
});
//rest of the code
}
}

How do I configure AWS Amplify to automatically add a new user to a group?

I've tried editing GROUP in
amplify/backend/function/<appnameXXXXX>PostConfirmation/function-parameters.json
followed by amplify push but that has no effect (and seems to leave the backend unchanged). I've tried editing the GROUP environment variable in the Lambda Console for <appnameXXXXX>PostConfirmation-<env> followed by amplify pull, which does update the local file above as expected, but new users are still not added to the specified group.
What am I doing wrong? How do I configure my app to automatically add a new user to a default group?
Relevant files, neither of which I've edited (except as noted for GROUP above):
function-parameters.json:
{
"trigger": true,
"modules": [
"add-to-group"
],
"parentResource": "app_nameXXXXXX",
"functionName": "app_nameXXXXXXPostConfirmation",
"resourceName": "app_nameXXXXXXPostConfirmation",
"parentStack": "auth",
"triggerEnvs": [],
"triggerDir": "/snapshot/repo/build/node_modules/#aws-amplify/amplify-category-auth/provider-utils/awscloudformation/triggers/PostConfirmation",
"triggerTemplate": "PostConfirmation.json.ejs",
"triggerEventPath": "PostConfirmation.event.json",
"roleName": "app_nameXXXXXXPostConfirmation",
"skipEdit": true,
"GROUP": "Testers",
"enableCors": false
}
src/add-to-group.js:
const aws = require('aws-sdk');
const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({
apiVersion: '2016-04-18',
});
/**
* #type {import('#types/aws-lambda').PostConfirmationTriggerHandler}
*/
exports.handler = async event => {
const groupParams = {
GroupName: process.env.GROUP,
UserPoolId: event.userPoolId,
};
const addUserParams = {
GroupName: process.env.GROUP,
UserPoolId: event.userPoolId,
Username: event.userName,
};
/**
* Check if the group exists; if it doesn't, create it.
*/
try {
await cognitoidentityserviceprovider.getGroup(groupParams).promise();
} catch (e) {
await cognitoidentityserviceprovider.createGroup(groupParams).promise();
}
/**
* Then, add the user to the group.
*/
await cognitoidentityserviceprovider.adminAddUserToGroup(addUserParams).promise();
return event;
};
You can use amplify update function command to manage your function's env variables.
Which will add corresponding configs in, amplify/team-provider-info.json and your lambda functions's cloudformation template json file.
This is how the command generate the ENV variable.
Lambda function's cloudformation-template.json
{
"Parameters": {
...
"GROUP": {
"Type": "String"
},
...
},
"Resources": {
...
"LambdaFunction": {
...
"Properties": {
...
"Environment": {
"Variables": {
"GROUP": {
"Ref": "GROUP"
},
}
}
}
}
}
}
team-provider-info.json
{
"dev": {
"categories": {
"function": {
"<function-name>": {
"GROUP": "users",
}
}
}
}
}

Simple CDK Testing Failing

this is my set up
//bin
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import {Testing} from '../lib/index';
const app = new cdk.App();
new Testing(app, 'Testing');
//lib
import {Duration, Stack, StackProps} from 'aws-cdk-lib'
export class Testing extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Define construct contents here
// example resource
const queue = new sqs.Queue(this, 'TestingQueue', {
visibilityTimeout: Duration.seconds(300)
});
}
}
//test
import {Stack} from 'aws-cdk-lib/core';
import sqs = require ('../lib/index');
import'#aws-cdk/assert/jest'
test('SQS Queue Created', () => {
const stack = new Stack();
new sqs.Testing(stack, 'sqs');
expect(stack).toHaveResource('AWS::SQS::Queue')
});
//npm-package
"devDependencies": {
"#types/jest": "^26.0.10",
"#types/node": "10.17.27",
"aws-cdk-lib": "2.1.0",
"constructs": "^10.0.0",
"jest": "^26.4.2",
"ts-jest": "^26.2.0",
"typescript": "~3.9.7"
},
"peerDependencies": {
"#aws-cdk/assert": "^2.1.0",
"aws-cdk-lib": "2.1.0",
"constructs": "^10.0.0"
},
"jest": {
"moduleFileExtensions": [
"js"
]
}
I get this when I run: npm run build; npm run test.
None of 0 resources matches resource 'AWS::SQS::Queue' with { "$anything": true }.
I don't understand???
This should be straight forward. I can see the resource in cdk.out, the stack synthesisez, the stack deploys.
It only happens with fine grained assertions. The snapshot works.
You are asserting on the empty stack. Assert on Testing instead.
test('SQS Queue Created', () => {
const stack = new Stack(); // stack is empty, has no queue
const iHaveAQueue = new sqs.Testing(stack, 'sqs');
expect(stack).toHaveResource('AWS::SQS::Queue') // will fail
expect(iHaveAQueue).toHaveResource('AWS::SQS::Queue') // will pass
});

AWS cdk how to tag resources in TypeScript?

I have a cdk project in which I am creating an DynamoDB table and adding tag to it like below,
import * as core from "#aws-cdk/core";
import * as dynamodb from "#aws-cdk/aws-dynamodb";
import { Tag } from "#aws-cdk/core";
export class DynamoDbTable extends core.Construct {
constructor(scope: core.Construct, id: string) {
super(scope, id);
function addTags(resource : any) {
Tag.add(resource, "Key", "value");
}
const table = new dynamodb.Table(this, "abcd", {
partitionKey: { name: "name", type: dynamodb.AttributeType.STRING },
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
tableName: 'tableName',
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
addTags(table)
}
}
Above code works fine add tags to table but this tagging method is deprecated now here so how can I replace this tagging method?
You can tag a construct and CDK should add tags recursively. You shouldn't need to include your embedded addTags function. For example to use the newer non-deprecated method, in your code, you can use this to refer to the construct you are dealing with and do:
import { Tag } from "#aws-cdk/core";
export class DynamoDbTable extends core.Construct {
constructor(scope: core.Construct, id: string) {
super(scope, id);
const table = new dynamodb.Table(this, "abcd", {
partitionKey: { name: "name", type: dynamodb.AttributeType.STRING },
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
tableName: 'tableName',
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
});
Tags.of(this).add('Foo', 'Bar');
}
}

Referencing the physical resource ID in a AwsCustomResource in onUpdate or onDelete

I am trying to create an AwsCustomResource to make single AWS API calls, specifically (create|update|delete)-organisational-unit. In the onCreate I can see that to return the OU's Id as the physical resource ID I can specify a physicalResourceIdPath, however for the onUpdate and onDelete I need to specify this in the API call.
I can not see how, other than to use regular custom resources with my own Lambda to gain access to the event.
Here is what I have so far, 'help?' is what I'm missing.
import * as cdk from '#aws-cdk/core';
import { AwsCustomResource } from '#aws-cdk/custom-resources'
export interface OrganisationalUnitProps {
readonly name: string;
readonly parentId: string;
}
export class OrgansationalUnit extends AwsCustomResource {
constructor(scope: cdk.Construct, id: string, props: OrganisationalUnitProps) {
super(scope, id, {
onCreate: {
service: 'Organizations',
action: 'createOrganizationalUnit',
parameters: {
Name: props.name,
ParentId: props.parentId
},
physicalResourceIdPath: 'OrganizationalUnit.Id'
},
onDelete: {
service: 'Organizations',
action: 'deleteOrganizationalUnit',
parameters: {
OrganizationalUnitId: 'help?'
}
}
});
}
}
Whilst there isn't a perfectly clean way to do it yet, here is an answer I was given in this issue that I raised on the CDK github page.
You create two custom resources. One is just a create, and one is just a delete.
const connectDirectory = new AwsCustomResource(this, 'ConnectDirectory', {
onCreate: {
service: 'DirectoryService',
action: 'connectDirectory',
parameters: { ... },
physicalResourceId: PhysicalResourceId.fromResponse('DirectoryId')
},
});
const deleteDirectory = new AwsCustomResource(this, 'DeleteDirectory', {
onDelete: {
service: 'DirectoryService',
action: 'deleteDirectory',
parameters: {
DirectoryId: connectDirectory.getResponseField('DirectoryId'),
},
},
});
That github issue has been accepted as a change request, so there may be a better answer in the future.
nowadays there is PhysicalResourceIdReference which you can use like this:
new AwsCustomResource(this, 'my-custom-resource', {
...
onCreate: {
...
physicalResourceId: PhysicalResourceId.fromResponse( ... )
},
onDelete: {
...
parameters: {
physicalResourceId: new PhysicalResourceIdReference(),
}
}
});