use Wikimedia\TestingAccessWrapper;
-class EtcConfigTest extends PHPUnit_Framework_TestCase {
+class EtcdConfigTest extends PHPUnit\Framework\TestCase {
+
+ use MediaWikiCoversValidator;
private function createConfigMock( array $options = [] ) {
return $this->getMockBuilder( EtcdConfig::class )
->getMock();
}
- private function createSimpleConfigMock( array $config ) {
+ private static function createEtcdResponse( array $response ) {
+ $baseResponse = [
+ 'config' => null,
+ 'error' => null,
+ 'retry' => false,
+ 'modifiedIndex' => 0,
+ ];
+ return array_merge( $baseResponse, $response );
+ }
+
+ private function createSimpleConfigMock( array $config, $index = 0 ) {
$mock = $this->createConfigMock();
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [
- $config,
- null, // error
- false // retry?
- ] );
+ ->willReturn( self::createEtcdResponse( [
+ 'config' => $config,
+ 'modifiedIndex' => $index,
+ ] ) );
return $mock;
}
$config->get( 'unknown' );
}
+ /**
+ * @covers EtcdConfig::getModifiedIndex
+ */
+ public function testGetModifiedIndex() {
+ $config = $this->createSimpleConfigMock(
+ [ 'some' => 'value' ],
+ 123
+ );
+ $this->assertSame( 123, $config->getModifiedIndex() );
+ }
+
/**
* @covers EtcdConfig::__construct
*/
->willReturn( [
'config' => [ 'known' => 'from-cache' ],
'expires' => INF,
+ 'modifiedIndex' => 123
] );
$config = $this->createConfigMock( [ 'cache' => $cache ] );
*/
public function testConstructCacheSpec() {
$config = $this->createConfigMock( [ 'cache' => [
- 'class' => HashBagOStuff::class
+ 'class' => HashBagOStuff::class
] ] );
$config->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [
- [ 'known' => 'from-fetch' ],
- null, // error
- false // retry?
- ] );
+ ->willReturn( self::createEtcdResponse(
+ [ 'config' => [ 'known' => 'from-fetch' ], ] ) );
$this->assertSame( 'from-fetch', $config->get( 'known' ) );
}
'cache' => $cache,
] );
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
+ ->willReturn(
+ self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
$this->assertSame( 'from-fetch', $mock->get( 'known' ) );
}
'cache' => $cache,
] );
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [ null, 'Fake error', false ] );
+ ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake error', ] ) );
$this->setExpectedException( ConfigException::class );
$mock->get( 'key' );
[
'config' => [ 'known' => 'from-cache' ],
'expires' => INF,
+ 'modifiedIndex' => 123
]
) );
// .. misses lock
->willReturn( [
'config' => [ 'known' => 'from-cache' ],
'expires' => INF,
+ 'modifiedIndex' => 0,
] );
$cache->expects( $this->never() )->method( 'lock' );
->willReturn( [
'config' => [ 'known' => 'from-cache' ],
'expires' => INF,
+ 'modifiedIndex' => 0,
] );
$cache->expects( $this->never() )->method( 'lock' );
[
'config' => [ 'known' => 'from-cache-expired' ],
'expires' => -INF,
+ 'modifiedIndex' => 0,
]
);
// .. gets lock
'cache' => $cache,
] );
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [ [ 'known' => 'from-fetch' ], null, false ] );
+ ->willReturn( self::createEtcdResponse( [ 'config' => [ 'known' => 'from-fetch' ] ] ) );
$this->assertSame( 'from-fetch', $mock->get( 'known' ) );
}
[
'config' => [ 'known' => 'from-cache-expired' ],
'expires' => -INF,
+ 'modifiedIndex' => 0,
]
);
// .. gets lock
'cache' => $cache,
] );
$mock->expects( $this->once() )->method( 'fetchAllFromEtcd' )
- ->willReturn( [ null, 'Fake failure', true ] );
+ ->willReturn( self::createEtcdResponse( [ 'error' => 'Fake failure', 'retry' => true ] ) );
$this->assertSame( 'from-cache-expired', $mock->get( 'known' ) );
}
->willReturn( [
'config' => [ 'known' => 'from-cache-expired' ],
'expires' => -INF,
+ 'modifiedIndex' => 0,
] );
// .. misses lock
$cache->expects( $this->once() )->method( 'lock' )
public static function provideFetchFromServer() {
return [
- [
+ '200 OK - Success' => [
'http' => [
'code' => 200,
'reason' => 'OK',
- 'headers' => [
- 'content-length' => 0,
- ],
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/foo',
+ 'value' => json_encode( [ 'val' => true ] ),
+ 'modifiedIndex' => 123
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'config' => [ 'foo' => true ], // data
+ 'modifiedIndex' => 123
+ ] ),
+ ],
+ '200 OK - Empty dir' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/foo',
+ 'value' => json_encode( [ 'val' => true ] ),
+ 'modifiedIndex' => 123
+ ],
+ [
+ 'key' => '/example/sub',
+ 'dir' => true,
+ 'modifiedIndex' => 234,
+ 'nodes' => [],
+ ],
+ [
+ 'key' => '/example/bar',
+ 'value' => json_encode( [ 'val' => false ] ),
+ 'modifiedIndex' => 125
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'config' => [ 'foo' => true, 'bar' => false ], // data
+ 'modifiedIndex' => 125 // largest modified index
+ ] ),
+ ],
+ '200 OK - Recursive' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ 'modifiedIndex' => 124,
+ 'nodes' => [
+ [
+ 'key' => 'b',
+ 'value' => json_encode( [ 'val' => true ] ),
+ 'modifiedIndex' => 123,
+
+ ],
+ [
+ 'key' => 'c',
+ 'value' => json_encode( [ 'val' => false ] ),
+ 'modifiedIndex' => 123,
+ ],
+ ],
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'config' => [ 'a/b' => true, 'a/c' => false ], // data
+ 'modifiedIndex' => 123 // largest modified index
+ ] ),
+ ],
+ '200 OK - Missing nodes at second level' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ 'modifiedIndex' => 0,
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => "Unexpected JSON response in dir 'a'; missing 'nodes' list.",
+ ] ),
+ ],
+ '200 OK - Directory with non-array "nodes" key' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/a',
+ 'dir' => true,
+ 'nodes' => 'not an array'
+ ],
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => "Unexpected JSON response in dir 'a'; 'nodes' is not an array.",
+ ] ),
+ ],
+ '200 OK - Correctly encoded garbage response' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'foo' => 'bar' ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => "Unexpected JSON response: Missing or invalid node at top level.",
+ ] ),
+ ],
+ '200 OK - Bad value' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => json_encode( [ 'node' => [ 'nodes' => [
+ [
+ 'key' => '/example/foo',
+ 'value' => ';"broken{value',
+ 'modifiedIndex' => 123,
+ ]
+ ] ] ] ),
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => "Failed to parse value for 'foo'.",
+ ] ),
+ ],
+ '200 OK - Empty node list' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [],
+ 'body' => '{"node":{"nodes":[], "modifiedIndex": 12 }}',
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'config' => [], // data
+ ] ),
+ ],
+ '200 OK - Invalid JSON' => [
+ 'http' => [
+ 'code' => 200,
+ 'reason' => 'OK',
+ 'headers' => [ 'content-length' => 0 ],
'body' => '',
'error' => '(curl error: no status set)',
],
- 'expect' => [
- // FIXME: Returning 4 values instead of 3
- null,
- 200,
- "Unexpected JSON response; missing 'nodes' list.",
- false
+ 'expect' => self::createEtcdResponse( [
+ 'error' => "Error unserializing JSON response.",
+ ] ),
+ ],
+ '404 Not Found' => [
+ 'http' => [
+ 'code' => 404,
+ 'reason' => 'Not Found',
+ 'headers' => [ 'content-length' => 0 ],
+ 'body' => '',
+ 'error' => '',
+ ],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => 'HTTP 404 (Not Found)',
+ ] ),
+ ],
+ '400 Bad Request - custom error' => [
+ 'http' => [
+ 'code' => 400,
+ 'reason' => 'Bad Request',
+ 'headers' => [ 'content-length' => 0 ],
+ 'body' => '',
+ 'error' => 'No good reason',
],
+ 'expect' => self::createEtcdResponse( [
+ 'error' => 'No good reason',
+ 'retry' => true, // retry
+ ] ),
],
];
}
/**
* @covers EtcdConfig::fetchAllFromEtcdServer
+ * @covers EtcdConfig::unserialize
+ * @covers EtcdConfig::parseResponse
+ * @covers EtcdConfig::parseDirectory
+ * @covers EtcdConfigParseError
* @dataProvider provideFetchFromServer
*/
public function testFetchFromServer( array $httpResponse, array $expected ) {