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 .env file. apps/backend/vitest.integration.config.ts parses the root .env and injects it into the test environment. In CI there is no .env; the job's env: 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.ts and apps/backend/app/authentication/repositories/magic-token-ddb.repository.integration.test.ts wrap their tests in describe.skipIf(!isDdbConfigured()), and isDdbConfigured() is true only when both DYNAMODB_ENDPOINT and DYNAMODB_TABLE_NAME are set. If test:integration reports 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, the GSI1 email index, PAY_PER_REQUEST, and the ttl TTL 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 unless ENVIRONMENT is not production and DYNAMODB_ENDPOINT points at localhost or 127.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 8000 free (stop any process using it).
  • Dependencies installed: pnpm install from 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; dummy is alphanumeric, which DynamoDB Local (>= 2.0.0) requires for AWS_ACCESS_KEY_ID. These temporarily override whatever pnpm script update-env wrote; 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 8000 already in use. Another process (a leftover container) is holding the port. Stop it, or remove the previous container with docker rm -f ws-mono-st-ddb.
  • All integration tests skip / "no tests" reported. DYNAMODB_ENDPOINT and DYNAMODB_TABLE_NAME must 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. Keep DYNAMODB_ENDPOINT=http://localhost:8000.