feat(form-builder): form_field_configs relational table + non-validation key split + drop validation_rules JSON columns
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user