Files
crewli/api/tests/Feature/MigrationRollbackTest.php
bert.hausmans b3eab6e0c8 feat(form-builder): add core services (schema, field, submission, value, field-access, locale, tag-sync, filter, webhook, anonymisation)
S2b Phase 1 per ARCH-FORM-BUILDER.md §20.2. Ten services + supporting
exceptions, jobs, and the organisations.default_locale column needed by
FormLocaleResolver. All services log via spatie/laravel-activitylog, write
operations are transactional, queued jobs are idempotent.

- FormSchemaService: CRUD, slug, version bump, duplicate, edit-lock,
  public_token rotation (7-day grace window), typed-confirmation delete.
- FormFieldService: CRUD, reorder, insertFromLibrary, binding-change guard
  (§6.5), conditional_logic + section cycle detection (§8, §4.8.1),
  is_filterable toggle triggers BackfillFormValueIndexedJob (§7.2, §22.10).
- FormSubmissionService: createDraft with idempotency, saveDraft (auto-save),
  submit with schema snapshot + signature hash computation (§9), review,
  delegate/revoke, soft delete. Fires S1 domain events (§17.1).
- FormValueService: bulk upsert with FieldAccessService RBAC (§24.2),
  Pattern A/C entity mirror writes (§6.1, §6.6) with cross-entity graceful
  skip for person.user_id=null.
- FieldAccessService: canRead/canWrite/filterVisibleFields honouring
  role_restrictions + subject-self (§18.3, §24.1).
- FormLocaleResolver: submitter → schema → org.default_locale → 'nl' (§16.2).
- FormTagSyncService: rebuildForPerson — replaces legacy TagSyncService
  deleted in S2a (§31.10).
- FilterQueryBuilder: generic filter applier for entity_column / tags /
  form_field sources (§7.4–§7.5).
- FormWebhookDispatcher + DeliverFormWebhookJob: HMAC-signed delivery with
  SSRF protection, exponential backoff {1m,5m,30m,2h,8h}, max 5 attempts,
  dead-letter on exhaustion (§17.5).
- FormSubmissionAnonymisationService: per-field anonymisation with separate
  activity log entries (§13.3, §23.4).

MigrationRollbackTest: pin the S2a drop migration by filename so future
migrations don't shift the step offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:47:39 +02:00

80 lines
2.4 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schema;
use PHPUnit\Framework\Attributes\Group;
use Tests\TestCase;
/**
* Sanity-checks the Form Builder migration chain after S2a.
*
* Post-S2a: the legacy registration_* tables are DROPPED by a one-way
* migration (2026_04_20_100000_drop_remaining_legacy_registration_tables).
* Rolling back that migration throws by design — restoring the legacy
* tables from the new form_* structure would be lossy.
*
* Tagged "slow" because it exercises the real migrator.
*/
#[Group('slow')]
final class MigrationRollbackTest extends TestCase
{
use WithoutMiddleware;
private const FORM_BUILDER_TABLES = [
'user_profiles',
'form_schemas',
'form_schema_sections',
'form_field_library',
'form_fields',
'form_submissions',
'form_submission_section_statuses',
'form_submission_delegations',
'form_values',
'form_value_options',
'form_templates',
'form_schema_webhooks',
'form_webhook_deliveries',
];
private const LEGACY_TABLES = [
'registration_form_fields',
'person_field_values',
'registration_field_templates',
];
public function test_form_builder_tables_present_after_migrate_fresh(): void
{
Artisan::call('migrate:fresh');
foreach (self::FORM_BUILDER_TABLES as $table) {
$this->assertTrue(Schema::hasTable($table), "{$table} should exist after migrate:fresh");
}
foreach (self::LEGACY_TABLES as $legacy) {
$this->assertFalse(
Schema::hasTable($legacy),
"legacy table {$legacy} should NOT exist after S2a purge"
);
}
}
public function test_drop_legacy_tables_migration_is_one_way(): void
{
Artisan::call('migrate:fresh');
// Target the S2a drop migration by name so subsequent unrelated
// migrations don't shift the step offset.
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Legacy registration tables cannot be restored');
$path = 'database/migrations/2026_04_20_100000_drop_remaining_legacy_registration_tables.php';
$migration = require base_path($path);
$migration->down();
}
}