gplx/mediawiki/tests/phpunit/unit/includes/Settings/Source/EtcdSourceTest.php

251 lines
5.4 KiB
PHP

<?php
namespace MediaWiki\Tests\Unit\Settings\Source;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use MediaWiki\Settings\SettingsBuilderException;
use MediaWiki\Settings\Source\EtcdSource;
use PHPUnit\Framework\TestCase;
/**
* @covers \MediaWiki\Settings\Source\EtcdSource
*/
class EtcdSourceTest extends TestCase {
public function testGetExpiryTtl() {
$source = new EtcdSource();
$this->assertSame( 10, $source->getExpiryTtl() );
}
public function testGetExpiryWeight() {
$source = new EtcdSource();
$this->assertSame( 1.0, $source->getExpiryWeight() );
}
public function testGetHashKey() {
$source = new EtcdSource( [ 'host' => 'an.example' ] );
$this->assertSame(
'https://_etcd-client-ssl._tcp.an.example/v2/keys/mediawiki/?recursive=true',
$source->getHashKey()
);
}
public function testGetHashKeyNoDiscovery() {
$source = new EtcdSource( [
'host' => 'an.example',
'discover' => false,
] );
$this->assertSame(
'https://an.example:2379/v2/keys/mediawiki/?recursive=true',
$source->getHashKey()
);
}
public function testToString() {
$source = new EtcdSource( [
'host' => 'an.example',
'discover' => false,
] );
$this->assertSame(
'https://an.example:2379/v2/keys/mediawiki/?recursive=true',
(string)$source
);
}
public function testLoad() {
$mapper = $this->mockCallable();
$client = $this->mockClientWithResponses( [ [ 200, 'valid.json' ] ] );
$resolver = $this->mockCallable();
$settings = [
'Fish/One' => 'fish',
'Fish/Two' => 'fish',
'MoreFish/Red' => 'fish',
'MoreFish/Blue' => 'fish',
];
$source = new EtcdSource( [], $mapper, $client, $resolver );
$mapper->expects( $this->once() )
->method( '__invoke' )
->with( $settings )
->willReturn( $settings );
$resolver->expects( $this->once() )
->method( '__invoke' )
->willReturn( [
[ 'an.example', 4001 ]
] );
$this->assertSame(
[
'Fish/One' => 'fish',
'Fish/Two' => 'fish',
'MoreFish/Red' => 'fish',
'MoreFish/Blue' => 'fish',
],
$source->load()
);
}
/**
* @dataProvider serverFailures
*/
public function testLoadAllServersFailed( GuzzleException $exception ) {
$client = $this->mockClientWithResponses( [
$exception,
$exception,
] );
$resolver = $this->mockCallable();
$source = new EtcdSource( [], null, $client, $resolver );
$resolver->expects( $this->once() )
->method( '__invoke' )
->willReturn( [
[ 'bad.example', 123 ],
[ 'bad.example', 321 ],
] );
$this->expectException( SettingsBuilderException::class );
$source->load();
}
/**
* @dataProvider serverFailures
*/
public function testLoadSomeServersFailed( GuzzleException $exception ) {
$client = $this->mockClientWithResponses( [
$exception,
[ 200, 'valid.json' ],
] );
$resolver = $this->mockCallable();
$source = new EtcdSource( [], null, $client, $resolver );
$resolver->expects( $this->once() )
->method( '__invoke' )
->willReturn( [
[ 'bad.example', 123 ],
[ 'ok.example', 321 ],
] );
$this->assertSame(
[
'Fish/One' => 'fish',
'Fish/Two' => 'fish',
'MoreFish/Red' => 'fish',
'MoreFish/Blue' => 'fish',
],
$source->load()
);
}
public function testLoadClientFailed() {
$client = $this->mockClientWithResponses( [
new ClientException(
'bad request',
new Request( 'GET', '/' ),
new Response( 400, [] )
)
] );
$resolver = $this->mockCallable();
$source = new EtcdSource( [], null, $client, $resolver );
$resolver->expects( $this->once() )
->method( '__invoke' )
->willReturn( [
[ 'ok.example', 123 ],
[ 'ok.example', 321 ],
] );
$this->expectException( SettingsBuilderException::class );
$source->load();
}
public function testLoadNotAnEtcdDirectory() {
$client = $this->mockClientWithResponses( [
[ 200, 'notadirectory.json' ],
] );
$resolver = $this->mockCallable();
$source = new EtcdSource( [], null, $client, $resolver );
$resolver->expects( $this->once() )
->method( '__invoke' )
->willReturn( [
[ 'an.example', 123 ],
] );
$this->expectException( SettingsBuilderException::class );
$source->load();
}
/**
* All possible server-side exceptions.
*
* @return array
*/
public function serverFailures(): array {
return [
[
new ConnectException(
'connection failure',
new Request( 'GET', '/foo' )
)
],
[
new ServerException(
'server side failure',
new Request( 'GET', '/foo' ),
new Response( 500, [], 'server error' )
),
],
];
}
/**
* @param array $responses
*
* @return Client
*/
private function mockClientWithResponses( array $responses ): Client {
return new Client( [
'handler' => HandlerStack::create(
new MockHandler( array_map( static function ( $response ) {
return is_array( $response )
? new Response(
$response[0],
[ 'content-type' => 'application/json' ],
file_get_contents( __DIR__ . '/fixtures/etcd/' . $response[1] )
)
: $response;
}, $responses ) )
),
] );
}
/**
* @return callable
*/
private function mockCallable() {
return $this
->getMockBuilder( __CLASS__ )
->addMethods( [ '__invoke' ] )
->getMock();
}
}