WS-5c commit 3 of 4. FormRequests (Store/Update) now reject bad
conditional_logic trees at the HTTP boundary — the `after()` hook
unwraps the `show_when` envelope, normalises legacy `{all|any: [...]}`
group shape to the service's internal form, and delegates to
`FormFieldConditionalLogicService::assertSpecsValid()`. Unknown
operators, root conditions, empty groups, and unknown field_slug
references produce a 422 with a readable error before any write.
`form_fields.conditional_logic` JSON column dropped. FormField model
`$fillable` and `$casts` no longer mention the column; factory default
no longer writes `null` to it. Snapshot fixtures in the dev seeder and
the legacy-forms migration command keep `conditional_logic` in their
snapshot JSON shape — that's the schema_snapshot contract, not the DB
column.
FormFieldController now maps InvalidConditionalLogicSpecException to
422 alongside FrozenSchemaException / CyclicDependencyException.
Rollback path: roll back WS-5c commits 1–3 together. Partial rollback
(drop-column reversed but backfill still applied) is not a supported
state — matching the WS-5a/b precedent on the family's full-rollback
contract.
Tests: 6 new (strict FormRequest rejection cases + JSON-column drop
assertion). Rollback step counts in WS-5a/b migration tests bumped +1
for the drop_conditional_logic_json_column migration. Baseline
1142 → 1148 green (3085 → 3099 assertions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
5.5 KiB
PHP
167 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\FormBuilder\ConditionalLogic;
|
|
|
|
use App\Enums\FormBuilder\FormFieldType;
|
|
use App\Models\FormBuilder\FormField;
|
|
use App\Models\FormBuilder\FormSchema;
|
|
use App\Models\Organisation;
|
|
use App\Models\User;
|
|
use Database\Seeders\RoleSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* WS-5c commit 3 — strict validator on save. The Store/Update
|
|
* FormRequests' `after()` hook delegates to
|
|
* `FormFieldConditionalLogicService::assertSpecsValid`, so bad specs
|
|
* fail at the HTTP boundary before any write lands.
|
|
*/
|
|
final class FormFieldStrictConditionalLogicRequestTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Organisation $org;
|
|
|
|
private User $admin;
|
|
|
|
private FormSchema $schema;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->seed(RoleSeeder::class);
|
|
$this->org = Organisation::factory()->create();
|
|
$this->admin = User::factory()->create();
|
|
$this->org->users()->attach($this->admin, ['role' => 'org_admin']);
|
|
$this->schema = FormSchema::factory()->create(['organisation_id' => $this->org->id]);
|
|
}
|
|
|
|
public function test_store_rejects_unknown_comparison_operator(): void
|
|
{
|
|
Sanctum::actingAs($this->admin);
|
|
|
|
FormField::factory()->create(['form_schema_id' => $this->schema->id, 'slug' => 'gate']);
|
|
|
|
$response = $this->postJson(
|
|
"/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields",
|
|
[
|
|
'field_type' => FormFieldType::TEXT->value,
|
|
'slug' => 'target',
|
|
'label' => 'Target',
|
|
'conditional_logic' => [
|
|
'show_when' => [
|
|
'all' => [
|
|
['field_slug' => 'gate', 'operator' => 'matches_regex', 'value' => 'y'],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonValidationErrors(['conditional_logic']);
|
|
}
|
|
|
|
public function test_store_rejects_root_condition_without_group(): void
|
|
{
|
|
Sanctum::actingAs($this->admin);
|
|
|
|
$response = $this->postJson(
|
|
"/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields",
|
|
[
|
|
'field_type' => FormFieldType::TEXT->value,
|
|
'slug' => 'target',
|
|
'label' => 'Target',
|
|
// Condition at root — invalid; ARCH §8 requires a group wrapper.
|
|
'conditional_logic' => [
|
|
'show_when' => [
|
|
'field_slug' => 'gate',
|
|
'operator' => 'equals',
|
|
'value' => 'y',
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonValidationErrors(['conditional_logic']);
|
|
}
|
|
|
|
public function test_store_rejects_empty_group(): void
|
|
{
|
|
Sanctum::actingAs($this->admin);
|
|
|
|
$response = $this->postJson(
|
|
"/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields",
|
|
[
|
|
'field_type' => FormFieldType::TEXT->value,
|
|
'slug' => 'target',
|
|
'label' => 'Target',
|
|
'conditional_logic' => ['show_when' => ['all' => []]],
|
|
],
|
|
);
|
|
|
|
$response->assertStatus(422);
|
|
$response->assertJsonValidationErrors(['conditional_logic']);
|
|
}
|
|
|
|
public function test_store_rejects_unknown_field_slug(): void
|
|
{
|
|
Sanctum::actingAs($this->admin);
|
|
|
|
$response = $this->postJson(
|
|
"/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields",
|
|
[
|
|
'field_type' => FormFieldType::TEXT->value,
|
|
'slug' => 'target',
|
|
'label' => 'Target',
|
|
'conditional_logic' => [
|
|
'show_when' => [
|
|
'all' => [
|
|
['field_slug' => 'nonexistent', 'operator' => 'equals', 'value' => 'y'],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$response->assertStatus(422);
|
|
$this->assertStringContainsString('nonexistent', (string) $response->json('message'));
|
|
}
|
|
|
|
public function test_store_accepts_valid_tree_and_persists_relational_rows(): void
|
|
{
|
|
Sanctum::actingAs($this->admin);
|
|
FormField::factory()->create(['form_schema_id' => $this->schema->id, 'slug' => 'gate']);
|
|
|
|
$response = $this->postJson(
|
|
"/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields",
|
|
[
|
|
'field_type' => FormFieldType::TEXT->value,
|
|
'slug' => 'target',
|
|
'label' => 'Target',
|
|
'conditional_logic' => [
|
|
'show_when' => [
|
|
'all' => [
|
|
['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
);
|
|
|
|
$response->assertStatus(201);
|
|
$this->assertSame([
|
|
'show_when' => [
|
|
'all' => [
|
|
['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'],
|
|
],
|
|
],
|
|
], $response->json('data.conditional_logic'));
|
|
}
|
|
}
|