feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns

This commit is contained in:
2026-04-24 22:42:35 +02:00
parent 9d2758a42c
commit d494478c08
31 changed files with 1233 additions and 60 deletions

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\FormBuilder\Configs;
use App\Enums\FormBuilder\FormFieldConfigType;
use App\Exceptions\FormBuilder\UnknownValidationRuleTypeException;
use App\Models\FormBuilder\FormField;
use App\Models\FormBuilder\FormFieldConfig;
use App\Models\FormBuilder\FormFieldLibrary;
use App\Models\FormBuilder\FormSchema;
use App\Models\Organisation;
use App\Models\Scopes\FormFieldConfigScope;
use App\Services\FormBuilder\FormFieldConfigService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route;
use Spatie\Activitylog\Models\Activity;
use Tests\TestCase;
/**
* Consolidated coverage for `form_field_configs` relation round-trip,
* scope isolation, cascade, and service-layer contract (replace, copy,
* toJsonShape). Mirrors the structure of the validation-rules test suite.
*/
final class FormFieldConfigServiceAndScopeTest extends TestCase
{
use RefreshDatabase;
public function test_field_morph_many_configs_and_owner_morphto_roundtrip(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldConfig::factory()->forField($field)
->ofType(FormFieldConfigType::TagCategories, ['categories' => ['Veiligheid']])->create();
FormFieldConfig::factory()->forField($field)
->ofType(FormFieldConfigType::StorageDisk, ['disk' => 'local'])->create();
$configs = $field->fresh()->configs;
$this->assertCount(2, $configs);
$first = $configs->firstWhere('config_type', FormFieldConfigType::TagCategories);
$this->assertSame(FormField::class, $first->fresh()->owner::class);
}
public function test_scope_isolates_configs_per_organisation_both_owner_types(): void
{
[$orgA, $fieldA, $libraryA] = $this->seedOrgWithConfigs();
[$orgB, $fieldB, $libraryB] = $this->seedOrgWithConfigs();
$this->withOrgRoute($orgA);
$ids = FormFieldConfig::query()->pluck('owner_id')->sort()->values()->all();
$expected = collect([$fieldA->id, $libraryA->id])->sort()->values()->all();
$this->assertSame($expected, $ids);
// Escape hatch.
$this->assertSame(
4,
FormFieldConfig::query()->withoutGlobalScope(FormFieldConfigScope::class)->count(),
);
$this->assertSame(2, FormFieldConfig::query()->count());
}
public function test_cascade_deletes_configs_on_owner_delete(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldConfig::factory()->forField($field)->create();
$this->assertSame(1, FormFieldConfig::query()->withoutGlobalScopes()
->where('owner_id', $field->id)->count());
$field->delete();
$this->assertSame(0, FormFieldConfig::query()->withoutGlobalScopes()
->where('owner_id', $field->id)->count());
}
public function test_replace_configs_enum_and_parameter_shape_enforced(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
$service = app(FormFieldConfigService::class);
$this->expectException(UnknownValidationRuleTypeException::class);
$service->replaceConfigs($field, [
['config_type' => 'not_a_thing', 'parameters' => []],
]);
}
public function test_replace_configs_rejects_bad_tag_categories_shape(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
$service = app(FormFieldConfigService::class);
$this->expectException(UnknownValidationRuleTypeException::class);
$service->replaceConfigs($field, [
['config_type' => 'tag_categories', 'parameters' => ['categories' => 'not-an-array']],
]);
}
public function test_replace_configs_emits_activity_log_on_field_only(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
$library = FormFieldLibrary::factory()->create(['organisation_id' => $org->id]);
$service = app(FormFieldConfigService::class);
$service->replaceConfigs($field, [
['config_type' => 'storage_disk', 'parameters' => ['disk' => 's3']],
]);
$service->replaceConfigs($library, [
['config_type' => 'storage_disk', 'parameters' => ['disk' => 'local']],
]);
$this->assertNotNull(Activity::query()
->where('subject_type', 'form_field')
->where('subject_id', $field->id)
->where('description', 'field.configs_replaced')
->first());
$this->assertNull(Activity::query()
->where('subject_type', 'form_field_library')
->where('description', 'field.configs_replaced')
->first());
}
public function test_copy_configs_clones_every_row(): void
{
$org = Organisation::factory()->create();
$library = FormFieldLibrary::factory()->create(['organisation_id' => $org->id]);
FormFieldConfig::factory()->forLibrary($library)
->ofType(FormFieldConfigType::TagCategories, ['categories' => ['Horeca']])->create();
FormFieldConfig::factory()->forLibrary($library)
->ofType(FormFieldConfigType::StorageDisk, ['disk' => 'local'])->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
app(FormFieldConfigService::class)->copyConfigs($library, $field);
$configs = FormFieldConfig::query()->where('owner_id', $field->id)->get();
$this->assertCount(2, $configs);
}
public function test_to_json_shape_nested_object_envelope(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldConfig::factory()->forField($field)
->ofType(FormFieldConfigType::TagCategories, ['categories' => ['Veiligheid']])->create();
FormFieldConfig::factory()->forField($field)
->ofType(FormFieldConfigType::StorageDisk, ['disk' => 's3'])->create();
$shape = app(FormFieldConfigService::class)->toJsonShape($field->fresh()->configs);
$this->assertSame(['categories' => ['Veiligheid']], $shape['tag_categories']);
$this->assertSame(['disk' => 's3'], $shape['storage_disk']);
}
/** @return array{0:Organisation,1:FormField,2:FormFieldLibrary} */
private function seedOrgWithConfigs(): array
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
$library = FormFieldLibrary::factory()->create(['organisation_id' => $org->id]);
FormFieldConfig::factory()->forField($field)->create();
FormFieldConfig::factory()->forLibrary($library)->create();
return [$org, $field, $library];
}
private function withOrgRoute(Organisation $org): void
{
$route = new Route(['GET'], '/_test', static fn () => null);
$route->bind(request());
$route->setParameter('organisation', $org);
request()->setRouteResolver(static fn () => $route);
}
}