test(form-builder): registry/model alignment consistency invariant (WS-6)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user