Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace core;
18
 
19
use file_system_filedir;
20
 
21
defined('MOODLE_INTERNAL') || die();
22
 
23
global $CFG;
24
require_once($CFG->libdir . '/filestorage/file_system.php');
25
require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
26
 
27
/**
28
 * Unit tests for file_system_filedir.
29
 *
30
 * @package   core
31
 * @category  test
32
 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
33
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34
 * @coversDefaultClass \file_system_filedir
35
 */
36
class file_system_filedir_test extends \advanced_testcase {
37
 
38
    /**
39
     * Shared test setUp.
40
     */
41
    public function setUp(): void {
42
        // Reset the file storage so that subsequent fetches to get_file_storage are called after
43
        // configuration is prepared.
44
        get_file_storage(true);
45
    }
46
 
47
    /**
48
     * Shared teset tearDown.
49
     */
50
    public function tearDown(): void {
51
        // Reset the file storage so that subsequent tests will use the standard file storage.
52
        get_file_storage(true);
53
    }
54
 
55
    /**
56
     * Helper function to help setup and configure the virtual file system stream.
57
     *
58
     * @param   array $filedir Directory structure and content of the filedir
59
     * @param   array $trashdir Directory structure and content of the sourcedir
60
     * @param   array $sourcedir Directory structure and content of a directory used for source files for tests
61
     * @return  \org\bovigo\vfs\vfsStream
62
     */
63
    protected function setup_vfile_root($filedir = [], $trashdir = [], $sourcedir = null) {
64
        global $CFG;
65
        $this->resetAfterTest();
66
 
67
        $content = [];
68
        if ($filedir !== null) {
69
            $content['filedir'] = $filedir;
70
        }
71
 
72
        if ($trashdir !== null) {
73
            $content['trashdir'] = $trashdir;
74
        }
75
 
76
        if ($sourcedir !== null) {
77
            $content['sourcedir'] = $sourcedir;
78
        }
79
 
80
        $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
81
 
82
        $CFG->filedir = \org\bovigo\vfs\vfsStream::url('root/filedir');
83
        $CFG->trashdir = \org\bovigo\vfs\vfsStream::url('root/trashdir');
84
 
85
        return $vfileroot;
86
    }
87
 
88
    /**
89
     * Helper to create a stored file objectw with the given supplied content.
90
     *
91
     * @param   string  $filecontent The content of the mocked file
92
     * @param   string  $filename The file name to use in the stored_file
93
     * @param   array   $mockedmethods A list of methods you intend to override
94
     *                  If no methods are specified, only abstract functions are mocked.
95
     * @return \stored_file
96
     */
97
    protected function get_stored_file($filecontent, $filename = null, $mockedmethods = []) {
98
        $contenthash = \file_storage::hash_from_string($filecontent);
99
        if (empty($filename)) {
100
            $filename = $contenthash;
101
        }
102
 
103
        $file = $this->getMockBuilder(\stored_file::class)
104
            ->onlyMethods($mockedmethods)
105
            ->setConstructorArgs([
106
                get_file_storage(),
107
                (object) [
108
                    'contenthash' => $contenthash,
109
                    'filesize' => strlen($filecontent),
110
                    'filename' => $filename,
111
                ]
112
            ])
113
            ->getMock();
114
 
115
        return $file;
116
    }
117
 
118
    /**
119
     * Get a testable mock of the file_system_filedir class.
120
     *
121
     * @param   array   $mockedmethods A list of methods you intend to override
122
     *                  If no methods are specified, only abstract functions are mocked.
123
     * @return \file_system
124
     */
125
    protected function get_testable_mock($mockedmethods = []) {
126
        $fs = $this->getMockBuilder(file_system_filedir::class)
127
            ->onlyMethods($mockedmethods)
128
            ->getMock();
129
 
130
        return $fs;
131
    }
132
 
133
    /**
134
     * Ensure that an appropriate error is shown when the filedir directory
135
     * is not writable.
136
     *
137
     * @covers ::__construct
138
     */
11 efrain 139
    public function test_readonly_filesystem_filedir(): void {
1 efrain 140
        $this->resetAfterTest();
141
 
142
        // Setup the filedir but remove permissions.
143
        $vfileroot = $this->setup_vfile_root(null);
144
 
145
        // Make the target path readonly.
146
        $vfileroot->chmod(0444)
147
            ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
148
 
149
        // This should generate an exception.
150
        $this->expectException('file_exception');
151
        $this->expectExceptionMessageMatches(
152
            '/Cannot create local file pool directories. Please verify permissions in dataroot./');
153
 
154
        new file_system_filedir();
155
    }
156
 
157
    /**
158
     * Ensure that an appropriate error is shown when the trash directory
159
     * is not writable.
160
     *
161
     * @covers ::__construct
162
     */
11 efrain 163
    public function test_readonly_filesystem_trashdir(): void {
1 efrain 164
        $this->resetAfterTest();
165
 
166
        // Setup the trashdir but remove permissions.
167
        $vfileroot = $this->setup_vfile_root([], null);
168
 
169
        // Make the target path readonly.
170
        $vfileroot->chmod(0444)
171
            ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
172
 
173
        // This should generate an exception.
174
        $this->expectException('file_exception');
175
        $this->expectExceptionMessageMatches(
176
            '/Cannot create local file pool directories. Please verify permissions in dataroot./');
177
 
178
        new file_system_filedir();
179
    }
180
 
181
    /**
182
     * Test that the standard Moodle warning message is put into the filedir.
183
     *
184
     * @covers ::__construct
185
     */
11 efrain 186
    public function test_warnings_put_in_place(): void {
1 efrain 187
        $this->resetAfterTest();
188
 
189
        $vfileroot = $this->setup_vfile_root(null);
190
 
191
        new file_system_filedir();
192
        $this->assertTrue($vfileroot->hasChild('filedir/warning.txt'));
193
        $this->assertEquals(
194
            'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
195
                'Do not manually move, change or rename any of the files and subdirectories here.',
196
            $vfileroot->getChild('filedir/warning.txt')->getContent()
197
        );
198
    }
199
 
200
    /**
201
     * Ensure that the default implementation of get_remote_path_from_hash
202
     * simply calls get_local_path_from_hash.
203
     *
204
     * @covers ::get_remote_path_from_hash
205
     */
11 efrain 206
    public function test_get_remote_path_from_hash(): void {
1 efrain 207
        $filecontent = 'example content';
208
        $contenthash = \file_storage::hash_from_string($filecontent);
209
        $expectedresult = (object) [];
210
 
211
        $fs = $this->get_testable_mock([
212
            'get_local_path_from_hash',
213
        ]);
214
 
215
        $fs->expects($this->once())
216
            ->method('get_local_path_from_hash')
217
            ->with($this->equalTo($contenthash), $this->equalTo(false))
218
            ->willReturn($expectedresult);
219
 
220
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash');
221
        $result = $method->invokeArgs($fs, [$contenthash]);
222
 
223
        $this->assertEquals($expectedresult, $result);
224
    }
225
 
226
    /**
227
     * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
228
     * a failed recovery.
229
     *
230
     * @covers ::get_local_path_from_storedfile
231
     */
11 efrain 232
    public function test_get_local_path_from_storedfile_with_recovery(): void {
1 efrain 233
        $filecontent = 'example content';
234
        $file = $this->get_stored_file($filecontent);
235
        $fs = $this->get_testable_mock([
236
            'get_local_path_from_hash',
237
            'recover_file',
238
        ]);
239
        $filepath = '/path/to/nonexistent/file';
240
 
241
        $fs->method('get_local_path_from_hash')
242
            ->willReturn($filepath);
243
 
244
        $fs->expects($this->once())
245
            ->method('recover_file')
246
            ->with($this->equalTo($file));
247
 
248
        $file = $this->get_stored_file('example content');
249
        $result = $fs->get_local_path_from_storedfile($file, true);
250
 
251
        $this->assertEquals($filepath, $result);
252
    }
253
 
254
    /**
255
     * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
256
     * a failed recovery.
257
     *
258
     * @covers ::get_local_path_from_storedfile
259
     */
11 efrain 260
    public function test_get_local_path_from_storedfile_without_recovery(): void {
1 efrain 261
        $filecontent = 'example content';
262
        $file = $this->get_stored_file($filecontent);
263
        $fs = $this->get_testable_mock([
264
            'get_local_path_from_hash',
265
            'recover_file',
266
        ]);
267
        $filepath = '/path/to/nonexistent/file';
268
 
269
        $fs->method('get_local_path_from_hash')
270
            ->willReturn($filepath);
271
 
272
        $fs->expects($this->never())
273
            ->method('recover_file');
274
 
275
        $file = $this->get_stored_file('example content');
276
        $result = $fs->get_local_path_from_storedfile($file, false);
277
 
278
        $this->assertEquals($filepath, $result);
279
    }
280
 
281
    /**
282
     * Test that the correct path is generated for the supplied content
283
     * hashes.
284
     *
285
     * @dataProvider contenthash_dataprovider
286
     * @param   string  $hash contenthash to test
287
     * @param   string  $hashdir Expected format of content directory
288
     *
289
     * @covers ::get_fulldir_from_hash
290
     */
11 efrain 291
    public function test_get_fulldir_from_hash($hash, $hashdir): void {
1 efrain 292
        global $CFG;
293
 
294
        $fs = new file_system_filedir();
295
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash');
296
        $result = $method->invokeArgs($fs, array($hash));
297
 
298
        $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
299
        $this->assertEquals($expectedpath, $result);
300
    }
301
 
302
    /**
303
     * Test that the correct path is generated for the supplied content
304
     * hashes when used with a stored_file.
305
     *
306
     * @dataProvider contenthash_dataprovider
307
     * @param   string  $hash contenthash to test
308
     * @param   string  $hashdir Expected format of content directory
309
     *
310
     * @covers ::get_fulldir_from_storedfile
311
     */
11 efrain 312
    public function test_get_fulldir_from_storedfile($hash, $hashdir): void {
1 efrain 313
        global $CFG;
314
 
315
        $file = $this->getMockBuilder('stored_file')
316
            ->disableOriginalConstructor()
317
            ->onlyMethods([
318
                'sync_external_file',
319
                'get_contenthash',
320
            ])
321
            ->getMock();
322
 
323
        $file->method('get_contenthash')->willReturn($hash);
324
 
325
        $fs = new file_system_filedir();
326
        $method = new \ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile');
327
        $result = $method->invokeArgs($fs, array($file));
328
 
329
        $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
330
        $this->assertEquals($expectedpath, $result);
331
    }
332
 
333
    /**
334
     * Test that the correct content directory is generated for the supplied
335
     * content hashes.
336
     *
337
     * @dataProvider contenthash_dataprovider
338
     * @param   string  $hash contenthash to test
339
     * @param   string  $hashdir Expected format of content directory
340
     *
341
     * @covers ::get_contentdir_from_hash
342
     */
11 efrain 343
    public function test_get_contentdir_from_hash($hash, $hashdir): void {
1 efrain 344
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
345
 
346
        $fs = new file_system_filedir();
347
        $result = $method->invokeArgs($fs, array($hash));
348
 
349
        $this->assertEquals($hashdir, $result);
350
    }
351
 
352
    /**
353
     * Test that the correct content path is generated for the supplied
354
     * content hashes.
355
     *
356
     * @dataProvider contenthash_dataprovider
357
     * @param   string  $hash contenthash to test
358
     * @param   string  $hashdir Expected format of content directory
359
     *
360
     * @covers ::get_contentpath_from_hash
361
     */
11 efrain 362
    public function test_get_contentpath_from_hash($hash, $hashdir): void {
1 efrain 363
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
364
 
365
        $fs = new file_system_filedir();
366
        $result = $method->invokeArgs($fs, array($hash));
367
 
368
        $expectedpath = sprintf('%s/%s', $hashdir, $hash);
369
        $this->assertEquals($expectedpath, $result);
370
    }
371
 
372
    /**
373
     * Test that the correct trash path is generated for the supplied
374
     * content hashes.
375
     *
376
     * @dataProvider contenthash_dataprovider
377
     * @param   string  $hash contenthash to test
378
     * @param   string  $hashdir Expected format of content directory
379
     *
380
     * @covers ::get_trash_fullpath_from_hash
381
     */
11 efrain 382
    public function test_get_trash_fullpath_from_hash($hash, $hashdir): void {
1 efrain 383
        global $CFG;
384
 
385
        $fs = new file_system_filedir();
386
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash');
387
        $result = $method->invokeArgs($fs, array($hash));
388
 
389
        $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash);
390
        $this->assertEquals($expectedpath, $result);
391
    }
392
 
393
    /**
394
     * Test that the correct trash directory is generated for the supplied
395
     * content hashes.
396
     *
397
     * @dataProvider contenthash_dataprovider
398
     * @param   string  $hash contenthash to test
399
     * @param   string  $hashdir Expected format of content directory
400
     *
401
     * @covers ::get_trash_fulldir_from_hash
402
     */
11 efrain 403
    public function test_get_trash_fulldir_from_hash($hash, $hashdir): void {
1 efrain 404
        global $CFG;
405
 
406
        $fs = new file_system_filedir();
407
        $method = new \ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash');
408
        $result = $method->invokeArgs($fs, array($hash));
409
 
410
        $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir);
411
        $this->assertEquals($expectedpath, $result);
412
    }
413
 
414
    /**
415
     * Ensure that copying a file to a target from a stored_file works as anticipated.
416
     *
417
     * @covers ::copy_content_from_storedfile
418
     */
11 efrain 419
    public function test_copy_content_from_storedfile(): void {
1 efrain 420
        $this->resetAfterTest();
421
        global $CFG;
422
 
423
        $filecontent = 'example content';
424
        $contenthash = \file_storage::hash_from_string($filecontent);
425
        $filedircontent = [
426
            $contenthash => $filecontent,
427
        ];
428
        $vfileroot = $this->setup_vfile_root($filedircontent, [], []);
429
 
430
        $fs = $this->getMockBuilder(file_system_filedir::class)
431
            ->disableOriginalConstructor()
432
            ->onlyMethods([
433
                'get_local_path_from_storedfile',
434
            ])
435
            ->getMock();
436
 
437
        $file = $this->getMockBuilder(\stored_file::class)
438
            ->disableOriginalConstructor()
439
            ->getMock();
440
 
441
        $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash);
442
        $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile);
443
 
444
        $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile');
445
        $CFG->preventfilelocking = true;
446
        $result = $fs->copy_content_from_storedfile($file, $targetfile);
447
 
448
        $this->assertTrue($result);
449
        $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent());
450
    }
451
 
452
    /**
453
     * Ensure that content recovery works.
454
     *
455
     * @covers ::recover_file
456
     */
11 efrain 457
    public function test_recover_file(): void {
1 efrain 458
        $this->resetAfterTest();
459
 
460
        // Setup the filedir.
461
        // This contains a virtual file which has a cache mismatch.
462
        $filecontent = 'example content';
463
        $contenthash = \file_storage::hash_from_string($filecontent);
464
 
465
        $trashdircontent = [
466
            '0f' => [
467
                'f3' => [
468
                    $contenthash => $filecontent,
469
                ],
470
            ],
471
        ];
472
 
473
        $vfileroot = $this->setup_vfile_root([], $trashdircontent);
474
 
475
        $file = new \stored_file(get_file_storage(), (object) [
476
            'contenthash' => $contenthash,
477
            'filesize' => strlen($filecontent),
478
        ]);
479
 
480
        $fs = new file_system_filedir();
481
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
482
        $result = $method->invokeArgs($fs, array($file));
483
 
484
        // Test the output.
485
        $this->assertTrue($result);
486
 
487
        $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
488
 
489
    }
490
 
491
    /**
492
     * Ensure that content recovery works.
493
     *
494
     * @covers ::recover_file
495
     */
11 efrain 496
    public function test_recover_file_already_present(): void {
1 efrain 497
        $this->resetAfterTest();
498
 
499
        // Setup the filedir.
500
        // This contains a virtual file which has a cache mismatch.
501
        $filecontent = 'example content';
502
        $contenthash = \file_storage::hash_from_string($filecontent);
503
 
504
        $filedircontent = $trashdircontent = [
505
            '0f' => [
506
                'f3' => [
507
                    $contenthash => $filecontent,
508
                ],
509
            ],
510
        ];
511
 
512
        $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
513
 
514
        $file = new \stored_file(get_file_storage(), (object) [
515
            'contenthash' => $contenthash,
516
            'filesize' => strlen($filecontent),
517
        ]);
518
 
519
        $fs = new file_system_filedir();
520
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
521
        $result = $method->invokeArgs($fs, array($file));
522
 
523
        // Test the output.
524
        $this->assertTrue($result);
525
 
526
        $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
527
    }
528
 
529
    /**
530
     * Ensure that content recovery works.
531
     *
532
     * @covers ::recover_file
533
     */
11 efrain 534
    public function test_recover_file_size_mismatch(): void {
1 efrain 535
        $this->resetAfterTest();
536
 
537
        // Setup the filedir.
538
        // This contains a virtual file which has a cache mismatch.
539
        $filecontent = 'example content';
540
        $contenthash = \file_storage::hash_from_string($filecontent);
541
 
542
        $trashdircontent = [
543
            '0f' => [
544
                'f3' => [
545
                    $contenthash => $filecontent,
546
                ],
547
            ],
548
        ];
549
        $vfileroot = $this->setup_vfile_root([], $trashdircontent);
550
 
551
        $file = new \stored_file(get_file_storage(), (object) [
552
            'contenthash' => $contenthash,
553
            'filesize' => strlen($filecontent) + 1,
554
        ]);
555
 
556
        $fs = new file_system_filedir();
557
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
558
        $result = $method->invokeArgs($fs, array($file));
559
 
560
        // Test the output.
561
        $this->assertFalse($result);
562
        $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
563
    }
564
 
565
    /**
566
     * Ensure that content recovery works.
567
     *
568
     * @covers ::recover_file
569
     */
11 efrain 570
    public function test_recover_file_has_mismatch(): void {
1 efrain 571
        $this->resetAfterTest();
572
 
573
        // Setup the filedir.
574
        // This contains a virtual file which has a cache mismatch.
575
        $filecontent = 'example content';
576
        $contenthash = \file_storage::hash_from_string($filecontent);
577
 
578
        $trashdircontent = [
579
            '0f' => [
580
                'f3' => [
581
                    $contenthash => $filecontent,
582
                ],
583
            ],
584
        ];
585
        $vfileroot = $this->setup_vfile_root([], $trashdircontent);
586
 
587
        $file = new \stored_file(get_file_storage(), (object) [
588
            'contenthash' => $contenthash . " different",
589
            'filesize' => strlen($filecontent),
590
        ]);
591
 
592
        $fs = new file_system_filedir();
593
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
594
        $result = $method->invokeArgs($fs, array($file));
595
 
596
        // Test the output.
597
        $this->assertFalse($result);
598
        $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
599
    }
600
 
601
    /**
602
     * Ensure that content recovery works when the content file is in the
603
     * alt trash directory.
604
     *
605
     * @covers ::recover_file
606
     */
11 efrain 607
    public function test_recover_file_alttrash(): void {
1 efrain 608
        $this->resetAfterTest();
609
 
610
        // Setup the filedir.
611
        // This contains a virtual file which has a cache mismatch.
612
        $filecontent = 'example content';
613
        $contenthash = \file_storage::hash_from_string($filecontent);
614
 
615
        $trashdircontent = [
616
            $contenthash => $filecontent,
617
        ];
618
        $vfileroot = $this->setup_vfile_root([], $trashdircontent);
619
 
620
        $file = new \stored_file(get_file_storage(), (object) [
621
            'contenthash' => $contenthash,
622
            'filesize' => strlen($filecontent),
623
        ]);
624
 
625
        $fs = new file_system_filedir();
626
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
627
        $result = $method->invokeArgs($fs, array($file));
628
 
629
        // Test the output.
630
        $this->assertTrue($result);
631
 
632
        $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
633
    }
634
 
635
    /**
636
     * Test that an appropriate error message is generated when adding a
637
     * file to the pool when the pool directory structure is not writable.
638
     *
639
     * @covers ::recover_file
640
     */
11 efrain 641
    public function test_recover_file_contentdir_readonly(): void {
1 efrain 642
        $this->resetAfterTest();
643
 
644
        $filecontent = 'example content';
645
        $contenthash = \file_storage::hash_from_string($filecontent);
646
        $filedircontent = [
647
            '0f' => [],
648
        ];
649
        $trashdircontent = [
650
            $contenthash => $filecontent,
651
        ];
652
        $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
653
 
654
        // Make the target path readonly.
655
        $vfileroot->getChild('filedir/0f')
656
            ->chmod(0444)
657
            ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
658
 
659
        $file = new \stored_file(get_file_storage(), (object) [
660
            'contenthash' => $contenthash,
661
            'filesize' => strlen($filecontent),
662
        ]);
663
 
664
        $fs = new file_system_filedir();
665
        $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file');
666
        $result = $method->invokeArgs($fs, array($file));
667
 
668
        // Test the output.
669
        $this->assertFalse($result);
670
    }
671
 
672
    /**
673
     * Test adding a file to the pool.
674
     *
675
     * @covers ::add_file_from_path
676
     */
11 efrain 677
    public function test_add_file_from_path(): void {
1 efrain 678
        $this->resetAfterTest();
679
        global $CFG;
680
 
681
        // Setup the filedir.
682
        // This contains a virtual file which has a cache mismatch.
683
        $filecontent = 'example content';
684
        $contenthash = \file_storage::hash_from_string($filecontent);
685
        $sourcedircontent = [
686
            'file' => $filecontent,
687
        ];
688
 
689
        $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent);
690
 
691
        // Note, the vfs file system does not support locks - prevent file locking here.
692
        $CFG->preventfilelocking = true;
693
 
694
        // Attempt to add the file to the file pool.
695
        $fs = new file_system_filedir();
696
        $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
697
        $result = $fs->add_file_from_path($sourcefile);
698
 
699
        // Test the output.
700
        $this->assertEquals($contenthash, $result[0]);
701
        $this->assertEquals(\core_text::strlen($filecontent), $result[1]);
702
        $this->assertTrue($result[2]);
703
 
704
        $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
705
    }
706
 
707
    /**
708
     * Test that an appropriate error message is generated when adding an
709
     * unavailable file to the pool is attempted.
710
     *
711
     * @covers ::add_file_from_path
712
     */
11 efrain 713
    public function test_add_file_from_path_file_unavailable(): void {
1 efrain 714
        $this->resetAfterTest();
715
 
716
        // Setup the filedir.
717
        $vfileroot = $this->setup_vfile_root();
718
 
719
        $this->expectException('file_exception');
720
        $this->expectExceptionMessageMatches(
721
            '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
722
 
723
        $fs = new file_system_filedir();
724
        $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file'));
725
    }
726
 
727
    /**
728
     * Test that an appropriate error message is generated when specifying
729
     * the wrong contenthash when adding a file to the pool.
730
     *
731
     * @covers ::add_file_from_path
732
     */
11 efrain 733
    public function test_add_file_from_path_mismatched_hash(): void {
1 efrain 734
        $this->resetAfterTest();
735
 
736
        $filecontent = 'example content';
737
        $contenthash = \file_storage::hash_from_string($filecontent);
738
        $sourcedir = [
739
            'file' => $filecontent,
740
        ];
741
        $vfileroot = $this->setup_vfile_root([], [], $sourcedir);
742
 
743
        $fs = new file_system_filedir();
744
        $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
745
        $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab');
746
        $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath");
747
    }
748
 
749
    /**
750
     * Test that an appropriate error message is generated when an existing
751
     * file in the pool has the wrong contenthash
752
     *
753
     * @covers ::add_file_from_path
754
     */
11 efrain 755
    public function test_add_file_from_path_existing_content_invalid(): void {
1 efrain 756
        $this->resetAfterTest();
757
 
758
        $filecontent = 'example content';
759
        $contenthash = \file_storage::hash_from_string($filecontent);
760
        $filedircontent = [
761
            '0f' => [
762
                'f3' => [
763
                    // This contains a virtual file which has a cache mismatch.
764
                    '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content',
765
                ],
766
            ],
767
        ];
768
        $sourcedir = [
769
            'file' => $filecontent,
770
        ];
771
        $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
772
 
773
        // Check that we hit the jackpot.
774
        $fs = new file_system_filedir();
775
        $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
776
        $result = $fs->add_file_from_path($filepath);
777
 
778
        // We provided a bad hash. Check that the file was replaced.
779
        $this->assertDebuggingCalled("Replacing invalid content file $contenthash");
780
 
781
        // Test the output.
782
        $this->assertEquals($contenthash, $result[0]);
783
        $this->assertEquals(\core_text::strlen($filecontent), $result[1]);
784
        $this->assertFalse($result[2]);
785
 
786
        // Fetch the new file structure.
787
        $structure = \org\bovigo\vfs\vfsStream::inspect(
788
            new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor()
789
        )->getStructure();
790
 
791
        $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]);
792
    }
793
 
794
    /**
795
     * Test that an appropriate error message is generated when adding a
796
     * file to the pool when the pool directory structure is not writable.
797
     *
798
     * @covers ::add_file_from_path
799
     */
11 efrain 800
    public function test_add_file_from_path_existing_cannot_write_hashpath(): void {
1 efrain 801
        $this->resetAfterTest();
802
 
803
        $filecontent = 'example content';
804
        $contenthash = \file_storage::hash_from_string($filecontent);
805
        $filedircontent = [
806
            '0f' => [],
807
        ];
808
        $sourcedir = [
809
            'file' => $filecontent,
810
        ];
811
        $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
812
 
813
        // Make the target path readonly.
814
        $vfileroot->getChild('filedir/0f')
815
            ->chmod(0444)
816
            ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
817
 
818
        $this->expectException('file_exception');
819
        $this->expectExceptionMessageMatches(
820
            "/Cannot create local file pool directories. Please verify permissions in dataroot./");
821
 
822
        // Attempt to add the file to the file pool.
823
        $fs = new file_system_filedir();
824
        $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
825
        $fs->add_file_from_path($sourcefile);
826
    }
827
 
828
    /**
829
     * Test adding a string to the pool.
830
     *
831
     * @covers ::add_file_from_string
832
     */
11 efrain 833
    public function test_add_file_from_string(): void {
1 efrain 834
        $this->resetAfterTest();
835
        global $CFG;
836
 
837
        $filecontent = 'example content';
838
        $contenthash = \file_storage::hash_from_string($filecontent);
839
        $vfileroot = $this->setup_vfile_root();
840
 
841
        // Note, the vfs file system does not support locks - prevent file locking here.
842
        $CFG->preventfilelocking = true;
843
 
844
        // Attempt to add the file to the file pool.
845
        $fs = new file_system_filedir();
846
        $result = $fs->add_file_from_string($filecontent);
847
 
848
        // Test the output.
849
        $this->assertEquals($contenthash, $result[0]);
850
        $this->assertEquals(\core_text::strlen($filecontent), $result[1]);
851
        $this->assertTrue($result[2]);
852
    }
853
 
854
    /**
855
     * Test that an appropriate error message is generated when adding a
856
     * string to the pool when the pool directory structure is not writable.
857
     *
858
     * @covers ::add_file_from_string
859
     */
11 efrain 860
    public function test_add_file_from_string_existing_cannot_write_hashpath(): void {
1 efrain 861
        $this->resetAfterTest();
862
 
863
        $filecontent = 'example content';
864
        $contenthash = \file_storage::hash_from_string($filecontent);
865
 
866
        $filedircontent = [
867
            '0f' => [],
868
        ];
869
        $vfileroot = $this->setup_vfile_root($filedircontent);
870
 
871
        // Make the target path readonly.
872
        $vfileroot->getChild('filedir/0f')
873
            ->chmod(0444)
874
            ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
875
 
876
        $this->expectException('file_exception');
877
        $this->expectExceptionMessageMatches(
878
            "/Cannot create local file pool directories. Please verify permissions in dataroot./");
879
 
880
        // Attempt to add the file to the file pool.
881
        $fs = new file_system_filedir();
882
        $fs->add_file_from_string($filecontent);
883
    }
884
 
885
    /**
886
     * Test adding a string to the pool when an item with the same
887
     * contenthash is already present.
888
     *
889
     * @covers ::add_file_from_string
890
     */
11 efrain 891
    public function test_add_file_from_string_existing_matches(): void {
1 efrain 892
        $this->resetAfterTest();
893
        global $CFG;
894
 
895
        $filecontent = 'example content';
896
        $contenthash = \file_storage::hash_from_string($filecontent);
897
        $filedircontent = [
898
            '0f' => [
899
                'f3' => [
900
                    $contenthash => $filecontent,
901
                ],
902
            ],
903
        ];
904
 
905
        $vfileroot = $this->setup_vfile_root($filedircontent);
906
 
907
        // Note, the vfs file system does not support locks - prevent file locking here.
908
        $CFG->preventfilelocking = true;
909
 
910
        // Attempt to add the file to the file pool.
911
        $fs = new file_system_filedir();
912
        $result = $fs->add_file_from_string($filecontent);
913
 
914
        // Test the output.
915
        $this->assertEquals($contenthash, $result[0]);
916
        $this->assertEquals(\core_text::strlen($filecontent), $result[1]);
917
        $this->assertFalse($result[2]);
918
    }
919
 
920
    /**
921
     * Test the cleanup of deleted files when there are no files to delete.
922
     *
923
     * @covers ::remove_file
924
     */
11 efrain 925
    public function test_remove_file_missing(): void {
1 efrain 926
        $this->resetAfterTest();
927
 
928
        $filecontent = 'example content';
929
        $contenthash = \file_storage::hash_from_string($filecontent);
930
        $vfileroot = $this->setup_vfile_root();
931
 
932
        $fs = new file_system_filedir();
933
        $fs->remove_file($contenthash);
934
 
935
        $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
936
        // No file to move to trash, so the trash path will also be empty.
937
        $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
938
        $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
939
        $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
940
    }
941
 
942
    /**
943
     * Test the cleanup of deleted files when a file already exists in the
944
     * trash for that path.
945
     *
946
     * @covers ::remove_file
947
     */
11 efrain 948
    public function test_remove_file_existing_trash(): void {
1 efrain 949
        $this->resetAfterTest();
950
 
951
        $filecontent = 'example content';
952
        $contenthash = \file_storage::hash_from_string($filecontent);
953
 
954
        $filedircontent = $trashdircontent = [
955
            '0f' => [
956
                'f3' => [
957
                    $contenthash => $filecontent,
958
                ],
959
            ],
960
        ];
961
        $trashdircontent['0f']['f3'][$contenthash] .= 'different';
962
        $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
963
 
964
        $fs = new file_system_filedir();
965
        $fs->remove_file($contenthash);
966
 
967
        $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
968
        $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
969
        $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent());
970
    }
971
 
972
    /**
973
     * Ensure that remove_file does nothing with an empty file.
974
     *
975
     * @covers ::remove_file
976
     */
11 efrain 977
    public function test_remove_file_empty(): void {
1 efrain 978
        $this->resetAfterTest();
979
        global $DB;
980
 
981
        $DB = $this->getMockBuilder(\moodle_database::class)
982
            ->onlyMethods(['record_exists'])
983
            ->getMockForAbstractClass();
984
 
985
        $DB->expects($this->never())
986
            ->method('record_exists');
987
 
988
        $fs = new file_system_filedir();
989
 
990
        $result = $fs->remove_file(\file_storage::hash_from_string(''));
991
        $this->assertNull($result);
992
    }
993
 
994
    /**
995
     * Ensure that remove_file does nothing when a file is still
996
     * in use.
997
     *
998
     * @covers ::remove_file
999
     */
11 efrain 1000
    public function test_remove_file_in_use(): void {
1 efrain 1001
        $this->resetAfterTest();
1002
        global $DB;
1003
 
1004
        $filecontent = 'example content';
1005
        $contenthash = \file_storage::hash_from_string($filecontent);
1006
        $filedircontent = [
1007
            '0f' => [
1008
                'f3' => [
1009
                    $contenthash => $filecontent,
1010
                ],
1011
            ],
1012
        ];
1013
        $vfileroot = $this->setup_vfile_root($filedircontent);
1014
 
1015
        $DB = $this->getMockBuilder(\moodle_database::class)
1016
            ->onlyMethods(['record_exists'])
1017
            ->getMockForAbstractClass();
1018
 
1019
        $DB->method('record_exists')->willReturn(true);
1020
 
1021
        $fs = new file_system_filedir();
1022
        $result = $fs->remove_file($contenthash);
1023
        $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1024
        $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1025
    }
1026
 
1027
    /**
1028
     * Ensure that remove_file removes the file when it is no
1029
     * longer in use.
1030
     *
1031
     * @covers ::remove_file
1032
     */
11 efrain 1033
    public function test_remove_file_expired(): void {
1 efrain 1034
        $this->resetAfterTest();
1035
        global $DB;
1036
 
1037
        $filecontent = 'example content';
1038
        $contenthash = \file_storage::hash_from_string($filecontent);
1039
        $filedircontent = [
1040
            '0f' => [
1041
                'f3' => [
1042
                    $contenthash => $filecontent,
1043
                ],
1044
            ],
1045
        ];
1046
        $vfileroot = $this->setup_vfile_root($filedircontent);
1047
 
1048
        $DB = $this->getMockBuilder(\moodle_database::class)
1049
            ->onlyMethods(['record_exists'])
1050
            ->getMockForAbstractClass();
1051
 
1052
        $DB->method('record_exists')->willReturn(false);
1053
 
1054
        $fs = new file_system_filedir();
1055
        $result = $fs->remove_file($contenthash);
1056
        $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1057
        $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1058
    }
1059
 
1060
    /**
1061
     * Test purging the cache.
1062
     *
1063
     * @covers ::empty_trash
1064
     */
11 efrain 1065
    public function test_empty_trash(): void {
1 efrain 1066
        $this->resetAfterTest();
1067
 
1068
        $filecontent = 'example content';
1069
        $contenthash = \file_storage::hash_from_string($filecontent);
1070
 
1071
        $filedircontent = $trashdircontent = [
1072
            '0f' => [
1073
                'f3' => [
1074
                    $contenthash => $filecontent,
1075
                ],
1076
            ],
1077
        ];
1078
        $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1079
 
1080
        $fs = new file_system_filedir();
1081
        $method = new \ReflectionMethod(file_system_filedir::class, 'empty_trash');
1082
        $result = $method->invoke($fs);
1083
 
1084
        $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1085
        $this->assertFalse($vfileroot->hasChild('trashdir'));
1086
        $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
1087
        $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
1088
        $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1089
    }
1090
 
1091
    /**
1092
     * Data Provider for contenthash to contendir conversion.
1093
     *
1094
     * @return  array
1095
     */
1096
    public function contenthash_dataprovider() {
1097
        return array(
1098
            array(
1099
                'contenthash'   => 'eee4943847a35a4b6942c6f96daafde06bcfdfab',
1100
                'contentdir'    => 'ee/e4',
1101
            ),
1102
            array(
1103
                'contenthash'   => 'aef05a62ae81ca0005d2569447779af062b7cda0',
1104
                'contentdir'    => 'ae/f0',
1105
            ),
1106
        );
1107
    }
1108
}