aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--includes/libs/filebackend/FileBackend.php11
-rw-r--r--tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php115
-rw-r--r--tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php419
3 files changed, 425 insertions, 120 deletions
diff --git a/includes/libs/filebackend/FileBackend.php b/includes/libs/filebackend/FileBackend.php
index eb2fcaa9dc7d..02f42185132b 100644
--- a/includes/libs/filebackend/FileBackend.php
+++ b/includes/libs/filebackend/FileBackend.php
@@ -211,7 +211,6 @@ abstract class FileBackend implements LoggerAwareInterface {
: 50;
$this->obResetFunc = $config['obResetFunc'] ?? [ $this, 'resetOutputBuffer' ];
$this->streamMimeFunc = $config['streamMimeFunc'] ?? null;
- $this->statusWrapper = $config['statusWrapper'] ?? null;
$this->profiler = $config['profiler'] ?? null;
if ( !is_callable( $this->profiler ) ) {
@@ -1571,6 +1570,9 @@ abstract class FileBackend implements LoggerAwareInterface {
* @return string|null Parent storage path or null on failure
*/
final public static function parentStoragePath( $storagePath ) {
+ // XXX dirname() depends on platform and locale! If nothing enforces that the storage path
+ // doesn't contain characters like '\', behavior can vary by platform. We should use
+ // explode() instead.
$storagePath = dirname( $storagePath );
list( , , $rel ) = self::splitStoragePath( $storagePath );
@@ -1585,6 +1587,8 @@ abstract class FileBackend implements LoggerAwareInterface {
* @return string
*/
final public static function extensionFromPath( $path, $case = 'lowercase' ) {
+ // This will treat a string starting with . as not having an extension, but store paths have
+ // to start with 'mwstore://', so "garbage in, garbage out".
$i = strrpos( $path, '.' );
$ext = $i ? substr( $path, $i + 1 ) : '';
@@ -1701,7 +1705,12 @@ abstract class FileBackend implements LoggerAwareInterface {
return $this->profiler ? ( $this->profiler )( $section ) : null;
}
+ /**
+ * @codeCoverageIgnore Let's not reset output buffering during tests
+ */
protected function resetOutputBuffer() {
+ // XXX According to documentation, ob_get_status() always returns a non-empty array and this
+ // condition will always be true
while ( ob_get_status() ) {
if ( !ob_end_clean() ) {
// Could not remove output buffer handler; abort now
diff --git a/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php b/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
index f7f2b6341bd6..b8ca41d4a58d 100644
--- a/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
+++ b/tests/phpunit/includes/filebackend/FileBackendIntegrationTest.php
@@ -127,121 +127,6 @@ class FileBackendIntegrationTest extends MediaWikiIntegrationTestCase {
}
/**
- * @dataProvider provider_testIsStoragePath
- */
- public function testIsStoragePath( $path, $isStorePath ) {
- $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
- "FileBackend::isStoragePath on path '$path'" );
- }
-
- public static function provider_testIsStoragePath() {
- return [
- [ 'mwstore://', true ],
- [ 'mwstore://backend', true ],
- [ 'mwstore://backend/container', true ],
- [ 'mwstore://backend/container/', true ],
- [ 'mwstore://backend/container/path', true ],
- [ 'mwstore://backend//container/', true ],
- [ 'mwstore://backend//container//', true ],
- [ 'mwstore://backend//container//path', true ],
- [ 'mwstore:///', true ],
- [ 'mwstore:/', false ],
- [ 'mwstore:', false ],
- ];
- }
-
- /**
- * @dataProvider provider_testSplitStoragePath
- */
- public function testSplitStoragePath( $path, $res ) {
- $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
- "FileBackend::splitStoragePath on path '$path'" );
- }
-
- public static function provider_testSplitStoragePath() {
- return [
- [ 'mwstore://backend/container', [ 'backend', 'container', '' ] ],
- [ 'mwstore://backend/container/', [ 'backend', 'container', '' ] ],
- [ 'mwstore://backend/container/path', [ 'backend', 'container', 'path' ] ],
- [ 'mwstore://backend/container//path', [ 'backend', 'container', '/path' ] ],
- [ 'mwstore://backend//container/path', [ null, null, null ] ],
- [ 'mwstore://backend//container//path', [ null, null, null ] ],
- [ 'mwstore://', [ null, null, null ] ],
- [ 'mwstore://backend', [ null, null, null ] ],
- [ 'mwstore:///', [ null, null, null ] ],
- [ 'mwstore:/', [ null, null, null ] ],
- [ 'mwstore:', [ null, null, null ] ]
- ];
- }
-
- /**
- * @dataProvider provider_normalizeStoragePath
- */
- public function testNormalizeStoragePath( $path, $res ) {
- $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
- "FileBackend::normalizeStoragePath on path '$path'" );
- }
-
- public static function provider_normalizeStoragePath() {
- return [
- [ 'mwstore://backend/container', 'mwstore://backend/container' ],
- [ 'mwstore://backend/container/', 'mwstore://backend/container' ],
- [ 'mwstore://backend/container/path', 'mwstore://backend/container/path' ],
- [ 'mwstore://backend/container//path', 'mwstore://backend/container/path' ],
- [ 'mwstore://backend/container///path', 'mwstore://backend/container/path' ],
- [
- 'mwstore://backend/container///path//to///obj',
- 'mwstore://backend/container/path/to/obj'
- ],
- [ 'mwstore://', null ],
- [ 'mwstore://backend', null ],
- [ 'mwstore://backend//container/path', null ],
- [ 'mwstore://backend//container//path', null ],
- [ 'mwstore:///', null ],
- [ 'mwstore:/', null ],
- [ 'mwstore:', null ],
- ];
- }
-
- /**
- * @dataProvider provider_testParentStoragePath
- */
- public function testParentStoragePath( $path, $res ) {
- $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
- "FileBackend::parentStoragePath on path '$path'" );
- }
-
- public static function provider_testParentStoragePath() {
- return [
- [ 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ],
- [ 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ],
- [ 'mwstore://backend/container/path', 'mwstore://backend/container' ],
- [ 'mwstore://backend/container', null ],
- [ 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ],
- [ 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ],
- [ 'mwstore://backend/container/path/', 'mwstore://backend/container' ],
- [ 'mwstore://backend/container/', null ],
- ];
- }
-
- /**
- * @dataProvider provider_testExtensionFromPath
- */
- public function testExtensionFromPath( $path, $res ) {
- $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
- "FileBackend::extensionFromPath on path '$path'" );
- }
-
- public static function provider_testExtensionFromPath() {
- return [
- [ 'mwstore://backend/container/path.txt', 'txt' ],
- [ 'mwstore://backend/container/path.svg.png', 'png' ],
- [ 'mwstore://backend/container/path', '' ],
- [ 'mwstore://backend/container/path.', '' ],
- ];
- }
-
- /**
* @dataProvider provider_testStore
*/
public function testStore( $op ) {
diff --git a/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php b/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
index 707ff5197733..1fa064163df0 100644
--- a/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
+++ b/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
@@ -1,6 +1,9 @@
<?php
+declare( strict_types = 1 );
use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+use Psr\Log\NullLogger;
+use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
/**
@@ -270,14 +273,12 @@ class FileBackendTest extends MediaWikiUnitTestCase {
'streamMimeFunc set' => [ 'streamMimeFunc', 'smf', [ 'streamMimeFunc' => 'smf' ] ],
'profiler default value' => [ 'profiler', null ],
- 'profiler callable' => [ 'profiler', 'strtr', [ 'profiler' => 'strtr' ] ],
'profiler not callable' => [ 'profiler', null, [ 'profiler' => '!' ] ],
'logger default value' => [ 'logger', new Psr\Log\NullLogger, [ 'inexact' => true ] ],
'logger set' => [ 'logger', 'abcd', [ 'logger' => 'abcd' ] ],
'statusWrapper default value' => [ 'statusWrapper', null ],
- 'statusWrapper set' => [ 'statusWrapper', 'stat', [ 'statusWrapper' => 'stat' ] ],
'tmpFileFactory default value' =>
[ 'tmpFileFactory', new TempFSFileFactory, [ 'inexact' => true ] ],
@@ -300,6 +301,18 @@ class FileBackendTest extends MediaWikiUnitTestCase {
}
/**
+ * @covers ::setLogger
+ */
+ public function testSetLogger() : void {
+ $backend = $this->newMockFileBackend();
+ $logger = new NullLogger;
+ // See comment in testConstruct_properties about use of TestingAccessWrapper.
+ $this->assertNotSame( $logger, TestingAccessWrapper::newFromObject( $backend )->logger );
+ $backend->setLogger( $logger );
+ $this->assertSame( $logger, TestingAccessWrapper::newFromObject( $backend )->logger );
+ }
+
+ /**
* @covers ::__construct
* @covers ::getName
*/
@@ -410,6 +423,7 @@ class FileBackendTest extends MediaWikiUnitTestCase {
* @covers ::secure
* @covers ::publish
* @covers ::clean
+ * @covers ::newStatus
* @dataProvider provideReadOnly
* @param string $method
*/
@@ -450,6 +464,7 @@ class FileBackendTest extends MediaWikiUnitTestCase {
* @covers ::secure
* @covers ::publish
* @covers ::clean
+ * @covers ::newStatus
* @dataProvider provideReadOnly
* @param string $method Method to call
* @param string $internalMethod Internal method the call will be forwarded to
@@ -473,6 +488,7 @@ class FileBackendTest extends MediaWikiUnitTestCase {
/**
* @covers ::doOperations
* @covers ::doQuickOperations
+ * @covers ::newStatus
* @dataProvider provideDoMultipleOperations
* @param string $method
*/
@@ -668,8 +684,7 @@ class FileBackendTest extends MediaWikiUnitTestCase {
* @param int $timeout Only relevant for lockFiles
*/
public function testLockUnlockFiles( string $method, ?int $timeout = null ) : void {
- // TODO Test that normalizeStoragePath is being called
- $args = [ [ 'mwstore://a/b', 'mwstore://c/d/e' ], LockManager::LOCK_SH ];
+ $args = [ [ 'mwstore://a/b/', 'mwstore://c/d//e' ], LockManager::LOCK_SH ];
$mockLm = $this->getMockBuilder( LockManager::class )
->disableOriginalConstructor()
@@ -703,6 +718,85 @@ class FileBackendTest extends MediaWikiUnitTestCase {
}
/**
+ * @covers ::getScopedFileLocks
+ * @dataProvider provideGetScopedFileLocks
+ * @param array $paths
+ * @param int|string $type
+ * @param array $expectedPathsByType Expected to be passed to the LockManager
+ * @param StatusValue $lockStatus Returned from doLockByType()
+ * @param StatusValue|null $unlockStatus Returned from doUnlockByType() (if locking succeeded)
+ */
+ public function testGetScopedFileLocks(
+ array $paths, $type, array $expectedPathsByType, StatusValue $lockStatus,
+ ?StatusValue $unlockStatus = null
+ ) : void {
+ $mockLm = $this->getMockBuilder( LockManager::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'doLockByType', 'doUnlockByType', 'doLock', 'doUnlock' ] )
+ ->getMock();
+ $mockLm->expects( $this->once() )->method( 'doLockByType' )
+ ->with( $expectedPathsByType )
+ ->willReturn( $lockStatus );
+ $mockLm->expects( $this->exactly( $unlockStatus ? 1 : 0 ) )->method( 'doUnlockByType' )
+ ->with( $expectedPathsByType )
+ ->willReturn( $unlockStatus );
+
+ $backend = $this->newMockFileBackend( [ 'lockManager' => $mockLm ] );
+
+ $status = StatusValue::newGood( 'myvalue' );
+ $scopedLock = $backend->getScopedFileLocks( $paths, $type, $status );
+
+ $this->assertSame( 'myvalue', $status->getValue() );
+ $this->assertSame( $lockStatus->isOK(), $status->isOK() );
+ $this->assertSame( $lockStatus->getErrors(), $status->getErrors() );
+
+ if ( !$lockStatus->isOK() ) {
+ $this->assertNull( $scopedLock );
+ return;
+ }
+
+ $this->assertInstanceOf( ScopedLock::class, $scopedLock );
+ unset( $scopedLock );
+
+ $this->assertSame( 'myvalue', $status->getValue() );
+ $this->assertSame( $lockStatus->isOK(), $status->isOK() );
+ $this->assertSame( array_merge( $lockStatus->getErrors(), $unlockStatus->getErrors() ),
+ $status->getErrors() );
+ }
+
+ public static function provideGetScopedFileLocks() : array {
+ return [
+ 'Simple successful shared lock' => [
+ [ 'mwstore://a/b/' ], LockManager::LOCK_SH,
+ [ LockManager::LOCK_SH => [ 'mwstore://a/b' ] ],
+ StatusValue::newGood( 'value2' ), StatusValue::newGood( 'value3' ),
+ ],
+ 'Mixed lock' => [
+ [ LockManager::LOCK_SH => [ 'mwstore://a/b/' ],
+ LockManager::LOCK_EX => [ 'mwstore://c/d//e' ] ], 'mixed',
+ [ LockManager::LOCK_SH => [ 'mwstore://a/b' ],
+ LockManager::LOCK_EX => [ 'mwstore://c/d/e' ] ],
+ StatusValue::newGood(), StatusValue::newGood(),
+ ],
+ 'Mixed with only shared locks' => [
+ [ LockManager::LOCK_SH => [ 'mwstore://a/b/', 'mwstore://c/d//e' ] ], 'mixed',
+ [ LockManager::LOCK_SH => [ 'mwstore://a/b', 'mwstore://c/d/e' ] ],
+ StatusValue::newGood(), StatusValue::newGood(),
+ ],
+ 'Locking error' => [
+ [ 'mwstore://a/b/' ], LockManager::LOCK_EX,
+ [ LockManager::LOCK_EX => [ 'mwstore://a/b' ] ],
+ StatusValue::newFatal( 'XXX' ),
+ ],
+ 'Unlocking error' => [
+ [ 'mwstore://a/b/', 'mwstore://c/d//e' ], LockManager::LOCK_EX,
+ [ LockManager::LOCK_EX => [ 'mwstore://a/b', 'mwstore://c/d/e' ] ],
+ StatusValue::newGood(), StatusValue::newFatal( 'XXXX' ),
+ ],
+ ];
+ }
+
+ /**
* @covers ::__construct
* @covers ::getRootStoragePath
* @dataProvider provideConstruct_validName
@@ -745,6 +839,291 @@ class FileBackendTest extends MediaWikiUnitTestCase {
}
/**
+ * @covers ::isStoragePath
+ * @dataProvider provideIsStoragePath
+ * @param string $path
+ * @param bool $expected
+ */
+ public function testIsStoragePath( string $path, bool $expected ) : void {
+ $this->assertSame( $expected, FileBackend::isStoragePath( $path ) );
+ }
+
+ public static function provideIsStoragePath() : array {
+ $paths = [
+ 'mwstore://' => true,
+ 'mwstore://backend' => true,
+ 'mwstore://backend/container' => true,
+ 'mwstore://backend/container/' => true,
+ 'mwstore://backend/container/path' => true,
+ 'mwstore://backend//container/' => true,
+ 'mwstore://backend//container//' => true,
+ 'mwstore://backend//container//path' => true,
+ 'mwstore:///' => true,
+ 'mwstore:/' => false,
+ 'mwstore:' => false,
+ ];
+ $ret = [];
+ foreach ( $paths as $path => $expected ) {
+ $ret[$path] = [ $path, $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::splitStoragePath
+ * @dataProvider provideSplitStoragePath
+ * @param string $path
+ * @param array $expected
+ */
+ public function testSplitStoragePath( string $path, array $expected ) : void {
+ $this->assertSame( $expected, FileBackend::splitStoragePath( $path ) );
+ }
+
+ public static function provideSplitStoragePath() : array {
+ $paths = [
+ 'mwstore://backend/container' => [ 'backend', 'container', '' ],
+ 'mwstore://backend/container/' => [ 'backend', 'container', '' ],
+ 'mwstore://backend/container/path' => [ 'backend', 'container', 'path' ],
+ 'mwstore://backend/container//path' => [ 'backend', 'container', '/path' ],
+ 'mwstore://backend//container/path' => [ null, null, null ],
+ 'mwstore://backend//container' => [ null, null, null ],
+ 'mwstore://backend//container//path' => [ null, null, null ],
+ 'mwstore://' => [ null, null, null ],
+ 'mwstore://backend' => [ null, null, null ],
+ 'mwstore:///' => [ null, null, null ],
+ 'mwstore:/' => [ null, null, null ],
+ 'mwstore:' => [ null, null, null ],
+ ];
+ $ret = [];
+ foreach ( $paths as $path => $expected ) {
+ $ret[$path] = [ $path, $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::normalizeStoragePath
+ * @dataProvider provideNormalizeStoragePath
+ * @param string $path
+ * @param string|null $expected
+ */
+ public function testNormalizeStoragePath( string $path, ?string $expected ) : void {
+ $this->assertSame( $expected, FileBackend::normalizeStoragePath( $path ) );
+ }
+
+ public static function provideNormalizeStoragePath() : array {
+ $paths = [
+ 'mwstore://backend/container' => 'mwstore://backend/container',
+ 'mwstore://backend/container/' => 'mwstore://backend/container',
+ 'mwstore://backend/container/path' => 'mwstore://backend/container/path',
+ 'mwstore://backend/container//path' => 'mwstore://backend/container/path',
+ 'mwstore://backend/container///path' => 'mwstore://backend/container/path',
+ 'mwstore://backend/container///path//to///obj' =>
+ 'mwstore://backend/container/path/to/obj',
+ 'mwstore://' => null,
+ 'mwstore://backend' => null,
+ 'mwstore://backend//container' => null,
+ 'mwstore://backend//container/path' => null,
+ 'mwstore://backend//container//path' => null,
+ 'mwstore:///' => null,
+ 'mwstore:/' => null,
+ 'mwstore:' => null,
+ ];
+ $ret = [];
+ foreach ( $paths as $path => $expected ) {
+ $ret[$path] = [ $path, $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::parentStoragePath
+ * @dataProvider provideParentStoragePath
+ * @param string $path
+ * @param string|null $expected
+ */
+ public function testParentStoragePath( string $path, ?string $expected ) : void {
+ $this->assertSame( $expected, FileBackend::parentStoragePath( $path ) );
+ }
+
+ public static function provideParentStoragePath() : array {
+ $paths = [
+ 'mwstore://backend/container/path/to/obj' => 'mwstore://backend/container/path/to',
+ 'mwstore://backend/container/path/to' => 'mwstore://backend/container/path',
+ 'mwstore://backend/container/path' => 'mwstore://backend/container',
+ 'mwstore://backend/container' => null,
+ 'mwstore://backend/container/path/to/obj/' => 'mwstore://backend/container/path/to',
+ 'mwstore://backend/container/path/to/' => 'mwstore://backend/container/path',
+ 'mwstore://backend/container/path/' => 'mwstore://backend/container',
+ 'mwstore://backend/container/' => null,
+ ];
+ $ret = [];
+ foreach ( $paths as $path => $expected ) {
+ $ret[$path] = [ $path, $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::extensionFromPath
+ * @dataProvider provideExtensionFromPath
+ * @param array $args
+ * @param string $expected
+ */
+ public function testExtensionFromPath( array $args, string $expected ) : void {
+ $this->assertSame( $expected, FileBackend::extensionFromPath( ...$args ) );
+ }
+
+ public static function provideExtensionFromPath() : array {
+ $paths = [
+ 'mwstore://backend/container/path.Txt' => 'Txt',
+ 'mwstore://backend/container/path.svg.pNG' => 'pNG',
+ 'mwstore://backend/container/path' => '',
+ 'mwstore://backend/container/path.' => '',
+ ];
+ $ret = [];
+ foreach ( $paths as $path => $expected ) {
+ $ret[$path] = [ [ $path ], strtolower( $expected ) ];
+ $ret["$path (lowercase)"] = [ [ $path, 'lowercase' ], strtolower( $expected ) ];
+ $ret["$path (uppercase)"] = [ [ $path, 'uppercase' ], strtoupper( $expected ) ];
+ $ret["$path (rawcase)"] = [ [ $path, 'rawcase' ], $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::isPathTraversalFree
+ * @covers ::normalizeContainerPath
+ * @dataProvider provideIsPathTraversalFree
+ * @param string $path
+ * @param bool $expected
+ */
+ public function testIsPathTraversalFree( string $path, bool $expected ) : void {
+ $this->assertSame( $expected, FileBackend::isPathTraversalFree( $path ) );
+ }
+
+ public static function provideIsPathTraversalFree() : array {
+ $traversalFree = [
+ 'a\\b',
+ 'a//b',
+ '/a',
+ '\\a//b/',
+ ];
+
+ $hasTraversal = [];
+
+ $strippedPrefixes = [ '', '/', '//', '///', '\\', '\\\\', '\\\\\\' ];
+ $unstrippedPrefixes = [ '.', 'a', 'a/', '/a', ' ', "\0" ];
+ $suffixes = [ '', '.', 'a', '/', '/a', ' ', "\0" ];
+
+ foreach ( [ '.', '..' ] as $basePath ) {
+ foreach ( $strippedPrefixes as $prefix ) {
+ foreach ( $suffixes as $suffix ) {
+ if ( $suffix === '' ) {
+ $hasTraversal[] = "$prefix$basePath";
+ } else {
+ $traversalFree[] = "$prefix$basePath$suffix";
+ }
+ }
+ }
+ foreach ( $unstrippedPrefixes as $prefix ) {
+ foreach ( $suffixes as $suffix ) {
+ $traversalFree[] = "$prefix$basePath$suffix";
+ }
+ }
+ }
+
+ foreach ( [ './', '.\\', '../', '..\\' ] as $basePath ) {
+ foreach ( $strippedPrefixes as $prefix ) {
+ foreach ( $suffixes as $suffix ) {
+ $hasTraversal[] = "$prefix$basePath$suffix";
+ }
+ }
+ foreach ( $unstrippedPrefixes as $prefix ) {
+ foreach ( $suffixes as $suffix ) {
+ $traversalFree[] = "$prefix$basePath$suffix";
+ }
+ }
+ }
+
+ foreach (
+ [ '/./', '\\./', '/.\\', '\\.\\', '/../', '\\../', '/..\\', '\\..\\' ] as $basePath
+ ) {
+ foreach ( array_merge( $strippedPrefixes, $unstrippedPrefixes ) as $prefix ) {
+ foreach ( $suffixes as $suffix ) {
+ $hasTraversal[] = "$prefix$basePath$suffix";
+ }
+ }
+ }
+
+ // Some things might be traversal-free vis-a-vis one base path but a traversal for another
+ $traversalFree = array_diff( $traversalFree, $hasTraversal );
+
+ $ret = [];
+ foreach ( $traversalFree as $path ) {
+ $ret[$path] = [ $path, true ];
+ }
+ foreach ( $hasTraversal as $path ) {
+ $ret[$path] = [ $path, false ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::makeContentDisposition
+ * @dataProvider provideMakeContentDisposition
+ * @param array $args
+ * @param string $expected
+ */
+ public function testMakeContentDisposition( array $args, string $expected )
+ : void {
+ $this->assertSame( $expected, FileBackend::makeContentDisposition( ...$args ) );
+ }
+
+ public static function provideMakeContentDisposition() : array {
+ $tests = [
+ [ [ 'inline' ], 'inline' ],
+ [ [ 'inLINE' ], 'inline' ],
+ [ [ 'inLINE', '' ], 'inline' ],
+ [ [ 'attachment' ], 'attachment' ],
+ [ [ 'atTACHment' ], 'attachment' ],
+ [ [ 'atTACHment', '' ], 'attachment' ],
+
+ [ [ 'inline', 'filename.txt' ], "inline;filename*=UTF-8''filename.txt" ],
+ [ [ 'attachment', 'filename.txt' ], "attachment;filename*=UTF-8''filename.txt" ],
+
+ [ [ 'inline', 'path/filename!!!' ], "inline;filename*=UTF-8''filename%21%21%21" ],
+ ];
+ $ret = [];
+ foreach ( $tests as [ $args, $expected ] ) {
+ $ret[implode( ', ', $args )] = [ $args, $expected ];
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::makeContentDisposition
+ * @dataProvider provideMakeContentDisposition_invalid
+ * @param string ...$args
+ */
+ public function testMakeContentDisposition_invalid( string ...$args ) : void {
+ $this->expectException( InvalidArgumentException::class );
+ $this->expectExceptionMessage( "Invalid Content-Disposition type '{$args[0]}'." );
+
+ FileBackend::makeContentDisposition( ...$args );
+ }
+
+ public static function provideMakeContentDisposition_invalid() : array {
+ return [
+ [ 'foo' ],
+ [ 'foo', '' ],
+ [ 'foo', 'bar' ],
+ [ ' inline' ],
+ [ 'inline ' ],
+ ];
+ }
+
+ /**
* @covers ::doOperations
* @covers ::doOperation
* @covers ::resolveFSFileObjects
@@ -790,4 +1169,36 @@ class FileBackendTest extends MediaWikiUnitTestCase {
$this->assertTrue( file_exists( $path ) );
}
+
+ /**
+ * @covers ::__construct
+ * @covers ::wrapStatus
+ */
+ public function testWrapStatus() : void {
+ $expectedSv = StatusValue::newGood( 'myvalue' );
+ $backend = $this->newMockFileBackend( [ 'statusWrapper' =>
+ function ( StatusValue $sv ) use ( $expectedSv ) : StatusValue {
+ $this->assertEquals( StatusValue::newGood(), $sv );
+ return $expectedSv;
+ }
+ ] );
+ $this->assertSame( $expectedSv, $backend->doOperations( [] ) );
+ }
+
+ /**
+ * @covers ::scopedProfileSection
+ */
+ public function testScopedProfileSection() : void {
+ $scopedCallback = new ScopedCallback( function () {
+ } );
+ $backend = $this->newMockFileBackend( [ 'profiler' =>
+ function ( string $section ) use ( $scopedCallback ) : ScopedCallback {
+ $this->assertSame( 'mysection', $section );
+ return $scopedCallback;
+ }
+ ] );
+ // See comment in testConstruct_properties about use of TestingAccessWrapper.
+ $this->assertSame( $scopedCallback,
+ TestingAccessWrapper::newFromObject( $backend )->scopedProfileSection( 'mysection' ) );
+ }
}