UserProfileTest: belongs-to user, fillable/non-fillable boundaries, settings cast, lastSubmittedAt accessor (null + max from user-subject submissions only, ignoring drafts and is_test rows). FormSchemaTest: ULID PK, OrganisationScope filtering, polymorphic owner resolution to Event, purpose enum cast, hasMany fields/submissions, and logSchemaChange() actually creates an activity-log entry. FormFieldTest: belongs-to schema, field_type stored as string (not DB enum), binding/translations array casts, hasMany values, soft-delete preserves historical values, logFieldChange() creates an entry. FormSubmissionTest: belongs-to schema, polymorphic subject resolution, status enum cast, schema_snapshot array cast, hasMany values. FormValueTest: belongs-to submission/field, value array cast, hasMany options pivot rebuilt by observer, unique-pair DB constraint enforced. MultiTenancyTest: OrganisationScope correctly filters FormSchema / FormTemplate / FormFieldLibrary by route-resolved organisation. Pins the FormSchemaWebhook un-scoped behaviour explicitly so a future scope addition is an intentional decision, not an accident. MigrationRollbackTest (group 'slow'): full migrate:fresh → rollback 14 S1 steps → assert all 13 form-builder tables dropped + legacy tables intentionally retained → re-migrate and assert table list matches snapshot. Plus a separate test exercising the populate-user-profiles migration's down(). Supporting tweaks: - UserProfile::lastSubmittedAt accessor now returns Carbon|null instead of a raw timestamp string — testable, and matches Eloquent convention. - UserProfileFactory cooperates with UserObserver via newModel override (updates the auto-created row instead of inserting a duplicate). - AppServiceProvider morph map extended with all 12 form-builder model keys so logSchemaChange/logFieldChange resolve under enforceMorphMap. Suite: 945 passed (was 911), 2671 assertions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.1 KiB
PHP
115 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use PHPUnit\Framework\Attributes\Group;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Hard-resets the test database via migrate:fresh, rolls back every
|
|
* form-builder migration in reverse, asserts the new tables are gone (and
|
|
* legacy tables remain — Phase 8 was deferred to S2 per S1 wrap-up), then
|
|
* re-applies and asserts the table list matches the post-fresh snapshot.
|
|
*
|
|
* Slow because we exercise the real migrator against the real database.
|
|
* Tagged "slow" so CI can parallel-isolate or skip it where needed.
|
|
*/
|
|
#[Group('slow')]
|
|
final class MigrationRollbackTest extends TestCase
|
|
{
|
|
use WithoutMiddleware;
|
|
|
|
/** Migration steps added in S1 (Phase 3 + Phase 4). */
|
|
private const S1_MIGRATION_STEPS = 14;
|
|
|
|
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',
|
|
];
|
|
|
|
public function test_form_builder_migrations_are_fully_reversible(): void
|
|
{
|
|
Artisan::call('migrate:fresh');
|
|
$beforeTables = $this->tableList();
|
|
|
|
// S1 leaves the legacy registration_* tables in place — Phase 8
|
|
// was deferred to S2. Sanity-check that assumption is still true.
|
|
foreach (['registration_form_fields', 'person_field_values', 'registration_field_templates'] as $legacy) {
|
|
$this->assertTrue(Schema::hasTable($legacy), "legacy table {$legacy} should still exist after S1");
|
|
}
|
|
|
|
// Every form-builder table is present after fresh.
|
|
foreach (self::FORM_BUILDER_TABLES as $table) {
|
|
$this->assertTrue(Schema::hasTable($table), "{$table} should exist after migrate:fresh");
|
|
}
|
|
|
|
// Roll back exactly the S1 migration steps.
|
|
Artisan::call('migrate:rollback', ['--step' => self::S1_MIGRATION_STEPS]);
|
|
|
|
// All form-builder tables should now be gone.
|
|
foreach (self::FORM_BUILDER_TABLES as $table) {
|
|
$this->assertFalse(Schema::hasTable($table), "{$table} should be dropped by rollback");
|
|
}
|
|
|
|
// Legacy tables remain untouched by the rollback.
|
|
$this->assertTrue(Schema::hasTable('registration_form_fields'));
|
|
|
|
// Re-apply: tables are recreated, table list matches snapshot.
|
|
Artisan::call('migrate');
|
|
$afterTables = $this->tableList();
|
|
sort($beforeTables);
|
|
sort($afterTables);
|
|
$this->assertSame($beforeTables, $afterTables);
|
|
}
|
|
|
|
public function test_user_profiles_populate_migration_down_clears_backfilled_rows(): void
|
|
{
|
|
Artisan::call('migrate:fresh');
|
|
|
|
// The populate migration ran during fresh. Assert it left rows for
|
|
// any users present at migrate time (test DB has none, so 0 is OK).
|
|
$populatedCount = DB::table('user_profiles')->count();
|
|
|
|
// down() of the populate migration deletes all profiles.
|
|
Artisan::call('migrate:rollback', ['--step' => self::S1_MIGRATION_STEPS - 1]);
|
|
Artisan::call('migrate:rollback', ['--step' => 1]); // populate step
|
|
// Next rollback step now drops the table — handled by the other test.
|
|
|
|
// Re-apply for clean state for subsequent tests.
|
|
Artisan::call('migrate');
|
|
|
|
// Sanity: counts can be compared before/after but tests are isolated
|
|
// per RefreshDatabase so we mainly assert no exceptions.
|
|
$this->assertSame($populatedCount, DB::table('user_profiles')->count());
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function tableList(): array
|
|
{
|
|
return collect(Schema::getTables())
|
|
->pluck('name')
|
|
->reject(fn (string $n) => str_starts_with($n, 'sqlite_') || $n === 'migrations')
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|