ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
MigrateMailAttachmentsToIRSSUnitTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
23use PHPUnit\Framework\MockObject\MockObject;
30
32{
37 private array $messages = [];
38 private string $tmp_dir;
39
40 protected function setUp(): void
41 {
43
44 if (!defined('SYSTEM_USER_ID')) {
45 define('SYSTEM_USER_ID', 6);
46 }
47
48 $this->tmp_dir = sys_get_temp_dir() . '/mail-migration-unit-' . uniqid('', true);
49 mkdir($this->tmp_dir . '/mail', 0775, true);
50
51 $this->migration = new MigrateMailAttachmentsToIRSS();
52 $this->db = $this->createDatabaseDouble();
53 $this->helper = $this->createHelperDouble(false);
54
55 $io = $this->createStub(AdminInteraction::class);
56 $io->method('inform')->willReturnCallback(function (string $message): void {
57 $this->messages[] = $message;
58 });
59
60 $this->injectCollaborators($this->helper, $io);
61 }
62
63 private function createDatabaseDouble(bool $mock = false): ilDBInterface
64 {
65 $db = $mock
66 ? $this->createMock(ilDBInterface::class)
67 : $this->createStub(ilDBInterface::class);
68 $db->method('quote')->willReturnCallback(static fn(mixed $value): string => "'" . $value . "'");
69 $db->method('supportsTransactions')->willReturn(true);
70
71 return $db;
72 }
73
74 private function useDatabaseMock(): ilDBInterface&MockObject
75 {
76 $db = $this->createDatabaseDouble(true);
77 $this->db = $db;
78 assert($db instanceof MockObject);
79
80 return $db;
81 }
82
84 {
85 $methods = [
86 'getDatabase',
87 'getClientDataDir',
88 'moveFilesOfPathToCollection',
89 'movePathToStorage',
90 'getCollectionBuilder',
91 'getManager',
92 'getStakeholder',
93 ];
94 if ($mock) {
95 $helper = $this->getMockBuilder(ilResourceStorageMigrationHelper::class)
96 ->disableOriginalConstructor()
97 ->onlyMethods($methods)
98 ->getMock();
99 } else {
100 $helper = $this->getStubBuilder(ilResourceStorageMigrationHelper::class)
101 ->disableOriginalConstructor()
102 ->onlyMethods($methods)
103 ->getStub();
104 }
105 $helper->method('getDatabase')->willReturnCallback(fn(): ilDBInterface => $this->db);
106 $helper->method('getClientDataDir')->willReturn($this->tmp_dir);
107
108 return $helper;
109 }
110
111 private function useHelperMock(): ilResourceStorageMigrationHelper&MockObject
112 {
113 $helper = $this->createHelperDouble(true);
114 $this->helper = $helper;
115 new ReflectionClass($this->migration)->getProperty('helper')->setValue($this->migration, $helper);
116 assert($helper instanceof MockObject);
117
118 return $helper;
119 }
120
121 private function injectCollaborators(
124 ): void {
125 $reflection = new ReflectionClass($this->migration);
126 $reflection->getProperty('helper')->setValue($this->migration, $helper);
127 $reflection->getProperty('io')->setValue($this->migration, $io);
128 }
129
130 protected function tearDown(): void
131 {
132 exec('chmod -R u+rwx ' . escapeshellarg($this->tmp_dir) . ' && rm -rf ' . escapeshellarg($this->tmp_dir));
134 }
135
136 // ---------------------------------------------------------------------------------------------------------
137 // Order: most recently sent mails first
138 // ---------------------------------------------------------------------------------------------------------
139
140 // ---------------------------------------------------------------------------------------------------------
141 // Directories
142 // ---------------------------------------------------------------------------------------------------------
143
144 // ---------------------------------------------------------------------------------------------------------
145 // Drafts
146 // ---------------------------------------------------------------------------------------------------------
147
149 {
150 $captured_sql = '';
151 $db = $this->useDatabaseMock();
152 $db->expects($this->once())->method('setLimit')->with(25);
153 $db->method('query')->willReturnCallback(
154 function (string $sql) use (&$captured_sql): ilDBStatement {
155 $captured_sql = $sql;
156
157 return $this->createStub(ilDBStatement::class);
158 }
159 );
160 $db->method('fetchAssoc')->willReturnOnConsecutiveCalls(['path' => 'newest'], ['path' => 'older'], null);
161
162 $paths = $this->invokePrivate('nextUnmigratedPaths', []);
163
164 $this->assertSame(['newest', 'older'], $paths);
165 $this->assertStringContainsString('ORDER BY MAX(m.send_time) DESC', $captured_sql);
166 $this->assertStringNotContainsString('LIMIT', $captured_sql);
167 }
168
173 {
174 $captured_sql = '';
175 $db = $this->useDatabaseMock();
176 $db->expects($this->once())->method('setLimit')->with(50);
177 $db->method('query')->willReturnCallback(
178 function (string $sql) use (&$captured_sql): ilDBStatement {
179 $captured_sql = $sql;
180
181 return $this->createStub(ilDBStatement::class);
182 }
183 );
184 $db->method('fetchAssoc')->willReturn(null);
185
186 $this->invokePrivate('migrateSerializedMailAttachments', []);
187
188 $this->assertStringContainsString('LEFT JOIN mail_attachment ma ON ma.mail_id = m.mail_id', $captured_sql);
189 $this->assertStringContainsString('WHERE ma.mail_id IS NULL', $captured_sql);
190 $this->assertStringContainsString('LEFT JOIN mail_obj_data fo ON fo.obj_id = m.folder_id', $captured_sql);
191 $this->assertStringContainsString('ORDER BY m.send_time DESC, m.mail_id DESC', $captured_sql);
192 }
193
195 {
196 $dir = $this->createDirectory('6_100', ['a.pdf' => '1234']);
197 $helper = $this->useHelperMock();
198 $this->db->method('query')->willReturn($this->createStub(ilDBStatement::class));
199 $this->db->method('queryF')->willReturn($this->createStub(ilDBStatement::class));
200 $this->db->method('in')->willReturn('mail_id IN (1,2)');
201 $this->db->method('fetchAssoc')->willReturnOnConsecutiveCalls(
202 // paths to migrate
203 ['path' => '6_100'],
204 null,
205 // owner, mails of the directory
206 ['sender_id' => 6],
207 ['mail_id' => 1],
208 ['mail_id' => 2],
209 null
210 );
211 $rcid = new ResourceCollectionIdentification('dir-rcid');
212 $resource_id = new ResourceIdentification('dir-rid');
213 $collection = $this->createMock(ResourceCollection::class);
214 $collection->expects($this->once())->method('add')->with($resource_id);
215 $collection->method('count')->willReturn(1);
216 $collection->method('getIdentification')->willReturn($rcid);
217 $collection->method('getResourceIdentifications')->willReturn([$resource_id]);
218 $collection_builder = $this->createMock(CollectionBuilder::class);
219 $collection_builder->expects($this->once())->method('new')->with(6)->willReturn($collection);
220 $collection_builder->expects($this->once())->method('store')->with($collection)->willReturn(true);
221 $collection_builder->expects($this->once())->method('get')->with($rcid)->willReturn($collection);
222 $helper->method('getCollectionBuilder')->willReturn($collection_builder);
223 $this->expectConfidential($resource_id);
224 $helper->expects($this->once())
225 ->method('movePathToStorage')
226 ->willReturnCallback(function (
227 string $path,
228 int $owner,
229 ?Closure $file_name,
230 Closure $revision_title,
231 bool $copy
232 ) use ($dir, $resource_id): ResourceIdentification {
233 $this->assertSame($dir . '/a.pdf', $path);
234 $this->assertSame(6, $owner);
235 $this->assertNull($file_name);
236 $this->assertSame(md5('a.pdf'), $revision_title('a.pdf'));
237 $this->assertTrue($copy);
238
239 return $resource_id;
240 });
241 $statements = [];
242 $this->db->method('manipulateF')->willReturnCallback(
243 function (string $sql, array $types, array $values) use (&$statements): int {
244 $statements[] = [$sql, $values];
245 return 1;
246 }
247 );
248
249 $this->invokePrivate('migrateSentAttachmentDirectories', []);
250
251 $this->assertSame(['UPDATE mail_attachment SET rcid = %s WHERE path = %s', ['dir-rcid', '6_100']], $statements[0]);
252 $this->assertSame(['UPDATE mail SET attachments = %s WHERE mail_id IN (1,2)', ['dir-rcid']], $statements[1]);
253 $this->assertDirectoryDoesNotExist($dir);
254 }
255
257 {
258 $dir = $this->createDirectory('6_100', ['a.pdf' => '1234', 'b.pdf' => '5678']);
259 $helper = $this->useHelperMock();
260 $db = $this->useDatabaseMock();
261 $db->method('query')->willReturn($this->createStub(ilDBStatement::class));
262 $db->method('queryF')->willReturn($this->createStub(ilDBStatement::class));
263 $db->method('fetchAssoc')->willReturnOnConsecutiveCalls(
264 ['path' => '6_100'],
265 null,
266 ['sender_id' => 6]
267 );
268 $db->expects($this->never())->method('manipulateF');
269 $collection = $this->createStub(ResourceCollection::class);
270 $collection_builder = $this->createMock(CollectionBuilder::class);
271 $collection_builder->method('new')->willReturn($collection);
272 $collection_builder->expects($this->never())->method('store');
273 $helper->method('getCollectionBuilder')->willReturn($collection_builder);
274 $copied = 0;
275 $helper->expects($this->exactly(2))
276 ->method('movePathToStorage')
277 ->willReturnCallback(function () use (&$copied): ?ResourceIdentification {
278 $copied++;
279
280 return $copied === 1 ? new ResourceIdentification('dir-rid') : null;
281 });
282 $stakeholder = new ilMailAttachmentStakeholder(6);
283 $helper->method('getStakeholder')->willReturn($stakeholder);
284 $manager = $this->createMock(Manager::class);
285 $manager->expects($this->once())->method('remove')->with(
286 $this->callback(static fn(ResourceIdentification $rid): bool => $rid->serialize() === 'dir-rid'),
287 $stakeholder
288 );
289 $helper->method('getManager')->willReturn($manager);
290
291 $this->invokePrivate('migrateSentAttachmentDirectories', []);
292
293 $this->assertFileExists($dir . '/a.pdf');
294 $this->assertFileExists($dir . '/b.pdf');
295 $this->assertReported('WARNING', 'not every file could be stored');
296 }
297
299 {
300 $db = $this->useDatabaseMock();
301 $helper = $this->useHelperMock();
302 $db->method('query')->willReturn($this->createStub(ilDBStatement::class));
303 $db->method('queryF')->willReturn($this->createStub(ilDBStatement::class));
304 $db->method('fetchAssoc')->willReturnOnConsecutiveCalls(
305 ['path' => '6_104'],
306 null,
307 ['mail_id' => 1],
308 null
309 );
310 $helper->expects($this->never())->method('movePathToStorage');
311 $db->expects($this->once())->method('manipulateF')->with(
312 'UPDATE mail_attachment SET rcid = %s WHERE path = %s',
314 ['-', '6_104']
315 );
316
317 $this->invokePrivate('migrateSentAttachmentDirectories', []);
318
319 $this->assertReported('WARNING', 'does not exist');
320 }
321
323 {
324 file_put_contents($this->tmp_dir . '/mail/6_Übung 1.pdf', '%PDF');
325 $helper = $this->useHelperMock();
326 $collection = $this->createMock(ResourceCollection::class);
327 $collection->expects($this->once())->method('add');
328 $collection->method('count')->willReturn(1);
329 $collection->method('getIdentification')->willReturn(new ResourceCollectionIdentification('draft-rcid'));
330 $collection_builder = $this->createStub(CollectionBuilder::class);
331 $collection_builder->method('new')->willReturn($collection);
332 $collection_builder->method('store')->willReturn(true);
333 $helper->method('getCollectionBuilder')->willReturn($collection_builder);
334 $helper->expects($this->once())
335 ->method('movePathToStorage')
336 ->willReturnCallback(function (
337 string $path,
338 int $owner,
339 Closure $file_name,
340 Closure $revision_title,
341 bool $copy
343 $this->assertSame('Übung 1.pdf', $file_name());
344 $this->assertSame(md5('Übung 1.pdf'), $revision_title());
345 $this->assertTrue($copy, 'Pool files are copied, they stay in the pool');
346
347 return new ResourceIdentification('rid');
348 });
349 $this->expectConfidential(new ResourceIdentification('rid'));
350
351 $rcid = $this->invokePrivate(
352 'migratePoolFilenamesToCollection',
353 [['Übung 1.pdf', 'gone.pdf'], 6, $this->tmp_dir . '/mail', 42]
354 );
355
356 $this->assertSame('draft-rcid', $rcid->serialize());
357 $this->assertFileExists($this->tmp_dir . '/mail/6_Übung 1.pdf');
358 $this->assertReported('WARNING', 'gone.pdf');
359 }
360
362 {
363 file_put_contents($this->tmp_dir . '/mail/6_exists.pdf', '%PDF');
364 $db = $this->useDatabaseMock();
365 $collection = $this->createStub(ResourceCollection::class);
366 $collection->method('count')->willReturn(1);
367 $collection->method('getIdentification')->willReturn(new ResourceCollectionIdentification('draft-rcid'));
368 $collection_builder = $this->createStub(CollectionBuilder::class);
369 $collection_builder->method('new')->willReturn($collection);
370 $collection_builder->method('store')->willReturn(true);
371 $this->helper->method('getCollectionBuilder')->willReturn($collection_builder);
372 $this->helper->method('movePathToStorage')->willReturn(new ResourceIdentification('rid'));
373 $this->expectConfidential(new ResourceIdentification('rid'));
374 $db->expects($this->once())->method('manipulateF')->with(
375 'INSERT INTO mail_attachment (mail_id, path, rcid) VALUES (%s, %s, %s)',
376 $this->anything(),
377 [42, '', 'draft-rcid']
378 );
379 $db->expects($this->once())->method('update')->with(
380 'mail',
381 ['attachments' => [ilDBConstants::T_CLOB, 'draft-rcid']],
382 ['mail_id' => [ilDBConstants::T_INTEGER, 42]]
383 );
384
385 $this->invokePrivate('migrateSerializedMail', [42, 6, serialize(['exists.pdf']), $this->tmp_dir . '/mail', true]);
386 }
387
389 {
390 file_put_contents($this->tmp_dir . '/mail/6_exists.pdf', '%PDF');
391 $helper = $this->useHelperMock();
392 $helper->expects($this->never())->method('movePathToStorage');
393 $db = $this->useDatabaseMock();
394 $db->expects($this->never())->method('manipulateF');
395 $db->expects($this->once())->method('update')->with(
396 'mail',
397 ['attachments' => [ilDBConstants::T_CLOB, '']],
398 ['mail_id' => [ilDBConstants::T_INTEGER, 44]]
399 );
400
401 $this->invokePrivate('migrateSerializedMail', [44, 6, serialize(['exists.pdf']), $this->tmp_dir . '/mail', false]);
402
403 $this->assertFileExists($this->tmp_dir . '/mail/6_exists.pdf');
404 $this->assertReported('WARNING', 'outside drafts and outbox');
405 }
406
408 {
409 $db = $this->useDatabaseMock();
410 $db->expects($this->once())->method('update')->with(
411 'mail',
412 ['attachments' => [ilDBConstants::T_CLOB, '']],
413 ['mail_id' => [ilDBConstants::T_INTEGER, 43]]
414 );
415
416 $this->invokePrivate('migrateSerializedMail', [43, 6, 'a:1:{broken', $this->tmp_dir . '/mail', true]);
417
418 $this->assertReported('WARNING', 'could not be read');
419 }
420
421 // ---------------------------------------------------------------------------------------------------------
422 // Abort, reporting
423 // ---------------------------------------------------------------------------------------------------------
424
426 {
427 $this->db->method('query')->willThrowException(new RuntimeException('MySQL server has gone away'));
428
429 try {
430 $this->migration->step($this->createStub(Environment::class));
431 $this->fail('The exception must be rethrown');
432 } catch (RuntimeException $e) {
433 $this->assertSame('MySQL server has gone away', $e->getMessage());
434 }
435
436 $this->assertReported('ERROR', 'MySQL server has gone away');
437 }
438
440 {
441 $this->assertTrue(new ilMailAttachmentStakeholder(6)->areNewResourcesConfidential());
442 }
443
445 {
446 $this->db->method('queryF')->willReturn($this->createStub(ilDBStatement::class));
447 $this->db->method('fetchAssoc')->willReturnOnConsecutiveCalls(['sender_id' => 42], ['sender_id' => 0]);
448
449 $this->assertSame(42, $this->invokePrivate('resolveOwnerIdForPath', ['6_1']));
450 $this->assertSame(SYSTEM_USER_ID, $this->invokePrivate('resolveOwnerIdForPath', ['6_2']));
451 }
452
453 // ---------------------------------------------------------------------------------------------------------
454
458 private function createDirectory(string $relative_path, array $files): string
459 {
460 $dir = $this->tmp_dir . '/mail/' . $relative_path;
461 mkdir($dir, 0775, true);
462 foreach ($files as $name => $content) {
463 file_put_contents($dir . '/' . $name, $content);
464 }
465
466 return $dir;
467 }
468
469 private function expectConfidential(ResourceIdentification $identification): void
470 {
471 $manager = $this->createMock(Manager::class);
472 $manager->expects($this->once())->method('setConfidential')->with($identification, true);
473 $this->helper->method('getManager')->willReturn($manager);
474 }
475
476 private function assertReported(string $level, string $needle): void
477 {
478 foreach ($this->messages as $message) {
479 if (str_contains($message, $level) && str_contains($message, $needle)) {
480 $this->assertMatchesRegularExpression('/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/', $message);
481
482 return;
483 }
484 }
485
486 $this->fail(sprintf('No %s containing "%s" reported, got: %s', $level, $needle, implode("\n", $this->messages)));
487 }
488
492 private function invokePrivate(string $method, array $arguments): mixed
493 {
494 return new ReflectionClass($this->migration)->getMethod($method)->invoke($this->migration, ...$arguments);
495 }
496}
Migrates mail attachments to the IRSS, the attachments of the most recently sent mails first,...
injectCollaborators(ilResourceStorageMigrationHelper $helper, AdminInteraction $io)
expectConfidential(ResourceIdentification $identification)
createDirectory(string $relative_path, array $files)
testDraftsOfTheMostRecentlySentMailsAreMigratedFirstAndNeverMailsWithDirectory()
Regression: mails with a not yet migrated directory must never be treated as pool references.
const SYSTEM_USER_ID
This file contains constants for PHPStan analyis, see: https://phpstan.org/config-reference#constants...
Definition: constants.php:26
This defines ways in which objectives may interact with admins during the setup.
An environment holds resources to be used in the setup process.
Definition: Environment.php:28
Interface ilDBInterface.
Interface ilDBStatement.