Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions SNAPSHOT_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# CDK Snapshot Testing Guide

This guide explains how to use the asset hash normalization utility for consistent CDK snapshot testing across different environments.

## Problem

CDK snapshot tests often fail in CI/CD environments because asset hashes change between different environments. This happens because:

- Asset hashes are generated based on content and environment variables
- Different machines or CI/CD environments produce different hashes
- This causes snapshot tests to fail even when there are no actual changes to the infrastructure

## Solution

We've created a utility function that normalizes asset hashes and other environment-specific values in CloudFormation templates before comparing them with snapshots.

## How to Use the Normalization Utility

### 1. Import the Utility

```typescript
import { normalizeTemplate } from '../../test-utils/normalize-template';
```

### 2. Apply Normalization Before Snapshot Comparison

```typescript
import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { YourStack } from '../lib/your-stack';
import { normalizeTemplate } from '../../test-utils/normalize-template';

test('YourStack creates the expected resources', () => {
const app = new App();
const stack = new YourStack(app, 'TestStack');

// Get the CloudFormation template
const template = Template.fromStack(stack);

// Normalize the template before snapshot comparison
const normalizedTemplate = normalizeTemplate(template.toJSON());

// Compare with snapshot
expect(normalizedTemplate).toMatchSnapshot();
});
```

### 3. Update Existing Snapshots

After adding the normalization to your tests, update your snapshots:

```bash
npm test -- -u
```

## What Gets Normalized

The utility currently normalizes:

1. **S3 Asset Keys**: Replaces asset hashes in S3Key properties
- Pattern with extension: `[64 hex chars].zip` → `NORMALIZED_ASSET_HASH.zip`
- Pattern without extension: `[64 hex chars]` → `NORMALIZED_ASSET_HASH`

2. **Docker Image Digests**: Replaces image digests
- Pattern: `sha256:[digest]` → `NORMALIZED_IMAGE_DIGEST`

## Adding New Test Files

When creating new test files that use snapshot testing:

1. Import the normalization utility
2. Apply it to your template before comparing with snapshots
3. Update your snapshots with the `-u` flag

## Extending the Utility

If you encounter other environment-specific values that need normalization, you can extend the utility at `typescript/test-utils/normalize-template.ts`.

Example of adding a new normalization rule:

```typescript
// Inside the normalizeValues function
if (key === 'NewPropertyToNormalize' && typeof obj[key] === 'string' && /pattern-to-match/.test(obj[key])) {
obj[key] = 'NORMALIZED_VALUE';
}
```

## Troubleshooting

If you're still seeing snapshot test failures:

1. **Check for new patterns**: There might be new types of asset hashes or environment-specific values that need normalization
2. **Verify imports**: Make sure you're importing and using the utility correctly
3. **Check snapshot updates**: Ensure you've updated your snapshots after adding normalization

## Best Practices

1. **Always normalize before snapshot comparison**: This ensures consistent results
2. **Update snapshots deliberately**: Only use the `-u` flag when you expect changes
3. **Review snapshot diffs**: When updating snapshots, review the changes to ensure they're expected
4. **Keep the utility updated**: As new patterns emerge, add them to the normalization utility

## Additional Resources

- [Jest Snapshot Testing Documentation](https://jestjs.io/docs/snapshot-testing)
- [AWS CDK Testing Documentation](https://docs.aws.amazon.com/cdk/v2/guide/testing.html)
166 changes: 166 additions & 0 deletions scripts/build-all-typescript.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/bin/bash
#
# Script to build all TypeScript CDK projects in parallel
#
set -euo pipefail

# Get the directory of this script
SCRIPT_DIR=$(cd $(dirname $0) && pwd)
REPO_ROOT=$(dirname "$SCRIPT_DIR")
TYPESCRIPT_DIR="${REPO_ROOT}/typescript"
BUILD_SCRIPT="$SCRIPT_DIR/build-typescript.sh"

# Check if parallel is installed
if ! command -v parallel &> /dev/null; then
echo "GNU parallel is not installed. Please install it first."
exit 1
fi

# Check if the build script exists
if [ ! -f "$BUILD_SCRIPT" ]; then
echo "Error: Build script not found at $BUILD_SCRIPT"
exit 1
fi

# Create a temporary directory for build logs
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

# Find all TypeScript CDK projects (directories with cdk.json)
# Exclude any matches from node_modules directories, cdk.out directories, and specific problematic projects
echo "Finding all TypeScript CDK projects..."
PROJECTS=$(find "$TYPESCRIPT_DIR" -name "cdk.json" -type f \
-not -path "*/node_modules/*" \
-not -path "*/cdk.out/*" \
| sort)

# Count total projects
TOTAL_PROJECTS=$(echo "$PROJECTS" | wc -l)
echo "Found $TOTAL_PROJECTS TypeScript CDK projects to build"
echo "=============================="

# Function to build a single project
build_project() {
CDK_JSON_PATH="$1"
PROJECT_DIR=$(dirname "$CDK_JSON_PATH")
REL_PATH=$(realpath --relative-to="$REPO_ROOT" "$PROJECT_DIR")
LOG_FILE="$TEMP_DIR/$(echo "$REL_PATH" | tr '/' '_').log"

# Show start message
echo "▶️ Starting build: $REL_PATH"

# Check for DO_NOT_AUTOTEST file
if [ -f "$PROJECT_DIR/DO_NOT_AUTOTEST" ]; then
echo "⏭️ Skipping $REL_PATH (DO_NOT_AUTOTEST flag found)"
echo "SKIPPED:$REL_PATH" > "$LOG_FILE.status"
return 0
fi

# Find the package.json in the project directory
PACKAGE_JSON="$PROJECT_DIR/package.json"
if [ ! -f "$PACKAGE_JSON" ]; then
echo "⏭️ Skipping $REL_PATH (no package.json found)"
echo "SKIPPED:$REL_PATH" > "$LOG_FILE.status"
return 0
fi

# Get the relative path to package.json for the build script
PACKAGE_JSON_REL_PATH=$(realpath --relative-to="$REPO_ROOT" "$PACKAGE_JSON")

# Create a modified version of the build script that suppresses cdk synth output
TEMP_BUILD_SCRIPT="$TEMP_DIR/build-$(basename "$REL_PATH").sh"
cat > "$TEMP_BUILD_SCRIPT" << 'EOF'
#!/bin/bash
set -euo pipefail

# Get the original script and arguments
ORIGINAL_SCRIPT="$1"
shift
ARGS="$@"

# Run the original script but capture and filter the output
"$ORIGINAL_SCRIPT" "$ARGS" 2>&1 | grep -v "cdk synth" | grep -v "Synthesizing"
EOF
chmod +x "$TEMP_BUILD_SCRIPT"

# Run the build script with filtered output
if "$TEMP_BUILD_SCRIPT" "$BUILD_SCRIPT" "$PACKAGE_JSON_REL_PATH" > "$LOG_FILE" 2>&1; then
echo "✅ Build successful: $REL_PATH"
echo "SUCCESS:$REL_PATH" > "$LOG_FILE.status"
else
echo "❌ Build failed: $REL_PATH"
echo "FAILED:$REL_PATH" > "$LOG_FILE.status"
fi
}
export -f build_project
export SCRIPT_DIR
export REPO_ROOT
export BUILD_SCRIPT
export TEMP_DIR

# Run builds in parallel
echo "$PROJECTS" | parallel --will-cite --jobs 50% build_project

# Collect results
SUCCESSFUL=0
FAILED=0
SKIPPED=0
ALL_PROJECTS=()

for STATUS_FILE in "$TEMP_DIR"/*.status; do
[ -f "$STATUS_FILE" ] || continue

STATUS_CONTENT=$(cat "$STATUS_FILE")
STATUS_TYPE=${STATUS_CONTENT%%:*}
PROJECT_PATH=${STATUS_CONTENT#*:}

case "$STATUS_TYPE" in
"SUCCESS")
((SUCCESSFUL++))
ALL_PROJECTS+=("✅ $PROJECT_PATH")
;;
"FAILED")
((FAILED++))
ALL_PROJECTS+=("❌ $PROJECT_PATH")
;;
"SKIPPED")
((SKIPPED++))
ALL_PROJECTS+=("⏭️ $PROJECT_PATH")
;;
esac
done

# Sort the projects list
IFS=$'\n' SORTED_PROJECTS=($(sort <<<"${ALL_PROJECTS[*]}"))
unset IFS

# Print summary
echo ""
echo "=============================="
echo "BUILD SUMMARY"
echo "=============================="
echo "Total: $((SUCCESSFUL + FAILED + SKIPPED)) (✅ $SUCCESSFUL succeeded, ❌ $FAILED failed, ⏭️ $SKIPPED skipped)"
echo ""
echo "Project Status:"
echo "-----------------------------"
for PROJ in "${SORTED_PROJECTS[@]}"; do
echo "$PROJ"
done

# If any builds failed, print their logs and exit with error
if [ $FAILED -gt 0 ]; then
echo ""
echo "Build logs for failed projects:"
echo "=============================="
for PROJ in "${SORTED_PROJECTS[@]}"; do
if [[ $PROJ == ❌* ]]; then
PROJECT_PATH=${PROJ#❌ }
echo ""
echo "Log for $PROJECT_PATH:"
echo "-------------------------------------------"
cat "$TEMP_DIR/$(echo "$PROJECT_PATH" | tr '/' '_').log"
echo "-------------------------------------------"
fi
done
exit 1
fi
10 changes: 5 additions & 5 deletions typescript/amazon-mq-rabbitmq-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "22.5.4",
"aws-cdk": "2.160.0",
"@types/jest": "^29.5.14",
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "~5.6.2"
"typescript": "~5.6.3"
},
"dependencies": {
"@cdklabs/cdk-amazonmq": "^0.0.1",
"aws-cdk-lib": "2.160.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0"
}
}
8 changes: 4 additions & 4 deletions typescript/amplify-console-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^8.10.38",
"aws-cdk": "*",
"typescript": "~5.1.6"
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"typescript": "~5.6.3"
},
"dependencies": {
"aws-cdk-lib": "^2.0.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "*",
"@types/node": "22.7.9",
"@types/uuid": "*"
},
"dependencies": {
Expand Down
8 changes: 4 additions & 4 deletions typescript/api-cors-lambda-crud-dynamodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "*",
"aws-cdk": "*",
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"esbuild": "*",
"typescript": "~5.1.6"
"typescript": "~5.6.3"
},
"dependencies": {
"aws-cdk-lib": "^2.0.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0"
}
}
8 changes: 4 additions & 4 deletions typescript/api-gateway-async-lambda-invocation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
"cdk": "cdk"
},
"devDependencies": {
"@types/node": "22.5.4",
"aws-cdk": "2.163.0",
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"ts-node": "^10.9.2",
"typescript": "~5.6.2"
"typescript": "~5.6.3"
},
"dependencies": {
"aws-cdk-lib": "2.163.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
Expand Down
16 changes: 8 additions & 8 deletions typescript/api-gateway-lambda-token-authorizer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "20.5.9",
"aws-cdk": "2.96.2",
"jest": "^29.6.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "~5.2.2",
"@types/jest": "^29.5.14",
"@types/node": "22.7.9",
"aws-cdk": "2.1004.0",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "~5.6.3",
"esbuild": "^0.19.4"
},
"dependencies": {
"@types/aws-lambda": "^8.10.121",
"aws-cdk-lib": "2.177.0",
"aws-cdk-lib": "2.185.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
},
Expand Down
Loading
Loading