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_creates_field(): void { Sanctum::actingAs($this->admin); $response = $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields", [ 'field_type' => FormFieldType::SELECT->value, 'slug' => 'shirtmaat', 'label' => 'Shirtmaat', 'options' => [ ['value' => 'XS', 'label' => 'XS', 'sort_order' => 0], ['value' => 'S', 'label' => 'S', 'sort_order' => 1], ['value' => 'M', 'label' => 'M', 'sort_order' => 2], ['value' => 'L', 'label' => 'L', 'sort_order' => 3], ], ]); $response->assertCreated(); $this->assertSame('Shirtmaat', $response->json('data.label')); } public function test_reorder_applies_new_order(): void { Sanctum::actingAs($this->admin); $fieldA = FormField::factory()->create(['form_schema_id' => $this->schema->id, 'sort_order' => 0]); $fieldB = FormField::factory()->create(['form_schema_id' => $this->schema->id, 'sort_order' => 1]); $this->postJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields/reorder", ['field_ids' => [$fieldB->id, $fieldA->id]], )->assertOk(); $this->assertSame(0, $fieldB->fresh()->sort_order); $this->assertSame(1, $fieldA->fresh()->sort_order); } public function test_binding_change_blocked_without_force_when_submissions_exist(): void { Sanctum::actingAs($this->admin); $field = FormField::factory()->create([ 'form_schema_id' => $this->schema->id, ]); FormSubmission::factory()->create([ 'form_schema_id' => $this->schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, ]); $response = $this->putJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields/{$field->id}", ['binding' => ['mode' => 'entity_owned', 'entity' => 'user_profile', 'column' => 'bio']], ); $response->assertStatus(422); $this->assertStringContainsString('Binding change blocked', (string) $response->json('message')); } public function test_binding_change_with_force_succeeds(): void { Sanctum::actingAs($this->admin); $field = FormField::factory()->create([ 'form_schema_id' => $this->schema->id, ]); FormSubmission::factory()->create([ 'form_schema_id' => $this->schema->id, 'status' => FormSubmissionStatus::SUBMITTED->value, ]); $this->putJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields/{$field->id}", [ 'binding' => ['mode' => 'entity_owned', 'entity' => 'user_profile', 'column' => 'bio'], 'force_binding_change' => true, ], )->assertOk(); } public function test_cyclic_conditional_logic_is_rejected(): void { Sanctum::actingAs($this->admin); // field A depends on field B — relational rows via factory helper // (WS-5c commit 2 moved cycle detection to the relational-backed // `FormFieldConditionalLogicService::assertNoCycles`). $fieldA = FormField::factory() ->withConditionalLogic([ 'operator' => 'all', 'children' => [ ['field_slug' => 'b', 'operator' => 'equals', 'value' => true], ], ]) ->create([ 'form_schema_id' => $this->schema->id, 'slug' => 'a', ]); $fieldB = FormField::factory()->create(['form_schema_id' => $this->schema->id, 'slug' => 'b']); // Updating fieldB to depend on fieldA would close the A → B → A loop. $response = $this->putJson( "/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields/{$fieldB->id}", ['conditional_logic' => ['show_when' => ['all' => [['field_slug' => 'a', 'operator' => 'equals', 'value' => true]]]]], ); $response->assertStatus(422); $this->assertStringContainsString('Cyclic', (string) $response->json('message')); } public function test_unauthenticated_returns_401(): void { $this->postJson("/api/v1/organisations/{$this->org->id}/forms/schemas/{$this->schema->id}/fields", [ 'field_type' => FormFieldType::TEXT->value, 'slug' => 'x', 'label' => 'X', ])->assertStatus(401); } }