Proyectos de Subversion Moodle

Rev

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