test: add comprehensive automated test coverage
- Vitest multi-project setup (unit / integration / component) - Real-Postgres integration harness via in-process PGlite (TEST_DATABASE_URL override), migrations applied per worker; production code untouched - Unit: routing, locale, validation/Zod schemas, metadata, mail, site-theme, marquee, media, portfolio helpers, plus architecture-rule tests - Integration: Prisma data layer, API routes, and all server actions - Component: UI primitives and form components (jsdom + Testing Library) - 371 tests passing
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Test Plan — mohfarawati.de
|
||||
|
||||
Comprehensive automated test coverage for the multilingual Next.js portfolio + admin
|
||||
workspace. Focus: **code correctness**. Scope excludes Playwright / browser E2E (per request).
|
||||
|
||||
## 1. Goals & principles
|
||||
|
||||
- Cover every application module: pure helpers, validation/schemas, data layer (Prisma),
|
||||
server actions, API routes, middleware, forms, components, and architecture rules.
|
||||
- **Do not change production behaviour.** Tests observe the code as-is. If a test reveals a
|
||||
real bug or an architecture-rule violation, it is flagged for the owner — production code is
|
||||
not changed without approval.
|
||||
- Deterministic and self-contained: no external network, no reliance on a running app server.
|
||||
- Real Postgres for the data layer (not mocks). See §3.
|
||||
|
||||
## 2. Test taxonomy & runner layout
|
||||
|
||||
Vitest with three **projects** (isolated environments), selected by file location:
|
||||
|
||||
| Project | Env | Location | Parallel | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `unit` | node | `tests/unit/**` | yes | Pure functions, schemas, formatting, mapping logic |
|
||||
| `integration` | node | `tests/integration/**` | serial | Prisma data layer, server actions, API routes, middleware |
|
||||
| `component` | jsdom | `tests/component/**` | yes | React components & forms (RTL) |
|
||||
|
||||
Legacy flat `tests/*.test.ts` files are folded into the new structure (kept passing).
|
||||
|
||||
### Tooling added (dev-only)
|
||||
- `@testing-library/react`, `@testing-library/dom`, `@testing-library/jest-dom`,
|
||||
`@testing-library/user-event`, `jsdom`, `@vitejs/plugin-react` — component tests.
|
||||
- `@electric-sql/pglite`, `pglite-prisma-adapter` — an **embedded real Postgres**
|
||||
(Postgres compiled to WASM) that runs the project's actual migrations and Prisma queries
|
||||
in-process, via a Prisma driver adapter.
|
||||
|
||||
### Database strategy (real Postgres)
|
||||
`tests/helpers/integration-setup.ts` + `tests/helpers/global-db-setup.ts`:
|
||||
- If `TEST_DATABASE_URL` is set → the real `lib/prisma` singleton is used unchanged, pointed
|
||||
at that Postgres (e.g. the Docker instance). The global setup resets the schema and applies
|
||||
every `prisma/migrations/*/migration.sql` once before the run.
|
||||
- Otherwise → `lib/prisma` is mocked (test-only) with a Prisma client backed by an in-process
|
||||
PGlite database. Each worker gets its own isolated database with the migrations applied —
|
||||
real Postgres semantics, no external server, no shared-state races. **Production code is
|
||||
never modified.**
|
||||
- `resetDb()` (TRUNCATE all tables, restart identities) runs in `beforeEach`.
|
||||
- The integration project runs **serially** (`fileParallelism: false`); each file gets a fresh
|
||||
database connection.
|
||||
|
||||
### Next.js runtime mocks (integration)
|
||||
Server actions/middleware depend on the Next runtime. `tests/helpers/next-mocks.ts` provides:
|
||||
- `next/navigation` → `redirect()` throws a catchable `NEXT_REDIRECT` carrying the URL.
|
||||
- `next/dist/client/components/redirect-error` → `isRedirectError()` recognises the above.
|
||||
- `next/cache` → `revalidatePath()` spy (no-op, asserted).
|
||||
- `next/headers` → controllable `cookies()` / `headers()` stores.
|
||||
- `@/lib/admin-auth` `isAdminAuthenticated` → toggled per test (auth guard tests).
|
||||
- `nodemailer` → captured transport (no real SMTP).
|
||||
|
||||
## 3. Coverage matrix
|
||||
|
||||
### 3.1 Unit — pure lib
|
||||
|
||||
| Module | Cases |
|
||||
|---|---|
|
||||
| `admin-routing` | host resolution (forwarded/comma/port), `isAdminHost`, `hasDedicatedAdminHost`, legacy/dev/internal path predicates, `toInternalAdminPath`, `fromDevelopmentAdminPath`, `getAdminAppPath` dev vs prod, `buildAdminUrl`/`buildSiteUrl`, env overrides, normalization edge cases |
|
||||
| `admin-feedback` | `withFlash` (success/error/both/none, encoding), `readFlash` |
|
||||
| `admin-navigation` | tree shape, `active`/`expanded` flags for each section, portfolio child mapping, href de-dup filter |
|
||||
| `form-data` | `isCheckedFormValue` truthy/falsey set |
|
||||
| `locale` | `isSupportedLocale`, `resolveLocale`, `getDirection` (rtl for ar), `stripLocalePrefix`, `getLocalizedPath(WithDefault)` incl. prefix stripping/rebuilding |
|
||||
| `utils` | `cn` merge/dedupe/conditional |
|
||||
| `site-theme` | `buildSiteThemeTokens` (hex→hsl, derive dark/secondary, clamps), `buildSiteThemeStyleText` structure, invalid hex fallback |
|
||||
| `marquee-settings` | defaults, `parseMarqueeSettingsValue` (invalid json, partial, trims), `syncMarqueeSettingsToGermanSource`, `splitMarqueeRowItems` (newline/comma/blank) |
|
||||
| `site-settings` | `normalizeSiteDefaultLocale`, `normalizeSitePrimaryColor`, `buildDefaultSiteSettings`, `parseSiteSettingsValue` (merge, invalid json, legacy title, invalid locale/color) |
|
||||
| `site-icons` | `buildSiteIconUrls` (version, relative vs absolute favicon url, name fallback), `buildSiteIconResponse` transparent fallback for non-managed paths |
|
||||
| `media-storage` (pure) | `sanitizeBaseName`, `getExtensionForMimeType` (all mimes + unknown), `isManagedMediaFilePath`, `resolveMediaUploadPath` (root confinement + traversal guard) |
|
||||
| `media-validation` | `mediaFieldInputSchema`: library needs assetId, external needs url, url format rule, upload mode, kind enum, trimming |
|
||||
| `media-service` (pure) | `inferMediaKindFromMimeType`, `inferMediaKindFromFileName`, `getKindFromUploadFile` |
|
||||
| `portfolio` (pure) | `resolvePortfolioProjectViewMode`, `getLocalizedValue` (direct/fallback/any) |
|
||||
| `portfolio-form-progress` | slug/year/sortOrder validators, section readiness per type, asset readiness, wizard progress, first incomplete step |
|
||||
| `portfolio-validation` | category/section/asset/project schemas: required fields, slug regex, coercions, view modes, section superRefine per type, media refinements, url rules |
|
||||
| `metadata` | `applyTitleTemplateFn`, `buildLocaleAlternates`, `buildAppMetadataFromConfig`, `buildLocalizedMetadataFromConfig` (title template skip, description fallback, og/twitter, icons) |
|
||||
| `mail` | `createSmtpTransport` (required host/user/pass errors, port/secure), `sendMail` (from with/without name), `sendContactMessage` (recipient + fallback, body fields), `sendTestEmail` (recipient fallback, transport reject) |
|
||||
|
||||
### 3.2 Integration — data layer (real DB)
|
||||
|
||||
| Module | Cases |
|
||||
|---|---|
|
||||
| `app-config` | maintenance get/set, site settings get (fallback name from `siteName` key) / update roundtrip, mail settings get/update, marquee get/update (german sync), `getSiteSettingsMediaBindings` (mediaUsage → bindings per field) |
|
||||
| `media` | create asset, get by id (+usages), list, `getMediaOptions` kind filter, `replaceEntityMediaUsages` (transactional replace, unique constraint), `deleteEntityMediaUsages`, `getPortfolioMediaBindings` routing by usageType, `countMediaUsageReferences` |
|
||||
| `portfolio` (queries) | admin categories (+project counts), active categories/by-slug, admin projects (status/category filters + ordering), published projects/by-slug, by-id with media bindings, localized mapping, `onDelete` Restrict/Cascade behaviour |
|
||||
| `media-service.resolveMediaSelection` | library (found/missing), external (creates asset, filename from url), missing+required error, not-required empty |
|
||||
| `admin-auth` (lockout) | `registerFailedAdminAttempt` increments & locks at threshold, `getAdminLockState`, `resetAdminFailedAttempts`, IP hashing via mocked headers; token `isPasswordValid`/verify with env |
|
||||
|
||||
### 3.3 Integration — API routes & middleware
|
||||
|
||||
| Target | Cases |
|
||||
|---|---|
|
||||
| `GET /api/health` | 200 + `database: up`; 503 + `database: down` when query throws |
|
||||
| `GET /api/site/default-locale` | returns runtime `defaultLocale` + `maintenanceEnabled`, no-store header |
|
||||
| `proxy` (middleware) | runtime default-locale passthrough, safe fallback on fetch failure, maintenance redirect, `SITE_RUNTIME_ORIGIN`, admin host rewrite → internal, dev `/root` handling, legacy 404 in prod, internal path 404 for non-admin in prod, basic-auth challenge/valid |
|
||||
|
||||
### 3.4 Integration — server actions
|
||||
|
||||
| Action file | Cases |
|
||||
|---|---|
|
||||
| `contact/actions` | valid → sends mail + redirect `/success`; invalid (short name/bad email/short message) → redirect with error; locale resolution |
|
||||
| `maintenance/actions` | unauth → redirect to admin root; enable/disable toggles config + revalidates + success flash |
|
||||
| `marquee/actions` | unauth guard; empty german rows throw per-row error → error flash; valid → saves (german-synced) + success flash |
|
||||
| `smtp/actions` | `parseMailSettingsFormData` (port parse error, password retention when blank), save → success; `sendTestEmailAction` success + failure |
|
||||
| `site-settings/actions` | brand save (primary color normalize, media selection + usage wiring, cleanup on error), localization save (siteName required, title template must contain `{pageTitle}`), `parseJsonObject` guard |
|
||||
| `media/actions` | create (kind image/document, missing file error), delete (not found, in-use guard, managed-file removal) |
|
||||
| `portfolio/actions` | `upsertCategoryAction` create/update + P2002 unique message; `deleteCategoryAction` blocks when projects exist; `saveProjectAction` create + update, sections/assets replace, `publishedAt` first-publish logic, media usage wiring, validation + error cleanup of created media; `deleteProjectAction` not-found + cascade + usage cleanup |
|
||||
|
||||
### 3.5 Component (jsdom + RTL)
|
||||
|
||||
Global mocks: `framer-motion`, `gsap`, `next/link`, `next/image`, `next-intl`, `next/navigation`.
|
||||
|
||||
| Component | Cases |
|
||||
|---|---|
|
||||
| `ui/badge` | variant classes, custom className merge, passthrough props |
|
||||
| `ui/input` | renders, forwardRef, type/placeholder/disabled, className merge |
|
||||
| `ui/textarea`, `ui/label`, `ui/card`*, `ui/app-card`, `ui/separator`, `ui/table`* | render, props, ref, composition |
|
||||
| `admin/admin-flash` | null when empty, success `role=status`, error `role=alert`, both, className |
|
||||
| `admin/marquee-settings-form` | renders 4 rows, default values, submit wiring to action |
|
||||
| `site/portfolio-category-filter` | "all" link + per-category links, active state, localized labels, hrefs |
|
||||
| `dashboard/dashboard-card`, `dashboard/stats-card` | presentational render, props |
|
||||
| `layout/container`, `layout/hero-badge`, `home/section-heading`, `home/bento-card` | presentational render, children, className |
|
||||
|
||||
(*multi-part primitives tested for sub-component composition.)
|
||||
|
||||
### 3.6 Architecture-rule tests (`tests/integration/architecture`)
|
||||
|
||||
Enforced from `CLAUDE.md`:
|
||||
1. **No Prisma in client components** — no file containing `"use client"` imports `lib/prisma`.
|
||||
2. **Business logic out of UI** — components don't import server-only data modules directly
|
||||
(allow-list of pure `lib/*` view/format helpers).
|
||||
3. **Server actions are guarded** — every exported action in `app/**/actions.ts` calls an auth
|
||||
guard (`ensureAdmin`/`requireAdminAuth`) except the public contact action.
|
||||
4. **`"use server"` directive** — every `actions.ts` begins with `"use server"`.
|
||||
5. **`lib/*` does not import from `app/`** — dependency direction.
|
||||
6. **Admin mirror parity** — every `page.tsx` under `app/_admin/**` has matching re-export
|
||||
stubs under `app/root/**` and `app/admin-internal/**` pointing back to `_admin`.
|
||||
7. **No browser storage in components** — no `localStorage`/`sessionStorage` usage.
|
||||
|
||||
## 4. Deliverables & running
|
||||
|
||||
```bash
|
||||
npm test # all projects
|
||||
npx vitest run --project unit
|
||||
npx vitest run --project integration
|
||||
npx vitest run --project component
|
||||
TEST_DATABASE_URL=postgres://… # optional: run integration against real Postgres
|
||||
```
|
||||
|
||||
Findings (real bugs / rule violations) are reported to the owner; production code is only
|
||||
changed after approval.
|
||||
Reference in New Issue
Block a user