aboutsummaryrefslogtreecommitdiffstats
path: root/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php')
-rw-r--r--tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php793
1 files changed, 793 insertions, 0 deletions
diff --git a/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php b/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
new file mode 100644
index 000000000000..707ff5197733
--- /dev/null
+++ b/tests/phpunit/unit/includes/libs/filebackend/FileBackendTest.php
@@ -0,0 +1,793 @@
+<?php
+
+use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @coversDefaultClass FileBackend
+ */
+class FileBackendTest extends MediaWikiUnitTestCase {
+ /**
+ * createMock() stubs out all methods, which isn't desirable for testing an abstract base class,
+ * since we often want to test that the base class calls certain methods that the derived class
+ * is meant to override. getMockBuilder() can be set to override only certain methods, but then
+ * you have to manually specify all abstract methods or else it doesn't work.
+ * getMockForAbstractClass() automatically fills in stubs for the abstract methods, but by
+ * default doesn't allow overriding any other methods. So we have to write our own.
+ *
+ * @param string|array ...$args Zero or more of the following:
+ * - A nonempty associative array, interpreted as $config to be passed to the constructor. The
+ * 'name' and 'domainId' will be given default values if not present.
+ * - A nonempty indexed array or a string, interpreted as a list of methods to override.
+ * - An empty array, which is ignored.
+ * @return FileBackend A mock with no methods overridden except those specified in
+ * $methodsToMock, and all abstract methods.
+ */
+ private function newMockFileBackend( ...$args ) : FileBackend {
+ $methodsToMock = [];
+ $config = [];
+ foreach ( $args as $arg ) {
+ if ( is_string( $arg ) ) {
+ $methodsToMock = [ $arg ];
+ } elseif ( is_array( $arg ) ) {
+ if ( isset( $arg[0] ) ) {
+ $methodsToMock = $arg;
+ } elseif ( $arg ) {
+ $config = $arg;
+ }
+ } else {
+ throw new InvalidArgumentException(
+ 'Arguments must be strings or nonempty arrays' );
+ }
+ }
+
+ $config += [ 'name' => 'test_name' ];
+ if ( !array_key_exists( 'wikiId', $config ) ) {
+ $config += [ 'domainId' => '' ];
+ }
+
+ // getMockForAbstractClass has a lot of undocumented parameters that we need to set
+ // https://github.com/sebastianbergmann/phpunit-mock-objects/blob/5.0.10/src/Generator.php#L268
+ // TODO Would be better to use getMockBuilder and replace the un-overridden abstract methods
+ // with something that throws.
+ return $this->getMockForAbstractClass( FileBackend::class,
+ /* $arguments */ [ $config ],
+ /* $mockClassName */ '',
+ /* $callOriginalConstructor */ true,
+ /* $callOriginalClone */ false,
+ /* $callAutoload */ true,
+ /* $mockedMethods */ $methodsToMock,
+ /* $cloneArguments */ false
+ );
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_validName
+ * @param mixed $name
+ */
+ public function testConstruct_validName( $name ) : void {
+ $this->newMockFileBackend( [ 'name' => $name ] );
+
+ // No exception
+ $this->assertTrue( true );
+ }
+
+ public static function provideConstruct_validName() : array {
+ return [
+ 'True' => [ true ],
+ 'Positive integer' => [ 7 ],
+ 'Zero integer' => [ 0 ],
+ 'Zero float' => [ 0.0 ],
+ 'Negative integer' => [ -7 ],
+ 'Negative float' => [ -7.0 ],
+ '255 chars is allowed' => [ str_repeat( 'a', 255 ) ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_invalidName
+ * @param mixed $name
+ */
+ public function testConstruct_invalidName( $name ) : void {
+ $this->expectException( InvalidArgumentException::class );
+ $this->expectExceptionMessage( "Backend name '$name' is invalid." );
+
+ $this->newMockFileBackend( [ 'name' => $name, 'domainId' => false ] );
+ }
+
+ public static function provideConstruct_invalidName() : array {
+ return [
+ 'Empty string' => [ '' ],
+ '256 chars is too long' => [ str_repeat( 'a', 256 ) ],
+ '!' => [ '!' ],
+ 'With space' => [ 'a b' ],
+ 'False' => [ false ],
+ 'Null' => [ null ],
+ 'Positive float' => [ 13.402 ],
+ 'Negative float' => [ -13.402 ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstruct_noName() : void {
+ $this->expectException( PHPUnit\Framework\Error\Notice::class );
+ $this->expectExceptionMessage( 'Undefined index: name' );
+
+ $this->getMockBuilder( FileBackend::class )
+ ->setConstructorArgs( [ [] ] )
+ ->getMock();
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_validDomainId
+ * @param string $domainId
+ */
+ public function testConstruct_validDomainId( string $domainId ) : void {
+ $this->newMockFileBackend( [ 'domainId' => $domainId ] );
+
+ // No exception
+ $this->assertTrue( true );
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_validDomainId
+ * @param string $wikiId
+ */
+ public function testConstruct_validWikiId( string $wikiId ) : void {
+ $this->newMockFileBackend( [ 'wikiId' => $wikiId ] );
+
+ // No exception
+ $this->assertTrue( true );
+ }
+
+ public static function provideConstruct_validDomainId() : array {
+ return [
+ 'Empty string' => [ '' ],
+ '1000 chars' => [ str_repeat( 'a', 1000 ) ],
+ 'Null character' => [ "\0" ],
+ 'Invalid UTF-8' => [ "\xff" ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_invalidDomainId
+ * @param mixed $domainId
+ */
+ public function testConstruct_invalidDomainId( $domainId ) : void {
+ $this->expectException( InvalidArgumentException::class );
+ $this->expectExceptionMessage( "Backend domain ID not provided for 'test_name'." );
+
+ $this->newMockFileBackend( [ 'domainId' => $domainId ] );
+ }
+
+ public static function provideConstruct_invalidDomainId() : array {
+ return [
+ // We don't include null because that will fall back to wikiId
+ 'False' => [ false ],
+ 'True' => [ true ],
+ 'Integer' => [ 7 ],
+ 'Function' => [ function () {
+ } ],
+ 'Float' => [ -13.402 ],
+ 'Object' => [ new stdclass ],
+ 'Array' => [ [] ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_invalidWikiId
+ * @param mixed $wikiId
+ */
+ public function testConstruct_invalidWikiId( $wikiId ) : void {
+ $this->expectException( InvalidArgumentException::class );
+ $this->expectExceptionMessage( "Backend domain ID not provided for 'test_name'." );
+
+ $this->newMockFileBackend( [ 'wikiId' => $wikiId ] );
+ }
+
+ public static function provideConstruct_invalidWikiId() : array {
+ return [
+ 'Null' => [ null ],
+ ] + self::provideConstruct_invalidDomainId();
+ }
+
+ /**
+ * @covers ::__construct
+ */
+ public function testConstruct_noDomainId() : void {
+ $this->expectException( PHPUnit\Framework\Error\Notice::class );
+ $this->expectExceptionMessage( 'Undefined index: wikiId' );
+
+ $this->getMockBuilder( FileBackend::class )
+ ->setConstructorArgs( [ [ 'name' => 'test_name' ] ] )
+ ->getMock();
+ }
+
+ /**
+ * @covers ::__construct
+ * @dataProvider provideConstruct_properties
+ * @param string $property
+ * @param mixed $expected
+ * @param array $config Can also include the key 'inexact' to tell us to not check equality
+ * strictly.
+ */
+ public function testConstruct_properties(
+ string $property, $expected, array $config = []
+ ) : void {
+ $backend = $this->newMockFileBackend( $config );
+
+ if ( $expected instanceof Closure ) {
+ $expected = $expected( $backend );
+ }
+
+ $assertMethod = isset( $config['inexact'] ) ? 'assertEquals' : 'assertSame';
+ unset( $config['inexact'] );
+
+ // We need to test this for the sake of subclasses that actually use the property. There
+ // doesn't seem to be any better way to do it. It shouldn't be tested in the subclasses,
+ // because we're testing the behavior of this class' constructor. We could make our own
+ // subclass, but we'd have to stub 26 abstract methods.
+ $this->$assertMethod( $expected,
+ TestingAccessWrapper::newFromObject( $backend )->$property );
+ }
+
+ public static function provideConstruct_properties() : array {
+ $tmpFileFactory = new TempFSFileFactory( 'some_unique_path' );
+
+ return [
+ 'parallelize default value' => [ 'parallelize', 'off' ],
+ 'parallelize null' => [ 'parallelize', 'off', [ 'parallelize' => null ] ],
+ 'parallelize cast to string' => [ 'parallelize', '1', [ 'parallelize' => true ] ],
+ 'parallelize case-preserving' =>
+ [ 'parallelize', 'iMpLiCiT', [ 'parallelize' => 'iMpLiCiT' ] ],
+
+ 'concurrency default value' => [ 'concurrency', 50 ],
+ 'concurrency null' => [ 'concurrency', 50, [ 'concurrency' => null ] ],
+ 'concurrency cast to int' => [ 'concurrency', 51, [ 'concurrency' => '51x' ] ],
+
+ 'obResetFunc default value' => [ 'obResetFunc',
+ // I'd've thought the return type should be 'callable', but apparently protected
+ // methods aren't callable.
+ function ( FileBackend $backend ) : array {
+ return [ $backend, 'resetOutputBuffer' ];
+ } ],
+ 'obResetFunc null' => [ 'obResetFunc',
+ function ( FileBackend $backend ) : array {
+ return [ $backend, 'resetOutputBuffer' ];
+ } ],
+ 'obResetFunc set' => [ 'obResetFunc', 'wfSomeImaginaryFunction',
+ [ 'obResetFunc' => 'wfSomeImaginaryFunction' ] ],
+
+ 'streamMimeFunc default value' => [ 'streamMimeFunc', null ],
+ '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 ] ],
+ 'tmpDirectory null' => [ 'tmpFileFactory', new TempFSFileFactory,
+ [ 'tmpDirectory' => null, 'inexact' => true ] ],
+ 'tmpDirectory set' => [ 'tmpFileFactory', new TempFSFileFactory( 'dir' ),
+ [ 'tmpDirectory' => 'dir', 'inexact' => true ] ],
+ 'tmpFileFactory null' => [ 'tmpFileFactory', new TempFSFileFactory,
+ [ 'tmpFileFactory' => null, 'inexact' => true ] ],
+ 'tmpFileFactory set' => [ 'tmpFileFactory', $tmpFileFactory,
+ [ 'tmpFileFactory' => $tmpFileFactory ] ],
+ 'tmpDirectory and tmpFileFactory set' => [
+ 'tmpFileFactory',
+ new TempFSFileFactory( 'dir' ),
+ [ 'tmpDirectory' => 'dir', 'tmpFileFactory' => $tmpFileFactory, 'inexact' => true ],
+ ],
+ 'tmpDirectory null and tmpFileFactory set' => [ 'tmpFileFactory', $tmpFileFactory,
+ [ 'tmpDirectory' => null, 'tmpFileFactory' => $tmpFileFactory ] ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getName
+ */
+ public function testGetName() : void {
+ $backend = $this->newMockFileBackend();
+ $this->assertSame( 'test_name', $backend->getName() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getDomainId
+ * @dataProvider provideGetDomainId
+ * @param array $config
+ */
+ public function testGetDomainId( array $config ) : void {
+ $backend = $this->newMockFileBackend( $config );
+ $this->assertSame( 'test_domain', $backend->getDomainId() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getWikiId
+ * @dataProvider provideGetDomainId
+ * @param array $config
+ */
+ public function testGetWikiId( array $config ) : void {
+ $backend = $this->newMockFileBackend( $config );
+ $this->assertSame( 'test_domain', $backend->getWikiId() );
+ }
+
+ public static function provideGetDomainId() : array {
+ return [
+ 'Only domainId' => [ [ 'domainId' => 'test_domain' ] ],
+ 'Only wikiId' => [ [ 'wikiId' => 'test_domain' ] ],
+ 'null domainId' => [ [ 'domainId' => null, 'wikiId' => 'test_domain' ] ],
+ 'wikiId is ignored if domainId is present' =>
+ [ [ 'domainId' => 'test_domain', 'wikiId' => 'other_domain' ] ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::isReadOnly
+ * @covers ::getReadOnlyReason
+ */
+ public function testIsReadOnly_default() : void {
+ $backend = $this->newMockFileBackend();
+ $this->assertFalse( $backend->isReadOnly() );
+ $this->assertFalse( $backend->getReadOnlyReason() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::isReadOnly
+ * @covers ::getReadOnlyReason
+ */
+ public function testIsReadOnly() : void {
+ $backend = $this->newMockFileBackend( [ 'readOnly' => '.' ] );
+ $this->assertTrue( $backend->isReadOnly() );
+ $this->assertSame( '.', $backend->getReadOnlyReason() );
+ }
+
+ /**
+ * @covers ::getFeatures
+ */
+ public function testGetFeatures() : void {
+ $backend = $this->newMockFileBackend();
+ $this->assertSame( FileBackend::ATTR_UNICODE_PATHS, $backend->getFeatures() );
+ }
+
+ /**
+ * @covers ::hasFeatures
+ * @dataProvider provideHasFeatures
+ * @param bool $expected
+ * @param int $testedFeatures
+ * @param int $actualFeatures
+ */
+ public function testHasFeatures(
+ bool $expected, int $actualFeatures, int $testedFeatures
+ ) : void {
+ $backend = $this->createMock( FileBackend::class );
+ $backend->method( 'getFeatures' )->willReturn( $actualFeatures );
+
+ $this->assertSame( $expected, $backend->hasFeatures( $testedFeatures ) );
+ }
+
+ public static function provideHasFeatures() : array {
+ return [
+ 'Nothing has nothing' => [ true, 0, 0 ],
+ "Nothing doesn't have something" => [ false, 0, 1 ],
+ 'Something has nothing' => [ true, 1, 0 ],
+ 'Something has itself' => [ true, 1, 1 ],
+ "Something doesn't have something else" => [ false, 0b01, 0b10 ],
+ "Something doesn't have itself and something else" => [ false, 0b01, 0b11 ],
+ 'Two things have the first one' => [ true, 0b11, 0b01 ],
+ 'Two things have the second one' => [ true, 0b11, 0b10 ],
+ 'Two things have both' => [ true, 0b11, 0b11 ],
+ "Two things don't have a third" => [ false, 0b11, 0b100 ],
+ ];
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @covers ::doQuickOperations
+ * @covers ::doQuickOperation
+ * @covers ::prepare
+ * @covers ::secure
+ * @covers ::publish
+ * @covers ::clean
+ * @dataProvider provideReadOnly
+ * @param string $method
+ */
+ public function testReadOnly( string $method ) : void {
+ $backend = $this->newMockFileBackend( [ 'readOnly' => '.' ] );
+ $status = $backend->$method( [] );
+ $this->assertSame( [ [
+ 'type' => 'error',
+ 'message' => 'backend-fail-readonly',
+ 'params' => [ 'test_name', '.' ],
+ ] ], $status->getErrors() );
+ $this->assertFalse( $status->isOK() );
+ }
+
+ public static function provideReadOnly() : array {
+ return [
+ 'doOperations' => [ 'doOperations', 'doOperationsInternal', [ [ [] ] ] ],
+ 'doOperation' => [ 'doOperation', 'doOperationsInternal', [ [ 'op' => '' ] ] ],
+ 'doQuickOperations' => [ 'doQuickOperations', 'doQuickOperationsInternal', [ [ [] ] ] ],
+ 'doQuickOperation' => [
+ 'doQuickOperation',
+ 'doQuickOperationsInternal',
+ [ [ 'op' => '' ] ]
+ ],
+ 'prepare' => [ 'prepare', 'doPrepare' ],
+ 'secure' => [ 'secure', 'doSecure' ],
+ 'publish' => [ 'publish', 'doPublish' ],
+ 'clean' => [ 'clean', 'doClean' ],
+ ];
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @covers ::doQuickOperations
+ * @covers ::doQuickOperation
+ * @covers ::prepare
+ * @covers ::secure
+ * @covers ::publish
+ * @covers ::clean
+ * @dataProvider provideReadOnly
+ * @param string $method Method to call
+ * @param string $internalMethod Internal method the call will be forwarded to
+ * @param array $args To be passed to $method before a final argument of
+ * [ 'bypassReadOnly' => true ]
+ */
+ public function testDoOperations_bypassReadOnly(
+ string $method, string $internalMethod, array $args = []
+ ) : void {
+ $backend = $this->newMockFileBackend( [ 'readOnly' => '.' ], $internalMethod );
+ $backend->expects( $this->once() )->method( $internalMethod )
+ ->willReturn( StatusValue::newGood( 'myvalue' ) );
+
+ $status = $backend->$method( ...array_merge( $args, [ [ 'bypassReadOnly' => true ] ] ) );
+
+ $this->assertTrue( $status->isOK() );
+ $this->assertEmpty( $status->getErrors() );
+ $this->assertSame( 'myvalue', $status->getValue() );
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doQuickOperations
+ * @dataProvider provideDoMultipleOperations
+ * @param string $method
+ */
+ public function testDoOperations_noOp( string $method ) : void {
+ $backend = $this->newMockFileBackend(
+ [ 'doOperationsInternal', 'doQuickOperationsInternal' ] );
+ $backend->expects( $this->never() )->method( 'doOperationsInternal' );
+ $backend->expects( $this->never() )->method( 'doQuickOperationsInternal' );
+
+ $status = $backend->$method( [] );
+ $this->assertTrue( $status->isOK() );
+ $this->assertEmpty( $status->getErrors() );
+ }
+
+ public static function provideDoMultipleOperations() : array {
+ return [
+ 'doOperations' => [ 'doOperations' ],
+ 'doQuickOperations' => [ 'doQuickOperations' ],
+ ];
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @dataProvider provideDoOperations
+ * @param string $method 'doOperation' or 'doOperations'
+ */
+ public function testDoOperations_nonLockingNoForce( string $method ) : void {
+ $backend = $this->newMockFileBackend( [ 'doOperationsInternal' ] );
+ $backend->expects( $this->once() )->method( 'doOperationsInternal' )
+ ->with( [ [] ], [] );
+ $backend->$method( $method === 'doOperation' ? [] : [ [] ], [ 'nonLocking' => true ] );
+ }
+
+ public static function provideDoOperations() : array {
+ return [
+ 'doOperations' => [ 'doOperations' ],
+ 'doOperation' => [ 'doOperation' ],
+ ];
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @dataProvider provideDoOperations
+ * @param string $method 'doOperation' or 'doOperations'
+ */
+ public function testDoOperations_nonLockingForce( string $method ) : void {
+ $backend = $this->newMockFileBackend( [ 'doOperationsInternal' ] );
+ $backend->expects( $this->once() )->method( 'doOperationsInternal' )
+ ->with( [ [] ], [ 'nonLocking' => true, 'force' => true ] );
+ $backend->$method(
+ $method === 'doOperation' ? [] : [ [] ],
+ [ 'nonLocking' => true, 'force' => true ]
+ );
+ }
+
+ // XXX Can't test newScopedIgnoreUserAbort() because it's a no-op in CLI
+
+ /**
+ * @covers ::create
+ * @covers ::store
+ * @covers ::copy
+ * @covers ::move
+ * @covers ::delete
+ * @covers ::describe
+ * @covers ::quickCreate
+ * @covers ::quickStore
+ * @covers ::quickCopy
+ * @covers ::quickMove
+ * @covers ::quickDelete
+ * @covers ::quickDescribe
+ * @dataProvider provideAction
+ * @param string $prefix '' or 'quick'
+ * @param string $action
+ */
+ public function testAction( string $prefix, string $action ) : void {
+ $backend = $this->newMockFileBackend( 'do' . ucfirst( $prefix ) . 'OperationsInternal' );
+ $expectedOp = [ 'op' => $action, 'foo' => 'bar' ];
+ if ( $prefix === 'quick' ) {
+ $expectedOp['overwrite'] = true;
+ }
+ $backend->expects( $this->once() )
+ ->method( 'do' . ucfirst( $prefix ) . 'OperationsInternal' )
+ ->with( [ $expectedOp ], [ 'baz' => 'quuz' ] )
+ ->willReturn( StatusValue::newGood( 'myvalue' ) );
+
+ $method = $prefix ? $prefix . ucfirst( $action ) : $action;
+ $status = $backend->$method( [ 'op' => 'ignored', 'foo' => 'bar' ], [ 'baz' => 'quuz' ] );
+
+ $this->assertTrue( $status->isOK() );
+ $this->assertSame( 'myvalue', $status->getValue() );
+ }
+
+ public static function provideAction() : array {
+ $ret = [];
+ foreach ( [ '', 'quick' ] as $prefix ) {
+ foreach ( [ 'create', 'store', 'copy', 'move', 'delete', 'describe' ] as $action ) {
+ $key = $prefix ? $prefix . ucfirst( $action ) : $action;
+ $ret[$key] = [ $prefix, $action ];
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * @covers ::prepare
+ * @covers ::secure
+ * @covers ::publish
+ * @covers ::clean
+ * @dataProvider provideForwardToDo
+ * @param string $method
+ */
+ public function testForwardToDo( string $method ) : void {
+ $backend = $this->newMockFileBackend( 'do' . ucfirst( $method ) );
+ $backend->expects( $this->once() )->method( 'do' . ucfirst( $method ) )
+ ->with( [ 'foo' => 'bar' ] )
+ ->willReturn( StatusValue::newGood( 'myvalue' ) );
+
+ $status = $backend->$method( [ 'foo' => 'bar' ] );
+
+ $this->assertTrue( $status->isOK() );
+ $this->assertEmpty( $status->getErrors() );
+ $this->assertSame( 'myvalue', $status->getValue() );
+ }
+
+ public static function provideForwardToDo() : array {
+ return [
+ 'prepare' => [ 'prepare' ],
+ 'secure' => [ 'secure' ],
+ 'publish' => [ 'publish' ],
+ 'clean' => [ 'clean' ],
+ ];
+ }
+
+ /**
+ * @covers ::getFileContents
+ * @covers ::getLocalReference
+ * @covers ::getLocalCopy
+ * @dataProvider provideForwardToMulti
+ * @param string $method
+ */
+ public function testForwardToMulti( string $method ) : void {
+ $backend = $this->newMockFileBackend( "{$method}Multi" );
+ $backend->expects( $this->once() )->method( "{$method}Multi" )
+ ->with( [ 'srcs' => [ 'mysrc' ], 'foo' => 'bar', 'src' => 'mysrc' ] )
+ ->willReturn( [ 'mysrc' => 'mycontents' ] );
+
+ $result = $backend->$method( [ 'srcs' => 'ignored', 'foo' => 'bar', 'src' => 'mysrc' ] );
+
+ $this->assertSame( 'mycontents', $result );
+ }
+
+ public static function provideForwardToMulti() : array {
+ return [
+ 'getFileContents' => [ 'getFileContents' ],
+ 'getLocalReference' => [ 'getLocalReference' ],
+ 'getLocalCopy' => [ 'getLocalCopy' ],
+ ];
+ }
+
+ /**
+ * @covers ::getTopDirectoryList
+ * @covers ::getTopFileList
+ * @dataProvider provideForwardFromTop
+ * @param string $methodSuffix
+ */
+ public function testForwardFromTop( string $methodSuffix ) : void {
+ $backend = $this->newMockFileBackend( "get$methodSuffix" );
+ $backend->expects( $this->once() )->method( "get$methodSuffix" )
+ ->with( [ 'topOnly' => true, 'foo' => 'bar' ] )
+ ->willReturn( [ 'something' ] );
+
+ $method = "getTop$methodSuffix";
+ $result = $backend->$method( [ 'topOnly' => 'ignored', 'foo' => 'bar' ] );
+
+ $this->assertSame( [ 'something' ], $result );
+ }
+
+ public static function provideForwardFromTop() : array {
+ return [
+ 'getTopDirectoryList' => [ 'DirectoryList' ],
+ 'getTopFileList' => [ 'FileList' ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::lockFiles
+ * @covers ::unlockFiles
+ * @dataProvider provideLockUnlockFiles
+ * @param string $method
+ * @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 ];
+
+ $mockLm = $this->getMockBuilder( LockManager::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'do' . ucfirst( $method ) . 'ByType', 'doLock', 'doUnlock' ] )
+ ->getMock();
+ // XXX PHPUnit can't override final methods (T231419)
+ //$mockLm->expects( $this->once() )->method( $method )
+ // ->with( ...array_merge( $args, [ $timeout ?? 0 ] ) )
+ // ->willReturn( StatusValue::newGood( 'myvalue' ) );
+ //$mockLm->expects( $this->never() )->method( $this->anythingBut( $method ) );
+ $mockLm->expects( $this->once() )->method( 'do' . ucfirst( $method ) . 'ByType' )
+ ->with( [ LockManager::LOCK_SH => [ 'mwstore://a/b', 'mwstore://c/d/e' ] ] )
+ ->willReturn( StatusValue::newGood( 'myvalue' ) );
+
+ $backend = $this->newMockFileBackend( [ 'lockManager' => $mockLm ] );
+ $backendMethod = "{$method}Files";
+
+ $status = $backend->$backendMethod( ...array_merge( $args, (array)$timeout ) );
+
+ $this->assertTrue( $status->isOK() );
+ $this->assertEmpty( $status->getErrors() );
+ $this->assertSame( 'myvalue', $status->getValue() );
+ }
+
+ public static function provideLockUnlockFiles() : array {
+ return [
+ [ 'lock' ],
+ [ 'lock', 731 ],
+ [ 'unlock' ],
+ ];
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getRootStoragePath
+ * @dataProvider provideConstruct_validName
+ * @param mixed $name
+ */
+ public function testGetRootStoragePath( $name ) : void {
+ $backend = $this->newMockFileBackend( [ 'name' => $name ] );
+ $this->assertSame( "mwstore://$name", $backend->getRootStoragePath() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getContainerStoragePath
+ * @dataProvider provideConstruct_validName
+ * @param mixed $name
+ */
+ public function testGetContainerStoragePath( $name ) : void {
+ $backend = $this->newMockFileBackend( [ 'name' => $name ] );
+ $this->assertSame( "mwstore://$name/mycontainer",
+ $backend->getContainerStoragePath( 'mycontainer' ) );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getJournal
+ */
+ public function testGetFileJournal_default() : void {
+ $backend = $this->newMockFileBackend();
+ $this->assertEquals( new NullFileJournal, $backend->getJournal() );
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::getJournal
+ */
+ public function testGetJournal() : void {
+ $mockJournal = $this->createNoOpMock( FileJournal::class );
+ $backend = $this->newMockFileBackend( [ 'fileJournal' => $mockJournal ] );
+ $this->assertSame( $mockJournal, $backend->getJournal() );
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @covers ::resolveFSFileObjects
+ * @dataProvider provideDoOperations
+ * @param string $method 'doOperation' or 'doOperations'
+ */
+ public function testResolveFSFileObjects( string $method ) : void {
+ $tmpFile = ( new TempFSFileFactory )->newTempFSFile( 'a' );
+
+ $backend = $this->newMockFileBackend( 'doOperationsInternal' );
+ $backend->expects( $this->once() )->method( 'doOperationsInternal' )
+ ->with( [ [ 'src' => $tmpFile->getPath(), 'srcRef' => $tmpFile ] ] )
+ ->willReturn( StatusValue::newGood() );
+
+ $op = [ 'src' => $tmpFile ];
+ if ( $method === 'doOperations' ) {
+ $op = [ $op ];
+ }
+ $status = $backend->$method( $op );
+
+ $this->assertTrue( $status->isOK() );
+ $this->assertEmpty( $status->getErrors() );
+ }
+
+ /**
+ * @covers ::doOperations
+ * @covers ::doOperation
+ * @covers ::resolveFSFileObjects
+ * @dataProvider provideDoOperations
+ * @param string $method 'doOperation' or 'doOperations'
+ */
+ public function testResolveFSFileObjects_preservesTempFiles( string $method ) : void {
+ $tmpFile = ( new TempFSFileFactory )->newTempFSFile( 'a' );
+ $path = $tmpFile->getPath();
+
+ $backend = $this->newMockFileBackend();
+
+ $op = [ 'src' => $tmpFile ];
+ if ( $method === 'doOperations' ) {
+ $op = [ $op ];
+ }
+ $status = $backend->$method( $op );
+
+ $this->assertTrue( file_exists( $path ) );
+ }
+}