Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assertions): add stack tagging assertions #29247

Merged
merged 14 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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';
jamestelfer marked this conversation as resolved.
Show resolved Hide resolved
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/,
jamestelfer marked this conversation as resolved.
Show resolved Hide resolved
);
});

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