3 use MediaWiki\FileBackend\FSFile\TempFSFileFactory
;
4 use MediaWiki\FileBackend\LockManager\LockManagerGroupFactory
;
5 use MediaWiki\Logger\LoggerFactory
;
8 * Code shared by the FileBackendGroup integration and unit tests. They need merely provide a
9 * suitable newObj() method and everything else works magically.
11 trait FileBackendGroupTestTrait
{
13 * @param array $options Dictionary to use as a source for ServiceOptions before defaults, plus
14 * the following options are available to override other arguments:
15 * * 'configuredROMode'
20 abstract protected function newObj( array $options = [] ) : FileBackendGroup
;
23 * @param string $domain Expected argument that LockManagerGroupFactory::getLockManagerGroup
26 abstract protected function getLockManagerGroupFactory( $domain )
27 : LockManagerGroupFactory
;
30 * @return string As from wfWikiID()
32 abstract protected static function getWikiID();
37 /** @var WANObjectCache */
40 /** @var LockManagerGroupFactory */
43 /** @var TempFSFileFactory */
44 private $tmpFileFactory;
46 private static function getDefaultLocalFileRepo() {
48 'class' => LocalRepo
::class,
50 'directory' => 'upload-dir',
51 'thumbDir' => 'thumb/',
52 'transcodedDir' => 'transcoded/',
54 'scriptDirUrl' => 'script-path/',
55 'url' => 'upload-path/',
57 'thumbScriptUrl' => false,
58 'transformVia404' => false,
59 'deletedDir' => 'deleted/',
60 'deletedHashLevels' => 3,
61 'backend' => 'local-backend',
65 private static function getDefaultOptions() {
67 'DirectoryMode' => 0775,
69 'ForeignFileRepos' => [],
70 'LocalFileRepo' => self
::getDefaultLocalFileRepo(),
71 'wikiId' => self
::getWikiID(),
76 * @covers ::__construct
78 public function testConstructor_overrideImplicitBackend() {
79 $obj = $this->newObj( [ 'FileBackends' =>
80 [ [ 'name' => 'local-backend', 'class' => '', 'lockManager' => 'fsLockManager' ] ]
82 $this->assertSame( '', $obj->config( 'local-backend' )['class'] );
86 * @covers ::__construct
88 public function testConstructor_backendObject() {
89 // 'backend' being an object makes that repo from configuration ignored
90 // XXX This is not documented in DefaultSettings.php, does it do anything useful?
91 $obj = $this->newObj( [ 'ForeignFileRepos' => [ [ 'backend' => new stdclass
] ] ] );
92 $this->assertSame( FSFileBackend
::class, $obj->config( 'local-backend' )['class'] );
96 * @dataProvider provideRegister_domainId
97 * @param string $key Key to check in return value of config()
98 * @param string|callable $expected Expected value of config()[$key], or callable returning it
99 * @param array $extraBackendsOptions To add to the FileBackends entry passed to newObj()
100 * @param array $otherExtraOptions To add to the array passed to newObj() (e.g., services)
103 public function testRegister(
104 $key, $expected, array $extraBackendsOptions = [], array $otherExtraOptions = []
106 if ( $expected instanceof Closure
) {
107 // Lame hack to get around providers being called too early
108 $expected = $expected();
110 if ( $key === 'domainId' ) {
111 // This will change the expected LMG name too
112 $otherExtraOptions['lmgFactory'] = $this->getLockManagerGroupFactory( $expected );
114 $obj = $this->newObj( $otherExtraOptions +
[
116 $extraBackendsOptions +
[
117 'name' => 'myname', 'class' => '', 'lockManager' => 'fsLockManager'
121 $this->assertSame( $expected, $obj->config( 'myname' )[$key] );
124 public static function provideRegister_domainId() {
126 'domainId with neither wikiId nor domainId set' => [
129 return self
::getWikiID();
132 'domainId with wikiId set but no domainId' =>
133 [ 'domainId', 'id0', [ 'wikiId' => 'id0' ] ],
134 'domainId with wikiId and domainId set' =>
135 [ 'domainId', 'dom1', [ 'wikiId' => 'id0', 'domainId' => 'dom1' ] ],
136 'readOnly without readOnly set' => [ 'readOnly', false ],
137 'readOnly with readOnly set to string' =>
138 [ 'readOnly', 'cuz', [ 'readOnly' => 'cuz' ] ],
139 'readOnly without readOnly set but with string in passed object' => [
143 [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ],
145 'readOnly with readOnly set to false but string in passed object' => [
148 [ 'readOnly' => false ],
149 [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ],
155 * @dataProvider provideRegister_exception
156 * @param array $fileBackends Value of FileBackends to pass to constructor
157 * @param string $class Expected exception class
158 * @param string $msg Expected exception message
159 * @covers ::__construct
162 public function testRegister_exception( $fileBackends, $class, $msg ) {
163 $this->setExpectedException( $class, $msg );
164 $this->newObj( [ 'FileBackends' => $fileBackends ] );
167 public static function provideRegister_exception() {
170 [ [] ], InvalidArgumentException
::class, "Cannot register a backend with no name."
173 [ [ 'name' => 'dupe', 'class' => '' ], [ 'name' => 'dupe' ] ],
174 LogicException
::class,
175 "Backend with name 'dupe' already registered.",
178 [ [ 'name' => 'classless' ] ],
179 InvalidArgumentException
::class,
180 "Backend with name 'classless' has no class.",
186 * @covers ::__construct
190 public function testGet() {
191 $backend = $this->newObj()->get( 'local-backend' );
192 $this->assertTrue( $backend instanceof FSFileBackend
);
198 public function testGetUnrecognized() {
199 $this->setExpectedException( InvalidArgumentException
::class,
200 "No backend defined with the name 'unrecognized'." );
201 $this->newObj()->get( 'unrecognized' );
205 * @covers ::__construct
208 public function testConfig() {
209 $obj = $this->newObj();
210 $config = $obj->config( 'local-backend' );
212 // XXX How to actually test that a profiler is loaded?
213 $this->assertNull( $config['profiler']( 'x' ) );
214 // Equality comparison doesn't work for closures, so just set to null
215 $config['profiler'] = null;
217 $this->assertEquals( [
218 'mimeCallback' => [ $obj, 'guessMimeInternal' ],
219 'obResetFunc' => 'wfResetOutputBuffers',
220 'streamMimeFunc' => [ StreamFile
::class, 'contentTypeFromPath' ],
221 'tmpFileFactory' => $this->tmpFileFactory
,
222 'statusWrapper' => [ Status
::class, 'wrap' ],
223 'wanCache' => $this->wanCache
,
224 'srvCache' => $this->srvCache
,
225 'logger' => LoggerFactory
::getInstance( 'FileOperation' ),
226 // This was set to null above in $config, it's not really null
228 'name' => 'local-backend',
229 'containerPaths' => [
230 'local-public' => 'upload-dir',
231 'local-thumb' => 'thumb/',
232 'local-transcoded' => 'transcoded/',
233 'local-deleted' => 'deleted/',
234 'local-temp' => 'upload-dir/temp',
237 'directoryMode' => 0775,
238 'domainId' => self
::getWikiID(),
240 'class' => FSFileBackend
::class,
242 $this->lmgFactory
->getLockManagerGroup( self
::getWikiID() )->get( 'fsLockManager' ),
244 FileJournal
::factory( [ 'class' => NullFileJournal
::class ], 'local-backend' ),
247 // For config values that are objects, check object identity.
248 $this->assertSame( [ $obj, 'guessMimeInternal' ], $config['mimeCallback'] );
249 $this->assertSame( $this->tmpFileFactory
, $config['tmpFileFactory'] );
250 $this->assertSame( $this->wanCache
, $config['wanCache'] );
251 $this->assertSame( $this->srvCache
, $config['srvCache'] );
255 * @dataProvider provideConfig_default
256 * @param string $expected Expected default value
257 * @param string $inputName Name to set to null in LocalFileRepo setting
258 * @param string|array $key Key to check in array returned by config(), or array [ 'key1',
259 * 'key2' ] for nested key
260 * @covers ::__construct
263 public function testConfig_defaultNull( $expected, $inputName, $key ) {
264 $config = self
::getDefaultLocalFileRepo();
265 $config[$inputName] = null;
267 $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' );
269 $actual = is_string( $key ) ?
$result[$key] : $result[$key[0]][$key[1]];
271 $this->assertSame( $expected, $actual );
275 * @dataProvider provideConfig_default
276 * @param string $expected Expected default value
277 * @param string $inputName Name to unset in LocalFileRepo setting
278 * @param string|array $key Key to check in array returned by config(), or array [ 'key1',
279 * 'key2' ] for nested key
280 * @covers ::__construct
283 public function testConfig_defaultUnset( $expected, $inputName, $key ) {
284 $config = self
::getDefaultLocalFileRepo();
285 unset( $config[$inputName] );
287 $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' );
289 $actual = is_string( $key ) ?
$result[$key] : $result[$key[0]][$key[1]];
291 $this->assertSame( $expected, $actual );
294 public static function provideConfig_default() {
296 'deletedDir' => [ false, 'deletedDir', [ 'containerPaths', 'local-deleted' ] ],
297 'thumbDir' => [ 'upload-dir/thumb', 'thumbDir', [ 'containerPaths', 'local-thumb' ] ],
299 'upload-dir/transcoded', 'transcodedDir', [ 'containerPaths', 'local-transcoded' ]
301 'fileMode' => [ 0644, 'fileMode', 'fileMode' ],
308 public function testConfig_fileJournal() {
309 $mockJournal = $this->createMock( FileJournal
::class );
310 $mockJournal->expects( $this->never() )->method( $this->anything() );
312 $obj = $this->newObj( [ 'FileBackends' => [ [
315 'lockManager' => 'fsLockManager',
316 'fileJournal' => [ 'factory' =>
317 function () use ( $mockJournal ) {
323 $this->assertSame( $mockJournal, $obj->config( 'name' )['fileJournal'] );
329 public function testConfigUnrecognized() {
330 $this->setExpectedException( InvalidArgumentException
::class,
331 "No backend defined with the name 'unrecognized'." );
332 $this->newObj()->config( 'unrecognized' );
336 * @dataProvider provideBackendFromPath
337 * @covers ::backendFromPath
338 * @param string|null $expected Name of backend that will be returned from 'get', or null
339 * @param string $storagePath
341 public function testBackendFromPath( $expected = null, $storagePath ) {
342 $obj = $this->newObj( [ 'FileBackends' => [
343 [ 'name' => '', 'class' => stdclass
::class, 'lockManager' => 'fsLockManager' ],
344 [ 'name' => 'a', 'class' => stdclass
::class, 'lockManager' => 'fsLockManager' ],
345 [ 'name' => 'b', 'class' => stdclass
::class, 'lockManager' => 'fsLockManager' ],
348 $expected === null ?
null : $obj->get( $expected ),
349 $obj->backendFromPath( $storagePath )
353 public static function provideBackendFromPath() {
355 'Empty string' => [ null, '' ],
356 'mwstore://' => [ null, 'mwstore://' ],
357 'mwstore://a' => [ null, 'mwstore://a' ],
358 'mwstore:///' => [ null, 'mwstore:///' ],
359 'mwstore://a/' => [ null, 'mwstore://a/' ],
360 'mwstore://a//' => [ null, 'mwstore://a//' ],
361 'mwstore://a/b' => [ 'a', 'mwstore://a/b' ],
362 'mwstore://a/b/' => [ 'a', 'mwstore://a/b/' ],
363 'mwstore://a/b////' => [ 'a', 'mwstore://a/b////' ],
364 'mwstore://a/b/c' => [ 'a', 'mwstore://a/b/c' ],
365 'mwstore://a/b/c/d' => [ 'a', 'mwstore://a/b/c/d' ],
366 'mwstore://b/b' => [ 'b', 'mwstore://b/b' ],
367 'mwstore://c/b' => [ null, 'mwstore://c/b' ],
372 * @dataProvider provideGuessMimeInternal
373 * @covers ::guessMimeInternal
374 * @param string $storagePath
375 * @param string|null $content
376 * @param string|null $fsPath
377 * @param string|null $expectedExtensionType Expected return of
378 * MimeAnalyzer::guessTypesForExtension
379 * @param string|null $expectedGuessedMimeType Expected return value of
380 * MimeAnalyzer::guessMimeType (null if expected not to be called)
382 public function testGuessMimeInternal(
386 $expectedExtensionType,
387 $expectedGuessedMimeType
389 $mimeAnalyzer = $this->createMock( MimeAnalyzer
::class );
390 $mimeAnalyzer->expects( $this->once() )->method( 'guessTypesForExtension' )
391 ->willReturn( $expectedExtensionType );
392 $tmpFileFactory = $this->createMock( TempFSFileFactory
::class );
394 if ( !$expectedExtensionType && $fsPath ) {
395 $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' );
396 $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' )
397 ->with( $fsPath, false )->willReturn( $expectedGuessedMimeType );
398 } elseif ( !$expectedExtensionType && strlen( $content ) ) {
399 // XXX What should we do about the file creation here? Really we should mock
400 // file_put_contents() somehow. It's not very nice to ignore the value of
402 $tmpFile = ( new TempFSFileFactory() )->newTempFSFile( 'mime_', '' );
404 $tmpFileFactory->expects( $this->once() )->method( 'newTempFSFile' )
405 ->with( 'mime_', '' )->willReturn( $tmpFile );
406 $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' )
407 ->with( $tmpFile->getPath(), false )->willReturn( $expectedGuessedMimeType );
409 $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' );
410 $mimeAnalyzer->expects( $this->never() )->method( 'guessMimeType' );
413 $mimeAnalyzer->expects( $this->never() )
414 ->method( $this->anythingBut( 'guessTypesForExtension', 'guessMimeType' ) );
415 $tmpFileFactory->expects( $this->never() )
416 ->method( $this->anythingBut( 'newTempFSFile' ) );
418 $obj = $this->newObj( [
419 'mimeAnalyzer' => $mimeAnalyzer,
420 'tmpFileFactory' => $tmpFileFactory,
423 $this->assertSame( $expectedExtensionType ??
$expectedGuessedMimeType ??
'unknown/unknown',
424 $obj->guessMimeInternal( $storagePath, $content, $fsPath ) );
427 public static function provideGuessMimeInternal() {
430 [ 'foo.txt', null, null, 'text/plain', null ],
432 [ 'foo', null, null, null, null ],
433 'Empty content, with extension' =>
434 [ 'foo.txt', '', null, 'text/plain', null ],
435 'Empty content, no extension' =>
436 [ 'foo', '', null, null, null ],
437 'Non-empty content, with extension' =>
438 [ 'foo.txt', '<b>foo</b>', null, 'text/plain', null ],
439 'Non-empty content, no extension' =>
440 [ 'foo', '<b>foo</b>', null, null, 'text/html' ],
441 'Empty path, with extension' =>
442 [ 'foo.txt', null, '', 'text/plain', null ],
443 'Empty path, no extension' =>
444 [ 'foo', null, '', null, null ],
445 'Non-empty path, with extension' =>
446 [ 'foo.txt', null, '/bogus/path', 'text/plain', null ],
447 'Non-empty path, no extension' =>
448 [ 'foo', null, '/bogus/path', null, 'text/html' ],
449 'Empty path and content, with extension' =>
450 [ 'foo.txt', '', '', 'text/plain', null ],
451 'Empty path and content, no extension' =>
452 [ 'foo', '', '', null, null ],
453 'Non-empty path and content, with extension' =>
454 [ 'foo.txt', '<b>foo</b>', '/bogus/path', 'text/plain', null ],
455 'Non-empty path and content, no extension' =>
456 [ 'foo', '<b>foo</b>', '/bogus/path', null, 'image/jpeg' ],