Passed
Pull Request — master (#5)
by Wilmer
01:14
created

FileHelperTest::testCreateDirectoryPermissions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 11
rs 10
c 2
b 0
f 1
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Files\Tests;
5
6
use PHPUnit\Framework\TestCase;
7
use Yiisoft\Files\FileHelper;
8
9
/**
10
 * File helper tests class.
11
 */
12
final class FileHelperTest extends TestCase
13
{
14
    /**
15
     * @var string test files path.
16
     */
17
    private $testFilePath = '';
18
19
    /**
20
     * Setup.
21
     *
22
     * @return void
23
     */
24
    public function setUp(): void
25
    {
26
        $this->testFilePath = FileHelper::normalizePath(sys_get_temp_dir() . '/' . get_class($this));
27
28
        FileHelper::createDirectory($this->testFilePath, 0777);
29
30
        if (!file_exists($this->testFilePath)) {
31
            $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!');
32
        }
33
    }
34
35
    /**
36
     * Check if chmod works as expected.
37
     *
38
     * On remote file systems and vagrant mounts chmod returns true but file permissions are not set properly.
39
     */
40
    private function isChmodReliable(): bool
41
    {
42
        $directory = $this->testFilePath . '/test_chmod';
43
        mkdir($directory);
44
        chmod($directory, 0700);
45
        $mode = $this->getMode($directory);
46
        rmdir($directory);
47
48
        return $mode === '0700';
49
    }
50
51
    /**
52
     * TearDown.
53
     *
54
     * @return void
55
     */
56
    public function tearDown(): void
57
    {
58
        FileHelper::removeDirectory($this->testFilePath);
59
    }
60
61
    /**
62
     * Get file permission mode.
63
     *
64
     * @param string $file file name.
65
     *
66
     * @return string permission mode.
67
     */
68
    private function getMode(string $file): string
69
    {
70
        return substr(sprintf('%04o', fileperms($file)), -4);
71
    }
72
73
    /**
74
     * Asserts that file has specific permission mode.
75
     *
76
     * @param int $expectedMode expected file permission mode.
77
     * @param string $fileName file name.
78
     * @param string $message error message
79
     *
80
     * @return void
81
     */
82
    private function assertFileMode(int $expectedMode, string $fileName, string $message = ''): void
83
    {
84
        $expectedMode = sprintf('%04o', $expectedMode);
85
        $this->assertEquals($expectedMode, $this->getMode($fileName), $message);
86
    }
87
88
    /**
89
     * Create directory.
90
     *
91
     * @return void
92
     */
93
    public function testCreateDirectory(): void
94
    {
95
        $basePath = $this->testFilePath;
96
        $directory = $basePath . '/test_dir_level_1/test_dir_level_2';
97
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true if directory was created!');
98
        $this->assertFileExists($directory, 'Unable to create directory recursively!');
99
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true for already existing directories!');
100
    }
101
102
    /**
103
     * Create directory permissions.
104
     *
105
     * @return void
106
     */
107
    public function testCreateDirectoryPermissions(): void
108
    {
109
        if (!$this->isChmodReliable()) {
110
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
111
        }
112
113
        $basePath = $this->testFilePath;
114
        $dirName = $basePath . '/test_dir_perms';
115
116
        $this->assertTrue(FileHelper::createDirectory($dirName, 0700));
117
        $this->assertFileMode(0700, $dirName);
118
    }
119
120
    /**
121
     * Remove directory.
122
     *
123
     * @return void
124
     */
125
    public function testRemoveDirectory(): void
126
    {
127
        $dirName = 'test_dir_for_remove';
128
129
        $this->createFileStructure([
130
            $dirName => [
131
                'file1.txt' => 'file 1 content',
132
                'file2.txt' => 'file 2 content',
133
                'test_sub_dir' => [
134
                    'sub_dir_file_1.txt' => 'sub dir file 1 content',
135
                    'sub_dir_file_2.txt' => 'sub dir file 2 content',
136
                ],
137
            ],
138
        ]);
139
140
        $basePath = $this->testFilePath;
141
        $dirName = $basePath . '/' . $dirName;
142
143
        FileHelper::removeDirectory($dirName);
144
145
        $this->assertFileNotExists($dirName, 'Unable to remove directory!');
146
147
        // should be silent about non-existing directories
148
        FileHelper::removeDirectory($basePath . '/nonExisting');
149
    }
150
151
    /**
152
     * Remove directory symlinks1.
153
     *
154
     * @return void
155
     */
156
    public function testRemoveDirectorySymlinks1(): void
157
    {
158
        $dirName = 'remove-directory-symlinks-1';
159
160
        $this->createFileStructure([
161
            $dirName => [
162
                'file' => 'Symlinked file.',
163
                'directory' => [
164
                    'standard-file-1' => 'Standard file 1.',
165
                ],
166
                'symlinks' => [
167
                    'standard-file-2' => 'Standard file 2.',
168
                    'symlinked-file' => ['symlink', '../file'],
169
                    'symlinked-directory' => ['symlink', '../directory'],
170
                ],
171
            ],
172
        ]);
173
174
        $basePath = $this->testFilePath . '/' . $dirName . '/';
175
176
        $this->assertFileExists($basePath . 'file');
177
        $this->assertDirectoryExists($basePath . 'directory');
178
        $this->assertFileExists($basePath . 'directory/standard-file-1');
179
        $this->assertDirectoryExists($basePath . 'symlinks');
180
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
181
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
182
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
183
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
184
185
        FileHelper::removeDirectory($basePath . 'symlinks');
186
187
        $this->assertFileExists($basePath . 'file');
188
        $this->assertDirectoryExists($basePath . 'directory');
189
        $this->assertFileExists($basePath . 'directory/standard-file-1'); // symlinked directory still have it's file
190
        $this->assertDirectoryNotExists($basePath . 'symlinks');
191
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
192
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
193
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
194
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
195
    }
196
197
    /**
198
     * Remove directory symlinks2.
199
     *
200
     * @return void
201
     */
202
    public function testRemoveDirectorySymlinks2(): void
203
    {
204
        $dirName = 'remove-directory-symlinks-2';
205
206
        $this->createFileStructure([
207
            $dirName => [
208
                'file' => 'Symlinked file.',
209
                'directory' => [
210
                    'standard-file-1' => 'Standard file 1.',
211
                ],
212
                'symlinks' => [
213
                    'standard-file-2' => 'Standard file 2.',
214
                    'symlinked-file' => ['symlink', '../file'],
215
                    'symlinked-directory' => ['symlink', '../directory'],
216
                ],
217
            ],
218
        ]);
219
220
        $basePath = $this->testFilePath . '/' . $dirName . '/';
221
222
        $this->assertFileExists($basePath . 'file');
223
        $this->assertDirectoryExists($basePath . 'directory');
224
        $this->assertFileExists($basePath . 'directory/standard-file-1');
225
        $this->assertDirectoryExists($basePath . 'symlinks');
226
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
227
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
228
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
229
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
230
231
        FileHelper::removeDirectory($basePath . 'symlinks', ['traverseSymlinks' => true]);
232
233
        $this->assertFileExists($basePath . 'file');
234
        $this->assertDirectoryExists($basePath . 'directory');
235
        $this->assertFileNotExists($basePath . 'directory/standard-file-1'); // symlinked directory doesn't have it's file now
236
        $this->assertDirectoryNotExists($basePath . 'symlinks');
237
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
238
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
239
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
240
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
241
    }
242
243
    /**
244
     * Normalize path.
245
     *
246
     * @return void
247
     */
248
    public function testNormalizePath(): void
249
    {
250
        $this->assertEquals('/a/b', FileHelper::normalizePath('//a\\b/'));
251
        $this->assertEquals('/b/c', FileHelper::normalizePath('/a/../b/c'));
252
        $this->assertEquals('/c', FileHelper::normalizePath('/a\\b/../..///c'));
253
        $this->assertEquals('/c', FileHelper::normalizePath('/a/.\\b//../../c'));
254
        $this->assertEquals('c', FileHelper::normalizePath('/a/.\\b/../..//../c'));
255
        $this->assertEquals('../c', FileHelper::normalizePath('//a/.\\b//..//..//../../c'));
256
257
        // relative paths
258
        $this->assertEquals('.', FileHelper::normalizePath('.'));
259
        $this->assertEquals('.', FileHelper::normalizePath('./'));
260
        $this->assertEquals('a', FileHelper::normalizePath('.\\a'));
261
        $this->assertEquals('a/b', FileHelper::normalizePath('./a\\b'));
262
        $this->assertEquals('.', FileHelper::normalizePath('./a\\../'));
263
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a'));
264
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a/../a'));
265
        $this->assertEquals('../../b', FileHelper::normalizePath('../..\\a/../b'));
266
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a'));
267
        $this->assertEquals('../a', FileHelper::normalizePath('././..\\a'));
268
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a/../a'));
269
        $this->assertEquals('../b', FileHelper::normalizePath('./..\\a/../b'));
270
271
        // Windows file system may have paths for network shares that start with two backslashes. These two backslashes
272
        // should not be touched.
273
        // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
274
        // https://github.com/yiisoft/yii2/issues/13034
275
        $this->assertEquals('\\\\server/share/path/file', FileHelper::normalizePath('\\\\server\share\path\file'));
276
    }
277
278
    /**
279
     * Copy directory.
280
     *
281
     * @depends testCreateDirectory
282
     *
283
     * @return void
284
     */
285
    public function testCopyDirectory(): void
286
    {
287
        $source = 'test_src_dir';
288
        $files = [
289
            'file1.txt' => 'file 1 content',
290
            'file2.txt' => 'file 2 content',
291
        ];
292
293
        $this->createFileStructure([
294
            $source => $files,
295
        ]);
296
297
        $basePath = $this->testFilePath;
298
        $source = $basePath . '/' . $source;
299
        $destination = $basePath . '/test_dst_dir';
300
301
        FileHelper::copyDirectory($source, $destination);
302
303
        $this->assertFileExists($destination, 'Destination directory does not exist!');
304
305
        foreach ($files as $name => $content) {
306
            $fileName = $destination . '/' . $name;
307
            $this->assertFileExists($fileName);
308
            $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
309
        }
310
    }
311
312
    /**
313
     * Copy directory recursive.
314
     *
315
     * @return void
316
     */
317
    public function testCopyDirectoryRecursive(): void
318
    {
319
        $source = 'test_src_dir_rec';
320
        $structure = [
321
            'directory1' => [
322
                'file1.txt' => 'file 1 content',
323
                'file2.txt' => 'file 2 content',
324
            ],
325
            'directory2' => [
326
                'file3.txt' => 'file 3 content',
327
                'file4.txt' => 'file 4 content',
328
            ],
329
            'file5.txt' => 'file 5 content',
330
        ];
331
332
        $this->createFileStructure([
333
            $source => $structure,
334
        ]);
335
336
        $basePath = $this->testFilePath;
337
        $source = $basePath . '/' . $source;
338
        $destination = $basePath . '/test_dst_dir';
339
340
        FileHelper::copyDirectory($source, $destination);
341
342
        $this->assertFileExists($destination, 'Destination directory does not exist!');
343
344
        $checker = function ($structure, $dstDirName) use (&$checker) {
345
            foreach ($structure as $name => $content) {
346
                if (is_array($content)) {
347
                    $checker($content, $dstDirName . '/' . $name);
348
                } else {
349
                    $fileName = $dstDirName . '/' . $name;
350
                    $this->assertFileExists($fileName);
351
                    $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
352
                }
353
            }
354
        };
355
        $checker($structure, $destination);
356
    }
357
358
    /**
359
     * Copy directory not recursive.
360
     *
361
     * @return void
362
     */
363
    public function testCopyDirectoryNotRecursive(): void
364
    {
365
        $source = 'test_src_dir_not_rec';
366
        $structure = [
367
            'directory1' => [
368
                'file1.txt' => 'file 1 content',
369
                'file2.txt' => 'file 2 content',
370
            ],
371
            'directory2' => [
372
                'file3.txt' => 'file 3 content',
373
                'file4.txt' => 'file 4 content',
374
            ],
375
            'file5.txt' => 'file 5 content',
376
        ];
377
378
        $this->createFileStructure([
379
            $source => $structure,
380
        ]);
381
382
        $basePath = $this->testFilePath;
383
        $source = $basePath . '/' . $source;
384
        $destination = $basePath . '/' . 'test_dst_dir';
385
386
        FileHelper::copyDirectory($source, $destination, ['recursive' => false]);
387
388
        $this->assertFileExists($destination, 'Destination directory does not exist!');
389
390
        foreach ($structure as $name => $content) {
391
            $fileName = $destination . '/' . $name;
392
            if (is_array($content)) {
393
                $this->assertFileNotExists($fileName);
394
            } else {
395
                $this->assertFileExists($fileName);
396
                $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
397
            }
398
        }
399
    }
400
401
    /**
402
     * Copy directory permissions.
403
     *
404
     * @depends testCopyDirectory
405
     *
406
     * @return void
407
     */
408
    public function testCopyDirectoryPermissions(): void
409
    {
410
        if (!$this->isChmodReliable()) {
411
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
412
        }
413
414
        $isWindows = DIRECTORY_SEPARATOR === '\\';
415
416
        if ($isWindows) {
417
            $this->markTestSkipped('Skipping tests on Windows because fileperms() always return 0777.');
418
        }
419
420
        $source = 'test_src_dir';
421
        $subDirectory = 'test_sub_dir';
422
        $fileName = 'test_file.txt';
423
424
        $this->createFileStructure([
425
            $source => [
426
                $subDirectory => [],
427
                $fileName => 'test file content',
428
            ],
429
        ]);
430
431
        $basePath = $this->testFilePath;
432
        $source = $basePath . '/' . $source;
433
        $destination = $basePath . '/test_dst_dir';
434
        $directoryMode = 0755;
435
        $fileMode = 0755;
436
        $options = [
437
            'dirMode' => $directoryMode,
438
            'fileMode' => $fileMode,
439
        ];
440
441
        FileHelper::copyDirectory($source, $destination, $options);
442
443
        $this->assertFileMode($directoryMode, $destination, 'Destination directory has wrong mode!');
444
        $this->assertFileMode($directoryMode, $destination . '/' . $subDirectory, 'Copied sub directory has wrong mode!');
445
        $this->assertFileMode($fileMode, $destination . '/' . $fileName, 'Copied file has wrong mode!');
446
    }
447
448
    /**
449
     * Copy directory to it self.
450
     *
451
     * @see https://github.com/yiisoft/yii2/issues/10710
452
     *
453
     * @return void
454
     */
455
    public function testCopyDirectoryToItself(): void
456
    {
457
        $directoryName = 'test_dir';
458
459
        $this->createFileStructure([
460
            $directoryName => [],
461
        ]);
462
        $this->expectException(\InvalidArgumentException::class);
463
464
        $directoryName = $this->testFilePath . '/test_dir';
465
466
        FileHelper::copyDirectory($directoryName, $directoryName);
467
    }
468
469
    /**
470
     * Copy directory to sudirectory of it self.
471
     *
472
     * @see https://github.com/yiisoft/yii2/issues/10710
473
     *
474
     * @return void
475
     */
476
    public function testCopyDirToSubdirOfItself(): void
477
    {
478
        $this->createFileStructure([
479
            'data' => [],
480
            'backup' => ['data' => []],
481
        ]);
482
        $this->expectException(\InvalidArgumentException::class);
483
484
        FileHelper::copyDirectory(
485
            $this->testFilePath . '/backup',
486
            $this->testFilePath . '/backup/data'
487
        );
488
    }
489
490
    /**
491
     * Copy directory to another with same name.
492
     *
493
     * @see https://github.com/yiisoft/yii2/issues/10710
494
     *
495
     * @return void
496
     */
497
    public function testCopyDirToAnotherWithSameName(): void
498
    {
499
        $this->createFileStructure([
500
            'data' => [],
501
            'backup' => ['data' => []],
502
        ]);
503
504
        FileHelper::copyDirectory(
505
            $this->testFilePath . '/data',
506
            $this->testFilePath . '/backup/data'
507
        );
508
509
        $this->assertFileExists($this->testFilePath . '/backup/data');
510
    }
511
512
    /**
513
     * Copy directory with same name.
514
     *
515
     * @see https://github.com/yiisoft/yii2/issues/10710
516
     *
517
     * @return void
518
     */
519
    public function testCopyDirWithSameName(): void
520
    {
521
        $this->createFileStructure([
522
            'data' => [],
523
            'data-backup' => [],
524
        ]);
525
526
        FileHelper::copyDirectory(
527
            $this->testFilePath . '/data',
528
            $this->testFilePath . '/data-backup'
529
        );
530
531
        $this->assertTrue(true, 'no error');
532
    }
533
534
    public function testsFilterPath()
535
    {
536
        $source = 'test_src_dir';
537
        $files = [
538
            'file1.txt' => 'file 1 content',
539
            'file2.php' => 'file 2 content',
540
            'web' => [],
541
            'testweb' => []
542
        ];
543
544
        $this->createFileStructure([
545
            $source => $files,
546
        ]);
547
548
        $basePath = $this->testFilePath;
549
        $source = $basePath . '/' . $source;
550
    
551
        // tests callback true.
552
        $options = [
553
            'filter' => function ($source) {
554
                return strpos($source, 'test') !== false;
555
            },
556
        ];
557
558
        $this->assertTrue(FileHelper::filterPath($source, $options));
559
560
        // tests callback false.
561
        $options = [
562
            'filter' => function ($source) {
563
                return strpos($source, 'public') !== false;
564
            },
565
        ];
566
567
        $this->assertFalse(FileHelper::filterPath($source, $options));
568
    }
569
570
    /**
571
     * Creates test files structure.
572
     *
573
     * @param array $items file system objects to be created in format: objectName => objectContent
574
     *                         Arrays specifies directories, other values - files.
575
     * @param string $basePath structure base file path.
576
     *
577
     * @return void
578
     */
579
    private function createFileStructure(array $items, ?string $basePath = null): void
580
    {
581
        $basePath = $basePath ?? $this->testFilePath;
582
583
        if (empty($basePath)) {
584
            $basePath = $this->testFilePath;
585
        }
586
        foreach ($items as $name => $content) {
587
            $itemName = $basePath . DIRECTORY_SEPARATOR . $name;
588
            if (is_array($content)) {
589
                if (isset($content[0], $content[1]) && $content[0] === 'symlink') {
590
                    symlink($basePath . '/' . $content[1], $itemName);
591
                } else {
592
                    mkdir($itemName, 0777, true);
593
                    $this->createFileStructure($content, $itemName);
594
                }
595
            } else {
596
                file_put_contents($itemName, $content);
597
            }
598
        }
599
    }
600
}
601