DynamoDB Integration Tests
CI's Verify DB job (db-integration-tests in .github/workflows/verify.yml) runs the backend DynamoDB integration tests against a real DynamoDB Local engine, alongside the Postgres integration tests. Those tests do not run in the ordinary unit suite, so a failure that only shows up in CI can be hard to reproduce. This guide gives you a copy-paste recipe that stands up the same DynamoDB environment locally, so you can reproduce a CI DynamoDB failure, fix it, and confirm green before you push.
Every value below is taken verbatim from the db-integration-tests job in .github/workflows/verify.yml — that job is the source of truth. A test in apps/infra/ci/workflows.test.ts fails if this guide ever drifts from it.
How the backend picks up the DynamoDB config
- Locally, the integration test runner reads DynamoDB settings from the root
.envfile.apps/backend/vitest.integration.config.tsparses the root.envand injects it into the test environment. In CI there is no.env; the job'senv:block supplies the same variables instead. - The DynamoDB integration suites skip themselves when the engine is not configured.
apps/backend/app/common/repositories/user-ddb.repository.integration.test.tsandapps/backend/app/authentication/repositories/magic-token-ddb.repository.integration.test.tswrap their tests indescribe.skipIf(!isDdbConfigured()), andisDdbConfigured()is true only when bothDYNAMODB_ENDPOINTandDYNAMODB_TABLE_NAMEare set. Iftest:integrationreports zero DynamoDB tests, the environment is not configured — the tests did not pass, they were skipped. - The test table is created automatically.
setupTestDynamoTable()(apps/backend/app/common/test/ddb-test.util.ts) creates the table with the production schema (PK/SK, theGSI1email index,PAY_PER_REQUEST, and thettlTTL attribute) before the suite and retries while the engine boots, so there is no separate setup step. - A safety guard refuses non-local engines.
assertNotProductionDdb()throws unlessENVIRONMENTis notproductionandDYNAMODB_ENDPOINTpoints atlocalhostor127.0.0.1. The harness creates, wipes, and deletes tables, so it will not run against a real AWS endpoint.
Prerequisites
- Docker installed and running.
- Local TCP port
8000free (stop any process using it). - Dependencies installed:
pnpm installfrom the repo root.
Step 1 — Start the CI-matching DynamoDB Local
Run a throwaway amazon/dynamodb-local:3.3.0 container on the port CI uses:
docker run --rm -d --name ws-mono-st-ddb -p 8000:8000 amazon/dynamodb-local:3.3.0
Step 2 — Point the root .env at the container
Set these keys in the root .env file so the integration tests connect to your local engine (values match the db-integration-tests job exactly):
DYNAMODB_ENDPOINT=http://localhost:8000
DYNAMODB_TABLE_NAME=ws-mono-st-test-backend-table
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
DynamoDB Local ignores the credential values but the AWS SDK still needs some credentials present;
dummyis alphanumeric, which DynamoDB Local (>= 2.0.0) requires forAWS_ACCESS_KEY_ID. These temporarily override whateverpnpm script update-envwrote; note the values you are replacing (or back the file up) and restore them in Step 5.
Step 3 — Run the integration tests
Run the exact command CI runs:
pnpm --filter ws-mono-st-backend test:integration
The test table is created automatically before the suite. If the output shows no DynamoDB tests, DYNAMODB_ENDPOINT/DYNAMODB_TABLE_NAME are not set in the root .env (see Step 2) — the tests were skipped, not passed.
Step 4 — Reproduce, fix, confirm
You should now see the same failure CI's Verify DB job reports. Fix the code, re-run the command from Step 3, and confirm it is green locally before pushing.
Step 5 — Tear down and restore
Remove the container and restore your .env:
docker rm -f ws-mono-st-ddb
Then restore the original values in the root .env — re-run pnpm script update-env --pr <n> (or --env <name>), or restore the backup you took in Step 2.
Writing a test for a new DynamoDB repository
The harness needs no bespoke infrastructure beyond the container above. Mirror the shipped suites:
import {
cleanDynamoTable,
getTestEnvironmentConfig,
isDdbConfigured,
setupTestDynamoTable,
teardownTestDynamoTable,
} from '../test/ddb-test.util';
import { CommonEnvironmentSchema, EnvironmentService } from '../utils/environment.util';
import { MyDdbRepository } from './my-ddb.repository';
describe.skipIf(!isDdbConfigured())('MyDdbRepository (integration)', () => {
let repo: MyDdbRepository;
beforeAll(async () => {
await setupTestDynamoTable();
repo = new MyDdbRepository(new EnvironmentService(CommonEnvironmentSchema, getTestEnvironmentConfig()));
});
afterAll(async () => await teardownTestDynamoTable());
beforeEach(async () => await cleanDynamoTable());
// ... assertions against real read/write/query behavior
});
Name the file *.integration.test.ts so it runs under test:integration (not the unit suite).
Troubleshooting
- Port
8000already in use. Another process (a leftover container) is holding the port. Stop it, or remove the previous container withdocker rm -f ws-mono-st-ddb. - All integration tests skip / "no tests" reported.
DYNAMODB_ENDPOINTandDYNAMODB_TABLE_NAMEmust both be set in the root.env; otherwise the suites skip themselves. - "Refusing to run DynamoDB integration tests against a non-local endpoint" error. The safety guard rejects any endpoint that is not
localhost/127.0.0.1. KeepDYNAMODB_ENDPOINT=http://localhost:8000.