test(forms): model tests, multi-tenancy, migration rollback (Phase 9)
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>
This commit is contained in:
114
api/tests/Feature/MigrationRollbackTest.php
Normal file
114
api/tests/Feature/MigrationRollbackTest.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user