Skip to content

Commit

Permalink
feat(assertions): add stack tagging assertions
Browse files Browse the repository at this point in the history
Adds a Tag class to the assertions library that permits assertions
against tags on synthesized CDK stacks.
  • Loading branch information
jamestelfer committed Nov 12, 2023
1 parent e5959a0 commit 76744ae
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 2 deletions.
34 changes: 34 additions & 0 deletions packages/aws-cdk-lib/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -595,3 +595,37 @@ Annotations.fromStack(stack).hasError(
Match.stringLikeRegexp('.*Foo::Bar.*'),
);
```

## Asserting Stack tags

Tags applied to a `Stack` are not part of the rendered template: instead, they
are included as properties in the Cloud Assembly Manifest. To test that stacks
are tagged as expected, simple assertions can be written.

Given the following setup:

```ts nofixture
import { App, Stack } from 'aws-cdk-lib';
import { Tags } from 'aws-cdk-lib/assertions';

const app = new App();
const stack = new Stack(app, 'MyStack', {
tags: {
'tag-name': 'tag-value'
}
});
```

It is possible to test against these values:

```ts
// using a default 'objectLike' Matcher
Tags.fromStack(stack).hasValues({
'tag-name': 'tag-value'
});

// or another Matcher
Tags.fromStack(stack).hasValues({
'tag-name': Match.anyValue()
});
```
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/assertions/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './capture';
export * from './template';
export * from './match';
export * from './matcher';
export * from './annotations';
export * from './annotations';
export * from './tags';
69 changes: 69 additions & 0 deletions packages/aws-cdk-lib/assertions/lib/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Match } from './match';
import { Matcher } from './matcher';
import { Stack, Stage } from '../../core';

type ManifestTags = { [key: string]: string };

/**
* Allows assertions on the tags associated with a synthesized CDK stack's
* manifest. Stack tags are not part of the synthesized template, so can only be
* checked from the manifest in this manner.
*/
export class Tags {
/**
* Find tags associated with a synthesized CDK `Stack`.
*
* @param stack the CDK Stack to find tags on.
*/
public static fromStack(stack: Stack): Tags {
return new Tags(getManifestTags(stack));
}

private readonly _tags: ManifestTags;

private constructor(tags: ManifestTags) {
this._tags = tags;
}

/**
* Assert that the given Matcher or object matches the tags associated with
* the synthesized CDK Stack's manifest.
*
* @param tags the expected set of tags. This should be a
* string or Matcher object.
*/
public hasValues(tags: any): void {
const matcher = Matcher.isMatcher(tags) ? tags : Match.objectLike(tags);

const result = matcher.test(this.all());
if (result.hasFailed()) {
throw new Error(
'Stack tags did not match as expected:\n' + result.renderMismatch(),
);
}
}

/**
* Get the tags associated with the manifest. This will be an empty object if
* no tags were supplied.
*
* @returns The tags associated with the stack's synthesized manifest.
*/
public all(): ManifestTags {
return this._tags;
}
}

function getManifestTags(stack: Stack): ManifestTags {
const root = stack.node.root;
if (!Stage.isStage(root)) {
throw new Error('unexpected: all stacks must be part of a Stage or an App');
}

// synthesis is not forced: the stack will only be synthesized once regardless
// of the number of times this is called.
const assembly = root.synth();

const artifact = assembly.getStackArtifact(stack.artifactId);
return artifact.tags;
}
95 changes: 95 additions & 0 deletions packages/aws-cdk-lib/assertions/test/tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { IConstruct } from 'constructs';
import {
Annotations,
App,
Aspects,
CfnResource,
IAspect,
Stack,
} from '../../core';
import { Tags as _Tags, Match, Tags } from '../lib';

describe('Tags', () => {
let app: App;

beforeEach(() => {
app = new App();
});

describe('hasValues', () => {
test('simple match', () => {
const stack = new Stack(app, 'stack', {
tags: { 'tag-one': 'tag-one-value' },
});
const tags = Tags.fromStack(stack);
tags.hasValues({
'tag-one': 'tag-one-value',
});
});

test('with matchers', () => {
const stack = new Stack(app, 'stack', {
tags: { 'tag-one': 'tag-one-value' },
});
const tags = Tags.fromStack(stack);
tags.hasValues({
'tag-one': Match.anyValue(),
});
});

test('no tags with absent matcher will fail', () => {
const stack = new Stack(app, 'stack');
const tags = Tags.fromStack(stack);

expect(() => tags.hasValues(Match.absent())).toThrow(
/Received \[object Object], but key should be absent/,
);
});

test('no tags matches empty object successfully', () => {
const stack = new Stack(app, 'stack');
const tags = Tags.fromStack(stack);

tags.hasValues(Match.objectEquals({}));
});

test('no match', () => {
const stack = new Stack(app, 'stack', {
tags: { 'tag-one': 'tag-one-value' },
});
const tags = Tags.fromStack(stack);

expect(() =>
tags.hasValues({
'tag-one': 'mismatched value',
}),
).toThrow(/Expected mismatched value but received tag-one-value/);
});
});

describe('all', () => {
test('simple match', () => {
const stack = new Stack(app, 'stack', {
tags: { 'tag-one': 'tag-one-value' },
});
const tags = Tags.fromStack(stack);
expect(tags.all()).toStrictEqual({
'tag-one': 'tag-one-value',
});
});

test('no tags', () => {
const stack = new Stack(app, 'stack');
const tags = Tags.fromStack(stack);

expect(tags.all()).toStrictEqual({});
});

test('empty tags', () => {
const stack = new Stack(app, 'stack', { tags: {} });
const tags = Tags.fromStack(stack);

expect(tags.all()).toStrictEqual({});
});
});
});
2 changes: 1 addition & 1 deletion packages/aws-cdk-lib/rosetta/assertions/default.ts-fixture
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct } from 'constructs';
import { Aspects, CfnResource, Stack } from 'aws-cdk-lib';
import { Annotations, Capture, Match, Template } from 'aws-cdk-lib/assertions';
import { Annotations, Capture, Match, Tags, Template } from 'aws-cdk-lib/assertions';

interface Expect {
toEqual(what: any): void;
Expand Down

0 comments on commit 76744ae

Please sign in to comment.