Files
OpenFrame/docker-compose.test.yml
yusufipk 1d099c68f2 test: add unit, API, component and end-to-end test suites
The repo had no automated tests. Every change was verified by hand.

Adds four layers, 2023 tests in total, runnable with one command:

- 1191 unit tests over the pure logic in lib/, including the full
  computeProjectAccess permission matrix and the billing gate
- 167 component and hook tests in jsdom, covering the hooks that hold
  real logic rather than presentational wrappers
- 647 API integration tests against a real Postgres, with only auth()
  mocked, including a data-driven sweep asserting that none of the 60
  route modules answers 2xx to an unauthenticated caller
- 18 Playwright specs driving a real browser against a real build

Infrastructure: vitest.config.ts with three projects, a disposable
Postgres and MinIO in docker-compose.test.yml, factories and helpers
under tests/, scripts/test.sh as the single entry point, a pre-push
hook running bun run verify, and CI split into check, test and e2e jobs.

The test database is built with prisma db push plus a replay of the
hand-written SQL, because prisma migrate deploy cannot build this schema
from empty: the migration history has no captured baseline. This mirrors
what scripts/docker-db-bootstrap.ts already does in production, and
tests/setup/db-global.ts carries a drift guard so a new migration fails
the run until someone reviews it.

Production code is unchanged apart from one pure-function extraction out
of use-video-player.ts, which was too large to test in jsdom.

Several tests pin behaviour that looks wrong, each marked KNOWN BUG in
place. TESTING.md section 12 records where the plan turned out to be
wrong, and AGENTS.md now states which layer a change needs a test in.
2026-07-26 11:17:26 +07:00

160 lines
5.9 KiB
YAML

# Disposable infrastructure for the automated test suites. Everything here is
# on non-default ports so it can never collide with the dev stack in
# docker-compose.yml.
#
# The `openframe-test` network is declared external so that the test runner
# container can reach these services by service name. Create it once with:
# podman network create openframe-test
#
# Two tiers, split by compose profile:
#
# (no profile) postgres-test. Needed by the `api` Vitest project and by the
# Playwright suite. Started by `bun run test:db:up`.
# e2e minio-test + minio-test-init (real S3-compatible storage for
# the direct video upload flow) and app-test (the app under
# test). Started by `scripts/test.sh e2e`.
#
# Usage:
# podman compose -f docker-compose.test.yml up -d --wait postgres-test
# podman run --rm --network openframe-test -v "$PWD":/workspace:z -w /workspace \
# docker.io/oven/bun:alpine sh -c "bun run test:api"
#
# podman compose -f docker-compose.test.yml --profile e2e up -d --wait \
# postgres-test minio-test
# scripts/test.sh e2e
#
# app-test is opt-in and only needed when you want the app under test to run as
# a container instead of being started by Playwright's own `webServer`:
# podman compose -f docker-compose.test.yml --profile e2e up -d --wait app-test
# E2E_BASE_URL=http://app-test:3100 scripts/test.sh e2e
services:
postgres-test:
image: postgres:16-alpine
container_name: openframe-postgres-test
environment:
POSTGRES_USER: openframe
POSTGRES_PASSWORD: openframe
POSTGRES_DB: openframe_test
# fsync off: the data directory is a tmpfs that is thrown away anyway, so
# durability buys nothing and costs a lot of wall clock time.
command: ['postgres', '-c', 'fsync=off', '-c', 'full_page_writes=off']
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U openframe -d openframe_test']
interval: 2s
timeout: 3s
retries: 30
ports:
- '127.0.0.1:55432:5432'
networks:
- openframe-test
# S3-compatible object storage for the direct video upload path. The browser
# PUTs the file straight at this endpoint with a presigned URL, so the
# hostname the app signs for has to be the hostname the browser resolves:
# both the Playwright container and the app under test are on this network,
# so both use `minio-test:9000`. MinIO's default CORS policy allows every
# origin and exposes ETag, which is exactly what lib/client/r2-video-upload.ts
# needs, so no bucket CORS configuration is involved.
minio-test:
profiles: ['e2e']
image: docker.io/minio/minio:latest
container_name: openframe-minio-test
command: ['server', '/data', '--console-address', ':9001']
environment:
MINIO_ROOT_USER: openframe
MINIO_ROOT_PASSWORD: openframe-test-secret
# lib/r2.ts builds its S3 client with `region: 'auto'`, so MinIO has to
# accept that region name in the SigV4 signature.
MINIO_REGION_NAME: auto
# Same reasoning as postgres-test: the data is disposable, so keep it in RAM.
tmpfs:
- /data
healthcheck:
test: ['CMD', 'mc', 'ready', 'local']
interval: 2s
timeout: 3s
retries: 30
ports:
- '127.0.0.1:59000:9000'
- '127.0.0.1:59001:9001'
networks:
- openframe-test
# Creates the bucket. Nothing at runtime does: ensureR2BucketExists() lives in
# scripts/self-host-bootstrap.ts, not on the request path, so a missing bucket
# would surface as a presigned PUT returning NoSuchBucket.
minio-test-init:
profiles: ['e2e']
image: docker.io/minio/mc:latest
container_name: openframe-minio-test-init
depends_on:
minio-test:
condition: service_healthy
entrypoint:
- sh
- -c
- >
mc alias set testminio http://minio-test:9000 openframe openframe-test-secret &&
mc mb --ignore-existing testminio/openframe-test &&
mc ls testminio
networks:
- openframe-test
# The app under test, for the case where you do not want Playwright to start
# it. This is the same command playwright.config.ts uses for its `webServer`,
# run against the mounted working tree so `.next` stays warm between runs.
#
# NODE_ENV is left to `next build` / `next start` (production). The runtime
# values below are duplicated from .env.test.example on purpose: `next build`
# never loads `.env.test`, and NEXT_PUBLIC_APP_URL is inlined at build time.
app-test:
profiles: ['e2e']
image: docker.io/oven/bun:alpine
container_name: openframe-app-test
depends_on:
postgres-test:
condition: service_healthy
minio-test:
condition: service_healthy
working_dir: /workspace
volumes:
- .:/workspace:z
environment:
DATABASE_URL: postgresql://openframe:openframe@postgres-test:5432/openframe_test?schema=public
NEXTAUTH_URL: http://app-test:3100
NEXTAUTH_SECRET: test-secret-not-used-for-anything-real
NEXT_PUBLIC_APP_URL: http://app-test:3100
OPENFRAME_ENABLE_STRIPE: 'true'
STRIPE_SECRET_KEY: sk_test_openframe_dummy
STRIPE_PRICE_ID: price_test_openframe_dummy
OPENFRAME_REQUIRE_INVITE_CODE: 'true'
INVITE_CODE: test-invite
TRUSTED_PROXY_MODE: none
OPENFRAME_ENABLE_S3_VIDEO_UPLOADS: 'true'
OPENFRAME_ENABLE_BUNNY_UPLOADS: 'false'
R2_ENDPOINT: http://minio-test:9000
R2_ACCESS_KEY_ID: openframe
R2_SECRET_ACCESS_KEY: openframe-test-secret
R2_BUCKET_NAME: openframe-test
PORT: '3100'
command:
- sh
- -c
- './node_modules/.bin/next build && ./node_modules/.bin/next start -p 3100 -H 0.0.0.0'
healthcheck:
test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1:3100/login']
interval: 5s
timeout: 5s
retries: 120
ports:
- '127.0.0.1:3100:3100'
networks:
- openframe-test
networks:
openframe-test:
external: true