251 lines
5.4 KiB
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();
|
|
}
|
|
}
|