2020-09-21 20:47:18 +00:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License along
|
|
|
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
|
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
|
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
use MediaWiki\Config\ServiceOptions;
|
|
|
|
|
use MediaWiki\Http\HttpRequestFactory;
|
|
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
|
|
|
use PHPUnit\Framework\TestCase;
|
2020-10-26 22:03:36 +00:00
|
|
|
use Psr\Http\Message\ResponseInterface;
|
2020-09-21 20:47:18 +00:00
|
|
|
use Psr\Log\NullLogger;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Trait for test cases that need to mock HTTP requests.
|
|
|
|
|
*
|
|
|
|
|
* @stable to use in extensions
|
|
|
|
|
* @since 1.36
|
|
|
|
|
*/
|
|
|
|
|
trait MockHttpTrait {
|
|
|
|
|
/**
|
|
|
|
|
* @see MediaWikiIntegrationTestCase::setService()
|
2020-12-18 21:39:51 +00:00
|
|
|
*
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @phpcs:ignore MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam
|
|
|
|
|
* @param object|callable $service
|
2020-09-21 20:47:18 +00:00
|
|
|
*/
|
|
|
|
|
abstract protected function setService( $name, $service );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Install a mock HttpRequestFactory in MediaWikiServices, for the duration
|
|
|
|
|
* of the current test case.
|
|
|
|
|
*
|
2020-10-26 22:03:36 +00:00
|
|
|
* @param null|string|array|callable|MWHttpRequest|MultiHttpClient|GuzzleHttp\Client $request
|
|
|
|
|
* A list of MWHttpRequest to return on consecutive calls to HttpRequestFactory::create().
|
2020-09-21 20:47:18 +00:00
|
|
|
* These MWHttpRequest also represent the desired response.
|
|
|
|
|
* For convenience, a single MWHttpRequest can be given,
|
|
|
|
|
* or a callable producing such an MWHttpRequest,
|
|
|
|
|
* or a string that will be used as the response body of a successful request.
|
|
|
|
|
* If a MultiHttpClient is given, createMultiClient() is supported.
|
2020-10-26 22:03:36 +00:00
|
|
|
* If a GuzzleHttp\Client is given, createGuzzleClient() is supported.
|
|
|
|
|
* If null is given, any call to create(), createMultiClient() or createGuzzleClient()
|
|
|
|
|
* will cause the test to fail.
|
2020-09-21 20:47:18 +00:00
|
|
|
*/
|
|
|
|
|
private function installMockHttp( $request = null ) {
|
|
|
|
|
$this->setService( 'HttpRequestFactory', function () use ( $request ) {
|
|
|
|
|
return $this->makeMockHttpRequestFactory( $request );
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return a mock HttpRequestFactory in MediaWikiServices.
|
|
|
|
|
*
|
|
|
|
|
* @param null|string|array|callable|MWHttpRequest|MultiHttpClient $request A list of
|
|
|
|
|
* MWHttpRequest to return on consecutive calls to HttpRequestFactory::create().
|
|
|
|
|
* These MWHttpRequest also represent the desired response.
|
|
|
|
|
* For convenience, a single MWHttpRequest can be given,
|
|
|
|
|
* or a callable producing such an MWHttpRequest,
|
|
|
|
|
* or a string that will be used as the response body of a successful request.
|
|
|
|
|
* If a MultiHttpClient is given, createMultiClient() is supported.
|
|
|
|
|
* If null or a MultiHttpClient is given instead of a MWHttpRequest,
|
|
|
|
|
* a call to create() will cause the test to fail.
|
|
|
|
|
*
|
|
|
|
|
* @return HttpRequestFactory
|
|
|
|
|
*/
|
|
|
|
|
private function makeMockHttpRequestFactory( $request = null ) {
|
|
|
|
|
$options = new ServiceOptions( HttpRequestFactory::CONSTRUCTOR_OPTIONS, [
|
|
|
|
|
'HTTPTimeout' => 1,
|
|
|
|
|
'HTTPConnectTimeout' => 1,
|
|
|
|
|
'HTTPMaxTimeout' => 1,
|
|
|
|
|
'HTTPMaxConnectTimeout' => 1,
|
|
|
|
|
] );
|
|
|
|
|
|
2021-02-07 13:10:36 +00:00
|
|
|
$failCallback = static function ( /* discard any arguments */ ) {
|
2021-01-26 18:13:33 +00:00
|
|
|
TestCase::fail( 'method should not be called' );
|
|
|
|
|
};
|
|
|
|
|
|
2020-09-21 20:47:18 +00:00
|
|
|
/** @var HttpRequestFactory|MockObject $mockHttpRequestFactory */
|
|
|
|
|
$mockHttpRequestFactory = $this->getMockBuilder( HttpRequestFactory::class )
|
|
|
|
|
->setConstructorArgs( [ $options, new NullLogger() ] )
|
2020-10-26 22:03:36 +00:00
|
|
|
->onlyMethods( [ 'create', 'createMultiClient', 'createGuzzleClient' ] )
|
2020-09-21 20:47:18 +00:00
|
|
|
->getMock();
|
|
|
|
|
|
|
|
|
|
if ( $request instanceof MultiHttpClient ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'createMultiClient' )
|
|
|
|
|
->willReturn( $request );
|
|
|
|
|
} else {
|
|
|
|
|
$mockHttpRequestFactory->method( 'createMultiClient' )
|
2021-01-27 10:51:28 +00:00
|
|
|
->willReturn( $this->createNoOpMock( MultiHttpClient::class ) );
|
2020-09-21 20:47:18 +00:00
|
|
|
}
|
|
|
|
|
|
2020-10-26 22:03:36 +00:00
|
|
|
if ( $request instanceof GuzzleHttp\Client ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'createGuzzleClient' )
|
|
|
|
|
->willReturn( $request );
|
|
|
|
|
} else {
|
|
|
|
|
$mockHttpRequestFactory->method( 'createGuzzleClient' )
|
2021-01-27 10:51:28 +00:00
|
|
|
->willReturn( $this->createNoOpMock( GuzzleHttp\Client::class ) );
|
2020-10-26 22:03:36 +00:00
|
|
|
}
|
|
|
|
|
|
2020-09-21 20:47:18 +00:00
|
|
|
if ( $request === null ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
2021-01-26 18:13:33 +00:00
|
|
|
->willReturnCallback( $failCallback );
|
2020-09-21 20:47:18 +00:00
|
|
|
} elseif ( $request instanceof MultiHttpClient ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
2021-01-26 18:13:33 +00:00
|
|
|
->willReturnCallback( $failCallback );
|
2020-10-26 22:03:36 +00:00
|
|
|
} elseif ( $request instanceof GuzzleHttp\Client ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
2021-01-26 18:13:33 +00:00
|
|
|
->willReturnCallback( $failCallback );
|
2020-09-21 20:47:18 +00:00
|
|
|
} elseif ( $request instanceof MWHttpRequest ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
|
|
|
|
->willReturn( $request );
|
|
|
|
|
} elseif ( is_callable( $request ) ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
|
|
|
|
->willReturnCallback( $request );
|
|
|
|
|
} elseif ( is_array( $request ) ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
|
|
|
|
->willReturnOnConsecutiveCalls( ...$request );
|
|
|
|
|
} elseif ( is_string( $request ) ) {
|
|
|
|
|
$mockHttpRequestFactory->method( 'create' )
|
|
|
|
|
->willReturn( $this->makeFakeHttpRequest( $request ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $mockHttpRequestFactory;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a fake MWHTTPRequest. The request also represents the desired response.
|
|
|
|
|
*
|
|
|
|
|
* @note Not all methods on MWHTTPRequest are mocked, calling other methods will
|
|
|
|
|
* cause the test to fail.
|
|
|
|
|
*
|
|
|
|
|
* @param string $body The response body.
|
|
|
|
|
* @param int $statusCode The response status code. Use 0 to indicate an internal error.
|
|
|
|
|
* @param string[] $headers Any response headers.
|
|
|
|
|
*
|
|
|
|
|
* @return MWHttpRequest
|
|
|
|
|
*/
|
|
|
|
|
private function makeFakeHttpRequest( $body = 'Lorem Ipsum', $statusCode = 200, $headers = [] ) {
|
|
|
|
|
$mockHttpRequest = $this->createNoOpMock(
|
|
|
|
|
MWHttpRequest::class,
|
|
|
|
|
[ 'execute', 'setCallback', 'isRedirect', 'getFinalUrl',
|
|
|
|
|
'getResponseHeaders', 'getResponseHeader', 'setHeader',
|
|
|
|
|
'getStatus', 'getContent'
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mockHttpRequest->method( 'isRedirect' )->willReturn(
|
|
|
|
|
$statusCode >= 300 && $statusCode < 400
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mockHttpRequest->method( 'getFinalUrl' )->willReturn( $headers[ 'Location' ] ?? '' );
|
|
|
|
|
|
|
|
|
|
$mockHttpRequest->method( 'getResponseHeaders' )->willReturn( $headers );
|
|
|
|
|
$mockHttpRequest->method( 'getResponseHeader' )->willReturnCallback(
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $name ) use ( $headers ) {
|
2020-09-21 20:47:18 +00:00
|
|
|
return $headers[$name] ?? null;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$dataCallback = null;
|
|
|
|
|
$mockHttpRequest->method( 'setCallback' )->willReturnCallback(
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $callback ) use ( &$dataCallback ) {
|
2020-09-21 20:47:18 +00:00
|
|
|
$dataCallback = $callback;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$status = Status::newGood( $statusCode );
|
|
|
|
|
|
|
|
|
|
if ( $statusCode === 0 ) {
|
|
|
|
|
$status->fatal( 'http-internal-error' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mockHttpRequest->method( 'getContent' )->willReturn( $body );
|
|
|
|
|
$mockHttpRequest->method( 'getStatus' )->willReturn( $statusCode );
|
|
|
|
|
|
|
|
|
|
$mockHttpRequest->method( 'execute' )->willReturnCallback(
|
|
|
|
|
function () use ( &$dataCallback, $body, $status ) {
|
|
|
|
|
if ( $dataCallback ) {
|
|
|
|
|
$dataCallback( $this, $body );
|
|
|
|
|
}
|
|
|
|
|
return $status;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $mockHttpRequest;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Constructs a fake MultiHttpClient which will return the given response.
|
|
|
|
|
*
|
|
|
|
|
* @note Not all methods on MultiHttpClient are mocked, calling other methods will
|
|
|
|
|
* cause the test to fail.
|
|
|
|
|
*
|
|
|
|
|
* @param array $responses An array mapping request keys to responses.
|
|
|
|
|
* Each response may be a string (the response body), or an array with the
|
|
|
|
|
* following keys (all optional): 'code', 'reason', 'headers', 'body', 'error'.
|
|
|
|
|
* If the 'response' key is set, the associated value is expected to be the
|
|
|
|
|
* response array and contain the 'code', 'body', etc fields. This allows
|
|
|
|
|
* $responses to have the same structure as the return value of runMulti().
|
|
|
|
|
*
|
|
|
|
|
* @return MultiHttpClient
|
|
|
|
|
*/
|
|
|
|
|
private function makeFakeHttpMultiClient( $responses = [] ) {
|
|
|
|
|
$mockHttpRequestMulti = $this->createNoOpMock(
|
|
|
|
|
MultiHttpClient::class,
|
|
|
|
|
[ 'run', 'runMulti' ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mockHttpRequestMulti->method( 'run' )->willReturnCallback(
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( array $req, array $opts = [] ) use ( $mockHttpRequestMulti ) {
|
2020-09-21 20:47:18 +00:00
|
|
|
return $mockHttpRequestMulti->runMulti( [ $req ], $opts )[0]['response'];
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mockHttpRequestMulti->method( 'runMulti' )->willReturnCallback(
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( array $reqs, array $opts = [] ) use ( $responses ) {
|
2020-09-21 20:47:18 +00:00
|
|
|
foreach ( $reqs as $key => &$req ) {
|
|
|
|
|
$resp = $responses[$key] ?? [ 'code' => 0, 'error' => 'unknown' ];
|
|
|
|
|
|
|
|
|
|
if ( is_string( $resp ) ) {
|
|
|
|
|
$resp = [ 'body' => $resp ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( isset( $resp['response'] ) ) {
|
|
|
|
|
// $responses is not just an array of responses,
|
|
|
|
|
// but a request/response structure.
|
|
|
|
|
$resp = $resp['response'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$req['response'] = $resp + [
|
|
|
|
|
'code' => 200,
|
|
|
|
|
'reason' => '',
|
|
|
|
|
'headers' => [],
|
|
|
|
|
'body' => '',
|
|
|
|
|
'error' => '',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$req['response'][0] = $req['response']['code'];
|
|
|
|
|
$req['response'][1] = $req['response']['reason'];
|
|
|
|
|
$req['response'][2] = $req['response']['headers'];
|
|
|
|
|
$req['response'][3] = $req['response']['body'];
|
|
|
|
|
$req['response'][4] = $req['response']['error'];
|
|
|
|
|
|
|
|
|
|
unset( $req );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $reqs;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $mockHttpRequestMulti;
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-26 22:03:36 +00:00
|
|
|
/**
|
|
|
|
|
* Constructs a fake GuzzleHttp\Client which will return the given response.
|
|
|
|
|
*
|
|
|
|
|
* @note Not all methods on GuzzleHttp\Client are mocked, calling other methods will
|
|
|
|
|
* cause the test to fail.
|
|
|
|
|
*
|
|
|
|
|
* @param ResponseInterface|string $response The response to return.
|
|
|
|
|
*
|
|
|
|
|
* @return GuzzleHttp\Client
|
|
|
|
|
*/
|
|
|
|
|
private function makeFakeGuzzleClient( $response ) {
|
|
|
|
|
if ( is_string( $response ) ) {
|
|
|
|
|
$response = new GuzzleHttp\Psr7\Response( 200, [], $response );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$mockHttpClient = $this->createNoOpMock(
|
|
|
|
|
GuzzleHttp\Client::class,
|
|
|
|
|
[ 'request', 'get', 'put', 'post' ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$mockHttpClient->method( 'request' )->willReturn( $response );
|
|
|
|
|
$mockHttpClient->method( 'get' )->willReturn( $response );
|
|
|
|
|
$mockHttpClient->method( 'put' )->willReturn( $response );
|
|
|
|
|
$mockHttpClient->method( 'post' )->willReturn( $response );
|
|
|
|
|
|
|
|
|
|
return $mockHttpClient;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-21 20:47:18 +00:00
|
|
|
}
|