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); + } }