feat(form-builder): form_field_validation_rules table + polymorphic owner + scope + cascade

This commit is contained in:
2026-04-24 22:01:36 +02:00
parent 87fc964ead
commit fedaed1b32
17 changed files with 798 additions and 37 deletions

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\FormBuilder\ValidationRules;
use App\Models\FormBuilder\FormField;
use App\Models\FormBuilder\FormFieldBinding;
use App\Models\FormBuilder\FormFieldLibrary;
use App\Models\FormBuilder\FormFieldValidationRule;
use App\Models\FormBuilder\FormSchema;
use App\Models\Organisation;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Asserts that the renamed `FormFieldChildTablesCascadeObserver` cleans up
* validation-rule rows alongside binding rows when the owner is deleted.
*/
final class FormFieldValidationRuleCascadeTest extends TestCase
{
use RefreshDatabase;
public function test_soft_delete_of_field_cascades_validation_rules(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldValidationRule::factory()->forField($field)->create();
FormFieldValidationRule::factory()
->forField($field)
->ofType(\App\Enums\FormBuilder\FormFieldValidationRuleType::MaxLength, ['value' => 10])
->create();
$this->assertSame(2, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field')
->where('owner_id', $field->id)
->count(),
);
$field->delete(); // soft delete on FormField
$this->assertSame(0, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field')
->where('owner_id', $field->id)
->count(),
);
}
public function test_delete_of_library_entry_cascades_validation_rules(): void
{
$org = Organisation::factory()->create();
$library = FormFieldLibrary::factory()->create(['organisation_id' => $org->id]);
FormFieldValidationRule::factory()->forLibrary($library)->create();
$this->assertSame(1, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field_library')
->where('owner_id', $library->id)
->count(),
);
$library->delete();
$this->assertSame(0, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field_library')
->where('owner_id', $library->id)
->count(),
);
}
public function test_deleting_one_field_does_not_cascade_others(): void
{
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$a = FormField::factory()->create(['form_schema_id' => $schema->id]);
$b = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldValidationRule::factory()->forField($a)->create();
FormFieldValidationRule::factory()->forField($b)->create();
$a->delete();
$this->assertSame(1, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field')
->where('owner_id', $b->id)
->count(),
);
}
public function test_cascade_observer_also_cleans_up_bindings_on_same_owner(): void
{
// Regression guard after renaming the observer: the combined
// observer must still clean up binding rows (WS-5a responsibility)
// not only validation rules (WS-5b addition).
$org = Organisation::factory()->create();
$schema = FormSchema::factory()->create(['organisation_id' => $org->id]);
$field = FormField::factory()->create(['form_schema_id' => $schema->id]);
FormFieldBinding::factory()->forField($field)->entityOwned('person', 'email')->create();
FormFieldValidationRule::factory()->forField($field)->create();
$field->delete();
$this->assertSame(0, FormFieldBinding::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field')
->where('owner_id', $field->id)
->count(),
);
$this->assertSame(0, FormFieldValidationRule::query()
->withoutGlobalScopes()
->where('owner_type', 'form_field')
->where('owner_id', $field->id)
->count(),
);
}
}