From d48d91bba7635ee1d65774f23e5a90e103455834 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Tue, 28 Apr 2026 20:29:26 +0200 Subject: [PATCH] test(form-builder): registry/model alignment consistency invariant (WS-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sessie 1 left BindingTypeRegistryConsistencyTest as the cross-cutting invariant for the binding registry. This commit extends it with a new assertion: every registry entity must map to a real Eloquent model class, and every registry attribute must exist as a column on that model's table. Future drift (someone adds a registry attribute without the column, or renames a column without updating the registry) becomes a test failure on the next test run, not a runtime surprise. Implementation: queries information_schema.COLUMNS via the active MySQL connection (opaque DBs are not in Crewli's deployment matrix per CLAUDE.md). Skips the 'artist' entity entirely — it's intentionally absent from v1 registry per BACKLOG ARTIST-ADV-BINDING-MODEL. Pre-existing tests not touched by this commit (already updated in previous Task 2 commit a404865 for the renames): - BindingTypeRegistryTest (collection-target tests use Config::set synthetic injection) - AppendStrategyRequiresCollectionTargetTest (same pattern) - MaxOneIdentityKeyPerTargetEntityTest (company.email → company.contact_email) Refs: WS-6 sessie 3a binding-target drift audit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../BindingTypeRegistryConsistencyTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/api/tests/Feature/FormBuilder/Bindings/BindingTypeRegistryConsistencyTest.php b/api/tests/Feature/FormBuilder/Bindings/BindingTypeRegistryConsistencyTest.php index ac4b3362..9a6d6a19 100644 --- a/api/tests/Feature/FormBuilder/Bindings/BindingTypeRegistryConsistencyTest.php +++ b/api/tests/Feature/FormBuilder/Bindings/BindingTypeRegistryConsistencyTest.php @@ -8,6 +8,7 @@ use App\Enums\FormBuilder\BindingTargetType; use App\FormBuilder\Bindings\BindingTypeRegistry; use App\FormBuilder\Publishing\RequiresIdentityKeyBinding; use App\FormBuilder\Purposes\PurposeRegistry; +use Illuminate\Support\Facades\DB; use Tests\TestCase; /** @@ -91,4 +92,69 @@ final class BindingTypeRegistryConsistencyTest extends TestCase } } } + + /** + * Sessie 3a.5 — drift-prevention invariant: every registry entity + * must map to a real Eloquent model class, and every registry + * attribute must exist as a column on that model's table. + * + * Without this assertion, a renamed model column or a typo in + * binding_targets.php only surfaces at apply time (or worse, in + * production after publish). With it, drift becomes a test + * failure on the next test run. + */ + public function test_every_registry_entity_maps_to_an_eloquent_model_with_the_attribute(): void + { + $entityToModel = [ + 'person' => \App\Models\Person::class, + 'company' => \App\Models\Company::class, + 'user' => \App\Models\User::class, + // Note: 'artist' intentionally absent from registry in v1 + // (BACKLOG ARTIST-ADV-BINDING-MODEL). + ]; + + $registry = config('form_builder.binding_targets'); + $this->assertIsArray($registry); + + foreach ($registry as $entity => $attributes) { + $this->assertArrayHasKey( + $entity, + $entityToModel, + "Registry entity '{$entity}' has no Eloquent model class mapped. " + ."Add it to entityToModel here, or remove the entity from the registry.", + ); + + $modelClass = $entityToModel[$entity]; + $this->assertTrue( + class_exists($modelClass), + "Entity '{$entity}' maps to {$modelClass} but the class does not exist.", + ); + + $instance = new $modelClass; + $columns = $this->columnsOnTable($instance->getTable()); + + foreach (array_keys($attributes) as $attribute) { + $this->assertContains( + $attribute, + $columns, + "Registry says '{$entity}.{$attribute}' exists, but column " + ."'{$attribute}' is not on table '{$instance->getTable()}'.", + ); + } + } + } + + /** + * @return list + */ + private function columnsOnTable(string $table): array + { + $rows = DB::select( + 'SELECT COLUMN_NAME FROM information_schema.COLUMNS ' + .'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?', + [$table], + ); + + return array_map(static fn (object $r): string => (string) $r->COLUMN_NAME, $rows); + } }