LocalRepo::class, 'name' => 'local', 'directory' => 'upload-dir', 'thumbDir' => 'thumb/', 'transcodedDir' => 'transcoded/', 'fileMode' => 0664, 'scriptDirUrl' => 'script-path/', 'url' => 'upload-path/', 'hashLevels' => 2, 'thumbScriptUrl' => false, 'transformVia404' => false, 'deletedDir' => 'deleted/', 'deletedHashLevels' => 3, 'backend' => 'local-backend', ]; } private static function getDefaultOptions() { return [ 'DirectoryMode' => 0775, 'FileBackends' => [], 'ForeignFileRepos' => [], 'LocalFileRepo' => self::getDefaultLocalFileRepo(), 'fallbackWikiId' => self::getWikiID(), ]; } /** * @covers ::__construct */ public function testConstructor_overrideImplicitBackend() { $obj = $this->newObj( [ 'FileBackends' => [ [ 'name' => 'local-backend', 'class' => '', 'lockManager' => 'fsLockManager' ] ] ] ); $this->assertSame( '', $obj->config( 'local-backend' )['class'] ); } /** * @covers ::__construct */ public function testConstructor_backendObject() { // 'backend' being an object makes that repo from configuration ignored // XXX This is not documented in DefaultSettings.php, does it do anything useful? $obj = $this->newObj( [ 'ForeignFileRepos' => [ [ 'backend' => (object)[] ] ] ] ); $this->assertSame( FSFileBackend::class, $obj->config( 'local-backend' )['class'] ); } /** * @dataProvider provideRegister_domainId * @param string $key Key to check in return value of config() * @param string|callable $expected Expected value of config()[$key], or callable returning it * @param array $extraBackendsOptions To add to the FileBackends entry passed to newObj() * @param array $otherExtraOptions To add to the array passed to newObj() (e.g., services) * @covers ::register */ public function testRegister( $key, $expected, array $extraBackendsOptions = [], array $otherExtraOptions = [] ) { if ( $expected instanceof Closure ) { // Lame hack to get around providers being called too early $expected = $expected(); } if ( $key === 'domainId' ) { // This will change the expected LMG name too $otherExtraOptions['lmgFactory'] = $this->getLockManagerGroupFactory( $expected ); } $obj = $this->newObj( $otherExtraOptions + [ 'FileBackends' => [ $extraBackendsOptions + [ 'name' => 'myname', 'class' => '', 'lockManager' => 'fsLockManager' ] ], ] ); $this->assertSame( $expected, $obj->config( 'myname' )[$key] ); } public static function provideRegister_domainId() { return [ 'domainId with neither wikiId nor domainId set' => [ 'domainId', function () { return self::getWikiID(); }, ], 'domainId with wikiId set but no domainId' => [ 'domainId', 'id0', [ 'wikiId' => 'id0' ] ], 'domainId with wikiId and domainId set' => [ 'domainId', 'dom1', [ 'wikiId' => 'id0', 'domainId' => 'dom1' ] ], 'readOnly without readOnly set' => [ 'readOnly', false ], 'readOnly with readOnly set to string' => [ 'readOnly', 'cuz', [ 'readOnly' => 'cuz' ] ], 'readOnly without readOnly set but with string in passed object' => [ 'readOnly', 'cuz', [], [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], ], 'readOnly with readOnly set to false but string in passed object' => [ 'readOnly', false, [ 'readOnly' => false ], [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], ], ]; } /** * @dataProvider provideRegister_exception * @param array $fileBackends Value of FileBackends to pass to constructor * @param string $class Expected exception class * @param string $msg Expected exception message * @covers ::__construct * @covers ::register */ public function testRegister_exception( $fileBackends, $class, $msg ) { $this->expectException( $class ); $this->expectExceptionMessage( $msg ); $this->newObj( [ 'FileBackends' => $fileBackends ] ); } public static function provideRegister_exception() { return [ 'Nameless' => [ [ [] ], InvalidArgumentException::class, "Cannot register a backend with no name." ], 'Duplicate' => [ [ [ 'name' => 'dupe', 'class' => '' ], [ 'name' => 'dupe' ] ], LogicException::class, "Backend with name 'dupe' already registered.", ], 'Classless' => [ [ [ 'name' => 'classless' ] ], InvalidArgumentException::class, "Backend with name 'classless' has no class.", ], ]; } /** * @covers ::__construct * @covers ::config * @covers ::get */ public function testGet() { $backend = $this->newObj()->get( 'local-backend' ); $this->assertTrue( $backend instanceof FSFileBackend ); } /** * @covers ::get */ public function testGetUnrecognized() { $this->expectException( InvalidArgumentException::class ); $this->expectExceptionMessage( "No backend defined with the name 'unrecognized'." ); $this->newObj()->get( 'unrecognized' ); } /** * @covers ::__construct * @covers ::config */ public function testConfig() { $obj = $this->newObj(); $config = $obj->config( 'local-backend' ); // XXX How to actually test that a profiler is loaded? $this->assertNull( $config['profiler']( 'x' ) ); // Equality comparison doesn't work for closures, so just set to null $config['profiler'] = null; $this->assertEquals( [ 'mimeCallback' => [ $obj, 'guessMimeInternal' ], 'obResetFunc' => 'wfResetOutputBuffers', 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], 'tmpFileFactory' => $this->tmpFileFactory, 'statusWrapper' => [ Status::class, 'wrap' ], 'wanCache' => $this->wanCache, // If $this->srvCache is null, we don't know what it should be, so just fill in the // actual value. Equality to a new HashBagOStuff doesn't work because of the token. 'srvCache' => $this->srvCache ?? $config['srvCache'], 'logger' => LoggerFactory::getInstance( 'FileOperation' ), // This was set to null above in $config, it's not really null 'profiler' => null, 'name' => 'local-backend', 'containerPaths' => [ 'local-public' => 'upload-dir', 'local-thumb' => 'thumb/', 'local-transcoded' => 'transcoded/', 'local-deleted' => 'deleted/', 'local-temp' => 'upload-dir/temp', ], 'fileMode' => 0664, 'directoryMode' => 0775, 'domainId' => self::getWikiID(), 'readOnly' => false, 'class' => FSFileBackend::class, 'lockManager' => $this->lmgFactory->getLockManagerGroup( self::getWikiID() )->get( 'fsLockManager' ), 'fileJournal' => new NullFileJournal, ], $config ); // For config values that are objects, check object identity. $this->assertSame( [ $obj, 'guessMimeInternal' ], $config['mimeCallback'] ); $this->assertSame( $this->tmpFileFactory, $config['tmpFileFactory'] ); $this->assertSame( $this->wanCache, $config['wanCache'] ); if ( $this->srvCache === null ) { $this->assertInstanceOf( HashBagOStuff::class, $config['srvCache'] ); $this->assertSame( [], TestingAccessWrapper::newFromObject( $config['srvCache'] )->bag ); } else { $this->assertSame( $this->srvCache, $config['srvCache'] ); } } /** * @dataProvider provideConfig_default * @param string $expected Expected default value * @param string $inputName Name to set to null in LocalFileRepo setting * @param string|array $key Key to check in array returned by config(), or array [ 'key1', * 'key2' ] for nested key * @covers ::__construct * @covers ::config */ public function testConfig_defaultNull( $expected, $inputName, $key ) { $config = self::getDefaultLocalFileRepo(); $config[$inputName] = null; $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; $this->assertSame( $expected, $actual ); } /** * @dataProvider provideConfig_default * @param string $expected Expected default value * @param string $inputName Name to unset in LocalFileRepo setting * @param string|array $key Key to check in array returned by config(), or array [ 'key1', * 'key2' ] for nested key * @covers ::__construct * @covers ::config */ public function testConfig_defaultUnset( $expected, $inputName, $key ) { $config = self::getDefaultLocalFileRepo(); unset( $config[$inputName] ); $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; $this->assertSame( $expected, $actual ); } public static function provideConfig_default() { return [ 'deletedDir' => [ false, 'deletedDir', [ 'containerPaths', 'local-deleted' ] ], 'thumbDir' => [ 'upload-dir/thumb', 'thumbDir', [ 'containerPaths', 'local-thumb' ] ], 'transcodedDir' => [ 'upload-dir/transcoded', 'transcodedDir', [ 'containerPaths', 'local-transcoded' ] ], 'fileMode' => [ 0644, 'fileMode', 'fileMode' ], ]; } /** * @covers ::config */ public function testConfig_fileJournal() { $mockJournal = $this->createMock( FileJournal::class ); $mockJournal->expects( $this->never() )->method( $this->anything() ); $obj = $this->newObj( [ 'FileBackends' => [ [ 'name' => 'name', 'class' => '', 'lockManager' => 'fsLockManager', 'fileJournal' => [ 'factory' => function () use ( $mockJournal ) { return $mockJournal; } ], ] ] ] ); $this->assertSame( $mockJournal, $obj->config( 'name' )['fileJournal'] ); } /** * @covers ::config */ public function testConfigUnrecognized() { $this->expectException( InvalidArgumentException::class ); $this->expectExceptionMessage( "No backend defined with the name 'unrecognized'." ); $this->newObj()->config( 'unrecognized' ); } /** * @dataProvider provideBackendFromPath * @covers ::backendFromPath * @param string|null $expected Name of backend that will be returned from 'get', or null * @param string $storagePath */ public function testBackendFromPath( $expected, $storagePath ) { $obj = $this->newObj( [ 'FileBackends' => [ [ 'name' => '', 'class' => stdClass::class, 'lockManager' => 'fsLockManager' ], [ 'name' => 'a', 'class' => stdClass::class, 'lockManager' => 'fsLockManager' ], [ 'name' => 'b', 'class' => stdClass::class, 'lockManager' => 'fsLockManager' ], ] ] ); $this->assertSame( $expected === null ? null : $obj->get( $expected ), $obj->backendFromPath( $storagePath ) ); } public static function provideBackendFromPath() { return [ 'Empty string' => [ null, '' ], 'mwstore://' => [ null, 'mwstore://' ], 'mwstore://a' => [ null, 'mwstore://a' ], 'mwstore:///' => [ null, 'mwstore:///' ], 'mwstore://a/' => [ null, 'mwstore://a/' ], 'mwstore://a//' => [ null, 'mwstore://a//' ], 'mwstore://a/b' => [ 'a', 'mwstore://a/b' ], 'mwstore://a/b/' => [ 'a', 'mwstore://a/b/' ], 'mwstore://a/b////' => [ 'a', 'mwstore://a/b////' ], 'mwstore://a/b/c' => [ 'a', 'mwstore://a/b/c' ], 'mwstore://a/b/c/d' => [ 'a', 'mwstore://a/b/c/d' ], 'mwstore://b/b' => [ 'b', 'mwstore://b/b' ], 'mwstore://c/b' => [ null, 'mwstore://c/b' ], ]; } /** * @dataProvider provideGuessMimeInternal * @covers ::guessMimeInternal * @param string $storagePath * @param string|null $content * @param string|null $fsPath * @param string|null $expectedExtensionType Expected return of * MimeAnalyzer::getMimeTypeFromExtensionOrNull * @param string|null $expectedGuessedMimeType Expected return value of * MimeAnalyzer::guessMimeType (null if expected not to be called) */ public function testGuessMimeInternal( $storagePath, $content, $fsPath, $expectedExtensionType, $expectedGuessedMimeType ) { $mimeAnalyzer = $this->createMock( MimeAnalyzer::class ); $mimeAnalyzer->expects( $this->once() )->method( 'getMimeTypeFromExtensionOrNull' ) ->willReturn( $expectedExtensionType ); $tmpFileFactory = $this->createMock( TempFSFileFactory::class ); if ( !$expectedExtensionType && $fsPath ) { $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) ->with( $fsPath, false )->willReturn( $expectedGuessedMimeType ); } elseif ( !$expectedExtensionType && strlen( $content ) ) { // XXX What should we do about the file creation here? Really we should mock // file_put_contents() somehow. It's not very nice to ignore the value of // $wgTmpDirectory. $tmpFile = ( new TempFSFileFactory() )->newTempFSFile( 'mime_', '' ); $tmpFileFactory->expects( $this->once() )->method( 'newTempFSFile' ) ->with( 'mime_', '' )->willReturn( $tmpFile ); $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) ->with( $tmpFile->getPath(), false )->willReturn( $expectedGuessedMimeType ); } else { $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); $mimeAnalyzer->expects( $this->never() )->method( 'guessMimeType' ); } $mimeAnalyzer->expects( $this->never() ) ->method( $this->anythingBut( 'getMimeTypeFromExtensionOrNull', 'guessMimeType' ) ); $tmpFileFactory->expects( $this->never() ) ->method( $this->anythingBut( 'newTempFSFile' ) ); $obj = $this->newObj( [ 'mimeAnalyzer' => $mimeAnalyzer, 'tmpFileFactory' => $tmpFileFactory, ] ); $this->assertSame( $expectedExtensionType ?? $expectedGuessedMimeType ?? 'unknown/unknown', $obj->guessMimeInternal( $storagePath, $content, $fsPath ) ); } public static function provideGuessMimeInternal() { return [ 'With extension' => [ 'foo.txt', null, null, 'text/plain', null ], 'No extension' => [ 'foo', null, null, null, null ], 'Empty content, with extension' => [ 'foo.txt', '', null, 'text/plain', null ], 'Empty content, no extension' => [ 'foo', '', null, null, null ], 'Non-empty content, with extension' => [ 'foo.txt', 'foo', null, 'text/plain', null ], 'Non-empty content, no extension' => [ 'foo', 'foo', null, null, 'text/html' ], 'Empty path, with extension' => [ 'foo.txt', null, '', 'text/plain', null ], 'Empty path, no extension' => [ 'foo', null, '', null, null ], 'Non-empty path, with extension' => [ 'foo.txt', null, '/bogus/path', 'text/plain', null ], 'Non-empty path, no extension' => [ 'foo', null, '/bogus/path', null, 'text/html' ], 'Empty path and content, with extension' => [ 'foo.txt', '', '', 'text/plain', null ], 'Empty path and content, no extension' => [ 'foo', '', '', null, null ], 'Non-empty path and content, with extension' => [ 'foo.txt', 'foo', '/bogus/path', 'text/plain', null ], 'Non-empty path and content, no extension' => [ 'foo', 'foo', '/bogus/path', null, 'image/jpeg' ], ]; } }