From 4b2aef359f7c217751db201e42ed6ac4e3fb6d46 Mon Sep 17 00:00:00 2001 From: Vedmaka Date: Sun, 5 Nov 2023 18:44:44 +0400 Subject: [PATCH] Splits extensions and skins parser functions [WIK-1041] (#7) * Only return extensions via parser function * Fixes indentation * Adds `skins` and `skin-urls` flags, makes `extensions` and `extension-urls` to only return extensions data --- src/RemoteWiki.php | 141 +++- tests/phpunit/integration/RemoteWikiTest.php | 190 ++--- tests/phpunit/unit/RemoteWikiTest.php | 696 ++++++++++--------- 3 files changed, 599 insertions(+), 428 deletions(-) diff --git a/src/RemoteWiki.php b/src/RemoteWiki.php index 37d3285..0ee80ef 100644 --- a/src/RemoteWiki.php +++ b/src/RemoteWiki.php @@ -2,7 +2,6 @@ namespace MediaWiki\Extension\RemoteWiki; -use Addwiki\Mediawiki\Api\Client\Action\Exception\UsageException; use Addwiki\Mediawiki\Api\Client\Action\Request\ActionRequest; use Addwiki\Mediawiki\Api\Client\Auth\UserAndPassword; use Addwiki\Mediawiki\Api\Client\MediaWiki; @@ -47,11 +46,12 @@ public function __construct( /** * @param Parser $parser + * @param null $endPoint + * @param null $type * * @return string */ public function remoteVersion( Parser $parser, $endPoint = null, $type = null ): string { - if ( !$this->validateEndpoint( $endPoint ) ) { return ''; } @@ -62,9 +62,15 @@ public function remoteVersion( Parser $parser, $endPoint = null, $type = null ): case 'extensions': $result = $this->getExtensionVersions( $api ); break; + case 'skins': + $result = $this->getSkinVersions( $api ); + break; case 'extension-urls': $result = $this->getExtensionURLs( $api ); break; + case 'skin-urls': + $result = $this->getSkinURLs( $api ); + break; case 'version': default: $result = $this->getGenerator( $api ); @@ -183,6 +189,27 @@ private function getExtensionVersions( MediaWiki $api ): string { } } + /** + * Get skin versions + * + * @param MediaWiki $api + * @return string + */ + private function getSkinVersions( MediaWiki $api ): string { + $result = $this->getSkinsInfo( $api ); + if ( is_string( $result ) ) { + // There was an error + return $result; + } else if ( + is_array( $result ) + && array_key_exists( 'versions', $result ) + ) { + return $result['versions']; + } else { + throw new LogicException( 'Invalid getSkinsInfo() result' ); + } + } + /** * Get extensions urls * @@ -204,6 +231,27 @@ private function getExtensionURLs( MediaWiki $api ): string { } } + /** + * Get skin urls + * + * @param MediaWiki $api + * @return string + */ + private function getSkinURLs( MediaWiki $api ): string { + $result = $this->getSkinsInfo( $api ); + if ( is_string( $result ) ) { + // There was an error + return $result; + } else if ( + is_array( $result ) + && array_key_exists( 'urls', $result ) + ) { + return $result['urls']; + } else { + throw new LogicException( 'Invalid getSkinsInfo() result' ); + } + } + /** * Get extensions information (both versions and URLs) * @@ -211,6 +259,71 @@ private function getExtensionURLs( MediaWiki $api ): string { * @return array|string */ private function getExtensionsInfo( MediaWiki $api ) { + $extensions = $this->getExtensionsData( $api ); + if ( empty( $extensions ) ) { + return $this->config->get( 'RemoteWikiVerbose' ) ? 'ERROR: empty extensions response' : ''; + } + // generate extension:version pairs and extension:URL pairs + $versions = []; + $urls = []; + foreach ( $extensions as $extension ) { + // filter out skins + if ( isset( $extension['type'] ) && $extension['type'] === 'skin' ) { + continue; + } + $versions[] = $extension['name'] . ':' . ( $extension['version'] ?? $extension['vcs-version'] ?? '?' ); + $urls[] = $extension['name'] . ':' . ( $extension['url'] ?? '?' ); + } + // URLs cannot be separated by a comma, that is a valid URL + // character, see + // https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + // instead we will use a |, which isn't a valid part of a URL + $result = [ + 'versions' => implode( ',', $versions ), + 'urls' => implode( '|', $urls ), + ]; + return $result; + } + + /** + * Get skins information (both versions and URLs) + * + * @param MediaWiki $api + * @return array|string + */ + private function getSkinsInfo( MediaWiki $api ) { + $extensions = $this->getExtensionsData( $api ); + if ( empty( $extensions ) ) { + return $this->config->get( 'RemoteWikiVerbose' ) ? 'ERROR: empty extensions response' : ''; + } + // generate extension:version pairs and extension:URL pairs + $versions = []; + $urls = []; + foreach ( $extensions as $extension ) { + // filter out skins + if ( isset( $extension['type'] ) && $extension['type'] !== 'skin' ) { + continue; + } + $versions[] = $extension['name'] . ':' . ( $extension['version'] ?? $extension['vcs-version'] ?? '?' ); + $urls[] = $extension['name'] . ':' . ( $extension['url'] ?? '?' ); + } + // URLs cannot be separated by a comma, that is a valid URL + // character, see + // https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + // instead we will use a |, which isn't a valid part of a URL + $result = [ + 'versions' => implode( ',', $versions ), + 'urls' => implode( '|', $urls ), + ]; + return $result; + } + + /** + * @param MediaWiki $api + * + * @return array + */ + private function getExtensionsData( MediaWiki $api ) { $reqKey = $this->cache->makeKey( $api->action()->getApiUrl(), 'extensions', @@ -230,31 +343,17 @@ private function getExtensionsInfo( MediaWiki $api ) { 'siprop' => 'extensions' ] ); + try { $result = $api->action()->request( $versionReq ); $extensions = $result['query']['extensions']; if ( empty( $extensions ) ) { - return $this->config->get('RemoteWikiVerbose') ? 'ERROR: empty extensions response' : ''; + return $extensions; } - // generate extension:version pairs and extension:URL pairs - $versions = []; - $urls = []; - foreach ( $extensions as $extension ) { - $versions[] = $extension['name'] . ':' . ( $extension['version'] ?? $extension['vcs-version'] ?? '?' ); - $urls[] = $extension['name'] . ':' . ( $extension['url'] ?? '?' ); - } - // URLs cannot be separated by a comma, that is a valid URL - // character, see - // https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 - // instead we will use a |, which isn't a valid part of a URL - $result = [ - 'versions' => implode( ',', $versions ), - 'urls' => implode( '|', $urls ), - ]; - $this->cache->set( $reqKey, $result, $cacheTTL ); - return $result; + $this->cache->set( $reqKey, $extensions, $cacheTTL ); + return $extensions; } catch ( Exception $e ) { - return $this->config->get('RemoteWikiVerbose') ? $e->getMessage() : ''; + return []; } } diff --git a/tests/phpunit/integration/RemoteWikiTest.php b/tests/phpunit/integration/RemoteWikiTest.php index 2756473..82eb84c 100644 --- a/tests/phpunit/integration/RemoteWikiTest.php +++ b/tests/phpunit/integration/RemoteWikiTest.php @@ -9,112 +9,112 @@ /** * This class tests against the actual current version and extensions of * MediaWiki.org. - * + * * @coversDefaultClass \MediaWiki\Extension\RemoteWiki\RemoteWiki * @group extension-RemoteWiki */ class RemoteWikiTest extends MediaWikiIntegrationTestCase { - protected function setUp(): void { - parent::setUp(); - $this->setMwGlobals( [ - // Disable caching - 'wgRemoteWikiCacheTTL' => 0, - // Set a reasonable time limit - 'wgRemoteWikiTimeout' => 60, - // Use verbose error messages for failures - 'wgRemoteWikiVerbose' => true, - ] ); - } + protected function setUp(): void { + parent::setUp(); + $this->setMwGlobals( [ + // Disable caching + 'wgRemoteWikiCacheTTL' => 0, + // Set a reasonable time limit + 'wgRemoteWikiTimeout' => 60, + // Use verbose error messages for failures + 'wgRemoteWikiVerbose' => true, + ] ); + } - /** - * The version should be in the form `1.{x}.0.{y}`, where `x` is the - * current major version number (eg 41 for 1.41) and `y` is the current - * weekly branch. The actual version looks like `1.41.0-wmf.11` but - * RemoteWiki::getGenerator() removes characters other than digits and - * periods. - * - * @covers ::remoteVersion - * @covers ::getGenerator - * @dataProvider provideVersionParams - */ - public function testVersion( string $parameter ) { - $parser = $this->createNoOpMock( Parser::class ); - $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; - $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); + /** + * The version should be in the form `1.{x}.0.{y}`, where `x` is the + * current major version number (eg 41 for 1.41) and `y` is the current + * weekly branch. The actual version looks like `1.41.0-wmf.11` but + * RemoteWiki::getGenerator() removes characters other than digits and + * periods. + * + * @covers ::remoteVersion + * @covers ::getGenerator + * @dataProvider provideVersionParams + */ + public function testVersion( string $parameter ) { + $parser = $this->createNoOpMock( Parser::class ); + $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; + $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); - $version = $remote->remoteVersion( $parser, $mwEndpoint, $parameter ); - $this->assertRegExp( - '/^1\.\d+\.0\.\d+$/', - $version, - 'Version retrieved' - ); - } + $version = $remote->remoteVersion( $parser, $mwEndpoint, $parameter ); + $this->assertRegExp( + '/^1\.\d+\.0\.\d+$/', + $version, + 'Version retrieved' + ); + } - public static function provideVersionParams() { - yield 'default when not provided' => [ '' ]; - yield 'explicit version request' => [ 'version' ]; - yield 'fallback for unknown' => [ 'foobar' ]; - } + public static function provideVersionParams() { + yield 'default when not provided' => [ '' ]; + yield 'explicit version request' => [ 'version' ]; + yield 'fallback for unknown' => [ 'foobar' ]; + } - /** - * This test assumes that MediaWiki.org has the 'Math' extension installed - * and enabled - since the extension is bundled with MediaWiki it is - * unlikely that it will become disabled, but if this test starts failing - * that might be the reason. - * - * @covers ::remoteVersion - * @covers ::getExtensionVersions - * @covers ::getExtensionsInfo - */ - public function testExtensionVersions() { - $parser = $this->createNoOpMock( Parser::class ); - $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; - $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); + /** + * This test assumes that MediaWiki.org has the 'Math' extension installed + * and enabled - since the extension is bundled with MediaWiki it is + * unlikely that it will become disabled, but if this test starts failing + * that might be the reason. + * + * @covers ::remoteVersion + * @covers ::getExtensionVersions + * @covers ::getExtensionsInfo + */ + public function testExtensionVersions() { + $parser = $this->createNoOpMock( Parser::class ); + $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; + $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); - $extensions = $remote->remoteVersion( - $parser, - $mwEndpoint, - 'extensions' - ); - $this->assertStringContainsString( - 'Math:', - $extensions, - 'Math version retrieved' - ); - $this->assertStringNotContainsString( - 'Math:http', - $extensions, - 'Math version is not a URL' - ); - } + $extensions = $remote->remoteVersion( + $parser, + $mwEndpoint, + 'extensions' + ); + $this->assertStringContainsString( + 'Math:', + $extensions, + 'Math version retrieved' + ); + $this->assertStringNotContainsString( + 'Math:http', + $extensions, + 'Math version is not a URL' + ); + } - /** - * This test assumes that MediaWiki.org has the 'Math' extension installed - * and enabled, and the URL for that extension begins with http (i.e. is - * set)- since the extension is bundled with MediaWiki it is unlikely that - * it will become disabled, but if this test starts failing that might be - * the reason. - * - * @covers ::remoteVersion - * @covers ::getExtensionURLs - * @covers ::getExtensionsInfo - */ - public function testExtensionUrls() { - $parser = $this->createNoOpMock( Parser::class ); - $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; - $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); + /** + * This test assumes that MediaWiki.org has the 'Math' extension installed + * and enabled, and the URL for that extension begins with http (i.e. is + * set)- since the extension is bundled with MediaWiki it is unlikely that + * it will become disabled, but if this test starts failing that might be + * the reason. + * + * @covers ::remoteVersion + * @covers ::getExtensionURLs + * @covers ::getExtensionsInfo + */ + public function testExtensionUrls() { + $parser = $this->createNoOpMock( Parser::class ); + $mwEndpoint = 'https://www.mediawiki.org/w/api.php'; + $remote = MediaWikiServices::getInstance()->getService( 'RemoteWiki' ); - $extensions = $remote->remoteVersion( - $parser, - $mwEndpoint, - 'extension-urls' - ); - $this->assertStringContainsString( - 'Math:http', - $extensions, - 'Math URL retrieved' - ); - } + $extensions = $remote->remoteVersion( + $parser, + $mwEndpoint, + 'extension-urls' + ); + $this->assertStringContainsString( + 'Math:http', + $extensions, + 'Math URL retrieved' + ); + } } diff --git a/tests/phpunit/unit/RemoteWikiTest.php b/tests/phpunit/unit/RemoteWikiTest.php index cac29ef..818d14b 100644 --- a/tests/phpunit/unit/RemoteWikiTest.php +++ b/tests/phpunit/unit/RemoteWikiTest.php @@ -22,335 +22,407 @@ */ class RemoteWikiTest extends MediaWikiUnitTestCase { - private const MW_API = 'https://www.mediawiki.org/w/api.php'; + private const MW_API = 'https://www.mediawiki.org/w/api.php'; - /** - * Get a RemoteWiki instance with the given configuration (falling back to - * reasonable defaults). - * - * @param array $config - * @return TestingAccessWrapper wrapping RemoteWiki - */ - private function getRemote( array $config = [] ): TestingAccessWrapper { - $defaults = [ - 'RemoteWikiBotPasswords' => [], - 'RemoteWikiCacheTTL' => 3600, - 'RemoteWikiVerbose' => true, - 'RemoteWikiTimeout' => 60, - ]; - $remote = new RemoteWiki( - new HashConfig( $config + $defaults ), - new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ) - ); - return TestingAccessWrapper::newFromObject( $remote ); - } + /** + * Get a RemoteWiki instance with the given configuration (falling back to + * reasonable defaults). + * + * @param array $config + * + * @return TestingAccessWrapper wrapping RemoteWiki + */ + private function getRemote( array $config = [] ): TestingAccessWrapper { + $defaults = [ + 'RemoteWikiBotPasswords' => [], + 'RemoteWikiCacheTTL' => 3600, + 'RemoteWikiVerbose' => true, + 'RemoteWikiTimeout' => 60, + ]; + $remote = new RemoteWiki( + new HashConfig( $config + $defaults ), new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ) + ); - /** - * @covers ::validateEndpoint - * @dataProvider provideValidateEndpoint - */ - public function testValidateEndpoint( string $endPoint, bool $expected ) { - $this->assertSame( - $expected, - $this->getRemote()->validateEndpoint( $endPoint ) - ); - } + return TestingAccessWrapper::newFromObject( $remote ); + } - public static function provideValidateEndpoint() { - yield 'empty string' => [ '', false ]; - // It took a while to find something that parse_url() will reject; - // the string ':' should be rejected per PHP bug #55399 - yield 'parse_url returns false' => [ ':', false ]; - // The string '?' is accepted by parse_url() but not FILTER_VALIDATE_URL - yield 'filter_var returns false' => [ '?', false ]; - yield 'wiki endpoint' => [ 'https://www.mediawiki.org/w/api.php', true ]; - yield 'valid url' => [ 'https://example.com', true ]; - } + /** + * @covers ::validateEndpoint + * @dataProvider provideValidateEndpoint + */ + public function testValidateEndpoint( string $endPoint, bool $expected ) { + $this->assertSame( + $expected, + $this->getRemote()->validateEndpoint( $endPoint ) + ); + } - /** - * @covers ::getWikiApi - */ - public function testGetWikiApi_cache() { - $remote = $this->getRemote(); - $firstApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); - $secondApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); - $this->assertSame( $firstApi, $secondApi, 'Api instances are reused' ); - $thirdApi = $remote->getWikiApi( 'https://www.mediawiki.org/w/api.php' ); - $this->assertSame( - $firstApi, - $thirdApi, - 'Api caching ignores scheme' - ); - $fourthApi = $remote->getWikiApi( 'https://en.wikipedia.org/w/api.php' ); - $this->assertNotSame( - $firstApi, - $fourthApi, - 'Api caching handles multiple endpoints' - ); - } + public static function provideValidateEndpoint() { + yield 'empty string' => [ + '', + false + ]; + // It took a while to find something that parse_url() will reject; + // the string ':' should be rejected per PHP bug #55399 + yield 'parse_url returns false' => [ + ':', + false + ]; + // The string '?' is accepted by parse_url() but not FILTER_VALIDATE_URL + yield 'filter_var returns false' => [ + '?', + false + ]; + yield 'wiki endpoint' => [ + 'https://www.mediawiki.org/w/api.php', + true + ]; + yield 'valid url' => [ + 'https://example.com', + true + ]; + } - /** - * @covers ::getWikiApi - */ - public function testGetWikiApi_config() { - $settings = [ - 'RemoteWikiBotPasswords' => [ - 'www.mediawiki.org/w/api.php' => [ - 'username' => 'Foo', - 'password' => 'Bar' - ], - ], - 'RemoteWikiTimeout' => 173, - ]; - $remote = $this->getRemote( $settings ); - $enwikiApi = $remote->getWikiApi( '//en.wikipedia.org/w/api.php' ); - // Check timeout and authentication - $enwikiApiAccess = TestingAccessWrapper::newFromObject( $enwikiApi ); - $this->assertSame( - 173, - $enwikiApiAccess->config['timeout'], - 'Timeout configuration is used for `timeout`' - ); - $this->assertSame( - 173, - $enwikiApiAccess->config['connect_timeout'], - 'Timeout configuration is used for `connect_timeout`' - ); - $this->assertInstanceOf( - NoAuth::class, - $enwikiApiAccess->auth, - 'Wikis can be accessed without bot passwords' - ); + /** + * @covers ::getWikiApi + */ + public function testGetWikiApi_cache() { + $remote = $this->getRemote(); + $firstApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); + $secondApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); + $this->assertSame( $firstApi, $secondApi, 'Api instances are reused' ); + $thirdApi = $remote->getWikiApi( 'https://www.mediawiki.org/w/api.php' ); + $this->assertSame( + $firstApi, + $thirdApi, + 'Api caching ignores scheme' + ); + $fourthApi = $remote->getWikiApi( 'https://en.wikipedia.org/w/api.php' ); + $this->assertNotSame( + $firstApi, + $fourthApi, + 'Api caching handles multiple endpoints' + ); + } - $mwApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); - $mwAuth = TestingAccessWrapper::newFromObject( $mwApi )->auth; - $this->assertInstanceOf( - UserAndPassword::class, - $mwAuth, - 'Bot passwords used if configured' - ); - $this->assertSame( 'Foo', $mwAuth->getUsername(), 'Auth username' ); - $this->assertSame( 'Bar', $mwAuth->getPassword(), 'Auth password' ); - } + /** + * @covers ::getWikiApi + */ + public function testGetWikiApi_config() { + $settings = [ + 'RemoteWikiBotPasswords' => [ + 'www.mediawiki.org/w/api.php' => [ + 'username' => 'Foo', + 'password' => 'Bar' + ], + ], + 'RemoteWikiTimeout' => 173, + ]; + $remote = $this->getRemote( $settings ); + $enwikiApi = $remote->getWikiApi( '//en.wikipedia.org/w/api.php' ); + // Check timeout and authentication + $enwikiApiAccess = TestingAccessWrapper::newFromObject( $enwikiApi ); + $this->assertSame( + 173, + $enwikiApiAccess->config['timeout'], + 'Timeout configuration is used for `timeout`' + ); + $this->assertSame( + 173, + $enwikiApiAccess->config['connect_timeout'], + 'Timeout configuration is used for `connect_timeout`' + ); + $this->assertInstanceOf( + NoAuth::class, + $enwikiApiAccess->auth, + 'Wikis can be accessed without bot passwords' + ); - /** - * @covers ::getGenerator - * @dataProvider provideTestGetGenerator_cache - */ - public function testGetGenerator_cache( int $ttl, int $apiCalls ) { - $remote = $this->getRemote( [ 'RemoteWikiCacheTTL' => $ttl ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->exactly( $apiCalls ) ) - ->method( 'request' ) - ->willReturn( [ - 'query' => [ - 'general' => [ 'generator' => 'MediaWiki 1.41.0-wmf.123', ] - ] - ] ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - '1.41.0.123', - $remote->remoteVersion( $parser, self::MW_API ), - 'Version is fetched' - ); - $this->assertSame( - '1.41.0.123', - $remote->remoteVersion( $parser, self::MW_API ), - 'Cache usage' - ); - } + $mwApi = $remote->getWikiApi( '//www.mediawiki.org/w/api.php' ); + $mwAuth = TestingAccessWrapper::newFromObject( $mwApi )->auth; + $this->assertInstanceOf( + UserAndPassword::class, + $mwAuth, + 'Bot passwords used if configured' + ); + $this->assertSame( 'Foo', $mwAuth->getUsername(), 'Auth username' ); + $this->assertSame( 'Bar', $mwAuth->getPassword(), 'Auth password' ); + } - public static function provideTestGetGenerator_cache() { - yield 'Cache is used, queried once' => [ 3600, 1 ]; - yield 'Cache is not used, queried twice' => [ 0, 2 ]; - } + /** + * @covers ::getGenerator + * @dataProvider provideTestGetGenerator_cache + */ + public function testGetGenerator_cache( int $ttl, int $apiCalls ) { + $remote = $this->getRemote( [ 'RemoteWikiCacheTTL' => $ttl ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->exactly( $apiCalls ) )->method( 'request' )->willReturn( [ + 'query' => [ + 'general' => [ 'generator' => 'MediaWiki 1.41.0-wmf.123', ] + ] + ] ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + '1.41.0.123', + $remote->remoteVersion( $parser, self::MW_API ), + 'Version is fetched' + ); + $this->assertSame( + '1.41.0.123', + $remote->remoteVersion( $parser, self::MW_API ), + 'Cache usage' + ); + } - /** - * @covers ::getGenerator - * @dataProvider provideTestGetGenerator_empty - */ - public function testGetGenerator_empty( bool $verbose, string $expected ) { - $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->once() ) - ->method( 'request' ) - ->willReturn( - [ 'query' => [ 'general' => [ 'generator' => '', ] ] ] - ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - $expected, - $remote->remoteVersion( $parser, self::MW_API ), - 'Empty version' - ); - } - - public static function provideTestGetGenerator_empty() { - yield 'Verbose' => [ true, 'ERROR: empty version response' ]; - yield 'Non-verbose' => [ false, '' ]; - } + public static function provideTestGetGenerator_cache() { + yield 'Cache is used, queried once' => [ + 3600, + 1 + ]; + yield 'Cache is not used, queried twice' => [ + 0, + 2 + ]; + } - /** - * @covers ::getGenerator - * @dataProvider provideTestGetGenerator_error - */ - public function testGetGenerator_error( bool $verbose, string $expected ) { - $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->once() ) - ->method( 'request' ) - ->willThrowException( new Exception( 'TESTING!!!' ) ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - $expected, - $remote->remoteVersion( $parser, self::MW_API ), - 'Error output' - ); - } - - public static function provideTestGetGenerator_error() { - yield 'Verbose' => [ true, 'TESTING!!!' ]; - yield 'Non-verbose' => [ false, '' ]; - } + /** + * @covers ::getGenerator + * @dataProvider provideTestGetGenerator_empty + */ + public function testGetGenerator_empty( bool $verbose, string $expected ) { + $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->once() )->method( 'request' )->willReturn( + [ 'query' => [ 'general' => [ 'generator' => '', ] ] ] + ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + $expected, + $remote->remoteVersion( $parser, self::MW_API ), + 'Empty version' + ); + } - /** - * @covers ::getExtensionsInfo - * @covers ::getExtensionVersions - * @covers ::getExtensionURLs - * @dataProvider provideTestGetExtensions_cache - */ - public function testGetExtensions_cache( int $ttl, int $apiCalls ) { - $remote = $this->getRemote( [ 'RemoteWikiCacheTTL' => $ttl ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->exactly( $apiCalls ) ) - ->method( 'request' ) - ->willReturn( [ - 'query' => [ - 'extensions' => [ - [ 'name' => 'foo', 'version' => '123', 'url' => 'abc' ], - [ 'name' => 'bar', 'vcs-version' => '456', 'url' => 'xyz' ], - [ 'name' => 'baz' ], - ] - ] - ] ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - 'foo:123,bar:456,baz:?', - $remote->remoteVersion( $parser, self::MW_API, 'extensions' ), - 'Extensions are fetched' - ); - $this->assertSame( - 'foo:123,bar:456,baz:?', - $remote->remoteVersion( $parser, self::MW_API, 'extensions' ), - 'Cache usage' - ); - $this->assertSame( - 'foo:abc|bar:xyz|baz:?', - $remote->remoteVersion( $parser, self::MW_API, 'extension-urls' ), - 'Cache usage' - ); - } + public static function provideTestGetGenerator_empty() { + yield 'Verbose' => [ + true, + 'ERROR: empty version response' + ]; + yield 'Non-verbose' => [ + false, + '' + ]; + } - public static function provideTestGetExtensions_cache() { - yield 'Cache is used, queried once' => [ 3600, 1 ]; - yield 'Cache is not used, queried three times' => [ 0, 3 ]; - } + /** + * @covers ::getGenerator + * @dataProvider provideTestGetGenerator_error + */ + public function testGetGenerator_error( bool $verbose, string $expected ) { + $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->once() )->method( 'request' )->willThrowException( new Exception( 'TESTING!!!' ) ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + $expected, + $remote->remoteVersion( $parser, self::MW_API ), + 'Error output' + ); + } - /** - * @covers ::getExtensionsInfo - * @covers ::getExtensionVersions - * @covers ::getExtensionURLs - * @dataProvider provideTestGetExtensions_empty - */ - public function testGetExtensions_empty( - bool $verbose, - string $type, - string $expected - ) { - $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->once() ) - ->method( 'request' ) - ->willReturn( [ 'query' => [ 'extensions' => [] ] ] ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - $expected, - $remote->remoteVersion( $parser, self::MW_API, $type ), - 'Empty extensions' - ); - } - - public static function provideTestGetExtensions_empty() { - $error = 'ERROR: empty extensions response'; - yield 'Verbose versions' => [ true, 'extensions', $error ]; - yield 'Verbose URLs' => [ true, 'extension-urls', $error ]; - yield 'Non-verbose versions' => [ false, 'extensions', '' ]; - yield 'Non-verbose URLs' => [ false, 'extension-urls', '' ]; - } + public static function provideTestGetGenerator_error() { + yield 'Verbose' => [ + true, + 'TESTING!!!' + ]; + yield 'Non-verbose' => [ + false, + '' + ]; + } - /** - * @covers ::getExtensionsInfo - * @covers ::getExtensionVersions - * @covers ::getExtensionURLs - * @dataProvider provideTestGetExtensions_error - */ - public function testGetExtensions_error( - bool $verbose, - string $type, - string $expected - ) { - $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); - $actionApi = $this->installApi( $remote, self::MW_API ); - $actionApi->expects( $this->once() ) - ->method( 'request' ) - ->willThrowException( new Exception( 'TESTING!!!' ) ); - $parser = $this->createNoOpMock( Parser::class ); - $this->assertSame( - $expected, - $remote->remoteVersion( $parser, self::MW_API, $type ), - 'Error output' - ); - } - - public static function provideTestGetExtensions_error() { - yield 'Verbose versions' => [ true, 'extensions', 'TESTING!!!' ]; - yield 'Verbose URLs' => [ true, 'extension-urls', 'TESTING!!!' ]; - yield 'Non-verbose versions' => [ false, 'extensions', '' ]; - yield 'Non-verbose URLs' => [ false, 'extension-urls', '' ]; - } + /** + * @covers ::getExtensionsInfo + * @covers ::getExtensionVersions + * @covers ::getExtensionURLs + * @dataProvider provideTestGetExtensions_cache + */ + public function testGetExtensions_cache( int $ttl, int $apiCalls ) { + $remote = $this->getRemote( [ 'RemoteWikiCacheTTL' => $ttl ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->exactly( $apiCalls ) )->method( 'request' )->willReturn( [ + 'query' => [ + 'extensions' => [ + [ + 'name' => 'foo', + 'version' => '123', + 'url' => 'abc' + ], + [ + 'name' => 'bar', + 'vcs-version' => '456', + 'url' => 'xyz' + ], + [ 'name' => 'baz' ], + ] + ] + ] ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + 'foo:123,bar:456,baz:?', + $remote->remoteVersion( $parser, self::MW_API, 'extensions' ), + 'Extensions are fetched' + ); + $this->assertSame( + 'foo:123,bar:456,baz:?', + $remote->remoteVersion( $parser, self::MW_API, 'extensions' ), + 'Cache usage' + ); + $this->assertSame( + 'foo:abc|bar:xyz|baz:?', + $remote->remoteVersion( $parser, self::MW_API, 'extension-urls' ), + 'Cache usage' + ); + } - /** - * We cannot intercept the *creation* of the `MediaWiki` api objects, but - * we can access the cache to install a fake version that will be used - * instead of creating a new real instance. This method is used to create - * and install that fake (mocked) version, and returns the ActionApi - * mock so that the `request` method can be configured in a more - * fine-grained manner. - * - * @param TestingAccessWrapper $remote MUST wrap a RemoteWiki instance - * @param string $endPoint - * @return ActionApi|MockObject - */ - private function installApi( - TestingAccessWrapper $remote, - string $endPoint - ) { - $actionApi = $this->createNoOpMock( - ActionApi::class, - [ 'getApiUrl', 'request' ] - ); - $actionApi->method( 'getApiUrl' )->willReturn( $endPoint ); + public static function provideTestGetExtensions_cache() { + yield 'Cache is used, queried once' => [ + 3600, + 1 + ]; + yield 'Cache is not used, queried three times' => [ + 0, + 3 + ]; + } - $api = $this->createNoOpMock( MediaWiki::class, [ 'action' ] ); - $api->method( 'action' )->willReturn( $actionApi ); + /** + * @covers ::getExtensionsInfo + * @covers ::getExtensionVersions + * @covers ::getExtensionURLs + * @dataProvider provideTestGetExtensions_empty + */ + public function testGetExtensions_empty( + bool $verbose, + string $type, + string $expected + ) { + $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->once() )->method( 'request' )->willReturn( [ 'query' => [ 'extensions' => [] ] ] ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + $expected, + $remote->remoteVersion( $parser, self::MW_API, $type ), + 'Empty extensions' + ); + } - // Based on getWikiApi() - $parsed = parse_url( $endPoint ); - $apiKey = rtrim( $parsed['host'] . $parsed['path'], '/' ); + public static function provideTestGetExtensions_empty() { + $error = 'ERROR: empty extensions response'; + yield 'Verbose versions' => [ + true, + 'extensions', + $error + ]; + yield 'Verbose URLs' => [ + true, + 'extension-urls', + $error + ]; + yield 'Non-verbose versions' => [ + false, + 'extensions', + '' + ]; + yield 'Non-verbose URLs' => [ + false, + 'extension-urls', + '' + ]; + } - // Cannot indirectly modify overloaded property - $remote->apis = [ $apiKey => $api ]; - return $actionApi; - } + /** + * @covers ::getExtensionsInfo + * @covers ::getExtensionVersions + * @covers ::getExtensionURLs + * @dataProvider provideTestGetExtensions_error + */ + public function testGetExtensions_error( + bool $verbose, + string $type, + string $expected + ) { + $remote = $this->getRemote( [ 'RemoteWikiVerbose' => $verbose ] ); + $actionApi = $this->installApi( $remote, self::MW_API ); + $actionApi->expects( $this->once() )->method( 'request' )->willThrowException( new Exception( 'TESTING!!!' ) ); + $parser = $this->createNoOpMock( Parser::class ); + $this->assertSame( + $expected, + $remote->remoteVersion( $parser, self::MW_API, $type ), + 'Error output' + ); + } + + public static function provideTestGetExtensions_error() { + yield 'Verbose versions' => [ + true, + 'extensions', + 'ERROR: empty extensions response' + ]; + yield 'Verbose URLs' => [ + true, + 'extension-urls', + 'ERROR: empty extensions response' + ]; + yield 'Non-verbose versions' => [ + false, + 'extensions', + '' + ]; + yield 'Non-verbose URLs' => [ + false, + 'extension-urls', + '' + ]; + } + + /** + * We cannot intercept the *creation* of the `MediaWiki` api objects, but + * we can access the cache to install a fake version that will be used + * instead of creating a new real instance. This method is used to create + * and install that fake (mocked) version, and returns the ActionApi + * mock so that the `request` method can be configured in a more + * fine-grained manner. + * + * @param TestingAccessWrapper $remote MUST wrap a RemoteWiki instance + * @param string $endPoint + * + * @return ActionApi|MockObject + */ + private function installApi( + TestingAccessWrapper $remote, + string $endPoint + ) { + $actionApi = $this->createNoOpMock( + ActionApi::class, [ + 'getApiUrl', + 'request' + ] + ); + $actionApi->method( 'getApiUrl' )->willReturn( $endPoint ); + + $api = $this->createNoOpMock( MediaWiki::class, [ 'action' ] ); + $api->method( 'action' )->willReturn( $actionApi ); + + // Based on getWikiApi() + $parsed = parse_url( $endPoint ); + $apiKey = rtrim( $parsed['host'] . $parsed['path'], '/' ); + + // Cannot indirectly modify overloaded property + $remote->apis = [ $apiKey => $api ]; + + return $actionApi; + } }