diff options
Diffstat (limited to 'tests/lib/Files/Storage/Wrapper')
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/AvailabilityTest.php | 158 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/EncodingTest.php | 239 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/EncryptionTest.php | 1029 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/JailTest.php | 54 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php | 69 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php | 180 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/QuotaTest.php | 232 | ||||
-rw-r--r-- | tests/lib/Files/Storage/Wrapper/WrapperTest.php | 40 |
8 files changed, 2001 insertions, 0 deletions
diff --git a/tests/lib/Files/Storage/Wrapper/AvailabilityTest.php b/tests/lib/Files/Storage/Wrapper/AvailabilityTest.php new file mode 100644 index 00000000000..d890081cbb6 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/AvailabilityTest.php @@ -0,0 +1,158 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Files\Storage\Wrapper; + +use OC\Files\Cache\Storage as StorageCache; +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Availability; +use OCP\Files\StorageNotAvailableException; + +class AvailabilityTest extends \Test\TestCase { + /** @var \PHPUnit\Framework\MockObject\MockObject|StorageCache */ + protected $storageCache; + /** @var \PHPUnit\Framework\MockObject\MockObject|Temporary */ + protected $storage; + /** @var Availability */ + protected $wrapper; + + protected function setUp(): void { + parent::setUp(); + + $this->storageCache = $this->createMock(StorageCache::class); + + $this->storage = $this->createMock(Temporary::class); + $this->storage->expects($this->any()) + ->method('getStorageCache') + ->willReturn($this->storageCache); + + $this->wrapper = new Availability(['storage' => $this->storage]); + } + + /** + * Storage is available + */ + public function testAvailable(): void { + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => true, 'last_checked' => 0]); + $this->storage->expects($this->never()) + ->method('test'); + $this->storage->expects($this->once()) + ->method('mkdir'); + + $this->wrapper->mkdir('foobar'); + } + + /** + * Storage marked unavailable, TTL not expired + * + */ + public function testUnavailable(): void { + $this->expectException(StorageNotAvailableException::class); + + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => false, 'last_checked' => time()]); + $this->storage->expects($this->never()) + ->method('test'); + $this->storage->expects($this->never()) + ->method('mkdir'); + + $this->wrapper->mkdir('foobar'); + } + + /** + * Storage marked unavailable, TTL expired + */ + public function testUnavailableRecheck(): void { + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => false, 'last_checked' => 0]); + $this->storage->expects($this->once()) + ->method('test') + ->willReturn(true); + $calls = [ + false, // prevents concurrent rechecks + true, // sets correct availability + ]; + $this->storage->expects($this->exactly(2)) + ->method('setAvailability') + ->willReturnCallback(function ($value) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, $value); + }); + $this->storage->expects($this->once()) + ->method('mkdir'); + + $this->wrapper->mkdir('foobar'); + } + + /** + * Storage marked available, but throws StorageNotAvailableException + * + */ + public function testAvailableThrowStorageNotAvailable(): void { + $this->expectException(StorageNotAvailableException::class); + + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => true, 'last_checked' => 0]); + $this->storage->expects($this->never()) + ->method('test'); + $this->storage->expects($this->once()) + ->method('mkdir') + ->willThrowException(new StorageNotAvailableException()); + $this->storageCache->expects($this->once()) + ->method('setAvailability') + ->with($this->equalTo(false)); + + $this->wrapper->mkdir('foobar'); + } + + /** + * Storage available, but call fails + * Method failure does not indicate storage unavailability + */ + public function testAvailableFailure(): void { + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => true, 'last_checked' => 0]); + $this->storage->expects($this->never()) + ->method('test'); + $this->storage->expects($this->once()) + ->method('mkdir') + ->willReturn(false); + $this->storage->expects($this->never()) + ->method('setAvailability'); + + $this->wrapper->mkdir('foobar'); + } + + /** + * Storage available, but throws exception + * Standard exception does not indicate storage unavailability + * + */ + public function testAvailableThrow(): void { + $this->expectException(\Exception::class); + + $this->storage->expects($this->once()) + ->method('getAvailability') + ->willReturn(['available' => true, 'last_checked' => 0]); + $this->storage->expects($this->never()) + ->method('test'); + $this->storage->expects($this->once()) + ->method('mkdir') + ->willThrowException(new \Exception()); + $this->storage->expects($this->never()) + ->method('setAvailability'); + + $this->wrapper->mkdir('foobar'); + } +} diff --git a/tests/lib/Files/Storage/Wrapper/EncodingTest.php b/tests/lib/Files/Storage/Wrapper/EncodingTest.php new file mode 100644 index 00000000000..cb6b6de0fb7 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/EncodingTest.php @@ -0,0 +1,239 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Files\Storage\Wrapper; + +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Encoding; + +class EncodingTest extends \Test\Files\Storage\Storage { + public const NFD_NAME = 'ümlaut'; + public const NFC_NAME = 'ümlaut'; + + /** + * @var Temporary + */ + private $sourceStorage; + + protected function setUp(): void { + parent::setUp(); + $this->sourceStorage = new Temporary([]); + $this->instance = new Encoding([ + 'storage' => $this->sourceStorage + ]); + } + + protected function tearDown(): void { + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + public static function directoryProvider(): array { + $a = parent::directoryProvider(); + $a[] = [self::NFC_NAME]; + return $a; + } + + public static function fileNameProvider(): array { + $a = parent::fileNameProvider(); + $a[] = [self::NFD_NAME . '.txt']; + return $a; + } + + public static function copyAndMoveProvider(): array { + $a = parent::copyAndMoveProvider(); + $a[] = [self::NFD_NAME . '.txt', self::NFC_NAME . '-renamed.txt']; + return $a; + } + + public static function accessNameProvider(): array { + return [ + [self::NFD_NAME], + [self::NFC_NAME], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] + public function testFputEncoding($accessName): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); + $this->assertEquals('bar', $this->instance->file_get_contents($accessName)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] + public function testFopenReadEncoding($accessName): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); + $fh = $this->instance->fopen($accessName, 'r'); + $data = fgets($fh); + fclose($fh); + $this->assertEquals('bar', $data); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] + public function testFopenOverwriteEncoding($accessName): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); + $fh = $this->instance->fopen($accessName, 'w'); + $data = fputs($fh, 'test'); + fclose($fh); + $data = $this->sourceStorage->file_get_contents(self::NFD_NAME); + $this->assertEquals('test', $data); + $this->assertFalse($this->sourceStorage->file_exists(self::NFC_NAME)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] + public function testFileExistsEncoding($accessName): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); + $this->assertTrue($this->instance->file_exists($accessName)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('accessNameProvider')] + public function testUnlinkEncoding($accessName): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'bar'); + $this->assertTrue($this->instance->unlink($accessName)); + $this->assertFalse($this->sourceStorage->file_exists(self::NFC_NAME)); + $this->assertFalse($this->sourceStorage->file_exists(self::NFD_NAME)); + } + + public function testNfcHigherPriority(): void { + $this->sourceStorage->file_put_contents(self::NFC_NAME, 'nfc'); + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'nfd'); + $this->assertEquals('nfc', $this->instance->file_get_contents(self::NFC_NAME)); + } + + public static function encodedDirectoriesProvider(): array { + return [ + [self::NFD_NAME, self::NFC_NAME], + [self::NFD_NAME . '/' . self::NFD_NAME, self::NFC_NAME . '/' . self::NFC_NAME], + [self::NFD_NAME . '/' . self::NFC_NAME . '/' . self::NFD_NAME, self::NFC_NAME . '/' . self::NFC_NAME . '/' . self::NFC_NAME], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('encodedDirectoriesProvider')] + public function testOperationInsideDirectory($sourceDir, $accessDir): void { + $this->sourceStorage->mkdir($sourceDir); + $this->instance->file_put_contents($accessDir . '/test.txt', 'bar'); + $this->assertTrue($this->instance->file_exists($accessDir . '/test.txt')); + $this->assertEquals('bar', $this->instance->file_get_contents($accessDir . '/test.txt')); + + $this->sourceStorage->file_put_contents($sourceDir . '/' . self::NFD_NAME, 'foo'); + $this->assertTrue($this->instance->file_exists($accessDir . '/' . self::NFC_NAME)); + $this->assertEquals('foo', $this->instance->file_get_contents($accessDir . '/' . self::NFC_NAME)); + + // try again to make it use cached path + $this->assertEquals('bar', $this->instance->file_get_contents($accessDir . '/test.txt')); + $this->assertTrue($this->instance->file_exists($accessDir . '/test.txt')); + $this->assertEquals('foo', $this->instance->file_get_contents($accessDir . '/' . self::NFC_NAME)); + $this->assertTrue($this->instance->file_exists($accessDir . '/' . self::NFC_NAME)); + } + + public function testCacheExtraSlash(): void { + $this->sourceStorage->file_put_contents(self::NFD_NAME, 'foo'); + $this->assertEquals(3, $this->instance->file_put_contents(self::NFC_NAME, 'bar')); + $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME)); + clearstatcache(); + $this->assertEquals(5, $this->instance->file_put_contents('/' . self::NFC_NAME, 'baric')); + $this->assertEquals('baric', $this->instance->file_get_contents(self::NFC_NAME)); + clearstatcache(); + $this->assertEquals(8, $this->instance->file_put_contents('/' . self::NFC_NAME, 'barbaric')); + $this->assertEquals('barbaric', $this->instance->file_get_contents('//' . self::NFC_NAME)); + } + + public static function sourceAndTargetDirectoryProvider(): array { + return [ + [self::NFC_NAME . '1', self::NFC_NAME . '2'], + [self::NFD_NAME . '1', self::NFC_NAME . '2'], + [self::NFC_NAME . '1', self::NFD_NAME . '2'], + [self::NFD_NAME . '1', self::NFD_NAME . '2'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sourceAndTargetDirectoryProvider')] + public function testCopyAndMoveEncodedFolder($sourceDir, $targetDir): void { + $this->sourceStorage->mkdir($sourceDir); + $this->sourceStorage->mkdir($targetDir); + $this->sourceStorage->file_put_contents($sourceDir . '/test.txt', 'bar'); + $this->assertTrue($this->instance->copy(self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test.txt')); + + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test.txt')); + $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test.txt')); + + $this->assertTrue($this->instance->rename(self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test2.txt')); + $this->assertFalse($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test2.txt')); + + $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test2.txt')); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('sourceAndTargetDirectoryProvider')] + public function testCopyAndMoveFromStorageEncodedFolder($sourceDir, $targetDir): void { + $this->sourceStorage->mkdir($sourceDir); + $this->sourceStorage->mkdir($targetDir); + $this->sourceStorage->file_put_contents($sourceDir . '/test.txt', 'bar'); + $this->assertTrue($this->instance->copyFromStorage($this->instance, self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test.txt')); + + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test.txt')); + $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test.txt')); + + $this->assertTrue($this->instance->moveFromStorage($this->instance, self::NFC_NAME . '1/test.txt', self::NFC_NAME . '2/test2.txt')); + $this->assertFalse($this->instance->file_exists(self::NFC_NAME . '1/test.txt')); + $this->assertTrue($this->instance->file_exists(self::NFC_NAME . '2/test2.txt')); + + $this->assertEquals('bar', $this->instance->file_get_contents(self::NFC_NAME . '2/test2.txt')); + } + + public function testNormalizedDirectoryEntriesOpenDir(): void { + $this->sourceStorage->mkdir('/test'); + $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); + + $this->assertTrue($this->instance->file_exists('/test/' . self::NFC_NAME)); + $this->assertTrue($this->instance->file_exists('/test/' . self::NFD_NAME)); + + $dh = $this->instance->opendir('/test'); + $content = []; + while (($file = readdir($dh)) !== false) { + if ($file != '.' and $file != '..') { + $content[] = $file; + } + } + + $this->assertCount(1, $content); + $this->assertEquals(self::NFC_NAME, $content[0]); + } + + public function testNormalizedDirectoryEntriesGetDirectoryContent(): void { + $this->sourceStorage->mkdir('/test'); + $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); + + $this->assertTrue($this->instance->file_exists('/test/' . self::NFC_NAME)); + $this->assertTrue($this->instance->file_exists('/test/' . self::NFD_NAME)); + + $content = iterator_to_array($this->instance->getDirectoryContent('/test')); + $this->assertCount(1, $content); + $this->assertEquals(self::NFC_NAME, $content[0]['name']); + } + + public function testNormalizedGetMetaData(): void { + $this->sourceStorage->mkdir('/test'); + $this->sourceStorage->mkdir('/test/' . self::NFD_NAME); + + $entry = $this->instance->getMetaData('/test/' . self::NFC_NAME); + $this->assertEquals(self::NFC_NAME, $entry['name']); + + $entry = $this->instance->getMetaData('/test/' . self::NFD_NAME); + $this->assertEquals(self::NFC_NAME, $entry['name']); + } + + /** + * Regression test of https://github.com/nextcloud/server/issues/50431 + */ + public function testNoMetadata() { + $this->assertNull($this->instance->getMetaData('/test/null')); + } + +} diff --git a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php new file mode 100644 index 00000000000..3e643714300 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php @@ -0,0 +1,1029 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Files\Storage\Wrapper; + +use Exception; +use OC\Encryption\Exceptions\ModuleDoesNotExistsException; +use OC\Encryption\File; +use OC\Encryption\Util; +use OC\Files\Cache\Cache; +use OC\Files\Cache\CacheEntry; +use OC\Files\Mount\MountPoint; +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Encryption; +use OC\Files\View; +use OC\Memcache\ArrayCache; +use OC\User\Manager; +use OCP\Encryption\IEncryptionModule; +use OCP\Encryption\IFile; +use OCP\Encryption\Keys\IStorage; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Cache\ICache; +use OCP\Files\Mount\IMountPoint; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\ITempManager; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\Files\Storage\Storage; + +class EncryptionTest extends Storage { + /** + * block size will always be 8192 for a PHP stream + * @see https://bugs.php.net/bug.php?id=21641 + */ + protected int $headerSize = 8192; + private Temporary $sourceStorage; + /** @var Encryption&MockObject */ + protected $instance; + private \OC\Encryption\Keys\Storage&MockObject $keyStore; + private Util&MockObject $util; + private \OC\Encryption\Manager&MockObject $encryptionManager; + private IEncryptionModule&MockObject $encryptionModule; + private Cache&MockObject $cache; + private LoggerInterface&MockObject $logger; + private File&MockObject $file; + private MountPoint&MockObject $mount; + private \OC\Files\Mount\Manager&MockObject $mountManager; + private \OC\Group\Manager&MockObject $groupManager; + private IConfig&MockObject $config; + private ArrayCache&MockObject $arrayCache; + /** dummy unencrypted size */ + private int $dummySize = -1; + + protected function setUp(): void { + parent::setUp(); + + $mockModule = $this->buildMockModule(); + $this->encryptionManager = $this->getMockBuilder(\OC\Encryption\Manager::class) + ->disableOriginalConstructor() + ->onlyMethods(['getEncryptionModule', 'isEnabled']) + ->getMock(); + $this->encryptionManager->expects($this->any()) + ->method('getEncryptionModule') + ->willReturn($mockModule); + + $this->arrayCache = $this->createMock(ArrayCache::class); + $this->config = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $this->groupManager = $this->getMockBuilder('\OC\Group\Manager') + ->disableOriginalConstructor() + ->getMock(); + + $this->util = $this->getMockBuilder(Util::class) + ->onlyMethods(['getUidAndFilename', 'isFile', 'isExcluded', 'stripPartialFileExtension']) + ->setConstructorArgs([new View(), new Manager( + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ), $this->groupManager, $this->config, $this->arrayCache]) + ->getMock(); + $this->util->expects($this->any()) + ->method('getUidAndFilename') + ->willReturnCallback(function ($path) { + return ['user1', $path]; + }); + $this->util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnCallback(function ($path) { + return $path; + }); + + $this->file = $this->getMockBuilder(File::class) + ->disableOriginalConstructor() + ->onlyMethods(['getAccessList']) + ->getMock(); + $this->file->expects($this->any())->method('getAccessList')->willReturn([]); + + $this->logger = $this->createMock(LoggerInterface::class); + + $this->sourceStorage = new Temporary([]); + + $this->keyStore = $this->createMock(\OC\Encryption\Keys\Storage::class); + + $this->mount = $this->getMockBuilder(MountPoint::class) + ->disableOriginalConstructor() + ->onlyMethods(['getOption']) + ->getMock(); + $this->mount->expects($this->any())->method('getOption')->willReturnCallback(function ($option, $default) { + if ($option === 'encrypt' && $default === true) { + global $mockedMountPointEncryptionEnabled; + if ($mockedMountPointEncryptionEnabled !== null) { + return $mockedMountPointEncryptionEnabled; + } + } + return true; + }); + + $this->cache = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + $this->cache->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($path) { + return ['encrypted' => false, 'path' => $path]; + }); + + $this->mountManager = $this->createMock(\OC\Files\Mount\Manager::class); + $this->mountManager->method('findByStorageId') + ->willReturn([]); + + $this->instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $this->sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $this->util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache + ] + ) + ->onlyMethods(['getMetaData', 'getCache', 'getEncryptionModule']) + ->getMock(); + + $this->instance->expects($this->any()) + ->method('getMetaData') + ->willReturnCallback(function ($path) { + return ['encrypted' => true, 'size' => $this->dummySize, 'path' => $path]; + }); + + $this->instance->expects($this->any()) + ->method('getCache') + ->willReturn($this->cache); + + $this->instance->expects($this->any()) + ->method('getEncryptionModule') + ->willReturn($mockModule); + } + + protected function buildMockModule(): IEncryptionModule&MockObject { + $this->encryptionModule = $this->getMockBuilder('\OCP\Encryption\IEncryptionModule') + ->disableOriginalConstructor() + ->onlyMethods(['getId', 'getDisplayName', 'begin', 'end', 'encrypt', 'decrypt', 'update', 'shouldEncrypt', 'getUnencryptedBlockSize', 'isReadable', 'encryptAll', 'prepareDecryptAll', 'isReadyForUser', 'needDetailedAccessList']) + ->getMock(); + + $this->encryptionModule->expects($this->any())->method('getId')->willReturn('UNIT_TEST_MODULE'); + $this->encryptionModule->expects($this->any())->method('getDisplayName')->willReturn('Unit test module'); + $this->encryptionModule->expects($this->any())->method('begin')->willReturn([]); + $this->encryptionModule->expects($this->any())->method('end')->willReturn(''); + $this->encryptionModule->expects($this->any())->method('encrypt')->willReturnArgument(0); + $this->encryptionModule->expects($this->any())->method('decrypt')->willReturnArgument(0); + $this->encryptionModule->expects($this->any())->method('update')->willReturn(true); + $this->encryptionModule->expects($this->any())->method('shouldEncrypt')->willReturn(true); + $this->encryptionModule->expects($this->any())->method('getUnencryptedBlockSize')->willReturn(8192); + $this->encryptionModule->expects($this->any())->method('isReadable')->willReturn(true); + $this->encryptionModule->expects($this->any())->method('needDetailedAccessList')->willReturn(false); + return $this->encryptionModule; + } + + /** + * + * @param string $path + * @param array $metaData + * @param bool $encrypted + * @param bool $unencryptedSizeSet + * @param int $storedUnencryptedSize + * @param array $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetMetaData')] + public function testGetMetaData($path, $metaData, $encrypted, $unencryptedSizeSet, $storedUnencryptedSize, $expected): void { + $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') + ->disableOriginalConstructor()->getMock(); + + $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + $cache->expects($this->any()) + ->method('get') + ->willReturnCallback( + function ($path) use ($encrypted) { + return new CacheEntry(['encrypted' => $encrypted, 'path' => $path, 'size' => 0, 'fileid' => 1]); + } + ); + + $this->instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $this->util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ] + ) + ->onlyMethods(['getCache', 'verifyUnencryptedSize']) + ->getMock(); + + if ($unencryptedSizeSet) { + $this->invokePrivate($this->instance, 'unencryptedSize', [[$path => $storedUnencryptedSize]]); + } + + $fileEntry = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + $sourceStorage->expects($this->once())->method('getMetaData')->with($path) + ->willReturn($metaData); + $sourceStorage->expects($this->any()) + ->method('getCache') + ->with($path) + ->willReturn($fileEntry); + if ($metaData !== null) { + $fileEntry->expects($this->any()) + ->method('get') + ->with($metaData['fileid']); + } + + $this->instance->expects($this->any())->method('getCache')->willReturn($cache); + if ($expected !== null) { + $this->instance->expects($this->any())->method('verifyUnencryptedSize') + ->with($path, 0)->willReturn($expected['size']); + } + + $result = $this->instance->getMetaData($path); + if (isset($expected['encrypted'])) { + $this->assertSame($expected['encrypted'], (bool)$result['encrypted']); + + if (isset($expected['encryptedVersion'])) { + $this->assertSame($expected['encryptedVersion'], $result['encryptedVersion']); + } + } + + if ($expected !== null) { + $this->assertSame($expected['size'], $result['size']); + } else { + $this->assertSame(null, $result); + } + } + + public static function dataTestGetMetaData(): array { + return [ + ['/test.txt', ['size' => 42, 'encrypted' => 2, 'encryptedVersion' => 2, 'fileid' => 1], true, true, 12, ['size' => 12, 'encrypted' => true, 'encryptedVersion' => 2]], + ['/test.txt', null, true, true, 12, null], + ['/test.txt', ['size' => 42, 'encrypted' => 0, 'fileid' => 1], false, false, 12, ['size' => 42, 'encrypted' => false]], + ['/test.txt', ['size' => 42, 'encrypted' => false, 'fileid' => 1], true, false, 12, ['size' => 12, 'encrypted' => true]] + ]; + } + + public function testFilesize(): void { + $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + $cache->expects($this->any()) + ->method('get') + ->willReturn(new CacheEntry(['encrypted' => true, 'path' => '/test.txt', 'size' => 0, 'fileid' => 1])); + + $this->instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $this->sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $this->util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ] + ) + ->onlyMethods(['getCache', 'verifyUnencryptedSize']) + ->getMock(); + + $this->instance->expects($this->any())->method('getCache')->willReturn($cache); + $this->instance->expects($this->any())->method('verifyUnencryptedSize') + ->willReturn(42); + + + $this->assertSame(42, + $this->instance->filesize('/test.txt') + ); + } + + /** + * + * @param int $encryptedSize + * @param int $unencryptedSize + * @param bool $failure + * @param int $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestVerifyUnencryptedSize')] + public function testVerifyUnencryptedSize($encryptedSize, $unencryptedSize, $failure, $expected): void { + $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') + ->disableOriginalConstructor()->getMock(); + + $this->instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $this->util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ] + ) + ->onlyMethods(['fixUnencryptedSize']) + ->getMock(); + + $sourceStorage->expects($this->once())->method('filesize')->willReturn($encryptedSize); + + $this->instance->expects($this->any())->method('fixUnencryptedSize') + ->with('/test.txt', $encryptedSize, $unencryptedSize) + ->willReturnCallback( + function () use ($failure, $expected) { + if ($failure) { + throw new Exception(); + } else { + return $expected; + } + } + ); + + $this->assertSame( + $expected, + $this->invokePrivate($this->instance, 'verifyUnencryptedSize', ['/test.txt', $unencryptedSize]) + ); + } + + public static function dataTestVerifyUnencryptedSize(): array { + return [ + [120, 80, false, 80], + [120, 120, false, 80], + [120, -1, false, 80], + [120, -1, true, -1] + ]; + } + + /** + * + * @param string $source + * @param string $target + * @param $encryptionEnabled + * @param boolean $renameKeysReturn + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyAndRename')] + public function testRename($source, + $target, + $encryptionEnabled, + $renameKeysReturn): void { + if ($encryptionEnabled) { + $this->keyStore + ->expects($this->once()) + ->method('renameKeys') + ->willReturn($renameKeysReturn); + } else { + $this->keyStore + ->expects($this->never())->method('renameKeys'); + } + $this->util->expects($this->any()) + ->method('isFile')->willReturn(true); + $this->encryptionManager->expects($this->once()) + ->method('isEnabled')->willReturn($encryptionEnabled); + + $this->instance->mkdir($source); + $this->instance->mkdir(dirname($target)); + $this->instance->rename($source, $target); + } + + public function testCopyEncryption(): void { + $this->instance->file_put_contents('source.txt', 'bar'); + $this->instance->copy('source.txt', 'target.txt'); + $this->assertSame('bar', $this->instance->file_get_contents('target.txt')); + $targetMeta = $this->instance->getMetaData('target.txt'); + $sourceMeta = $this->instance->getMetaData('source.txt'); + $this->assertSame($sourceMeta['encrypted'], $targetMeta['encrypted']); + $this->assertSame($sourceMeta['size'], $targetMeta['size']); + } + + /** + * data provider for testCopyTesting() and dataTestCopyAndRename() + * + * @return array + */ + public static function dataTestCopyAndRename(): array { + return [ + ['source', 'target', true, false, false], + ['source', 'target', true, true, false], + ['source', '/subFolder/target', true, false, false], + ['source', '/subFolder/target', true, true, true], + ['source', '/subFolder/target', false, true, false], + ]; + } + + public function testIsLocal(): void { + $this->encryptionManager->expects($this->once()) + ->method('isEnabled')->willReturn(true); + $this->assertFalse($this->instance->isLocal()); + } + + /** + * + * @param string $path + * @param boolean $rmdirResult + * @param boolean $isExcluded + * @param boolean $encryptionEnabled + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestRmdir')] + public function testRmdir($path, $rmdirResult, $isExcluded, $encryptionEnabled): void { + $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') + ->disableOriginalConstructor()->getMock(); + + $util = $this->getMockBuilder('\OC\Encryption\Util')->disableOriginalConstructor()->getMock(); + + $sourceStorage->expects($this->once())->method('rmdir')->willReturn($rmdirResult); + $util->expects($this->any())->method('isExcluded')->willReturn($isExcluded); + $this->encryptionManager->expects($this->any())->method('isEnabled')->willReturn($encryptionEnabled); + + $encryptionStorage = new Encryption( + [ + 'storage' => $sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/mountPoint', + 'mount' => $this->mount + ], + $this->encryptionManager, + $util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ); + + + if ($rmdirResult === true && $isExcluded === false && $encryptionEnabled === true) { + $this->keyStore->expects($this->once())->method('deleteAllFileKeys')->with('/mountPoint' . $path); + } else { + $this->keyStore->expects($this->never())->method('deleteAllFileKeys'); + } + + $encryptionStorage->rmdir($path); + } + + public static function dataTestRmdir(): array { + return [ + ['/file.txt', true, true, true], + ['/file.txt', false, true, true], + ['/file.txt', true, false, true], + ['/file.txt', false, false, true], + ['/file.txt', true, true, false], + ['/file.txt', false, true, false], + ['/file.txt', true, false, false], + ['/file.txt', false, false, false], + ]; + } + + /** + * + * @param boolean $excluded + * @param boolean $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyKeys')] + public function testCopyKeys($excluded, $expected): void { + $this->util->expects($this->once()) + ->method('isExcluded') + ->willReturn($excluded); + + if ($excluded) { + $this->keyStore->expects($this->never())->method('copyKeys'); + } else { + $this->keyStore->expects($this->once())->method('copyKeys')->willReturn(true); + } + + $this->assertSame($expected, + self::invokePrivate($this->instance, 'copyKeys', ['/source', '/target']) + ); + } + + public static function dataTestCopyKeys(): array { + return [ + [true, false], + [false, true], + ]; + } + + /** + * + * @param string $path + * @param bool $strippedPathExists + * @param string $strippedPath + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetHeader')] + public function testGetHeader($path, $strippedPathExists, $strippedPath): void { + $sourceStorage = $this->getMockBuilder('\OC\Files\Storage\Storage') + ->disableOriginalConstructor()->getMock(); + + $util = $this->getMockBuilder('\OC\Encryption\Util') + ->setConstructorArgs( + [ + new View(), + new Manager( + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ), + $this->groupManager, + $this->config, + $this->arrayCache + ] + )->getMock(); + + $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + $cache->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($path) { + return ['encrypted' => true, 'path' => $path]; + }); + + $instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ] + ) + ->onlyMethods(['getCache', 'readFirstBlock']) + ->getMock(); + + $instance->method('getCache')->willReturn($cache); + + $util->method('parseRawHeader') + ->willReturn([Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']); + + if ($strippedPathExists) { + $instance->method('readFirstBlock') + ->with($strippedPath)->willReturn(''); + } else { + $instance->method('readFirstBlock') + ->with($path)->willReturn(''); + } + + $util->expects($this->once())->method('stripPartialFileExtension') + ->with($path)->willReturn($strippedPath); + $sourceStorage->expects($this->once()) + ->method('is_file') + ->with($strippedPath) + ->willReturn($strippedPathExists); + + $this->invokePrivate($instance, 'getHeader', [$path]); + } + + public static function dataTestGetHeader(): array { + return [ + ['/foo/bar.txt', false, '/foo/bar.txt'], + ['/foo/bar.txt.part', false, '/foo/bar.txt'], + ['/foo/bar.txt.ocTransferId7437493.part', false, '/foo/bar.txt'], + ['/foo/bar.txt.part', true, '/foo/bar.txt'], + ['/foo/bar.txt.ocTransferId7437493.part', true, '/foo/bar.txt'], + ]; + } + + /** + * test if getHeader adds the default module correctly to the header for + * legacy files + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestGetHeaderAddLegacyModule')] + public function testGetHeaderAddLegacyModule($header, $isEncrypted, $strippedPathExists, $expected): void { + $sourceStorage = $this->getMockBuilder(\OC\Files\Storage\Storage::class) + ->disableOriginalConstructor()->getMock(); + + $sourceStorage->expects($this->once()) + ->method('is_file') + ->with('test.txt') + ->willReturn($strippedPathExists); + + $util = $this->getMockBuilder(Util::class) + ->onlyMethods(['stripPartialFileExtension', 'parseRawHeader']) + ->setConstructorArgs([new View(), new Manager( + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ), $this->groupManager, $this->config, $this->arrayCache]) + ->getMock(); + $util->expects($this->any()) + ->method('stripPartialFileExtension') + ->willReturnCallback(function ($path) { + return $path; + }); + + $cache = $this->createMock(Cache::class); + $cache->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($path) use ($isEncrypted) { + return ['encrypted' => $isEncrypted, 'path' => $path]; + }); + + $instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $sourceStorage, + 'root' => 'foo', + 'mountPoint' => '/', + 'mount' => $this->mount + ], + $this->encryptionManager, + $util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache, + ] + ) + ->onlyMethods(['readFirstBlock', 'getCache']) + ->getMock(); + + $instance->method('readFirstBlock')->willReturn(''); + + $util->method(('parseRawHeader'))->willReturn($header); + $instance->method('getCache')->willReturn($cache); + + $result = $this->invokePrivate($instance, 'getHeader', ['test.txt']); + $this->assertSameSize($expected, $result); + foreach ($result as $key => $value) { + $this->assertArrayHasKey($key, $expected); + $this->assertSame($expected[$key], $value); + } + } + + public static function dataTestGetHeaderAddLegacyModule(): array { + return [ + [['cipher' => 'AES-128'], true, true, ['cipher' => 'AES-128', Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']], + [[], true, false, []], + [[], true, true, [Util::HEADER_ENCRYPTION_MODULE_KEY => 'OC_DEFAULT_MODULE']], + [[], false, true, []], + ]; + } + + public static function dataCopyBetweenStorage(): array { + return [ + [true, true, true], + [true, false, false], + [false, true, false], + [false, false, false], + ]; + } + + public function testCopyBetweenStorageMinimumEncryptedVersion(): void { + $storage2 = $this->createMock(\OC\Files\Storage\Storage::class); + + $sourceInternalPath = $targetInternalPath = 'file.txt'; + $preserveMtime = $isRename = false; + + $storage2->expects($this->any()) + ->method('fopen') + ->willReturnCallback(function ($path, $mode) { + $temp = Server::get(ITempManager::class); + return fopen($temp->getTemporaryFile(), $mode); + }); + $storage2->method('getId') + ->willReturn('stroage2'); + $cache = $this->createMock(ICache::class); + $cache->expects($this->once()) + ->method('get') + ->with($sourceInternalPath) + ->willReturn(['encryptedVersion' => 0]); + $storage2->expects($this->once()) + ->method('getCache') + ->willReturn($cache); + $this->encryptionManager->expects($this->any()) + ->method('isEnabled') + ->willReturn(true); + global $mockedMountPointEncryptionEnabled; + $mockedMountPointEncryptionEnabled = true; + + $expectedCachePut = [ + 'encrypted' => true, + ]; + $expectedCachePut['encryptedVersion'] = 1; + + $this->cache->expects($this->once()) + ->method('put') + ->with($sourceInternalPath, $expectedCachePut); + + $this->invokePrivate($this->instance, 'copyBetweenStorage', [$storage2, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename]); + + $this->assertFalse(false); + } + + /** + * + * @param bool $encryptionEnabled + * @param bool $mountPointEncryptionEnabled + * @param bool $expectedEncrypted + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataCopyBetweenStorage')] + public function testCopyBetweenStorage($encryptionEnabled, $mountPointEncryptionEnabled, $expectedEncrypted): void { + $storage2 = $this->createMock(\OC\Files\Storage\Storage::class); + + $sourceInternalPath = $targetInternalPath = 'file.txt'; + $preserveMtime = $isRename = false; + + $storage2->expects($this->any()) + ->method('fopen') + ->willReturnCallback(function ($path, $mode) { + $temp = Server::get(ITempManager::class); + return fopen($temp->getTemporaryFile(), $mode); + }); + $storage2->method('getId') + ->willReturn('stroage2'); + if ($expectedEncrypted) { + $cache = $this->createMock(ICache::class); + $cache->expects($this->once()) + ->method('get') + ->with($sourceInternalPath) + ->willReturn(['encryptedVersion' => 12345]); + $storage2->expects($this->once()) + ->method('getCache') + ->willReturn($cache); + } + $this->encryptionManager->expects($this->any()) + ->method('isEnabled') + ->willReturn($encryptionEnabled); + // FIXME can not overwrite the return after definition + // $this->mount->expects($this->at(0)) + // ->method('getOption') + // ->with('encrypt', true) + // ->willReturn($mountPointEncryptionEnabled); + global $mockedMountPointEncryptionEnabled; + $mockedMountPointEncryptionEnabled = $mountPointEncryptionEnabled; + + $expectedCachePut = [ + 'encrypted' => $expectedEncrypted, + ]; + if ($expectedEncrypted === true) { + $expectedCachePut['encryptedVersion'] = 1; + } + + $this->arrayCache->expects($this->never())->method('set'); + + $this->cache->expects($this->once()) + ->method('put') + ->with($sourceInternalPath, $expectedCachePut); + + $this->invokePrivate($this->instance, 'copyBetweenStorage', [$storage2, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename]); + + $this->assertFalse(false); + } + + /** + * + * @param string $sourceInternalPath + * @param string $targetInternalPath + * @param bool $copyResult + * @param bool $encrypted + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestCopyBetweenStorageVersions')] + public function testCopyBetweenStorageVersions($sourceInternalPath, $targetInternalPath, $copyResult, $encrypted): void { + $sourceStorage = $this->createMock(\OC\Files\Storage\Storage::class); + + $targetStorage = $this->createMock(\OC\Files\Storage\Storage::class); + + $cache = $this->getMockBuilder('\OC\Files\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + + $mountPoint = '/mountPoint'; + + /** @var Encryption |MockObject $instance */ + $instance = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + [ + 'storage' => $targetStorage, + 'root' => 'foo', + 'mountPoint' => $mountPoint, + 'mount' => $this->mount + ], + $this->encryptionManager, + $this->util, + $this->logger, + $this->file, + null, + $this->keyStore, + $this->mountManager, + $this->arrayCache + ] + ) + ->onlyMethods(['updateUnencryptedSize', 'getCache']) + ->getMock(); + + $targetStorage->expects($this->once())->method('copyFromStorage') + ->with($sourceStorage, $sourceInternalPath, $targetInternalPath) + ->willReturn($copyResult); + + $instance->expects($this->any())->method('getCache') + ->willReturn($cache); + + $this->arrayCache->expects($this->once())->method('set') + ->with('encryption_copy_version_' . $sourceInternalPath, true); + + if ($copyResult) { + $cache->expects($this->once())->method('get') + ->with($sourceInternalPath) + ->willReturn(new CacheEntry(['encrypted' => $encrypted, 'size' => 42])); + if ($encrypted) { + $instance->expects($this->once())->method('updateUnencryptedSize') + ->with($mountPoint . $targetInternalPath, 42); + } else { + $instance->expects($this->never())->method('updateUnencryptedSize'); + } + } else { + $instance->expects($this->never())->method('updateUnencryptedSize'); + } + + $result = $this->invokePrivate( + $instance, + 'copyBetweenStorage', + [ + $sourceStorage, + $sourceInternalPath, + $targetInternalPath, + false, + false + ] + ); + + $this->assertSame($copyResult, $result); + } + + public static function dataTestCopyBetweenStorageVersions(): array { + return [ + ['/files/foo.txt', '/files_versions/foo.txt.768743', true, true], + ['/files/foo.txt', '/files_versions/foo.txt.768743', true, false], + ['/files/foo.txt', '/files_versions/foo.txt.768743', false, true], + ['/files/foo.txt', '/files_versions/foo.txt.768743', false, false], + ['/files_versions/foo.txt.6487634', '/files/foo.txt', true, true], + ['/files_versions/foo.txt.6487634', '/files/foo.txt', true, false], + ['/files_versions/foo.txt.6487634', '/files/foo.txt', false, true], + ['/files_versions/foo.txt.6487634', '/files/foo.txt', false, false], + + ]; + } + + /** + * @param string $path + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestIsVersion')] + public function testIsVersion($path, $expected): void { + $this->assertSame($expected, + $this->invokePrivate($this->instance, 'isVersion', [$path]) + ); + } + + public static function dataTestIsVersion(): array { + return [ + ['files_versions/foo', true], + ['/files_versions/foo', true], + ['//files_versions/foo', true], + ['files/versions/foo', false], + ['files/files_versions/foo', false], + ['files_versions_test/foo', false], + ]; + } + + /** + * + * @param bool $encryptMountPoint + * @param mixed $encryptionModule + * @param bool $encryptionModuleShouldEncrypt + * @param bool $expected + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataTestShouldEncrypt')] + public function testShouldEncrypt( + $encryptMountPoint, + $encryptionModule, + $encryptionModuleShouldEncrypt, + $expected, + ): void { + $encryptionManager = $this->createMock(\OC\Encryption\Manager::class); + $util = $this->createMock(Util::class); + $fileHelper = $this->createMock(IFile::class); + $keyStorage = $this->createMock(IStorage::class); + $mountManager = $this->createMock(\OC\Files\Mount\Manager::class); + $mount = $this->createMock(IMountPoint::class); + $arrayCache = $this->createMock(ArrayCache::class); + $path = '/welcome.txt'; + $fullPath = 'admin/files/welcome.txt'; + $defaultEncryptionModule = $this->createMock(IEncryptionModule::class); + + $wrapper = $this->getMockBuilder(Encryption::class) + ->setConstructorArgs( + [ + ['mountPoint' => '', 'mount' => $mount, 'storage' => ''], + $encryptionManager, + $util, + $this->logger, + $fileHelper, + null, + $keyStorage, + $mountManager, + $arrayCache + ] + ) + ->onlyMethods(['getFullPath', 'getEncryptionModule']) + ->getMock(); + + if ($encryptionModule === true) { + /** @var IEncryptionModule|MockObject $encryptionModule */ + $encryptionModule = $this->createMock(IEncryptionModule::class); + } + + $wrapper->method('getFullPath')->with($path)->willReturn($fullPath); + $wrapper->expects($encryptMountPoint ? $this->once() : $this->never()) + ->method('getEncryptionModule') + ->with($fullPath) + ->willReturnCallback( + function () use ($encryptionModule) { + if ($encryptionModule === false) { + throw new ModuleDoesNotExistsException(); + } + return $encryptionModule; + } + ); + $mount->expects($this->once())->method('getOption')->with('encrypt', true) + ->willReturn($encryptMountPoint); + + if ($encryptionModule !== null && $encryptionModule !== false) { + $encryptionModule + ->method('shouldEncrypt') + ->with($fullPath) + ->willReturn($encryptionModuleShouldEncrypt); + } + + if ($encryptionModule === null) { + $encryptionManager->expects($this->once()) + ->method('getEncryptionModule') + ->willReturn($defaultEncryptionModule); + } + $defaultEncryptionModule->method('shouldEncrypt')->willReturn(true); + + $result = $this->invokePrivate($wrapper, 'shouldEncrypt', [$path]); + + $this->assertSame($expected, $result); + } + + public static function dataTestShouldEncrypt(): array { + return [ + [false, false, false, false], + [true, false, false, false], + [true, true, false, false], + [true, true, true, true], + [true, null, false, true], + ]; + } +} diff --git a/tests/lib/Files/Storage/Wrapper/JailTest.php b/tests/lib/Files/Storage/Wrapper/JailTest.php new file mode 100644 index 00000000000..0043e37ba33 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/JailTest.php @@ -0,0 +1,54 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Files\Storage\Wrapper; + +use OC\Files\Filesystem; +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\Jail; + +class JailTest extends \Test\Files\Storage\Storage { + /** + * @var Temporary + */ + private $sourceStorage; + + protected function setUp(): void { + parent::setUp(); + $this->sourceStorage = new Temporary([]); + $this->sourceStorage->mkdir('foo'); + $this->instance = new Jail([ + 'storage' => $this->sourceStorage, + 'root' => 'foo' + ]); + } + + protected function tearDown(): void { + // test that nothing outside our jail is touched + $contents = []; + $dh = $this->sourceStorage->opendir(''); + while (($file = readdir($dh)) !== false) { + if (!Filesystem::isIgnoredDir($file)) { + $contents[] = $file; + } + } + $this->assertEquals(['foo'], $contents); + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + public function testMkDirRooted(): void { + $this->instance->mkdir('bar'); + $this->assertTrue($this->sourceStorage->is_dir('foo/bar')); + } + + public function testFilePutContentsRooted(): void { + $this->instance->file_put_contents('bar', 'asd'); + $this->assertEquals('asd', $this->sourceStorage->file_get_contents('foo/bar')); + } +} diff --git a/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php new file mode 100644 index 00000000000..b1b5582b4ed --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/KnownMtimeTest.php @@ -0,0 +1,69 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Files\Storage\Wrapper; + +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\KnownMtime; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Clock\ClockInterface; +use Test\Files\Storage\Storage; + +/** + * @group DB + */ +class KnownMtimeTest extends Storage { + /** @var Temporary */ + private $sourceStorage; + + /** @var ClockInterface|MockObject */ + private $clock; + private int $fakeTime = 0; + + protected function setUp(): void { + parent::setUp(); + $this->fakeTime = 0; + $this->sourceStorage = new Temporary([]); + $this->clock = $this->createMock(ClockInterface::class); + $this->clock->method('now')->willReturnCallback(function () { + if ($this->fakeTime) { + return new \DateTimeImmutable("@{$this->fakeTime}"); + } else { + return new \DateTimeImmutable(); + } + }); + $this->instance = $this->getWrappedStorage(); + } + + protected function tearDown(): void { + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + protected function getWrappedStorage() { + return new KnownMtime([ + 'storage' => $this->sourceStorage, + 'clock' => $this->clock, + ]); + } + + public function testNewerKnownMtime(): void { + $future = time() + 1000; + $this->fakeTime = $future; + + $this->instance->file_put_contents('foo.txt', 'bar'); + + // fuzzy match since the clock might have ticked + $this->assertLessThan(2, abs(time() - $this->sourceStorage->filemtime('foo.txt'))); + $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->stat('foo.txt')['mtime']); + $this->assertEquals($this->sourceStorage->filemtime('foo.txt'), $this->sourceStorage->getMetaData('foo.txt')['mtime']); + + $this->assertEquals($future, $this->instance->filemtime('foo.txt')); + $this->assertEquals($future, $this->instance->stat('foo.txt')['mtime']); + $this->assertEquals($future, $this->instance->getMetaData('foo.txt')['mtime']); + } +} diff --git a/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php b/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php new file mode 100644 index 00000000000..a2f3460c58c --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/PermissionsMaskTest.php @@ -0,0 +1,180 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Files\Storage\Wrapper; + +use OC\Files\Storage\Temporary; +use OC\Files\Storage\Wrapper\PermissionsMask; +use OC\Files\Storage\Wrapper\Wrapper; +use OCP\Constants; +use OCP\Files\Cache\IScanner; + +/** + * @group DB + */ +class PermissionsMaskTest extends \Test\Files\Storage\Storage { + /** + * @var Temporary + */ + private $sourceStorage; + + protected function setUp(): void { + parent::setUp(); + $this->sourceStorage = new Temporary([]); + $this->instance = $this->getMaskedStorage(Constants::PERMISSION_ALL); + } + + protected function tearDown(): void { + $this->sourceStorage->cleanUp(); + parent::tearDown(); + } + + protected function getMaskedStorage($mask) { + return new PermissionsMask([ + 'storage' => $this->sourceStorage, + 'mask' => $mask + ]); + } + + public function testMkdirNoCreate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->mkdir('foo')); + $this->assertFalse($storage->file_exists('foo')); + } + + public function testRmdirNoDelete(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); + $this->assertTrue($storage->mkdir('foo')); + $this->assertTrue($storage->file_exists('foo')); + $this->assertFalse($storage->rmdir('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testTouchNewFileNoCreate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->touch('foo')); + $this->assertFalse($storage->file_exists('foo')); + } + + public function testTouchNewFileNoUpdate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertTrue($storage->touch('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testTouchExistingFileNoUpdate(): void { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->touch('foo')); + } + + public function testUnlinkNoDelete(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_DELETE); + $this->assertTrue($storage->touch('foo')); + $this->assertTrue($storage->file_exists('foo')); + $this->assertFalse($storage->unlink('foo')); + $this->assertTrue($storage->file_exists('foo')); + } + + public function testPutContentsNewFileNoUpdate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertEquals(3, $storage->file_put_contents('foo', 'bar')); + $this->assertEquals('bar', $storage->file_get_contents('foo')); + } + + public function testPutContentsNewFileNoCreate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->file_put_contents('foo', 'bar')); + } + + public function testPutContentsExistingFileNoUpdate(): void { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->file_put_contents('foo', 'bar')); + } + + public function testFopenExistingFileNoUpdate(): void { + $this->sourceStorage->touch('foo'); + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_UPDATE); + $this->assertFalse($storage->fopen('foo', 'w')); + } + + public function testFopenNewFileNoCreate(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE); + $this->assertFalse($storage->fopen('foo', 'w')); + } + + public function testScanNewFiles(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE); + $storage->file_put_contents('foo', 'bar'); + $storage->getScanner()->scan(''); + + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); + $this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions()); + } + + public function testScanNewWrappedFiles(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE); + $wrappedStorage = new Wrapper(['storage' => $storage]); + $wrappedStorage->file_put_contents('foo', 'bar'); + $wrappedStorage->getScanner()->scan(''); + + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); + $this->assertEquals(Constants::PERMISSION_READ, $storage->getCache()->get('foo')->getPermissions()); + } + + public function testScanNewFilesNested(): void { + $storage = $this->getMaskedStorage(Constants::PERMISSION_READ + Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE); + $nestedStorage = new PermissionsMask([ + 'storage' => $storage, + 'mask' => Constants::PERMISSION_READ + Constants::PERMISSION_CREATE + ]); + $wrappedStorage = new Wrapper(['storage' => $nestedStorage]); + $wrappedStorage->file_put_contents('foo', 'bar'); + $wrappedStorage->getScanner()->scan(''); + + $this->assertEquals(Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE, $this->sourceStorage->getCache()->get('foo')->getPermissions()); + $this->assertEquals(Constants::PERMISSION_READ + Constants::PERMISSION_UPDATE, $storage->getCache()->get('foo')->getPermissions()); + $this->assertEquals(Constants::PERMISSION_READ, $wrappedStorage->getCache()->get('foo')->getPermissions()); + } + + public function testScanUnchanged(): void { + $this->sourceStorage->mkdir('foo'); + $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar'); + + $this->sourceStorage->getScanner()->scan('foo'); + + $storage = $this->getMaskedStorage(Constants::PERMISSION_READ); + $scanner = $storage->getScanner(); + $called = false; + $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called): void { + $called = true; + }); + $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE); + + $this->assertFalse($called); + } + + public function testScanUnchangedWrapped(): void { + $this->sourceStorage->mkdir('foo'); + $this->sourceStorage->file_put_contents('foo/bar.txt', 'bar'); + + $this->sourceStorage->getScanner()->scan('foo'); + + $storage = $this->getMaskedStorage(Constants::PERMISSION_READ); + $wrappedStorage = new Wrapper(['storage' => $storage]); + $scanner = $wrappedStorage->getScanner(); + $called = false; + $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function () use (&$called): void { + $called = true; + }); + $scanner->scan('foo', IScanner::SCAN_RECURSIVE, IScanner::REUSE_ETAG | IScanner::REUSE_SIZE); + + $this->assertFalse($called); + } +} diff --git a/tests/lib/Files/Storage/Wrapper/QuotaTest.php b/tests/lib/Files/Storage/Wrapper/QuotaTest.php new file mode 100644 index 00000000000..2878fe6ca92 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/QuotaTest.php @@ -0,0 +1,232 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Files\Storage\Wrapper; + +//ensure the constants are loaded +use OC\Files\Cache\CacheEntry; +use OC\Files\Storage\Local; +use OC\Files\Storage\Wrapper\Quota; +use OCP\Files; +use OCP\ITempManager; +use OCP\Server; + +/** + * Class QuotaTest + * + * @group DB + * + * @package Test\Files\Storage\Wrapper + */ +class QuotaTest extends \Test\Files\Storage\Storage { + /** + * @var string tmpDir + */ + private $tmpDir; + + protected function setUp(): void { + parent::setUp(); + + $this->tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); + $storage = new Local(['datadir' => $this->tmpDir]); + $this->instance = new Quota(['storage' => $storage, 'quota' => 10000000]); + } + + protected function tearDown(): void { + Files::rmdirr($this->tmpDir); + parent::tearDown(); + } + + /** + * @param integer $limit + */ + protected function getLimitedStorage($limit) { + $storage = new Local(['datadir' => $this->tmpDir]); + $storage->mkdir('files'); + $storage->getScanner()->scan(''); + return new Quota(['storage' => $storage, 'quota' => $limit]); + } + + public function testFilePutContentsNotEnoughSpace(): void { + $instance = $this->getLimitedStorage(3); + $this->assertFalse($instance->file_put_contents('files/foo', 'foobar')); + } + + public function testCopyNotEnoughSpace(): void { + $instance = $this->getLimitedStorage(9); + $this->assertEquals(6, $instance->file_put_contents('files/foo', 'foobar')); + $instance->getScanner()->scan(''); + $this->assertFalse($instance->copy('files/foo', 'files/bar')); + } + + public function testFreeSpace(): void { + $instance = $this->getLimitedStorage(9); + $this->assertEquals(9, $instance->free_space('')); + } + + public function testFreeSpaceWithUsedSpace(): void { + $instance = $this->getLimitedStorage(9); + $instance->getCache()->put( + '', ['size' => 3] + ); + $this->assertEquals(6, $instance->free_space('')); + } + + public function testFreeSpaceWithUnknownDiskSpace(): void { + $storage = $this->getMockBuilder(Local::class) + ->onlyMethods(['free_space']) + ->setConstructorArgs([['datadir' => $this->tmpDir]]) + ->getMock(); + $storage->expects($this->any()) + ->method('free_space') + ->willReturn(-2); + $storage->getScanner()->scan(''); + + $instance = new Quota(['storage' => $storage, 'quota' => 9]); + $instance->getCache()->put( + '', ['size' => 3] + ); + $this->assertEquals(6, $instance->free_space('')); + } + + public function testFreeSpaceWithUsedSpaceAndEncryption(): void { + $instance = $this->getLimitedStorage(9); + $instance->getCache()->put( + '', ['size' => 7] + ); + $this->assertEquals(2, $instance->free_space('')); + } + + public function testFWriteNotEnoughSpace(): void { + $instance = $this->getLimitedStorage(9); + $stream = $instance->fopen('files/foo', 'w+'); + $this->assertEquals(6, fwrite($stream, 'foobar')); + $this->assertEquals(3, fwrite($stream, 'qwerty')); + fclose($stream); + $this->assertEquals('foobarqwe', $instance->file_get_contents('files/foo')); + } + + public function testStreamCopyWithEnoughSpace(): void { + $instance = $this->getLimitedStorage(16); + $inputStream = fopen('data://text/plain,foobarqwerty', 'r'); + $outputStream = $instance->fopen('files/foo', 'w+'); + [$count, $result] = \OC_Helper::streamCopy($inputStream, $outputStream); + $this->assertEquals(12, $count); + $this->assertTrue($result); + fclose($inputStream); + fclose($outputStream); + } + + public function testStreamCopyNotEnoughSpace(): void { + $instance = $this->getLimitedStorage(9); + $inputStream = fopen('data://text/plain,foobarqwerty', 'r'); + $outputStream = $instance->fopen('files/foo', 'w+'); + [$count, $result] = \OC_Helper::streamCopy($inputStream, $outputStream); + $this->assertEquals(9, $count); + $this->assertFalse($result); + fclose($inputStream); + fclose($outputStream); + } + + public function testReturnFalseWhenFopenFailed(): void { + $failStorage = $this->getMockBuilder(Local::class) + ->onlyMethods(['fopen']) + ->setConstructorArgs([['datadir' => $this->tmpDir]]) + ->getMock(); + $failStorage->expects($this->any()) + ->method('fopen') + ->willReturn(false); + + $instance = new Quota(['storage' => $failStorage, 'quota' => 1000]); + + $this->assertFalse($instance->fopen('failedfopen', 'r')); + } + + public function testReturnRegularStreamOnRead(): void { + $instance = $this->getLimitedStorage(9); + + // create test file first + $stream = $instance->fopen('files/foo', 'w+'); + fwrite($stream, 'blablacontent'); + fclose($stream); + + $stream = $instance->fopen('files/foo', 'r'); + $meta = stream_get_meta_data($stream); + $this->assertEquals('plainfile', $meta['wrapper_type']); + fclose($stream); + + $stream = $instance->fopen('files/foo', 'rb'); + $meta = stream_get_meta_data($stream); + $this->assertEquals('plainfile', $meta['wrapper_type']); + fclose($stream); + } + + public function testReturnRegularStreamWhenOutsideFiles(): void { + $instance = $this->getLimitedStorage(9); + $instance->mkdir('files_other'); + + // create test file first + $stream = $instance->fopen('files_other/foo', 'w+'); + $meta = stream_get_meta_data($stream); + $this->assertEquals('plainfile', $meta['wrapper_type']); + fclose($stream); + } + + public function testReturnQuotaStreamOnWrite(): void { + $instance = $this->getLimitedStorage(9); + $stream = $instance->fopen('files/foo', 'w+'); + $meta = stream_get_meta_data($stream); + $expected_type = 'user-space'; + $this->assertEquals($expected_type, $meta['wrapper_type']); + fclose($stream); + } + + public function testSpaceRoot(): void { + $storage = $this->getMockBuilder(Local::class)->disableOriginalConstructor()->getMock(); + $cache = $this->getMockBuilder('\OC\Files\Cache\Cache')->disableOriginalConstructor()->getMock(); + $storage->expects($this->once()) + ->method('getCache') + ->willReturn($cache); + $storage->expects($this->once()) + ->method('free_space') + ->willReturn(2048); + $cache->expects($this->once()) + ->method('get') + ->with('files') + ->willReturn(new CacheEntry(['size' => 50])); + + $instance = new Quota(['storage' => $storage, 'quota' => 1024, 'root' => 'files']); + + $this->assertEquals(1024 - 50, $instance->free_space('')); + } + + public function testInstanceOfStorageWrapper(): void { + $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Local')); + $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Wrapper')); + $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')); + } + + public function testNoMkdirQuotaZero(): void { + $instance = $this->getLimitedStorage(0.0); + $this->assertFalse($instance->mkdir('files')); + $this->assertFalse($instance->mkdir('files/foobar')); + } + + public function testMkdirQuotaZeroTrashbin(): void { + $instance = $this->getLimitedStorage(0.0); + $this->assertTrue($instance->mkdir('files_trashbin')); + $this->assertTrue($instance->mkdir('files_trashbin/files')); + $this->assertTrue($instance->mkdir('files_versions')); + $this->assertTrue($instance->mkdir('cache')); + } + + public function testNoTouchQuotaZero(): void { + $instance = $this->getLimitedStorage(0.0); + $this->assertFalse($instance->touch('foobar')); + } +} diff --git a/tests/lib/Files/Storage/Wrapper/WrapperTest.php b/tests/lib/Files/Storage/Wrapper/WrapperTest.php new file mode 100644 index 00000000000..60f139450c7 --- /dev/null +++ b/tests/lib/Files/Storage/Wrapper/WrapperTest.php @@ -0,0 +1,40 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Files\Storage\Wrapper; + +use OC\Files\Storage\Local; +use OC\Files\Storage\Wrapper\Wrapper; +use OCP\Files; +use OCP\ITempManager; +use OCP\Server; + +class WrapperTest extends \Test\Files\Storage\Storage { + /** + * @var string tmpDir + */ + private $tmpDir; + + protected function setUp(): void { + parent::setUp(); + + $this->tmpDir = Server::get(ITempManager::class)->getTemporaryFolder(); + $storage = new Local(['datadir' => $this->tmpDir]); + $this->instance = new Wrapper(['storage' => $storage]); + } + + protected function tearDown(): void { + Files::rmdirr($this->tmpDir); + parent::tearDown(); + } + + public function testInstanceOfStorageWrapper(): void { + $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Local')); + $this->assertTrue($this->instance->instanceOfStorage('\OC\Files\Storage\Wrapper\Wrapper')); + } +} |