test(e2e): real-backend 409 conflict contract test (TEST-CONTRACT-001)

B4 of TEST-INFRA-001 (RFC-WS-FRONTEND-PRIMEVUE Amendment A-1).

- Add api/database/seeders/E2EBaselineSeeder.php — deterministic seed
  for Playwright e2e: e2e@test.local user (org_admin) on a fresh org +
  event + stage + StageDay + artist + engagement + performance
  (version=0). Writes seeded IDs to api/storage/app/e2e-fixtures.json
  so the Playwright fixture can construct API URLs without API
  discovery calls.
- Add apps/app/tests/playwright-e2e/global-setup.ts — runs
  `php artisan migrate:fresh --force --seed` against crewli_test (the
  existing PHPUnit MySQL test DB) before the test suite starts.
  Uses --env=testing to satisfy the dangerous-bash hook's migrate:fresh
  guard.
- Add apps/app/tests/playwright-e2e/utils/fixtures.ts — typed reader
  for e2e-fixtures.json. Cached after first read.
- Add apps/app/tests/playwright-e2e/utils/auth.ts — login helper that
  POSTs /api/v1/auth/login and returns user/org IDs. Uses Bearer-via-
  cookie flow (per api/.../SetAuthCookie.php), not stateful Sanctum.
- Add apps/app/tests/playwright-e2e/timetable/409-conflict.spec.ts —
  the contract test: first move with version=0 returns 200, second
  move with same stale version returns 409 with shape
  `errors.conflict: 'version_mismatch'`. Catches the schema-drift
  bug class that timetable-stabilization B5 surfaced.
- Update apps/app/playwright.config.ts — wire globalSetup, webServer
  for `php artisan serve --port=8001`, baseURL `http://localhost:8001`
  (NOT 127.0.0.1 — auth cookie's domain=localhost requires hostname
  match).
- Update .gitignore — runtime e2e-fixtures.json never committed.

DoD-19 met locally: `pnpm test:e2e` passes against a real Laravel
test server. CI integration deferred to TEST-INFRA-002 (per A-1
amendment).

Constraint: e2e tests share the crewli_test DB with PHPUnit. Running
both concurrently would collide. Documented in ARCH-TESTING.md (B5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 15:24:33 +02:00
parent f6509d938b
commit 2dfb1e8bae
7 changed files with 440 additions and 10 deletions

View File

@@ -0,0 +1,72 @@
import { execSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Playwright globalSetup — runs once before all e2e tests.
//
// Responsibilities:
// 1. Verify api/.env exists (Laravel needs APP_KEY etc.)
// 2. Run `php artisan migrate:fresh --force --seed --seeder=E2EBaselineSeeder`
// against crewli_test (override DB_DATABASE on the command line).
// The seeder writes seeded IDs to api/storage/app/e2e-fixtures.json.
// 3. Verify the fixtures file was produced.
//
// Failures here abort the test run before any spec runs — prevents
// confusing per-test failures when the test DB is in an unexpected state.
//
// Why crewli_test (and not a dedicated crewli_e2e DB):
// The PHPUnit suite already uses crewli_test with `RefreshDatabase`
// (transaction-rollback per test). Running e2e and PHPUnit
// concurrently would collide; the lifecycle here assumes serial
// execution, which is the realistic local-dev flow. Documented in
// dev-docs/ARCH-TESTING.md §5 (mock-vs-real-backend).
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..')
const API_DIR = path.join(REPO_ROOT, 'api')
const FIXTURES_PATH = path.join(API_DIR, 'storage', 'app', 'e2e-fixtures.json')
export default async function globalSetup(): Promise<void> {
console.log('[e2e setup] Repo root:', REPO_ROOT)
console.log('[e2e setup] API dir:', API_DIR)
if (!existsSync(path.join(API_DIR, '.env'))) {
throw new Error(
'[e2e setup] api/.env not found. Run `cp api/.env.example api/.env` and configure DB_* + APP_KEY.',
)
}
console.log('[e2e setup] Running migrate:fresh + E2EBaselineSeeder against crewli_test...')
// --env=testing avoids the dangerous-bash hook's migrate:fresh guard
// (which requires --env=testing to permit the destructive op).
// DB_DATABASE override forces crewli_test even though the .env may
// point at the dev DB.
try {
execSync(
'DB_DATABASE=crewli_test php artisan --env=testing migrate:fresh --force --seed --seeder="Database\\\\Seeders\\\\E2EBaselineSeeder"',
{
cwd: API_DIR,
stdio: 'inherit',
env: { ...process.env, DB_DATABASE: 'crewli_test' },
},
)
}
catch (err) {
throw new Error(`[e2e setup] migrate:fresh + seed failed: ${(err as Error).message}`)
}
if (!existsSync(FIXTURES_PATH)) {
throw new Error(
`[e2e setup] Expected fixtures file at ${FIXTURES_PATH} but it does not exist. `
+ 'Did E2EBaselineSeeder run? Check the artisan output above.',
)
}
const fixtures = JSON.parse(readFileSync(FIXTURES_PATH, 'utf-8')) as Record<string, unknown>
console.log('[e2e setup] Fixtures ready:', Object.keys(fixtures).join(', '))
}