Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ cdk.out

# scratchpad notes
scratch.txt

build.log
18 changes: 9 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,6 @@ manual.deploy.website: local.config
aws s3 sync website/build/ s3://$$(jq -r '.[] | select(.OutputKey=="sourceBucketName") | .OutputValue' cfn.outputs)/ --delete
aws cloudfront create-invalidation --distribution-id $$(jq -r '.[] | select(.OutputKey=="distributionId") | .OutputValue' cfn.outputs) --paths "/*"

manual.deploy.website: local.config
cd website && npm run build
aws s3 sync website/build/ s3://$$(jq -r '.[] | select(.OutputKey=="sourceBucketName") | .OutputValue' cfn.outputs)/ --delete
aws cloudfront create-invalidation --distribution-id $$(jq -r '.[] | select(.OutputKey=="distributionId") | .OutputValue' cfn.outputs) --paths "/*"

local.install: ## Install Javascript dependencies
npm install

Expand Down Expand Up @@ -127,13 +122,18 @@ local.config.docker: ## Setup local config based on branch

## Test targets

.PHONY: test test.website
test: ## Run all tests
cd website && npm test
.PHONY: test test.cdk test.website test.leaderboard
test: test.cdk test.website test.leaderboard ## Run all tests

test.website: ## Run website schema conformance tests
test.cdk: ## Run CDK tests
npm test

test.website: ## Run website tests
cd website && npm test

test.leaderboard: ## Run leaderboard tests
cd website-leaderboard && npm test

local.config.python: ## Setup a Python .venv
python3 -m venv --prompt drem .venv
source .venv/bin/activate
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ DREM offers event organizers tools for managing users, models, cars and fleets,

Instructions for the automated timer can be found [here](./leaderboard-timer/README.md)

## Testing

DREM uses a layered test pipeline covering CDK assertions, pre-deploy unit tests, and post-deploy live validation against the deployed stack. See the [testing strategy](./docs/testing-strategy.md) for full details.

Please note that the most recent release of Raspberry Pi OS "Bookworm" is currently not supported, please use "Bullseye" (Legacy) for the RPi operating system.

## Deployment
Expand Down
8 changes: 3 additions & 5 deletions bin/drem.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env node
import { App } from 'aws-cdk-lib';
// import { App, Aspects } from 'aws-cdk-lib';
// import { AwsSolutionsChecks } from 'cdk-nag';
import { App, Aspects } from 'aws-cdk-lib';
import { AwsSolutionsChecks } from 'cdk-nag';
import { BaseStack } from '../lib/base-stack';
import { CdkPipelineStack } from '../lib/cdk-pipeline-stack';
import { DeepracerEventManagerStack } from '../lib/drem-app-stack';

const app = new App();
// Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));

let accountId = process.env['CDK_DEFAULT_ACCOUNT'];

Expand Down Expand Up @@ -76,7 +75,6 @@ if (domainName) {
if (app.node.tryGetContext('manual_deploy') === 'True') {
console.info('Manual Deploy started....');

// Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true }));
const baseStack = new BaseStack(app, `drem-backend-${labelName}-base`, {
email: mailAddress,
labelName: labelName,
Expand Down
112 changes: 112 additions & 0 deletions docs/testing-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# DREM Testing Strategy

## Overview

DREM uses a layered testing approach built into the CDK pipeline. Tests run at two points: before deployment (fast, no AWS dependencies) and after deployment (slower, validates the live stack). All results are published as JUnit XML to the CodeBuild test report console.

---

## Pipeline test stages

```
Source → CDK Synth → [Pre-deploy tests] → Deploy → [Post-deploy tests]
```

### Pre-deploy — CDK (`test/`)

**Runner:** Jest + jest-junit
**When:** Before any deployment, as part of CDK synth
**Count:** 1

| File | What it tests |
|---|---|
| `test/deepracer-event-manager.test.ts` | Pipeline stack synthesizes with exactly one `AWS::CodePipeline::Pipeline` resource |

The intent here is a sanity check that the CDK app synthesizes correctly, not exhaustive infrastructure assertions. cdk-nag runs alongside this to enforce AWS security best practices on every synth.

---

### Pre-deploy — Website (`website/src/`)

**Runner:** Vitest
**Config:** `website/vitest.config.ts`
**When:** Before deployment, in the `PreDeployTests` CodeBuild step
**Count:** 40 (4 files)

| File | What it tests |
|---|---|
| `src/support-functions/time.test.ts` | Time formatting and calculation utilities |
| `src/admin/race-admin/support-functions/metricCalculations.test.ts` | Race metric calculations |
| `src/admin/race-admin/support-functions/raceTableConfig.test.ts` | Race table column configuration |
| `src/components/devices-table/deviceTableConfig.test.ts` | Device table column configuration |

These are pure unit tests with no network calls or AWS dependencies. They run in a Node environment and complete in seconds.

The `graphql-schema-conformance.test.ts` and `smoke.test.ts` files are **excluded** from this run via `vitest.config.ts` — they require a deployed environment and run post-deploy instead.

---

### Post-deploy — Website (`website/src/__tests__/`)

**Runner:** Vitest
**Config:** `website/vitest.config.post-deploy.ts` (no excludes)
**When:** After `MainSiteDeployToS3` completes, in the `PostDeployTests` CodeBuild step
**Count:** 16 (2 files)

#### GraphQL Schema Conformance — Layer 1 (`graphql-schema-conformance.test.ts`)

Validates that every GraphQL operation in the frontend codebase is valid against the **live AppSync schema**, fetched fresh from the deployed API via `aws appsync get-introspection-schema` at the start of the post-deploy step.

| Test group | What it checks |
|---|---|
| Schema itself | `schema.graphql` is parseable; has Query, Mutation, Subscription root types |
| `mutations.ts` | Exports ≥1 mutation; all are syntactically valid GraphQL; all validate against schema |
| `queries.ts` | Exports ≥1 query; all are syntactically valid GraphQL; all validate against schema |
| `subscriptions.ts` | Exports ≥1 subscription; all are syntactically valid GraphQL; all validate against schema |
| `.js` baselines | The original `.js` operation files all still pass (regression guard) |

This catches a class of bug where a TypeScript migration introduces invented or mistyped fields that don't exist in the real schema. No mocking, no network calls — pure static validation against the fetched schema file.

#### Smoke Tests (`smoke.test.ts`)

Playwright tests that hit the live CloudFront URL (`DREM_WEBSITE_URL` env var, set from the CDK stack output).

| Test | What it checks |
|---|---|
| `DREM_WEBSITE_URL env var is set` | Env var is present (guards against misconfiguration) |
| `main page returns HTTP 200` | CloudFront distribution is serving the site |
| `page title contains DREM` | Correct build was deployed (not a blank page or error page) |
| `page renders a root element` | React app mounted successfully (`#root` exists in DOM) |

These tests are written to be compatible with the CloudWatch Synthetics Playwright runtime (`syn-playwright-nodejs-*`), so they can be promoted to a continuous monitoring canary without rewriting.

---

## Test count summary

| Stage | Runner | Count |
|---|---|---|
| Pre-deploy CDK | Jest | 1 |
| Pre-deploy website | Vitest | 40 |
| Post-deploy conformance | Vitest + Playwright | 12 |
| Post-deploy smoke | Vitest + Playwright | 4 |
| **Total** | | **57** |

---

## What's not tested yet (known gaps)

- **React component rendering** — no `@testing-library/react` tests for UI components beyond table config
- **Lambda function logic** — backend business logic is not unit tested
- **Authentication flows** — Cognito login/redirect not covered by smoke tests
- **Leaderboard and overlay apps** — `website-leaderboard` has 2 tests (`time.test.ts`); `website-stream-overlays` has none
- **GraphQL Layer 2** — conformance tests validate syntax and field existence but not response shapes or resolver behaviour

---

## Future directions

- **CloudWatch Synthetics canary** — promote `smoke.test.ts` to a scheduled canary for continuous post-deploy monitoring between events
- **GraphQL Layer 2** — validate that operation response shapes match the TypeScript types generated from the schema
- **Component tests** — add Vitest + `@testing-library/react` tests for key UI components (leaderboard, race timer, model upload)
- **Lambda unit tests** — add Python unit tests for Lambda handlers, run in the pre-deploy stage
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ module.exports = {
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
},
reporters: [
'default',
['jest-junit', { outputDirectory: 'reports', outputName: 'junit-cdk.xml', suiteName: 'CDK Tests' }],
],
};
96 changes: 81 additions & 15 deletions lib/cdk-pipeline-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as iam from 'aws-cdk-lib/aws-iam';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as pipelines from 'aws-cdk-lib/pipelines';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
import { BaseStack } from './base-stack';
import { DeepracerEventManagerStack } from './drem-app-stack';
Expand All @@ -31,6 +32,8 @@ class InfrastructurePipelineStage extends Stage {
public readonly streamingOverlaySourceBucketName: cdk.CfnOutput;
public readonly termAndConditionsDistributionId: cdk.CfnOutput;
public readonly termAndConditionsSourceBucketName: cdk.CfnOutput;
public readonly dremWebsiteUrl: cdk.CfnOutput;
public readonly appsyncId: cdk.CfnOutput;

constructor(scope: Construct, id: string, props: InfrastructurePipelineStageProps) {
super(scope, id, props);
Expand Down Expand Up @@ -68,6 +71,8 @@ class InfrastructurePipelineStage extends Stage {
this.streamingOverlayDistributionId = stack.streamingOverlayDistributionId;
this.termAndConditionsSourceBucketName = stack.tacSourceBucketName;
this.termAndConditionsDistributionId = stack.tacWebsitedistributionId;
this.dremWebsiteUrl = stack.dremWebsiteUrl;
this.appsyncId = stack.appsyncId;
}
}
export interface CdkPipelineStackProps extends cdk.StackProps {
Expand Down Expand Up @@ -125,22 +130,24 @@ export class CdkPipelineStack extends cdk.Stack {
],
commands: [
'npm install',
// Tests - run before synth so pipeline fails fast on test failure
'npm test',
'cd website && npm test && cd ..',
'cd website-leaderboard && npm test && cd ..',
`npx cdk@${CDK_VERSION} synth --all -c email=${props.email} -c label=${props.labelName}` +
` -c account=${props.env.account} -c region=${props.env.region}` +
` -c source_branch=${props.sourceBranchName} -c source_repo=${props.sourceRepo}` +
(props.domainName ? ` -c domain_name=${props.domainName}` : ''),
],
// partialBuildSpec: codebuild.BuildSpec.fromObject(
// {
// "reports": {
// "pytest_reports": {
// "files": ["unittest-report.xml"],
// "base-directory": "reports",
// "file-format": "JUNITXML",
// }
// }
// }
// ),
partialBuildSpec: codebuild.BuildSpec.fromObject({
reports: {
jest_reports: {
files: ['junit-cdk.xml', 'junit-website.xml', 'junit-leaderboard.xml'],
'base-directory': 'reports',
'file-format': 'JUNITXML',
},
},
}),
rolePolicyStatements: [
new iam.PolicyStatement({
actions: ['sts:AssumeRole'],
Expand Down Expand Up @@ -188,8 +195,7 @@ export class CdkPipelineStack extends cdk.Stack {
];

// Main website Deploy to S3
infrastructure_stage.addPost(
new pipelines.CodeBuildStep('MainSiteDeployToS3', {
const mainSiteDeployStep = new pipelines.CodeBuildStep('MainSiteDeployToS3', {
installCommands: [`npm install -g @aws-amplify/cli@${AMPLIFY_VERSION}`],
buildEnvironment: {
privileged: true,
Expand Down Expand Up @@ -222,8 +228,8 @@ export class CdkPipelineStack extends cdk.Stack {
distributionId: infrastructure.distributionId,
},
rolePolicyStatements: rolePolicyStatementsForWebsiteDeployStages,
})
);
});
infrastructure_stage.addPost(mainSiteDeployStep);

// Leaderboard website Deploy to S3
infrastructure_stage.addPost(
Expand Down Expand Up @@ -322,7 +328,67 @@ export class CdkPipelineStack extends cdk.Stack {
})
);

// Post-deploy tests — run after MainSiteDeployToS3 completes
const postDeployStep = new pipelines.CodeBuildStep('PostDeployTests', {
buildEnvironment: {
computeType: codebuild.ComputeType.SMALL,
},
installCommands: [
`n ${NODE_VERSION}`,
'node --version',
'npx playwright install --with-deps chromium',
],
commands: [
'npm install',
'aws appsync get-introspection-schema --api-id $appsyncId --format SDL website/src/graphql/schema.graphql',
'cd website && npm run test:post-deploy && cd ..',
],
envFromCfnOutputs: {
appsyncId: infrastructure.appsyncId,
DREM_WEBSITE_URL: infrastructure.dremWebsiteUrl,
},
rolePolicyStatements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['appsync:GetIntrospectionSchema'],
resources: ['*'],
}),
],
partialBuildSpec: codebuild.BuildSpec.fromObject({
reports: {
post_deploy_reports: {
files: ['junit-post-deploy.xml'],
'base-directory': 'reports',
'file-format': 'JUNITXML',
},
},
}),
});
postDeployStep.addStepDependency(mainSiteDeployStep);
infrastructure_stage.addPost(postDeployStep);

pipeline.buildPipeline();

// Suppress cdk-nag findings for CDK Pipelines-managed resources we don't control
NagSuppressions.addStackSuppressions(this, [
{
id: 'AwsSolutions-IAM5',
reason: 'Wildcard permissions are managed by CDK Pipelines for CodeBuild and asset publishing roles',
},
{
id: 'AwsSolutions-CB4',
reason: 'KMS encryption for CodeBuild projects is managed by CDK Pipelines',
},
{
id: 'AwsSolutions-S1',
reason: 'Access logging for the pipeline artifacts bucket is managed by CDK Pipelines',
},
{
id: 'AwsSolutions-SNS3',
reason: 'SSL enforcement on the pipeline notification topic is not exposed by CDK Pipelines',
},
]);

const topic = new sns.Topic(this, 'PipelineTopic');
topic.addSubscription(new subs.EmailSubscription(props.email));
const rule = new notifications.NotificationRule(this, 'NotificationRule', {
Expand Down
6 changes: 4 additions & 2 deletions lib/drem-app-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export class DeepracerEventManagerStack extends cdk.Stack {
public readonly streamingOverlaySourceBucketName: cdk.CfnOutput;
public readonly tacWebsitedistributionId: cdk.CfnOutput;
public readonly tacSourceBucketName: cdk.CfnOutput; // this is missing
public readonly dremWebsiteUrl: cdk.CfnOutput;
public readonly appsyncId: cdk.CfnOutput;

constructor(scope: Construct, id: string, props: DeepracerEventManagerStackProps) {
super(scope, id, props);
Expand Down Expand Up @@ -205,7 +207,7 @@ export class DeepracerEventManagerStack extends cdk.Stack {
new cdk.CfnOutput(this, 'DremWebsite', {
value: 'https://' + props.cloudfrontDomainNames?.at(0) || props.cloudfrontDistribution.distributionDomainName,
});
new cdk.CfnOutput(this, 'DremWebsiteDistributionDomainName', {
this.dremWebsiteUrl = new cdk.CfnOutput(this, 'DremWebsiteDistributionDomainName', {
value: 'https://' + props.cloudfrontDistribution.distributionDomainName,
});
new cdk.CfnOutput(this, 'tacWebsite', {
Expand Down Expand Up @@ -287,7 +289,7 @@ export class DeepracerEventManagerStack extends cdk.Stack {
value: cwRumLeaderboardAppMonitor.config,
});

new cdk.CfnOutput(this, 'appsyncId', { value: appsyncResources.api.apiId });
this.appsyncId = new cdk.CfnOutput(this, 'appsyncId', { value: appsyncResources.api.apiId });

new cdk.CfnOutput(this, 'appsyncEndpoint', {
value: appsyncResources.api.graphqlUrl,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"prettier": "^3.8.1",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
Expand Down
Loading
Loading