Files
OpenFrame/.github/workflows/ci.yml
T
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

199 lines
8.8 KiB
YAML

name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun run check
test:
runs-on: ubuntu-latest
services:
# Postgres for the `api` Vitest project. This job runs directly on the
# runner, so the service is reachable on localhost through the published
# port, not by service name.
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: openframe
POSTGRES_PASSWORD: openframe
POSTGRES_DB: openframe_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U openframe -d openframe_test"
--health-interval 2s
--health-timeout 3s
--health-retries 30
env:
DATABASE_URL: postgresql://openframe:openframe@localhost:5432/openframe_test?schema=public
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Create .env.test from the committed example
# The `api` project's setup files read `.env.test`, which is gitignored.
# The example carries every value the suites need; only the database host
# differs, because locally it is the compose service and here it is a
# service container. The appended line wins inside the file and the
# exported job env wins over the file, so the override holds either way.
run: |
cp .env.test.example .env.test
printf '\nDATABASE_URL=%s\n' "$DATABASE_URL" >> .env.test
# No migration step here on purpose. The api project's globalSetup
# (tests/setup/db-global.ts) builds the schema itself, and it cannot use
# `prisma migrate deploy`: prisma/migrations is a stack of patches on top
# of a baseline that was never captured, so the second migration alters an
# enum that nothing in the history creates. That file explains it in full.
- name: Unit and component tests
run: bun run test
- name: API integration tests
run: bun run test:api
- name: Coverage report
# Diagnostic only. There is no coverage threshold gate on purpose, see
# TESTING.md section 11.
#
# Run under node rather than `bun run test:coverage`: @vitest/coverage-v8
# needs the V8 inspector API, which bun does not implement, so under bun
# every file reports "Coverage APIs are not supported" and the numbers
# come out as zero. The suite itself passes under both runtimes.
run: node node_modules/vitest/vitest.mjs run --project unit --coverage
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
if-no-files-found: ignore
retention-days: 7
e2e:
runs-on: ubuntu-latest
needs: [check]
container:
# Must stay pinned to the installed @playwright/test version, because the
# image carries the matching browser build and Playwright refuses a
# mismatched pair. Microsoft publishes the image a while after the npm
# release, which is why package.json pins 1.61.1 rather than the newer
# 1.62.0: no v1.62.0-noble image exists yet. Bump both together, and check
# the tag is published first:
# curl -sI https://mcr.microsoft.com/v2/playwright/manifests/v1.61.1-noble
image: mcr.microsoft.com/playwright:v1.61.1-noble
# Chromium needs a real /dev/shm in a container.
options: --ipc=host
# No --user override on purpose: the image has no bun, and installing one
# needs root. The runner is ephemeral, so root-owned files do no harm.
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: openframe
POSTGRES_PASSWORD: openframe
POSTGRES_DB: openframe_test
options: >-
--health-cmd "pg_isready -U openframe -d openframe_test"
--health-interval 2s
--health-timeout 3s
--health-retries 30
# Object storage for video-upload.spec.ts. The browser PUTs the file
# straight at the presigned URL, so there is nothing to mock at that
# boundary from inside a browser. Mirrors the minio-test service in
# docker-compose.test.yml.
minio:
image: minio/minio:latest
env:
MINIO_ROOT_USER: openframe
MINIO_ROOT_PASSWORD: openframe-test-secret
# lib/r2.ts signs with `region: 'auto'`, so MinIO has to accept it.
MINIO_REGION_NAME: auto
options: >-
--health-cmd "mc ready local"
--health-interval 2s
--health-timeout 3s
--health-retries 30
env:
# This job runs inside a container, so it shares a network with its
# services and reaches them by service name. No published ports are
# involved, which is why there are no `ports:` blocks above.
DATABASE_URL: postgresql://openframe:openframe@postgres:5432/openframe_test?schema=public
# The Playwright web server builds and starts the app, and `next build`
# does not run with NODE_ENV=test, so it never picks up `.env.test`. These
# values are therefore set on the job itself. Port 3100 matches
# playwright.config.ts.
NEXTAUTH_URL: http://localhost:3100
NEXTAUTH_SECRET: ci-secret-not-used-for-anything-real
NEXT_PUBLIC_APP_URL: http://localhost:3100
# Required. NextAuth v5 answers every /api/auth/* request with
# `UntrustedHost` in a production build unless the host is trusted, which
# is why .env.docker.example sets the same variable for real deployments.
AUTH_TRUST_HOST: 'true'
# Stripe ON, with dummy credentials, and deliberately not 'false'.
# hasBillingAccess() short-circuits to `true` when the flag is off and
# buildBillingAccessWhereInput() returns `{}`, which disarms the whole
# billing gate: billing-gate.spec.ts would then assert nothing. No spec
# walks into checkout, so nothing reaches Stripe. This also keeps the
# e2e job consistent with .env.test, which the api suite already runs
# with the flag on for the same reason.
OPENFRAME_ENABLE_STRIPE: 'true'
STRIPE_SECRET_KEY: sk_test_openframe_dummy
STRIPE_PRICE_ID: price_test_openframe_dummy
STRIPE_WEBHOOK_SECRET: whsec_test_openframe_dummy
OPENFRAME_REQUIRE_INVITE_CODE: 'true'
INVITE_CODE: test-invite
TRUSTED_PROXY_MODE: none
# Direct video uploads, pointed at the MinIO service. Without these the
# `Direct Upload` tab is not rendered and video-upload.spec.ts fails on its
# first assertion rather than silently testing nothing.
OPENFRAME_ENABLE_S3_VIDEO_UPLOADS: 'true'
OPENFRAME_ENABLE_BUNNY_UPLOADS: 'false'
R2_ENDPOINT: http://minio:9000
R2_ACCESS_KEY_ID: openframe
R2_SECRET_ACCESS_KEY: openframe-test-secret
R2_BUCKET_NAME: openframe-test
steps:
- uses: actions/checkout@v4
- name: Install bun
# oven-sh/setup-bun cannot be used inside this container: it unpacks a
# zip archive and the Playwright image ships no unzip. It ships node and
# npm, so npm installs the bun binary instead. bun is needed both for
# `bun run test:e2e` and for the web server command in
# playwright.config.ts.
run: npm install --global bun
- run: bun install
- name: Create .env.test from the committed example
run: |
cp .env.test.example .env.test
printf '\nDATABASE_URL=%s\n' "$DATABASE_URL" >> .env.test
- name: Create the MinIO bucket
# Nothing at runtime creates it: 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. The
# Playwright image has no mc, so this goes through the same image the
# service container uses.
run: |
curl -sSfL -o /usr/local/bin/mc https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x /usr/local/bin/mc
mc alias set ciminio "$R2_ENDPOINT" "$R2_ACCESS_KEY_ID" "$R2_SECRET_ACCESS_KEY"
mc mb --ignore-existing "ciminio/$R2_BUCKET_NAME"
# No `bun run test:db:bootstrap` step: tests/e2e/global-setup.ts calls the
# same setup function before the web server starts, and also clears the
# rate_limits table so a retry does not inherit a spent window.
- name: End-to-end tests
run: bun run test:e2e
- name: Upload the Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
if-no-files-found: ignore
retention-days: 7