service = $this->app->make(FormFieldBindingService::class); $this->org = Organisation::factory()->create(); $this->schema = FormSchema::factory()->create(['organisation_id' => $this->org->id]); } public function test_replace_bindings_is_transactional_and_swaps_old_for_new(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); FormFieldBinding::factory()->forField($field)->entityOwned('person', 'email')->create(); $this->service->replaceBindings($field, [[ 'target_entity' => 'person', 'target_attribute' => 'first_name', 'mode' => FormFieldBindingMode::EntityOwned->value, ]]); $rows = FormFieldBinding::query() ->withoutGlobalScopes() ->where('owner_type', 'form_field') ->where('owner_id', $field->id) ->get(); $this->assertCount(1, $rows); $this->assertSame('first_name', $rows->first()->target_attribute); } public function test_replace_bindings_with_empty_array_clears_all(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); FormFieldBinding::factory()->forField($field)->entityOwned('person', 'email')->create(); $this->service->replaceBindings($field, []); $this->assertSame(0, FormFieldBinding::query() ->withoutGlobalScopes() ->where('owner_type', 'form_field') ->where('owner_id', $field->id) ->count(), ); } public function test_replace_bindings_rejects_unregistered_target_entity(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("target_entity 'unicorn' is not registered"); $this->service->replaceBindings($field, [[ 'target_entity' => 'unicorn', 'target_attribute' => 'email', 'mode' => FormFieldBindingMode::EntityOwned->value, ]]); } public function test_replace_bindings_rejects_unregistered_target_attribute(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("target_attribute 'person.made_up_column'"); $this->service->replaceBindings($field, [[ 'target_entity' => 'person', 'target_attribute' => 'made_up_column', 'mode' => FormFieldBindingMode::EntityOwned->value, ]]); } public function test_replace_bindings_rejects_invalid_mode(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("mode 'form_owned'"); $this->service->replaceBindings($field, [[ 'target_entity' => 'person', 'target_attribute' => 'email', 'mode' => 'form_owned', ]]); } public function test_replace_bindings_logs_activity_on_change(): void { Activity::query()->delete(); $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->service->replaceBindings($field, [[ 'target_entity' => 'person', 'target_attribute' => 'email', 'mode' => FormFieldBindingMode::EntityOwned->value, ]]); $entry = Activity::query() ->where('subject_type', $field->getMorphClass()) ->where('subject_id', $field->id) ->where('description', 'field.bindings_replaced') ->first(); $this->assertNotNull($entry); } public function test_copy_bindings_preserves_every_column(): void { $library = FormFieldLibrary::factory()->create(['organisation_id' => $this->org->id]); $source = FormFieldBinding::factory()->forLibrary($library)->create([ 'target_entity' => 'user_profile', 'target_attribute' => 'bio', 'mode' => FormFieldBindingMode::Mirrored->value, 'sync_direction' => 'write_on_submit', 'merge_strategy' => FormFieldBindingMergeStrategy::Append->value, 'trust_level' => 80, 'is_identity_key' => true, ]); $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->service->copyBindings($library->fresh(), $field); $copy = FormFieldBinding::query() ->withoutGlobalScopes() ->where('owner_type', 'form_field') ->where('owner_id', $field->id) ->first(); $this->assertNotNull($copy); $this->assertSame($source->target_entity, $copy->target_entity); $this->assertSame($source->target_attribute, $copy->target_attribute); $this->assertSame(FormFieldBindingMode::Mirrored, $copy->mode); $this->assertSame('write_on_submit', $copy->sync_direction); $this->assertSame(FormFieldBindingMergeStrategy::Append, $copy->merge_strategy); $this->assertSame(80, $copy->trust_level); $this->assertTrue($copy->is_identity_key); } public function test_copy_bindings_is_noop_when_source_has_none(): void { $library = FormFieldLibrary::factory()->create(['organisation_id' => $this->org->id]); $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $this->service->copyBindings($library, $field); $this->assertSame(0, FormFieldBinding::query() ->withoutGlobalScopes() ->where('owner_type', 'form_field') ->where('owner_id', $field->id) ->count(), ); } public function test_to_json_shape_matches_arch_6_3_for_entity_owned(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $binding = FormFieldBinding::factory() ->forField($field) ->entityOwned('person', 'email') ->create(); $this->assertSame([ 'mode' => 'entity_owned', 'entity' => 'person', 'column' => 'email', ], $this->service->toJsonShape($binding)); } public function test_to_json_shape_matches_arch_6_3_for_mirrored(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $binding = FormFieldBinding::factory() ->forField($field) ->mirrored('user_profile', 'emergency_contact_name') ->create(); $this->assertSame([ 'mode' => 'mirrored', 'entity' => 'user_profile', 'column' => 'emergency_contact_name', 'sync_direction' => 'write_on_submit', ], $this->service->toJsonShape($binding)); } public function test_to_json_shape_returns_null_for_no_binding(): void { $this->assertNull($this->service->toJsonShape(null)); } public function test_bindings_for_returns_only_owner_bindings(): void { $field = FormField::factory()->create(['form_schema_id' => $this->schema->id]); $other = FormField::factory()->create(['form_schema_id' => $this->schema->id]); FormFieldBinding::factory()->forField($field)->entityOwned('person', 'email')->create(); FormFieldBinding::factory()->forField($field)->entityOwned('person', 'phone')->create(); FormFieldBinding::factory()->forField($other)->entityOwned('person', 'first_name')->create(); $this->assertCount(2, $this->service->bindingsFor($field)); } }