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