PHPUnit tests: prevent real Guzzle requests from tests.

Per I1702c11928f8760, tests should fail when trying to make a
HTTP request, unless they have explicitely defined the desired
response.

This patch implements the same behavior for the newly introduced
createGuzzleClient() method.

Bug: T263816
Bug: T262443
Change-Id: Ie3c2efb1387ecc5bc29ff1749cae4bb33e636e9f
This commit is contained in:
daniel 2020-10-26 23:03:36 +01:00
parent f58a698edf
commit 78e2089140
4 changed files with 129 additions and 5 deletions

View file

@ -21,6 +21,7 @@ use MediaWiki\Http\HttpRequestFactory;
use PHPUnit\Framework\MockObject\MockBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\NullLogger;
/**
@ -45,15 +46,16 @@ trait MockHttpTrait {
* Install a mock HttpRequestFactory in MediaWikiServices, for the duration
* of the current test case.
*
* @param null|string|array|callable|MWHttpRequest|MultiHttpClient $request A list of
* MWHttpRequest to return on consecutive calls to HttpRequestFactory::create().
* @param null|string|array|callable|MWHttpRequest|MultiHttpClient|GuzzleHttp\Client $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.
* 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.
*/
private function installMockHttp( $request = null ) {
$this->setService( 'HttpRequestFactory', function () use ( $request ) {
@ -87,7 +89,7 @@ trait MockHttpTrait {
/** @var HttpRequestFactory|MockObject $mockHttpRequestFactory */
$mockHttpRequestFactory = $this->getMockBuilder( HttpRequestFactory::class )
->setConstructorArgs( [ $options, new NullLogger() ] )
->onlyMethods( [ 'create', 'createMultiClient' ] )
->onlyMethods( [ 'create', 'createMultiClient', 'createGuzzleClient' ] )
->getMock();
if ( $request instanceof MultiHttpClient ) {
@ -98,12 +100,23 @@ trait MockHttpTrait {
->willReturnCallback( [ TestCase::class, 'fail' ] );
}
if ( $request instanceof GuzzleHttp\Client ) {
$mockHttpRequestFactory->method( 'createGuzzleClient' )
->willReturn( $request );
} else {
$mockHttpRequestFactory->method( 'createGuzzleClient' )
->willReturnCallback( [ TestCase::class, 'fail' ] );
}
if ( $request === null ) {
$mockHttpRequestFactory->method( 'create' )
->willReturnCallback( [ TestCase::class, 'fail' ] );
} elseif ( $request instanceof MultiHttpClient ) {
$mockHttpRequestFactory->method( 'create' )
->willReturnCallback( [ TestCase::class, 'fail' ] );
} elseif ( $request instanceof GuzzleHttp\Client ) {
$mockHttpRequestFactory->method( 'create' )
->willReturnCallback( [ TestCase::class, 'fail' ] );
} elseif ( $request instanceof MWHttpRequest ) {
$mockHttpRequestFactory->method( 'create' )
->willReturn( $request );
@ -249,4 +262,32 @@ trait MockHttpTrait {
return $mockHttpRequestMulti;
}
/**
* 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;
}
}

View file

@ -54,4 +54,14 @@ class NullHttpRequestFactory extends HttpRequestFactory {
return new NullMultiHttpClient( $options );
}
/**
* @param array $config
*
* @return \GuzzleHttp\Client
*/
public function createGuzzleClient( array $config = [] ): \GuzzleHttp\Client {
// NOTE: if needed, we can also return a mock here, like we do in createMultiClient()
Assert::fail( "HTTP request blocked. Use MockHttpTrait." );
}
}

View file

@ -385,6 +385,15 @@ class MediaWikiIntegrationTestCaseTest extends MediaWikiIntegrationTestCase {
$this->assertTrue( $prevented, 'create() should fail' );
try {
$httpRequestFactory->createGuzzleClient();
$prevented = false;
} catch ( AssertionFailedError $e ) {
// pass
}
$this->assertTrue( $prevented, 'createGuzzleClient() should fail' );
$multiClient = $httpRequestFactory->createMultiClient();
$req = [ 'url' => 'http://0.0.0.0/' ];

View file

@ -1,6 +1,7 @@
<?php
use PHPUnit\Framework\AssertionFailedError;
use Psr\Http\Message\ResponseInterface;
/**
* @covers MockHttpTrait
@ -217,4 +218,67 @@ class MockHttpTraitTest extends MediaWikiIntegrationTestCase {
$this->assertSame( $expected, $data );
}
public function provideGuzzleClientData() {
yield [
'Hello Wörld',
new GuzzleHttp\Psr7\Response( 200, [], 'Hello Wörld' ),
];
yield [
new GuzzleHttp\Psr7\Response( 404, [ 'Test' => 'hi' ], 'nope' ),
new GuzzleHttp\Psr7\Response( 404, [ 'Test' => 'hi' ], 'nope' ),
];
}
/**
* @dataProvider provideGuzzleClientData
*/
public function testFakeGuzzleClientEmulatesRequests( $response, $expected ) {
$client = $this->makeFakeGuzzleClient( $response );
$this->assertGuzzleResponse( $expected, $client->request( 'TEST', 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->get( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->put( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->post( 'http://b.example.com' ) );
}
/**
* @dataProvider provideGuzzleClientData
*/
public function testInstallMockHttpEmulatesGuzzleClient( $response, $expected ) {
$client = $this->makeFakeGuzzleClient( $response );
$this->installMockHttp( $client );
$client = $this->getServiceContainer()->getHttpRequestFactory()
->createGuzzleClient();
$this->assertGuzzleResponse( $expected, $client->request( 'TEST', 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->get( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->put( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->post( 'http://b.example.com' ) );
}
/**
* @dataProvider provideGuzzleClientData
*/
public function testMakeMockHttpRequestFactoryEmulatesGuzzleClient( $response, $expected ) {
$client = $this->makeFakeGuzzleClient( $response );
$client = $this->makeMockHttpRequestFactory( $client )
->createGuzzleClient();
$this->assertGuzzleResponse( $expected, $client->request( 'TEST', 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->get( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->put( 'http://b.example.com' ) );
$this->assertGuzzleResponse( $expected, $client->post( 'http://b.example.com' ) );
}
/**
* @param ResponseInterface $expected
* @param ResponseInterface $actual
*/
private function assertGuzzleResponse( $expected, ResponseInterface $actual ) {
$this->assertSame( $expected->getStatusCode(), $actual->getStatusCode(), 'Status' );
$this->assertSame( $expected->getHeaders(), $actual->getHeaders(), 'Headers' );
$this->assertSame( strval( $expected->getBody() ), strval( $actual->getBody() ), 'Body' );
}
}