ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
RemoveLocalLanguageChangesTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
26use ILIAS\UI\Component\Input\Factory as InputFactory;
27use PHPUnit\Framework\Attributes\DataProvider;
28
33{
35
37 {
38 [, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld(['de' => []]);
39
40 return $this->createActivity($lng_objects, $obj_language_factory);
41 }
42
43 protected function validPerformParameters(): array
44 {
45 return ['language_keys' => 'de'];
46 }
47
53 private function buildFakeLanguageWorld(array $language_objects): array
54 {
55 $lng_objects_list = [];
56 $fakes_by_obj_id = [];
57 $obj_id = 1;
58
59 foreach ($language_objects as $language_key => $flags) {
60 $lng_objects_list[] = ['obj_id' => $obj_id, 'title' => $language_key];
61 $fakes_by_obj_id[$obj_id] = new FakeRemoveLocalLanguageChangesObject(
62 is_installed: $flags['installed'] ?? true,
63 remove_local_changes_return_value: $flags['remove_local_changes_return'] ?? true,
64 modules_with_unwritten_overlay: $flags['unwritten_overlay'] ?? [],
65 );
66 $obj_id++;
67 }
68
69 $lng_objects = static fn(): array => $lng_objects_list;
70 $obj_language_factory = static fn(int $id): FakeRemoveLocalLanguageChangesObject => $fakes_by_obj_id[$id];
71
72 return [$fakes_by_obj_id, $lng_objects, $obj_language_factory];
73 }
74
76 {
77 $rbac = $this->createMock(\ilRbacSystem::class);
78 $rbac->expects($this->once())
79 ->method('checkAccessOfUser')
80 ->with(6, 'write', 42)
81 ->willReturn(true);
82
83 $activity = $this->createActivity(
84 static fn(): array => [],
85 static fn(int $id) => null,
86 $rbac,
87 42
88 );
89
90 $this->assertTrue($activity->isAllowedToPerform(6, ['language_keys' => 'de']));
91 }
92
94 {
95 $rbac = $this->createStub(\ilRbacSystem::class);
96 $rbac->method('checkAccessOfUser')->willReturn(false);
97
98 $activity = $this->createActivity(
99 static fn(): array => [],
100 static fn(int $id) => null,
101 $rbac
102 );
103
104 $this->assertFalse($activity->isAllowedToPerform(6, ['language_keys' => 'de']));
105 }
106
109 {
110 $lng_objects = static function (): array {
111 throw new \LogicException('lng_objects must never be called by isAllowedToPerform()');
112 };
113 $obj_language_factory = static function (int $id) {
114 throw new \LogicException('obj_language_factory must never be called by isAllowedToPerform()');
115 };
116
117 $rbac = $this->createStub(\ilRbacSystem::class);
118 $rbac->method('checkAccessOfUser')->willReturn(true);
119
120 $activity = $this->createActivity($lng_objects, $obj_language_factory, $rbac);
121
122 $this->assertTrue($activity->isAllowedToPerform(6, ['language_keys' => 'de']));
123 }
124
126 {
127 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
128 'de' => [],
129 ]);
130
131 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
132 'language_keys' => 'de',
133 ]);
134
135 $this->assertSame(['de'], $result['removed_local_changes_language_keys']);
136 $this->assertSame([], $result['invalid_language_file_keys']);
137 $this->assertSame([], $result['not_installed_language_keys']);
138 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount());
139 }
140
146 {
147 [, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
148 'de' => ['unwritten_overlay' => ['pilot']],
149 'en' => [],
150 'fr' => ['remove_local_changes_return' => false, 'unwritten_overlay' => ['pilot']],
151 ]);
152
153 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
154 'language_keys' => ['de', 'en', 'fr'],
155 ]);
156
157 $this->assertSame(['de', 'en'], $result['removed_local_changes_language_keys']);
158 $this->assertSame(['fr'], $result['invalid_language_file_keys']);
159 $this->assertSame(['de'], $result['overlay_write_failed_language_keys']);
160 }
161
164 {
165 [, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
166 'de' => [],
167 'en' => [],
168 'fr' => ['remove_local_changes_return' => false],
169 'it' => ['installed' => false],
170 ]);
171 $repository = $this->languageObjectProviderMock($lng_objects, $obj_language_factory);
172 $repository->expects($this->once())->method('refreshPlugins')->with(['de', 'en']);
173
174 $this->createActivity($lng_objects, $obj_language_factory, repository: $repository)
175 ->perform(['language_keys' => ['de', 'en', 'fr', 'it']]);
176 }
177
180 {
181 [, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
182 'de' => ['remove_local_changes_return' => false],
183 'en' => ['installed' => false],
184 ]);
185 $repository = $this->languageObjectProviderMock($lng_objects, $obj_language_factory);
186 $repository->expects($this->never())->method('refreshPlugins');
187
188 $this->createActivity($lng_objects, $obj_language_factory, repository: $repository)
189 ->perform(['language_keys' => ['de', 'en']]);
190 }
191
197 {
198 $lng_objects = static fn(): array => [['obj_id' => 1, 'title' => 'de']];
199 $repository = $this->createMock(RepositoryProvider::class);
200 $repository->method('listLanguageObjects')->willReturnCallback($lng_objects);
201 $repository->method('isInstalled')->willReturn(true);
202 $repository->method('removeLocalChanges')->willThrowException(new \RuntimeException('db gone'));
203 $repository->expects($this->once())->method('logWarning')->with($this->stringContains('setup.php update'));
204 $repository->expects($this->never())->method('refreshPlugins');
205
206 $rbac = $this->createStub(\ilRbacSystem::class);
207 $rbac->method('checkAccessOfUser')->willReturn(true);
208
209 $result = $this->createActivity($lng_objects, static fn(int $id) => null, $rbac, repository: $repository)
210 ->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, ['language_keys' => 'de']);
211
212 $this->assertTrue($result->isError());
213 $this->assertSame(500, $result->error()->getCode());
214 $this->assertSame('db gone', $result->error()->getPrevious()?->getMessage());
215 }
216
218 {
219 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
220 'de' => ['installed' => false],
221 ]);
222
223 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
224 'language_keys' => 'de',
225 ]);
226
227 $this->assertSame([], $result['removed_local_changes_language_keys']);
228 $this->assertSame([], $result['invalid_language_file_keys']);
229 $this->assertSame(['de'], $result['not_installed_language_keys']);
230 $this->assertSame(0, $fakes[1]->removeLocalChangesCallCount());
231 }
232
235 {
236 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
237 'de' => ['remove_local_changes_return' => false],
238 ]);
239
240 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
241 'language_keys' => 'de',
242 ]);
243
244 $this->assertSame([], $result['removed_local_changes_language_keys']);
245 $this->assertSame(['de'], $result['invalid_language_file_keys']);
246 $this->assertSame([], $result['not_installed_language_keys']);
247 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount());
248 }
249
252 {
253 $lng_objects = static fn(): array => [];
254 $factory_calls = [];
255 $obj_language_factory = static function (int $id) use (&$factory_calls) {
256 $factory_calls[] = $id;
257 throw new \LogicException('must never be called for an unknown language key');
258 };
259
260 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
261 'language_keys' => 'xx',
262 ]);
263
264 $this->assertSame([], $result['removed_local_changes_language_keys']);
265 $this->assertSame([], $result['invalid_language_file_keys']);
266 $this->assertSame(['xx'], $result['not_installed_language_keys']);
267 $this->assertSame([], $factory_calls);
268 }
269
272 {
273 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
274 'de' => [],
275 'fr' => ['remove_local_changes_return' => false],
276 'it' => ['installed' => false],
277 ]);
278
279 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
280 'language_keys' => ['de', 'fr', 'it', 'xx'],
281 ]);
282
283 $this->assertSame(['de'], $result['removed_local_changes_language_keys']);
284 $this->assertSame(['fr'], $result['invalid_language_file_keys']);
285 $this->assertSame(['it', 'xx'], $result['not_installed_language_keys']);
286
287 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount(), 'de must have its local changes removed');
288 $this->assertSame(1, $fakes[2]->removeLocalChangesCallCount(), 'fr is installed, so must still be attempted');
289 $this->assertSame(0, $fakes[3]->removeLocalChangesCallCount(), 'not-installed language must never be attempted');
290 }
291
293 {
294 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
295 'de' => [],
296 ]);
297
298 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
299 'language_keys' => [' de, de ', 'de'],
300 ]);
301
302 $this->assertSame(['de'], $result['removed_local_changes_language_keys']);
303 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount());
304 }
305
307 {
308 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
309 'de' => [],
310 'fr' => [],
311 'it' => [],
312 ]);
313
314 $result = $this->createActivity($lng_objects, $obj_language_factory)->perform([
315 'language_keys' => ' de, fr ,it',
316 ]);
317
318 $this->assertSame(['de', 'fr', 'it'], $result['removed_local_changes_language_keys']);
319 }
320
321 public static function invalidLanguageKeysProvider(): array
322 {
323 return [
324 'missing language_keys key' => [[]],
325 'empty (only whitespace/commas)' => [['language_keys' => ' , ']],
326 'nested array value' => [['language_keys' => ['de', ['fr']]]],
327 'non-array, non-string value' => [['language_keys' => 42]],
328 'three letters' => [['language_keys' => 'deu']],
329 'one letter' => [['language_keys' => 'd']],
330 'uppercase' => [['language_keys' => 'DE']],
331 'contains a digit' => [['language_keys' => 'de1']],
332 'contains a hyphen' => [['language_keys' => 'de-at']],
333 ];
334 }
335
336 #[DataProvider('invalidLanguageKeysProvider')]
338 {
339 $this->expectException(InvalidInputException::class);
340
341 $this->createActivity(static fn(): array => [], static fn(int $id) => null)->perform($parameters);
342 }
343
345 public function testNonArrayParametersAreRejected(): void
346 {
347 $this->expectException(InvalidInputException::class);
348
349 $this->createActivity(static fn(): array => [], static fn(int $id) => null)->perform('not-an-array');
350 }
351
354 {
355 $rbac = $this->createMock(\ilRbacSystem::class);
356 $rbac->expects($this->once())
357 ->method('checkAccessOfUser')
358 ->with(6, 'write', $this->anything())
359 ->willReturn(false);
360
361 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
362 'de' => [],
363 ]);
364 $result = $this->createActivity(
365 $lng_objects,
366 $obj_language_factory,
367 $rbac
368 )->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, ['language_keys' => 'de']);
369
370 $this->assertTrue($result->isError());
371 $this->assertSame(0, $fakes[1]->removeLocalChangesCallCount());
372 }
373
375 {
376 $rbac = $this->createStub(\ilRbacSystem::class);
377 $rbac->method('checkAccessOfUser')->willReturn(true);
378
379 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
380 'de' => [],
381 ]);
382
383 $result = $this->createActivity(
384 $lng_objects,
385 $obj_language_factory,
386 $rbac
387 )->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, ['language_keys' => 'de']);
388
389 $this->assertFalse($result->isError());
390 $this->assertSame(['de'], $result->value()['removed_local_changes_language_keys']);
391 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount());
392 }
393
396 {
397 $rbac = $this->createStub(\ilRbacSystem::class);
398 $rbac->method('checkAccessOfUser')->willReturn(true);
399
400 [$fakes, $lng_objects, $obj_language_factory] = $this->buildFakeLanguageWorld([
401 'de' => [],
402 'fr' => [],
403 ]);
404
405 $result = $this->createActivity($lng_objects, $obj_language_factory, $rbac)
406 ->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, ['language_keys' => ['de', 'fr']]);
407
408 $this->assertFalse($result->isError());
409 $this->assertSame(['de', 'fr'], $result->value()['removed_local_changes_language_keys']);
410 $this->assertSame(1, $fakes[1]->removeLocalChangesCallCount());
411 $this->assertSame(1, $fakes[2]->removeLocalChangesCallCount());
412 }
413
416 {
417 $rbac = $this->createStub(\ilRbacSystem::class);
418 $rbac->method('checkAccessOfUser')->willReturn(true);
419
420 $lng_objects = static fn(): array => [];
421 $obj_language_factory = static fn(int $id) => null;
422
423 // an empty language_keys value throws inside perform(), after the permission check passed
424 $result = $this->createActivity(
425 $lng_objects,
426 $obj_language_factory,
427 $rbac
428 )->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, ['language_keys' => ' , ']);
429
430 $this->assertTrue($result->isError());
431 $this->assertInstanceOf(InvalidInputException::class, $result->error());
432 $this->assertSame(400, $result->error()->getCode());
433 }
434
437 {
438 $rbac = $this->createMock(\ilRbacSystem::class);
439 $rbac->expects($this->never())->method('checkAccessOfUser');
440
441 $result = $this->createActivity(
442 static fn(): array => [],
443 static fn(int $id) => null,
444 $rbac
445 )->maybePerformAs($this->createRealFieldsUiFactory()->input(), 6, []);
446
447 $this->assertTrue($result->isError());
448 $this->assertInstanceOf(InvalidInputException::class, $result->error());
449 $this->assertNotSame('', $result->error()->getMessage());
450 }
451}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
Every legacy and repository access of the runtime language activities.
Cross-checks the declared output field names against the keys perform() returns, using a real Descrip...
Stand-in for \ilObjLanguage, which needs the global $DIC; injected via RemoveLocalLanguageChanges's $...
\ilObjLanguage cannot be constructed in a unit test (its constructor needs $DIC), so a fake object st...
testRefreshPluginsIsNeverCalledWhenNoLanguagesLocalChangesWereActuallyRemoved()
Without a single removed language refreshPlugins() is not called at all, not even with an empty list.
testPermissionDeniedBeforePerformNeverCallsObjectFactoryOrRemoveLocalChanges()
Denied permission short-circuits before perform(): no object is touched.
testRemoveLocalChangesThrowingIsLoggedWithSetupUpdateAdviceAndSurfacesAsA500ResultError()
A Throwable from removeLocalChanges() itself (not a null return) is logged as a warning naming "setup...
testALanguageWithAnUnwrittenOverlayIsListedAdditionallyToBeingRemoved()
A language whose migrated-module overlay could not be written still counts as removed and is also lis...
testMissingLanguageKeysParameterViaMaybePerformAsIsAResultErrorAndNeverChecksPermission()
A missing 'language_keys' fails the required Text field in grind(), before rbac is touched.
testThrowableFromPerformIsTurnedIntoAResultError()
A Throwable from perform() becomes a Result\Error.
testIsAllowedToPerformNeverTouchesLngObjectsOrObjectFactory()
isAllowedToPerform() is a pure permission check: no object access, no removeLocalChanges().
testNonArrayParametersAreRejected()
Pins InvalidInputException, not just its parent \InvalidArgumentException, which would pass silently.
testMaybePerformAsAcceptsAnArrayOfLanguageKeysAndRemovesLocalChangesForEachOne()
ilObjLanguageFolderGUI sends 'language_keys' as array of strings; the real grind() pipeline must join...
testUnknownLanguageKeyIsReportedAsNotInstalledWithoutTouchingTheObjectFactory()
A key without a "lng" object is not installed, and the object factory is never invoked for it.
testInstalledLanguageWhereRemoveLocalChangesFailsIsReportedAsInvalidLanguageFile()
removeLocalChanges() returning false (invalid language file) is reported as invalid_language_file_key...
testRefreshPluginsIsCalledOnceWithExactlyTheRemovedLanguageKeys()
Plugin language files are re-applied once, only for languages whose local changes were actually remov...
testMixedBulkRequestPartitionsEachKeyIntoTheCorrectBucket()
Every state in one request lands in its own bucket; removeLocalChanges() only runs for the two instal...
This is how a factory for inputs looks like.
Definition: Factory.php:27
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
createActivity(\Closure $lng_objects, \Closure $obj_language_factory, ?\ilRbacSystem $rbac=null, int $language_folder_ref_id=0, ?RepositoryProvider $repository=null,)