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); } }