test(customfield): cover UserCustomFieldValueService (6 public methods)
Cluster B2 der Testabdeckungs-Initiative. Baut auf CUSTOMFIELD-DI-REFACTOR-001 auf und deckt den 610-Zeilen-Service mit 25 Unit-Tests / 53 Assertions ab. Abdeckung aller 6 public methods: - validateForTenants (4 Tests) - buildDefinitionsByTenant (3 Tests) - buildUserValueMap (4 Tests) - buildPublicValuesByTenant (3 Tests) - syncForUser (7 Tests: alle Type-Branches + Repo-Failures) - extractCustomFieldFilterSpec (3 Tests: cf_/cfm_/cfd_ Praefixe) Tenant-Scope-Edges (GR-SEC-009) in allen vier relevanten Methoden explizit abgedeckt: buildDefinitionsByTenant, syncForUser (deleteByUserOutsideTenantIds), buildPublicValuesByTenant (tenantScopeId-Filter), extractCustomFieldFilterSpec (getUserTenantIds). Gates: QG-001 (1994 Tests) / QG-002 / QG-006 pass. Workflow: .agents/runs/TEST-CUSTOMFIELD-USER-001/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
561
tests/Service/CustomField/UserCustomFieldValueServiceTest.php
Normal file
561
tests/Service/CustomField/UserCustomFieldValueServiceTest.php
Normal file
@@ -0,0 +1,561 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\CustomField;
|
||||
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldDefinitionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueOptionRepositoryInterface;
|
||||
use MintyPHP\Repository\CustomField\UserCustomFieldValueRepositoryInterface;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UserCustomFieldValueServiceTest extends TestCase
|
||||
{
|
||||
private const UUID_A = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
||||
private const UUID_B = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
||||
private const UUID_C = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
|
||||
|
||||
// --- validateForTenants ------------------------------------------------
|
||||
|
||||
public function testValidateForTenantsSkipsWhenNotEditable(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame(['ok' => true, 'errors' => []], $service->validateForTenants([1], [], false));
|
||||
}
|
||||
|
||||
public function testValidateForTenantsSkipsWhenTenantsEmpty(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame(['ok' => true, 'errors' => []], $service->validateForTenants([0, -1], [], true));
|
||||
}
|
||||
|
||||
public function testValidateForTenantsReturnsPreparedErrorsWhenInvalidValue(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Score', 'type' => 'date'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([]);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
);
|
||||
|
||||
$result = $service->validateForTenants([1], [
|
||||
'custom_field_values' => ['5' => 'not-a-date'],
|
||||
], true);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertContains('Invalid value for Score', $result['errors']);
|
||||
}
|
||||
|
||||
public function testValidateForTenantsReturnsOkWhenClean(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([]);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
);
|
||||
|
||||
$result = $service->validateForTenants([1], [
|
||||
'custom_field_values' => ['5' => 'Fred'],
|
||||
], true);
|
||||
|
||||
$this->assertSame(['ok' => true, 'errors' => []], $result);
|
||||
}
|
||||
|
||||
// --- buildDefinitionsByTenant ------------------------------------------
|
||||
|
||||
public function testBuildDefinitionsByTenantReturnsEmptyForEmptyTenantIds(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame([], $service->buildDefinitionsByTenant([0, -1]));
|
||||
}
|
||||
|
||||
public function testBuildDefinitionsByTenantReturnsEmptyWhenNoDefinitions(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->expects($this->once())
|
||||
->method('listByTenantIds')
|
||||
->with([1, 2], true)
|
||||
->willReturn([]);
|
||||
|
||||
$service = $this->makeService(definitionRepository: $definitionRepo);
|
||||
|
||||
$this->assertSame([], $service->buildDefinitionsByTenant([1, 2]));
|
||||
}
|
||||
|
||||
public function testBuildDefinitionsByTenantGroupsByTenantAndMergesOptions(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 10, 'tenant_id' => 1, 'type' => 'select'],
|
||||
['id' => 11, 'tenant_id' => 2, 'type' => 'text'],
|
||||
]);
|
||||
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 10, 'id' => 100, 'option_key' => 'a', 'label' => 'A'],
|
||||
]);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
);
|
||||
|
||||
$result = $service->buildDefinitionsByTenant([1, 2]);
|
||||
|
||||
$this->assertArrayHasKey(1, $result);
|
||||
$this->assertArrayHasKey(2, $result);
|
||||
$this->assertCount(1, $result[1][0]['options']);
|
||||
$this->assertSame([], $result[2][0]['options']);
|
||||
}
|
||||
|
||||
// --- buildUserValueMap -------------------------------------------------
|
||||
|
||||
public function testBuildUserValueMapReturnsEmptyForInvalidUserId(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame([], $service->buildUserValueMap(0, [1]));
|
||||
}
|
||||
|
||||
public function testBuildUserValueMapReturnsEmptyForEmptyDefinitionIds(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame([], $service->buildUserValueMap(7, [0, -1]));
|
||||
}
|
||||
|
||||
public function testBuildUserValueMapReturnsEmptyWhenRepoHasNoRows(): void
|
||||
{
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('listByUserAndDefinitionIds')->willReturn([]);
|
||||
|
||||
$service = $this->makeService(valueRepository: $valueRepo);
|
||||
|
||||
$this->assertSame([], $service->buildUserValueMap(7, [1, 2]));
|
||||
}
|
||||
|
||||
public function testBuildUserValueMapMergesOptionIdsForMultiselectValues(): void
|
||||
{
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('listByUserAndDefinitionIds')->willReturn([
|
||||
['id' => 100, 'definition_id' => 10, 'value_text' => 'Fred', 'value_bool' => null, 'value_date' => null, 'option_id' => null],
|
||||
['id' => 101, 'definition_id' => 11, 'value_text' => null, 'value_bool' => 1, 'value_date' => null, 'option_id' => null],
|
||||
]);
|
||||
|
||||
$valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
|
||||
$valueOptionRepo->method('listOptionIdsByValueIds')->willReturn([
|
||||
100 => [200, 201],
|
||||
]);
|
||||
|
||||
$service = $this->makeService(
|
||||
valueRepository: $valueRepo,
|
||||
valueOptionRepository: $valueOptionRepo,
|
||||
);
|
||||
|
||||
$result = $service->buildUserValueMap(7, [10, 11]);
|
||||
|
||||
$this->assertSame([200, 201], $result[10]['option_ids']);
|
||||
$this->assertSame('Fred', $result[10]['value_text']);
|
||||
$this->assertSame([], $result[11]['option_ids']);
|
||||
$this->assertSame(1, $result[11]['value_bool']);
|
||||
}
|
||||
|
||||
// --- buildPublicValuesByTenant -----------------------------------------
|
||||
|
||||
public function testBuildPublicValuesByTenantReturnsEmptyForInvalidUserId(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$this->assertSame([], $service->buildPublicValuesByTenant(0, [['id' => 1]]));
|
||||
}
|
||||
|
||||
public function testBuildPublicValuesByTenantFiltersByTenantScopeId(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
// Only tenant 2 is resolved because tenantScopeId=2 filters out tenant 1.
|
||||
$definitionRepo->expects($this->once())
|
||||
->method('listByTenantIds')
|
||||
->with([2], true)
|
||||
->willReturn([]);
|
||||
|
||||
$service = $this->makeService(definitionRepository: $definitionRepo);
|
||||
|
||||
$result = $service->buildPublicValuesByTenant(7, [
|
||||
['id' => 1, 'uuid' => 't1', 'description' => 'T1', 'status' => 'active'],
|
||||
['id' => 2, 'uuid' => 't2', 'description' => 'T2', 'status' => 'active'],
|
||||
], 2);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testBuildPublicValuesByTenantAssemblesFieldsWithValues(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 10, 'tenant_id' => 1, 'uuid' => self::UUID_A, 'field_key' => 'nick', 'label' => 'Nickname', 'type' => 'text', 'is_filterable' => 0],
|
||||
['id' => 11, 'tenant_id' => 1, 'uuid' => self::UUID_B, 'field_key' => 'prio', 'label' => 'Priority', 'type' => 'select', 'is_filterable' => 1],
|
||||
]);
|
||||
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 11, 'id' => 200, 'option_key' => 'low', 'label' => 'Low'],
|
||||
['definition_id' => 11, 'id' => 201, 'option_key' => 'high', 'label' => 'High'],
|
||||
]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('listByUserAndDefinitionIds')->willReturn([
|
||||
['id' => 500, 'definition_id' => 10, 'value_text' => 'Fred', 'value_bool' => null, 'value_date' => null, 'option_id' => null],
|
||||
['id' => 501, 'definition_id' => 11, 'value_text' => null, 'value_bool' => null, 'value_date' => null, 'option_id' => 201],
|
||||
]);
|
||||
|
||||
$valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
|
||||
$valueOptionRepo->method('listOptionIdsByValueIds')->willReturn([]);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
valueOptionRepository: $valueOptionRepo,
|
||||
);
|
||||
|
||||
$result = $service->buildPublicValuesByTenant(7, [
|
||||
['id' => 1, 'uuid' => 't1', 'description' => 'Tenant 1', 'status' => 'active'],
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame('Tenant 1', $result[0]['tenant']['description']);
|
||||
$this->assertCount(2, $result[0]['fields']);
|
||||
$this->assertSame('Fred', $result[0]['fields'][0]['value']);
|
||||
$this->assertSame('high', $result[0]['fields'][1]['value']);
|
||||
$this->assertCount(2, $result[0]['fields'][1]['options']);
|
||||
}
|
||||
|
||||
// --- syncForUser -------------------------------------------------------
|
||||
|
||||
public function testSyncForUserRejectsInvalidUserId(): void
|
||||
{
|
||||
$service = $this->makeService();
|
||||
|
||||
$result = $service->syncForUser(0, [1], [], true);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame(['User not found'], $result['errors']);
|
||||
}
|
||||
|
||||
public function testSyncForUserFailsWhenOutsideCleanupFails(): void
|
||||
{
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(false);
|
||||
|
||||
$service = $this->makeService(valueRepository: $valueRepo);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [], true);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertContains('Custom field values can not be saved', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSyncForUserSkipsEditsWhenNotEditable(): void
|
||||
{
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
$valueRepo->expects($this->never())->method('upsertScalarValue');
|
||||
|
||||
$service = $this->makeService(valueRepository: $valueRepo);
|
||||
|
||||
$result = $service->syncForUser(7, [1], ['custom_field_values' => ['5' => 'Fred']], false);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSyncForUserWritesTextValue(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
$valueRepo->expects($this->once())
|
||||
->method('upsertScalarValue')
|
||||
->with(7, 5, $this->callback(static fn (array $data): bool => ($data['value_text'] ?? null) === 'Fred'))
|
||||
->willReturn(500);
|
||||
|
||||
$valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
|
||||
$valueOptionRepo->expects($this->once())
|
||||
->method('replaceForValueId')
|
||||
->with(500, [])
|
||||
->willReturn(true);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
valueOptionRepository: $valueOptionRepo,
|
||||
);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [
|
||||
'custom_field_values' => ['5' => 'Fred'],
|
||||
], true);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSyncForUserDeletesEmptyScalarValues(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Nickname', 'type' => 'text'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
$valueRepo->expects($this->once())
|
||||
->method('deleteByUserAndDefinitionIds')
|
||||
->with(7, [5])
|
||||
->willReturn(true);
|
||||
$valueRepo->expects($this->never())->method('upsertScalarValue');
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [
|
||||
'custom_field_values' => ['5' => ' '],
|
||||
], true);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSyncForUserRejectsInvalidDateValue(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Due', 'type' => 'date'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [
|
||||
'custom_field_values' => ['5' => 'not-a-date'],
|
||||
], true);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertContains('Invalid value for Due', $result['errors']);
|
||||
}
|
||||
|
||||
public function testSyncForUserWritesMultiselectOptionIds(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Tags', 'type' => 'multiselect'],
|
||||
]);
|
||||
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 5, 'id' => 200, 'active' => 1, 'option_key' => 'x', 'label' => 'X'],
|
||||
['definition_id' => 5, 'id' => 201, 'active' => 1, 'option_key' => 'y', 'label' => 'Y'],
|
||||
]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
$valueRepo->expects($this->once())
|
||||
->method('upsertScalarValue')
|
||||
->with(7, 5, $this->callback(static fn (array $d): bool => ($d['option_id'] ?? null) === null && ($d['value_text'] ?? null) === null))
|
||||
->willReturn(500);
|
||||
|
||||
$valueOptionRepo = $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class);
|
||||
$valueOptionRepo->expects($this->once())
|
||||
->method('replaceForValueId')
|
||||
->with(500, [200, 201])
|
||||
->willReturn(true);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
valueOptionRepository: $valueOptionRepo,
|
||||
);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [
|
||||
'custom_field_values_multi' => ['5' => [200, 201]],
|
||||
], true);
|
||||
|
||||
$this->assertTrue($result['ok']);
|
||||
}
|
||||
|
||||
public function testSyncForUserRejectsMultiselectValueNotInActiveOptions(): void
|
||||
{
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listByTenantIds')->willReturn([
|
||||
['id' => 5, 'tenant_id' => 1, 'label' => 'Tags', 'type' => 'multiselect'],
|
||||
]);
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 5, 'id' => 200, 'active' => 1],
|
||||
]);
|
||||
|
||||
$valueRepo = $this->createMock(UserCustomFieldValueRepositoryInterface::class);
|
||||
$valueRepo->method('deleteByUserOutsideTenantIds')->willReturn(true);
|
||||
|
||||
$service = $this->makeService(
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
valueRepository: $valueRepo,
|
||||
);
|
||||
|
||||
$result = $service->syncForUser(7, [1], [
|
||||
'custom_field_values_multi' => ['5' => [200, 999]],
|
||||
], true);
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertContains('Invalid value for Tags', $result['errors']);
|
||||
}
|
||||
|
||||
// --- extractCustomFieldFilterSpec -------------------------------------
|
||||
|
||||
public function testExtractCustomFieldFilterSpecReturnsEmptyWhenNoFilterableDefinitions(): void
|
||||
{
|
||||
$scope = $this->createMock(TenantScopeService::class);
|
||||
$scope->method('getUserTenantIds')->willReturn([1]);
|
||||
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listFilterableByTenantIds')->willReturn([]);
|
||||
|
||||
$service = $this->makeService(
|
||||
userScopeGateway: $scope,
|
||||
definitionRepository: $definitionRepo,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
['definitions' => [], 'filters' => [], 'query' => []],
|
||||
$service->extractCustomFieldFilterSpec(['cf_' . self::UUID_A => '123'], 7)
|
||||
);
|
||||
}
|
||||
|
||||
public function testExtractCustomFieldFilterSpecParsesSelectAndBooleanFilters(): void
|
||||
{
|
||||
$scope = $this->createMock(TenantScopeService::class);
|
||||
$scope->method('getUserTenantIds')->willReturn([1]);
|
||||
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listFilterableByTenantIds')->willReturn([
|
||||
['id' => 10, 'uuid' => self::UUID_A, 'type' => 'select'],
|
||||
['id' => 11, 'uuid' => self::UUID_B, 'type' => 'boolean'],
|
||||
]);
|
||||
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 10, 'id' => 200],
|
||||
]);
|
||||
|
||||
$service = $this->makeService(
|
||||
userScopeGateway: $scope,
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
);
|
||||
|
||||
$result = $service->extractCustomFieldFilterSpec([
|
||||
'cf_' . self::UUID_A => '200',
|
||||
'cf_' . self::UUID_B => 'yes',
|
||||
'cf_' . self::UUID_C => 'ignored', // UUID_C not among definitions
|
||||
], 7);
|
||||
|
||||
$this->assertSame(['10' => 200], $this->keyedAsStringInt($result['filters']['select']));
|
||||
$this->assertSame(['11' => 1], $this->keyedAsStringInt($result['filters']['boolean']));
|
||||
$this->assertArrayNotHasKey('cf_' . self::UUID_C, $result['query']);
|
||||
}
|
||||
|
||||
public function testExtractCustomFieldFilterSpecParsesMultiselectAndDateRange(): void
|
||||
{
|
||||
$scope = $this->createMock(TenantScopeService::class);
|
||||
$scope->method('getUserTenantIds')->willReturn([1]);
|
||||
|
||||
$definitionRepo = $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class);
|
||||
$definitionRepo->method('listFilterableByTenantIds')->willReturn([
|
||||
['id' => 10, 'uuid' => self::UUID_A, 'type' => 'multiselect'],
|
||||
['id' => 11, 'uuid' => self::UUID_B, 'type' => 'date'],
|
||||
]);
|
||||
|
||||
$optionRepo = $this->createMock(TenantCustomFieldOptionRepositoryInterface::class);
|
||||
$optionRepo->method('listByDefinitionIds')->willReturn([
|
||||
['definition_id' => 10, 'id' => 200],
|
||||
['definition_id' => 10, 'id' => 201],
|
||||
]);
|
||||
|
||||
$service = $this->makeService(
|
||||
userScopeGateway: $scope,
|
||||
definitionRepository: $definitionRepo,
|
||||
optionRepository: $optionRepo,
|
||||
);
|
||||
|
||||
$result = $service->extractCustomFieldFilterSpec([
|
||||
'cfm_' . self::UUID_A => '200,201,999', // 999 not allowed — should be filtered
|
||||
'cfd_' . self::UUID_B . '_from' => '2026-01-01',
|
||||
'cfd_' . self::UUID_B . '_to' => 'not-a-date', // invalid — ignored
|
||||
], 7);
|
||||
|
||||
$this->assertSame([200, 201], $result['filters']['multiselect'][10]);
|
||||
$this->assertSame('2026-01-01', $result['filters']['date'][11]['from']);
|
||||
$this->assertNull($result['filters']['date'][11]['to']);
|
||||
}
|
||||
|
||||
// --- helpers -----------------------------------------------------------
|
||||
|
||||
/** @param array<int, int> $map @return array<string, int> */
|
||||
private function keyedAsStringInt(array $map): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($map as $k => $v) {
|
||||
$out[(string) $k] = (int) $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function makeService(
|
||||
?TenantScopeService $userScopeGateway = null,
|
||||
?TenantCustomFieldDefinitionRepositoryInterface $definitionRepository = null,
|
||||
?TenantCustomFieldOptionRepositoryInterface $optionRepository = null,
|
||||
?UserCustomFieldValueRepositoryInterface $valueRepository = null,
|
||||
?UserCustomFieldValueOptionRepositoryInterface $valueOptionRepository = null,
|
||||
): UserCustomFieldValueService {
|
||||
return new UserCustomFieldValueService(
|
||||
$userScopeGateway ?? $this->createMock(TenantScopeService::class),
|
||||
$definitionRepository ?? $this->createMock(TenantCustomFieldDefinitionRepositoryInterface::class),
|
||||
$optionRepository ?? $this->createMock(TenantCustomFieldOptionRepositoryInterface::class),
|
||||
$valueRepository ?? $this->createMock(UserCustomFieldValueRepositoryInterface::class),
|
||||
$valueOptionRepository ?? $this->createMock(UserCustomFieldValueOptionRepositoryInterface::class),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user