Skip to content

Commit ed6c6e1

Browse files
committed
test(service): cover transactional data import service
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent d3d1d20 commit ed6c6e1

1 file changed

Lines changed: 251 additions & 0 deletions

File tree

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\ProfileFields\Tests\Unit\Service;
11+
12+
use OCA\ProfileFields\Db\FieldDefinition;
13+
use OCA\ProfileFields\Db\FieldValue;
14+
use OCA\ProfileFields\Service\DataImportService;
15+
use OCA\ProfileFields\Service\FieldDefinitionService;
16+
use OCA\ProfileFields\Service\FieldValueService;
17+
use OCA\ProfileFields\Service\ImportPayloadValidator;
18+
use OCP\IDBConnection;
19+
use PHPUnit\Framework\MockObject\MockObject;
20+
use PHPUnit\Framework\TestCase;
21+
22+
class DataImportServiceTest extends TestCase {
23+
private ImportPayloadValidator&MockObject $importPayloadValidator;
24+
private FieldDefinitionService&MockObject $fieldDefinitionService;
25+
private FieldValueService&MockObject $fieldValueService;
26+
private IDBConnection&MockObject $connection;
27+
private DataImportService $service;
28+
29+
protected function setUp(): void {
30+
parent::setUp();
31+
$this->importPayloadValidator = $this->createMock(ImportPayloadValidator::class);
32+
$this->fieldDefinitionService = $this->createMock(FieldDefinitionService::class);
33+
$this->fieldValueService = $this->createMock(FieldValueService::class);
34+
$this->connection = $this->createMock(IDBConnection::class);
35+
$this->service = new DataImportService(
36+
$this->importPayloadValidator,
37+
$this->fieldDefinitionService,
38+
$this->fieldValueService,
39+
$this->connection,
40+
);
41+
}
42+
43+
public function testImportDryRunReturnsSummaryWithoutPersisting(): void {
44+
$existingDefinition = $this->buildDefinition(7, 'cost_center', 'Cost center', 2, true);
45+
$existingValue = $this->buildValue(7, 'alice', ['value' => 'finance'], 'users', 'ops-admin');
46+
47+
$this->importPayloadValidator->expects($this->once())
48+
->method('validate')
49+
->willReturn($this->buildNormalizedPayload());
50+
51+
$this->fieldDefinitionService->expects($this->exactly(4))
52+
->method('findByFieldKey')
53+
->willReturnMap([
54+
['region', null],
55+
['cost_center', $existingDefinition],
56+
['region', null],
57+
['cost_center', $existingDefinition],
58+
]);
59+
60+
$this->fieldValueService->expects($this->once())
61+
->method('findByFieldDefinitionIdAndUserUid')
62+
->with(7, 'alice')
63+
->willReturn($existingValue);
64+
65+
$this->fieldValueService->expects($this->once())
66+
->method('serializeForResponse')
67+
->with($existingValue)
68+
->willReturn([
69+
'id' => 1,
70+
'field_definition_id' => 7,
71+
'user_uid' => 'alice',
72+
'value' => ['value' => 'finance'],
73+
'current_visibility' => 'users',
74+
'updated_by_uid' => 'ops-admin',
75+
'updated_at' => '2026-03-15T12:00:00+00:00',
76+
]);
77+
78+
$this->connection->expects($this->never())->method('beginTransaction');
79+
80+
$summary = $this->service->import(['schema_version' => 1], true);
81+
82+
$this->assertSame([
83+
'created_definitions' => 1,
84+
'updated_definitions' => 0,
85+
'skipped_definitions' => 1,
86+
'created_values' => 1,
87+
'updated_values' => 0,
88+
'skipped_values' => 1,
89+
], $summary);
90+
}
91+
92+
public function testImportPersistsCreatesAndUpdatesInsideTransaction(): void {
93+
$existingDefinition = $this->buildDefinition(7, 'cost_center', 'Old cost center', 1, true);
94+
$updatedDefinition = $this->buildDefinition(7, 'cost_center', 'Cost center', 2, true);
95+
$createdDefinition = $this->buildDefinition(8, 'region', 'Region', 0, true);
96+
$existingValue = $this->buildValue(7, 'alice', ['value' => 'legacy'], 'users', 'legacy-admin');
97+
98+
$this->importPayloadValidator->expects($this->once())
99+
->method('validate')
100+
->willReturn($this->buildNormalizedPayload());
101+
102+
$this->fieldDefinitionService->expects($this->exactly(2))
103+
->method('findByFieldKey')
104+
->willReturnMap([
105+
['region', null],
106+
['cost_center', $existingDefinition],
107+
]);
108+
109+
$this->fieldDefinitionService->expects($this->once())
110+
->method('create')
111+
->with($this->callback(static fn (array $definition): bool => $definition['field_key'] === 'region'))
112+
->willReturn($createdDefinition);
113+
114+
$this->fieldDefinitionService->expects($this->once())
115+
->method('update')
116+
->with(
117+
$existingDefinition,
118+
$this->callback(static fn (array $definition): bool => $definition['field_key'] === 'cost_center' && $definition['label'] === 'Cost center' && $definition['sort_order'] === 2),
119+
)
120+
->willReturn($updatedDefinition);
121+
122+
$this->fieldValueService->expects($this->exactly(2))
123+
->method('findByFieldDefinitionIdAndUserUid')
124+
->willReturnMap([
125+
[8, 'bob', null],
126+
[7, 'alice', $existingValue],
127+
]);
128+
129+
$this->fieldValueService->expects($this->once())
130+
->method('serializeForResponse')
131+
->with($existingValue)
132+
->willReturn([
133+
'id' => 1,
134+
'field_definition_id' => 7,
135+
'user_uid' => 'alice',
136+
'value' => ['value' => 'legacy'],
137+
'current_visibility' => 'users',
138+
'updated_by_uid' => 'legacy-admin',
139+
'updated_at' => '2026-03-15T12:00:00+00:00',
140+
]);
141+
142+
$this->fieldValueService->expects($this->exactly(2))
143+
->method('upsert')
144+
->with(
145+
$this->callback(static fn (FieldDefinition $definition): bool => in_array($definition->getFieldKey(), ['region', 'cost_center'], true)),
146+
$this->callback(static fn (string $userUid): bool => in_array($userUid, ['bob', 'alice'], true)),
147+
$this->callback(static fn (mixed $value): bool => in_array($value, ['emea', 'finance'], true)),
148+
$this->callback(static fn (string $updatedByUid): bool => in_array($updatedByUid, ['admin', 'ops-admin'], true)),
149+
'users',
150+
);
151+
152+
$this->connection->expects($this->once())->method('beginTransaction');
153+
$this->connection->expects($this->once())->method('commit');
154+
$this->connection->expects($this->never())->method('rollBack');
155+
156+
$summary = $this->service->import(['schema_version' => 1], false);
157+
158+
$this->assertSame([
159+
'created_definitions' => 1,
160+
'updated_definitions' => 1,
161+
'skipped_definitions' => 0,
162+
'created_values' => 1,
163+
'updated_values' => 1,
164+
'skipped_values' => 0,
165+
], $summary);
166+
}
167+
168+
/**
169+
* @return array{
170+
* schema_version: int,
171+
* definitions: list<array<string, mixed>>,
172+
* values: list<array<string, mixed>>,
173+
* }
174+
*/
175+
private function buildNormalizedPayload(): array {
176+
return [
177+
'schema_version' => 1,
178+
'definitions' => [
179+
[
180+
'field_key' => 'region',
181+
'label' => 'Region',
182+
'type' => 'text',
183+
'admin_only' => false,
184+
'user_editable' => false,
185+
'user_visible' => true,
186+
'initial_visibility' => 'users',
187+
'sort_order' => 0,
188+
'active' => true,
189+
],
190+
[
191+
'field_key' => 'cost_center',
192+
'label' => 'Cost center',
193+
'type' => 'text',
194+
'admin_only' => false,
195+
'user_editable' => false,
196+
'user_visible' => true,
197+
'initial_visibility' => 'users',
198+
'sort_order' => 2,
199+
'active' => true,
200+
],
201+
],
202+
'values' => [
203+
[
204+
'field_key' => 'region',
205+
'user_uid' => 'bob',
206+
'value' => ['value' => 'emea'],
207+
'current_visibility' => 'users',
208+
'updated_by_uid' => 'admin',
209+
'updated_at' => '2026-03-15T12:00:00+00:00',
210+
],
211+
[
212+
'field_key' => 'cost_center',
213+
'user_uid' => 'alice',
214+
'value' => ['value' => 'finance'],
215+
'current_visibility' => 'users',
216+
'updated_by_uid' => 'ops-admin',
217+
'updated_at' => '2026-03-15T12:00:00+00:00',
218+
],
219+
],
220+
];
221+
}
222+
223+
private function buildDefinition(int $id, string $fieldKey, string $label, int $sortOrder, bool $active): FieldDefinition {
224+
$definition = new FieldDefinition();
225+
$definition->setId($id);
226+
$definition->setFieldKey($fieldKey);
227+
$definition->setLabel($label);
228+
$definition->setType('text');
229+
$definition->setAdminOnly(false);
230+
$definition->setUserEditable(false);
231+
$definition->setUserVisible(true);
232+
$definition->setInitialVisibility('users');
233+
$definition->setSortOrder($sortOrder);
234+
$definition->setActive($active);
235+
$definition->setCreatedAt(new \DateTime('2026-03-01T12:00:00+00:00'));
236+
$definition->setUpdatedAt(new \DateTime('2026-03-02T12:00:00+00:00'));
237+
return $definition;
238+
}
239+
240+
private function buildValue(int $fieldDefinitionId, string $userUid, array $value, string $currentVisibility, string $updatedByUid): FieldValue {
241+
$fieldValue = new FieldValue();
242+
$fieldValue->setId(1);
243+
$fieldValue->setFieldDefinitionId($fieldDefinitionId);
244+
$fieldValue->setUserUid($userUid);
245+
$fieldValue->setValueJson(json_encode($value, JSON_THROW_ON_ERROR));
246+
$fieldValue->setCurrentVisibility($currentVisibility);
247+
$fieldValue->setUpdatedByUid($updatedByUid);
248+
$fieldValue->setUpdatedAt(new \DateTime('2026-03-15T12:00:00+00:00'));
249+
return $fieldValue;
250+
}
251+
}

0 commit comments

Comments
 (0)