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(); } }