Files
crewli/api/database/seeders/E2EBaselineSeeder.php
bert.hausmans 2dfb1e8bae 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>
2026-05-10 15:24:33 +02:00

113 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Artist;
use App\Models\ArtistEngagement;
use App\Models\Event;
use App\Models\Organisation;
use App\Models\Performance;
use App\Models\Stage;
use App\Models\StageDay;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
/**
* Seeds a deterministic baseline for Playwright e2e tests.
*
* Creates:
* - Roles (org_admin etc. via RoleSeeder)
* - One user: e2e@test.local / password ("password")
* - One organisation, attached as org_admin
* - One event spanning today..+30d
* - One stage with one StageDay
* - One artist + engagement + performance (version=0)
*
* Used by tests/playwright-e2e/. Idempotency: this seeder assumes a
* `migrate:fresh` was just run, so it creates without checking for
* existing rows. Re-running on a non-empty DB would create duplicates.
*
* NOT used by PHPUnit — PHPUnit uses factories per test class with
* RefreshDatabase. This is e2e-specific.
*/
final class E2EBaselineSeeder extends Seeder
{
public function run(): void
{
$this->call(RoleSeeder::class);
$org = Organisation::factory()->create([
'name' => 'E2E Test Organisation',
]);
$user = User::factory()->create([
'email' => 'e2e@test.local',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$org->users()->attach($user, ['role' => 'org_admin']);
$event = Event::factory()->create([
'organisation_id' => $org->id,
'name' => 'E2E Test Festival',
'start_date' => CarbonImmutable::now()->subDay(),
'end_date' => CarbonImmutable::now()->addDays(30),
]);
$stage = Stage::factory()->create([
'event_id' => $event->id,
'name' => 'E2E Stage',
]);
StageDay::query()->create([
'stage_id' => $stage->id,
'event_id' => $event->id,
]);
$artist = Artist::factory()->create([
'organisation_id' => $org->id,
'name' => 'E2E Artist',
]);
$engagement = ArtistEngagement::factory()->create([
'artist_id' => $artist->id,
'event_id' => $event->id,
]);
$start = CarbonImmutable::now()->addDays(2)->setTime(20, 0);
Performance::factory()->create([
'engagement_id' => $engagement->id,
'event_id' => $event->id,
'stage_id' => $stage->id,
'lane' => 0,
'start_at' => $start,
'end_at' => $start->addHour(),
'version' => 0,
]);
$performance = Performance::query()
->where('event_id', $event->id)
->where('stage_id', $stage->id)
->first();
// Write seeded IDs to a known location the Playwright e2e
// fixture reads. Avoids artisan-stdout-parsing fragility.
$fixturePath = storage_path('app/e2e-fixtures.json');
@mkdir(dirname($fixturePath), 0755, true);
file_put_contents($fixturePath, json_encode([
'user_email' => 'e2e@test.local',
'user_password' => 'password',
'organisation_id' => $org->id,
'event_id' => $event->id,
'stage_id' => $stage->id,
'performance_id' => $performance?->id,
], JSON_PRETTY_PRINT));
$this->command?->info("E2E fixtures written to {$fixturePath}");
}
}