mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
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.
199 lines
8.8 KiB
YAML
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
|