saas-testing-toolkit
v1.0.1
Published
Enterprise SaaS testing infrastructure — Vitest + Playwright + pgTAP + SOC2
Downloads
186
Readme
SaaS Testing Toolkit
Drop-in test infrastructure for Next.js + Supabase apps. Scans your codebase, generates RBAC tests, RLS verification, and a full E2E suite — then runs them against your actual schema.
What you get
- RBAC matrix — one test per role × route, auto-built from your
middleware.ts - RLS verification — pgTAP tests that prove tenant data never crosses org boundaries
- E2E suite — smoke, CRUD, features, lifecycle, and accessibility specs, all seeded and torn down automatically
- Compliance exports — SOC2 audit evidence in JSON, ready for an auditor
- Unit + component tests — generated from your server actions and React components, assertions automatically filled from return-type inference
- Security specs — session management, error disclosure, rate limiting, SRI checks
npm run check— pre-flight checker that tells you exactly what's missing before you run anything
Everything is generated from your actual schema and routes. You don't maintain a parallel test config that drifts away from production.
Requirements
| | Minimum |
|---|---|
| Node.js | 20.0.0 |
| npm | 10.0.0 |
| Next.js | App Router (src/app/) |
| Database | Supabase (PostgreSQL + RLS) |
| Supabase CLI | Latest (brew install supabase/tap/supabase) |
Pages Router is not supported. If your project uses
pages/, migrate to App Router first.
Install
1. Copy the toolkit into your project
cp -r saas-testing-toolkit/ path/to/your-project/
cd path/to/your-project
npm install2. Set up environment variables
cp .env.test.example .env.test.localThen start your local Supabase instance and fill in the values it prints:
supabase start
# Prints: API URL, anon key, service role key — paste into .env.test.localFill in E2E_ADMIN_EMAIL, E2E_ADMIN_PASSWORD, and BASE_URL (your local app URL, e.g. http://localhost:3001).
3. Run the pre-flight check
npm run checkThis verifies everything is in place before you run a single test. You'll see something like:
✓ Node.js version v22.1.0 (≥20 required)
✓ .env.test.local found
✓ Required env vars all 6 set
✓ Seed strategy A — supabase/migrations/
✓ Middleware middleware.ts (5 protected routes in matcher)
✓ Database schema supabase/prod_schema.sql (14 tables)
✓ Playwright browsers chromium installed
⚠ Codegen output e2e/pages/ is empty — codegen hasn't been run yet
→ Run: npm run codegen
No blockers. Run: npm run codegenFix any ✗ errors before continuing. ⚠ warnings are fine to proceed with.
4. Export your schema (if not already done)
supabase db dump --local > supabase/prod_schema.sqlSkip this if supabase/prod_schema.sql already exists.
5. Run codegen
npm run codegen -- --yesThis runs a 7-step pipeline that scans your project and generates:
Documentation
docs/TESTING-PROJECT.md— your project's testing spec: detected roles, tables, routes, modulesdocs/TESTING-MASTER.md— generic testing strategy template for team onboarding
Test infrastructure
src/__tests__/factories/index.ts— factory function per database tablesrc/__tests__/{actions,components,unit}/*.test.ts— tests for your server actions and components, with assertions filled automatically from return-type inferencesrc/__tests__/msw-handlers.ts— MSW mock handlers for your API routes
E2E infrastructure
e2e/pages/*.ts— Playwright page objects with your real URL paths and locatorse2e/config/toolkit.config.ts—PRIMARY_ENTITY, all page routes, API routes, audit actions, and org-scoped tables in correct teardown order — no TODO comments, no manual editinge2e/security/rbac.config.ts— one entry per protected route, with inferredallowedRolese2e/compliance/audit-log.spec.ts— audit trail coveragee2e/compliance/data-deletion.spec.ts— right-to-erasure coverage
Database
supabase/tests/001–004.sql— pgTAP tests for RLS, org isolation, role boundariesscripts/schema.config.ts— schema contract with all required tables and critical columns — used bynpm run verify-migrationto catch deploy-time drift
Run npm run codegen -- --dry-run first to preview what will be generated without writing any files.
6. Run the smoke suite
npm run test:local:smokeSmoke should go green on first run. If it doesn't, the error output will tell you exactly which selector or URL needs updating in e2e/auth.setup.ts.
How it works
YOUR CODEBASE TOOLKIT
───────────────── ───────────────────────────────────────
supabase/prod_schema.sql ─┐
src/app/**/page.tsx │ scan → generate → you fill in
middleware.ts │
src/lib/actions/*.ts │
src/components/**/*.tsx ─┘
Scan detects:
• Tables, columns, enums, RLS policies
• Routes and their page types (list / detail / create / edit)
• Which routes are protected and what roles are required
• Server action function signatures
• React component form fields
Generate produces:
• RBAC matrix wired to your actual routes and roles
• Test files with correct imports, mocks, and real expect() assertions
• Page objects with your real URL paths and locators
• E2E config with primary entity, routes, audit actions, tables (no TODOs)
• pgTAP tests with your real table names
• Schema contract with every table and critical column
You fill in:
• CRUD and feature spec bodies in e2e/crud/ and e2e/features/
• Any login form selectors that differ from standard input[type="email/password"]
• E2E spec assertions that require running the app (smoke, a11y, lifecycle)Reviewing the generated output
Most generated files require no manual editing. These two are worth a quick look:
e2e/auth.setup.ts
Codegen fills the post-login redirect URL from your routes. Verify the login form selectors match your actual markup if your login page uses non-standard inputs:
await page.goto("/login"); // ← auto-detected from routes
await page.fill('input[type="email"]', email); // ← update if your selector differs
await page.fill('input[type="password"]', password); // ← update if your selector differs
await page.click('button[type="submit"]');
await page.waitForURL("/dashboard"); // ← auto-detected from routessrc/__tests__/setup.ts
Verify the vi.mock("@/lib/supabase/server") path matches your actual Supabase client location. If your project keeps its clients in a non-standard path, update the two vi.mock(...) calls.
Everything else — e2e/config/toolkit.config.ts, scripts/schema.config.ts, src/__tests__/factories/, and all test files — is fully generated with no TODO comments.
Reference demo
The demo/ directory contains a minimal Contact CRM SaaS (Next.js 15 + Supabase) built specifically to show the pipeline in action against a realistic codebase. It uses CHECK constraints for roles (not PostgreSQL ENUMs), the org_role JWT claim (not role), and a 5-table schema — exactly the pattern most real Supabase projects follow.
Running npm run codegen -- --yes against the demo produces all 7 steps passing and 93 files generated with zero manual configuration.
See demo/README.md for the recorded proof-of-value run output.
Command reference
Setup and generation
| Command | What it does |
|---|---|
| npm run check | Pre-flight check — verify all prerequisites before running tests |
| npm run codegen | Run all generators against the current project |
| npm run setup | Interactive first-time setup wizard (human-only, not for CI) |
Unit tests
| Command | What it does |
|---|---|
| npm test | Vitest unit tests (watch mode) |
| npm run test:coverage | Vitest with V8 coverage report |
| npm run test:mutation | Stryker mutation testing — proves tests actually catch bugs |
E2E tests
| Command | What it does |
|---|---|
| npm run test:local | Full Playwright suite (all projects) |
| npm run test:local:smoke | Smoke only — fastest gate, run on every PR |
| npm run test:local:security | RBAC matrix + session + error disclosure |
| npm run test:local:a11y | Accessibility scans (axe-core) |
| npm run test:local:compliance | Audit log + data deletion specs |
Database tests and reporting
| Command | What it does |
|---|---|
| supabase test db | Run pgTAP tests (RLS, org isolation, role boundaries, auth required) |
| npm run report:compliance | Export SOC2 evidence JSON |
| npm run verify-migration | Post-deploy schema integrity check |
The E2E suite structure
Tests run in dependency order, sharing a single seeded database:
db-setup (global.setup.ts — auto-detects and runs your seed strategy)
└── setup (auth.setup.ts — logs in once, stores session cookie)
├── smoke/ happy path only, no mutations — runs on every PR
├── crud/ create → list → edit → delete per primary entity
├── features/ search, filters, pagination, import, file upload
├── lifecycle/ cross-entity flows (e.g. submission → placement)
└── a11y/ axe-core scans on load and after interaction
security/ RBAC matrix, session hijacking, error disclosure (independent)
compliance/ Audit log integrity, data deletion (runs after crud/)Seed strategy is auto-detected — no manual config needed:
| What exists in your project | What runs |
|---|---|
| supabase/migrations/ or supabase/seed.sql | supabase db reset --local |
| scripts/seed-test-db.ts | npx tsx scripts/seed-test-db.ts |
| src/app/api/test/seed/route.ts + SEED_SECRET | POST /api/test/seed |
If none of these exist, the suite fails immediately with instructions for which file to create.
CI/CD
Three workflows are included in .github/workflows/:
ci.yml — triggers on every PR
Smoke suite only. Typically finishes in under 3 minutes. Blocks merge on failure.
staging.yml — triggers on push to main
Full suite against the staging deployment: smoke → CRUD → security → compliance.
nightly.yml — runs at 2 AM
Security suite, accessibility scans, mutation testing, and SSL certificate expiry check.
To connect them to your project, set BASE_URL and PLAYWRIGHT_BASE_URL as GitHub Actions repository variables pointing at your environments.
Security and compliance coverage
| Spec | What it tests |
|---|---|
| rbac-matrix.spec.ts | Every protected route returns 401/403 for unauthorised roles |
| session-management.spec.ts | Session expiry, token rotation, logout invalidation |
| error-disclosure.spec.ts | Stack traces and internal paths never appear in HTTP responses |
| rate-limiting.spec.ts | Auth endpoints enforce rate limits |
| sri.spec.ts | External scripts have integrity attributes |
| audit-log.spec.ts | Create/update/delete actions write audit entries |
| data-deletion.spec.ts | DELETE removes all PII from every related table |
| supabase/tests/001–004.sql | RLS enabled, org isolation, role boundaries, auth required |
npm run report:compliance exports all passing results to a JSON file suitable for attaching to a SOC2 Type II audit.
Compatibility
| Stack | Support |
|---|---|
| Next.js App Router | Full |
| Next.js Pages Router | Not supported — migrate to App Router first |
| Supabase Auth | Full — roles auto-detected from schema enums |
| Clerk | Full — roles auto-detected |
| NextAuth / Auth.js v5 | Full — roles auto-detected |
| Other auth providers | Roles default to ["admin","editor","viewer"]; set ALL_ROLES manually in rbac.config.ts |
| Prisma / Drizzle | Vitest + Playwright layers work; pgTAP and seed scripts require Supabase CLI |
Troubleshooting
npm run check reports no seed strategy
Create one of the detection targets. For any Supabase project, the fastest fix is creating supabase/seed.sql (even if empty) — it signals Strategy A.
Smoke suite fails with "waiting for URL /dashboard"
Your post-login redirect differs from the default. In e2e/auth.setup.ts, change waitForURL("/dashboard") to your actual redirect path.
rbac.config.ts ROUTES array is empty after codegen
Codegen reads config.matcher from middleware.ts. Create it with a matcher listing your protected routes, then re-run npm run codegen.
pgTAP tests fail with "relation does not exist"
The generated SQL uses table names from your schema at the time codegen ran. If you renamed a table, re-run python3 docs/generate-pgtap-tests.py --project-root ..
supabase db reset --local fails during db-setup
Make sure supabase start is running before executing any test command that touches the database.
