diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/registration')
4 files changed, 1385 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php new file mode 100644 index 00000000..d69ad597 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright (C) 2018 Kunal Mehta <legoktm@member.fsf.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +/** + * @covers ExtensionJsonValidator + */ +class ExtensionJsonValidatorTest extends MediaWikiTestCase { + + /** + * @dataProvider provideValidate + */ + public function testValidate( $file, $expected ) { + // If a dependency is missing, skip this test. + $validator = new ExtensionJsonValidator( function ( $msg ) { + $this->markTestSkipped( $msg ); + } ); + + if ( is_string( $expected ) ) { + $this->setExpectedException( + ExtensionJsonValidationError::class, + $expected + ); + } + + $dir = __DIR__ . '/../../data/registration/'; + $this->assertSame( + $expected, + $validator->validate( $dir . $file ) + ); + } + + public function provideValidate() { + return [ + [ + 'notjson.txt', + 'notjson.txt is not valid JSON' + ], + [ + 'no_manifest_version.json', + 'no_manifest_version.json does not have manifest_version set.' + ], + [ + 'old_manifest_version.json', + 'old_manifest_version.json is using a non-supported schema version' + ], + [ + 'newer_manifest_version.json', + 'newer_manifest_version.json is using a non-supported schema version' + ], + [ + 'bad_spdx.json', + "bad_spdx.json did not pass validation. +[license-name] Invalid SPDX license identifier, see <https://spdx.org/licenses/>" + ], + [ + 'invalid.json', + "invalid.json did not pass validation. +[license-name] Array value found, but a string is required" + ], + [ + 'good.json', + true + ], + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php new file mode 100644 index 00000000..d9e091dc --- /dev/null +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php @@ -0,0 +1,742 @@ +<?php + +use Wikimedia\TestingAccessWrapper; + +/** + * @covers ExtensionProcessor + */ +class ExtensionProcessorTest extends MediaWikiTestCase { + + private $dir, $dirname; + + public function setUp() { + parent::setUp(); + $this->dir = __DIR__ . '/FooBar/extension.json'; + $this->dirname = dirname( $this->dir ); + } + + /** + * 'name' is absolutely required + * + * @var array + */ + public static $default = [ + 'name' => 'FooBar', + ]; + + public function testExtractInfo() { + // Test that attributes that begin with @ are ignored + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, self::$default + [ + '@metadata' => [ 'foobarbaz' ], + 'AnAttribute' => [ 'omg' ], + 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ], + 'SpecialPages' => [ 'Foo' => 'SpecialFoo' ], + 'callback' => 'FooBar::onRegistration', + ], 1 ); + + $extracted = $processor->getExtractedInfo(); + $attributes = $extracted['attributes']; + $this->assertArrayHasKey( 'AnAttribute', $attributes ); + $this->assertArrayNotHasKey( '@metadata', $attributes ); + $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes ); + $this->assertSame( + [ 'FooBar' => 'FooBar::onRegistration' ], + $extracted['callbacks'] + ); + $this->assertSame( + [ 'Foo' => 'SpecialFoo' ], + $extracted['globals']['wgSpecialPages'] + ); + } + + public function testExtractNamespaces() { + // Test that namespace IDs can be overwritten + if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) { + define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 ); + } + + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, self::$default + [ + 'namespaces' => [ + [ + 'id' => 332200, + 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A', + 'name' => 'Test_A', + 'defaultcontentmodel' => 'TestModel', + 'gender' => [ + 'male' => 'Male test', + 'female' => 'Female test', + ], + 'subpages' => true, + 'content' => true, + 'protection' => 'userright', + ], + [ // Test_X will use ID 123456 not 334400 + 'id' => 334400, + 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', + 'name' => 'Test_X', + 'defaultcontentmodel' => 'TestModel' + ], + ] + ], 1 ); + + $extracted = $processor->getExtractedInfo(); + + $this->assertArrayHasKey( + 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A', + $extracted['defines'] + ); + $this->assertArrayNotHasKey( + 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', + $extracted['defines'] + ); + + $this->assertSame( + $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'], + 332200 + ); + + $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] ); + $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] ); + $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] ); + $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] ); + + $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] ); + $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] ); + $this->assertSame( + [ 'male' => 'Male test', 'female' => 'Female test' ], + $extracted['globals']['wgExtraGenderNamespaces'][332200] + ); + // A has subpages, X does not + $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] ); + $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] ); + } + + public static function provideRegisterHooks() { + $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ]; + // Format: + // Current $wgHooks + // Content in extension.json + // Expected value of $wgHooks + return [ + // No hooks + [ + [], + self::$default, + $merge, + ], + // No current hooks, adding one for "FooBaz" in string format + [ + [], + [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default, + [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge, + ], + // Hook for "FooBaz", adding another one + [ + [ 'FooBaz' => [ 'PriorCallback' ] ], + [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default, + [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge, + ], + // No current hooks, adding one for "FooBaz" in verbose array format + [ + [], + [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default, + [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge, + ], + // Hook for "BarBaz", adding one for "FooBaz" + [ + [ 'BarBaz' => [ 'BarBazCallback' ] ], + [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default, + [ + 'BarBaz' => [ 'BarBazCallback' ], + 'FooBaz' => [ 'FooBazCallback' ], + ] + $merge, + ], + // Callbacks for FooBaz wrapped in an array + [ + [], + [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default, + [ + 'FooBaz' => [ 'Callback1' ], + ] + $merge, + ], + // Multiple callbacks for FooBaz hook + [ + [], + [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default, + [ + 'FooBaz' => [ 'Callback1', 'Callback2' ], + ] + $merge, + ], + ]; + } + + /** + * @dataProvider provideRegisterHooks + */ + public function testRegisterHooks( $pre, $info, $expected ) { + $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] ); + $processor->extractInfo( $this->dir, $info, 1 ); + $extracted = $processor->getExtractedInfo(); + $this->assertEquals( $expected, $extracted['globals']['wgHooks'] ); + } + + public function testExtractConfig1() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => 'somevalue', + 'Foo' => 10, + '@IGNORED' => 'yes', + ], + ] + self::$default; + $info2 = [ + 'config' => [ + '_prefix' => 'eg', + 'Bar' => 'somevalue' + ], + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 1 ); + $processor->extractInfo( $this->dir, $info2, 1 ); + $extracted = $processor->getExtractedInfo(); + $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] ); + $this->assertEquals( 10, $extracted['globals']['wgFoo'] ); + $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] ); + // Custom prefix: + $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] ); + } + + public function testExtractConfig2() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + 'Foo' => [ 'value' => 10 ], + 'Path' => [ 'value' => 'foo.txt', 'path' => true ], + 'Namespaces' => [ + 'value' => [ + '10' => true, + '12' => false, + ], + 'merge_strategy' => 'array_plus', + ], + ], + ] + self::$default; + $info2 = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + ], + 'config_prefix' => 'eg', + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 2 ); + $processor->extractInfo( $this->dir, $info2, 2 ); + $extracted = $processor->getExtractedInfo(); + $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] ); + $this->assertEquals( 10, $extracted['globals']['wgFoo'] ); + $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] ); + // Custom prefix: + $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] ); + $this->assertSame( + [ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ], + $extracted['globals']['wgNamespaces'] + ); + } + + /** + * @expectedException RuntimeException + */ + public function testDuplicateConfigKey1() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => '', + ] + ] + self::$default; + $info2 = [ + 'config' => [ + 'Bar' => 'g', + ], + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 1 ); + $processor->extractInfo( $this->dir, $info2, 1 ); + } + + /** + * @expectedException RuntimeException + */ + public function testDuplicateConfigKey2() { + $processor = new ExtensionProcessor; + $info = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + ] + ] + self::$default; + $info2 = [ + 'config' => [ + 'Bar' => [ 'value' => 'somevalue' ], + ], + 'name' => 'FooBar2', + ]; + $processor->extractInfo( $this->dir, $info, 2 ); + $processor->extractInfo( $this->dir, $info2, 2 ); + } + + public static function provideExtractExtensionMessagesFiles() { + $dir = __DIR__ . '/FooBar/'; + return [ + [ + [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ], + [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ] + ], + [ + [ + 'ExtensionMessagesFiles' => [ + 'FooBarAlias' => 'FooBar.alias.php', + 'FooBarMagic' => 'FooBar.magic.i18n.php', + ], + ], + [ + 'wgExtensionMessagesFiles' => [ + 'FooBarAlias' => $dir . 'FooBar.alias.php', + 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php', + ], + ], + ], + ]; + } + + /** + * @dataProvider provideExtractExtensionMessagesFiles + */ + public function testExtractExtensionMessagesFiles( $input, $expected ) { + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, $input + self::$default, 1 ); + $out = $processor->getExtractedInfo(); + foreach ( $expected as $key => $value ) { + $this->assertEquals( $value, $out['globals'][$key] ); + } + } + + public static function provideExtractMessagesDirs() { + $dir = __DIR__ . '/FooBar/'; + return [ + [ + [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ], + [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ] + ], + [ + [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ], + [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ] + ], + ]; + } + + /** + * @dataProvider provideExtractMessagesDirs + */ + public function testExtractMessagesDirs( $input, $expected ) { + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, $input + self::$default, 1 ); + $out = $processor->getExtractedInfo(); + foreach ( $expected as $key => $value ) { + $this->assertEquals( $value, $out['globals'][$key] ); + } + } + + public function testExtractCredits() { + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, self::$default, 1 ); + $this->setExpectedException( Exception::class ); + $processor->extractInfo( $this->dir, self::$default, 1 ); + } + + /** + * @dataProvider provideExtractResourceLoaderModules + */ + public function testExtractResourceLoaderModules( $input, $expected ) { + $processor = new ExtensionProcessor(); + $processor->extractInfo( $this->dir, $input + self::$default, 1 ); + $out = $processor->getExtractedInfo(); + foreach ( $expected as $key => $value ) { + $this->assertEquals( $value, $out['globals'][$key] ); + } + } + + public static function provideExtractResourceLoaderModules() { + $dir = __DIR__ . '/FooBar'; + return [ + // Generic module with localBasePath/remoteExtPath specified + [ + // Input + [ + 'ResourceModules' => [ + 'test.foo' => [ + 'styles' => 'foobar.js', + 'localBasePath' => '', + 'remoteExtPath' => 'FooBar', + ], + ], + ], + // Expected + [ + 'wgResourceModules' => [ + 'test.foo' => [ + 'styles' => 'foobar.js', + 'localBasePath' => $dir, + 'remoteExtPath' => 'FooBar', + ], + ], + ], + ], + // ResourceFileModulePaths specified: + [ + // Input + [ + 'ResourceFileModulePaths' => [ + 'localBasePath' => 'modules', + 'remoteExtPath' => 'FooBar/modules', + ], + 'ResourceModules' => [ + // No paths + 'test.foo' => [ + 'styles' => 'foo.js', + ], + // Different paths set + 'test.bar' => [ + 'styles' => 'bar.js', + 'localBasePath' => 'subdir', + 'remoteExtPath' => 'FooBar/subdir', + ], + // Custom class with no paths set + 'test.class' => [ + 'class' => 'FooBarModule', + 'extra' => 'argument', + ], + // Custom class with a localBasePath + 'test.class.with.path' => [ + 'class' => 'FooBarPathModule', + 'extra' => 'argument', + 'localBasePath' => '', + ] + ], + ], + // Expected + [ + 'wgResourceModules' => [ + 'test.foo' => [ + 'styles' => 'foo.js', + 'localBasePath' => "$dir/modules", + 'remoteExtPath' => 'FooBar/modules', + ], + 'test.bar' => [ + 'styles' => 'bar.js', + 'localBasePath' => "$dir/subdir", + 'remoteExtPath' => 'FooBar/subdir', + ], + 'test.class' => [ + 'class' => 'FooBarModule', + 'extra' => 'argument', + 'localBasePath' => "$dir/modules", + 'remoteExtPath' => 'FooBar/modules', + ], + 'test.class.with.path' => [ + 'class' => 'FooBarPathModule', + 'extra' => 'argument', + 'localBasePath' => $dir, + 'remoteExtPath' => 'FooBar/modules', + ] + ], + ], + ], + // ResourceModuleSkinStyles with file module paths + [ + // Input + [ + 'ResourceFileModulePaths' => [ + 'localBasePath' => '', + 'remoteSkinPath' => 'FooBar', + ], + 'ResourceModuleSkinStyles' => [ + 'foobar' => [ + 'test.foo' => 'foo.css', + ] + ], + ], + // Expected + [ + 'wgResourceModuleSkinStyles' => [ + 'foobar' => [ + 'test.foo' => 'foo.css', + 'localBasePath' => $dir, + 'remoteSkinPath' => 'FooBar', + ], + ], + ], + ], + // ResourceModuleSkinStyles with file module paths and an override + [ + // Input + [ + 'ResourceFileModulePaths' => [ + 'localBasePath' => '', + 'remoteSkinPath' => 'FooBar', + ], + 'ResourceModuleSkinStyles' => [ + 'foobar' => [ + 'test.foo' => 'foo.css', + 'remoteSkinPath' => 'BarFoo' + ], + ], + ], + // Expected + [ + 'wgResourceModuleSkinStyles' => [ + 'foobar' => [ + 'test.foo' => 'foo.css', + 'localBasePath' => $dir, + 'remoteSkinPath' => 'BarFoo', + ], + ], + ], + ], + ]; + } + + public static function provideSetToGlobal() { + return [ + [ + [ 'wgAPIModules', 'wgAvailableRights' ], + [], + [ + 'APIModules' => [ 'foobar' => 'ApiFooBar' ], + 'AvailableRights' => [ 'foobar', 'unfoobar' ], + ], + [ + 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ], + 'wgAvailableRights' => [ 'foobar', 'unfoobar' ], + ], + ], + [ + [ 'wgAPIModules', 'wgAvailableRights' ], + [ + 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ], + 'wgAvailableRights' => [ 'barbaz' ] + ], + [ + 'APIModules' => [ 'foobar' => 'ApiFooBar' ], + 'AvailableRights' => [ 'foobar', 'unfoobar' ], + ], + [ + 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ], + 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ], + ], + ], + [ + [ 'wgGroupPermissions' ], + [ + 'wgGroupPermissions' => [ + 'sysop' => [ 'delete' ] + ], + ], + [ + 'GroupPermissions' => [ + 'sysop' => [ 'undelete' ], + 'user' => [ 'edit' ] + ], + ], + [ + 'wgGroupPermissions' => [ + 'sysop' => [ 'delete', 'undelete' ], + 'user' => [ 'edit' ] + ], + ] + ] + ]; + } + + /** + * Attributes under manifest_version 2 + */ + public function testExtractAttributes() { + $processor = new ExtensionProcessor(); + // Load FooBar extension + $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 ); + $processor->extractInfo( + $this->dir, + [ + 'name' => 'Baz', + 'attributes' => [ + // Loaded + 'FooBar' => [ + 'Plugins' => [ + 'ext.baz.foobar', + ], + ], + // Not loaded + 'FizzBuzz' => [ + 'MorePlugins' => [ + 'ext.baz.fizzbuzz', + ], + ], + ], + ], + 2 + ); + + $info = $processor->getExtractedInfo(); + $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] ); + $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] ); + $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] ); + } + + /** + * Attributes under manifest_version 1 + */ + public function testAttributes1() { + $processor = new ExtensionProcessor(); + $processor->extractInfo( + $this->dir, + [ + 'name' => 'FooBar', + 'FooBarPlugins' => [ + 'ext.baz.foobar', + ], + 'FizzBuzzMorePlugins' => [ + 'ext.baz.fizzbuzz', + ], + ], + 1 + ); + $processor->extractInfo( + $this->dir, + [ + 'name' => 'FooBar2', + 'FizzBuzzMorePlugins' => [ + 'ext.bar.fizzbuzz', + ] + ], + 1 + ); + + $info = $processor->getExtractedInfo(); + $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] ); + $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] ); + $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] ); + $this->assertSame( + [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ], + $info['attributes']['FizzBuzzMorePlugins'] + ); + } + + public function testAttributes1_notarray() { + $processor = new ExtensionProcessor(); + $this->setExpectedException( + InvalidArgumentException::class, + "The value for 'FooBarPlugins' should be an array (from {$this->dir})" + ); + $processor->extractInfo( + $this->dir, + [ + 'FooBarPlugins' => 'ext.baz.foobar', + ] + self::$default, + 1 + ); + } + + public function testExtractPathBasedGlobal() { + $processor = new ExtensionProcessor(); + $processor->extractInfo( + $this->dir, + [ + 'ParserTestFiles' => [ + 'tests/parserTests.txt', + 'tests/extraParserTests.txt', + ], + 'ServiceWiringFiles' => [ + 'includes/ServiceWiring.php' + ], + ] + self::$default, + 1 + ); + $globals = $processor->getExtractedInfo()['globals']; + $this->assertArrayHasKey( 'wgParserTestFiles', $globals ); + $this->assertSame( [ + "{$this->dirname}/tests/parserTests.txt", + "{$this->dirname}/tests/extraParserTests.txt" + ], $globals['wgParserTestFiles'] ); + $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals ); + $this->assertSame( [ + "{$this->dirname}/includes/ServiceWiring.php" + ], $globals['wgServiceWiringFiles'] ); + } + + public function testGetRequirements() { + $info = self::$default + [ + 'requires' => [ + 'MediaWiki' => '>= 1.25.0', + 'extensions' => [ + 'Bar' => '*' + ] + ] + ]; + $processor = new ExtensionProcessor(); + $this->assertSame( + $info['requires'], + $processor->getRequirements( $info ) + ); + $this->assertSame( + [], + $processor->getRequirements( [] ) + ); + } + + public function testGetExtraAutoloaderPaths() { + $processor = new ExtensionProcessor(); + $this->assertSame( + [ "{$this->dirname}/vendor/autoload.php" ], + $processor->getExtraAutoloaderPaths( $this->dirname, [ + 'load_composer_autoloader' => true, + ] ) + ); + } + + /** + * Verify that extension.schema.json is in sync with ExtensionProcessor + * + * @coversNothing + */ + public function testGlobalSettingsDocumentedInSchema() { + global $IP; + $globalSettings = TestingAccessWrapper::newFromClass( + ExtensionProcessor::class )->globalSettings; + + $version = ExtensionRegistry::MANIFEST_VERSION; + $schema = FormatJson::decode( + file_get_contents( "$IP/docs/extension.schema.v$version.json" ), + true + ); + $missing = []; + foreach ( $globalSettings as $global ) { + if ( !isset( $schema['properties'][$global] ) ) { + $missing[] = $global; + } + } + + $this->assertEquals( [], $missing, + "The following global settings are not documented in docs/extension.schema.json" ); + } +} + +/** + * Allow overriding the default value of $this->globals + * so we can test merging + */ +class MockExtensionProcessor extends ExtensionProcessor { + public function __construct( $globals = [] ) { + $this->globals = $globals + $this->globals; + } +} diff --git a/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php b/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php new file mode 100644 index 00000000..67bc088d --- /dev/null +++ b/www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php @@ -0,0 +1,352 @@ +<?php + +/** + * @covers ExtensionRegistry + */ +class ExtensionRegistryTest extends MediaWikiTestCase { + + private $dataDir; + + public function setUp() { + parent::setUp(); + $this->dataDir = __DIR__ . '/../../data/registration'; + } + + public function testQueue_invalid() { + $registry = new ExtensionRegistry(); + $path = __DIR__ . '/doesnotexist.json'; + $this->setExpectedException( + Exception::class, + "$path does not exist!" + ); + $registry->queue( $path ); + } + + public function testQueue() { + $registry = new ExtensionRegistry(); + $path = "{$this->dataDir}/good.json"; + $registry->queue( $path ); + $this->assertArrayHasKey( + $path, + $registry->getQueue() + ); + $registry->clearQueue(); + $this->assertEmpty( $registry->getQueue() ); + } + + public function testLoadFromQueue_empty() { + $registry = new ExtensionRegistry(); + $registry->loadFromQueue(); + $this->assertEmpty( $registry->getAllThings() ); + } + + public function testLoadFromQueue_late() { + $registry = new ExtensionRegistry(); + $registry->finish(); + $registry->queue( "{$this->dataDir}/good.json" ); + $this->setExpectedException( + MWException::class, + "The following paths tried to load late: {$this->dataDir}/good.json" + ); + $registry->loadFromQueue(); + } + + /** + * @dataProvider provideExportExtractedDataGlobals + */ + public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) { + // Set globals for test + if ( $before ) { + foreach ( $before as $key => $value ) { + // mw prefixed globals does not exist normally + if ( substr( $key, 0, 2 ) == 'mw' ) { + $GLOBALS[$key] = $value; + } else { + $this->setMwGlobals( $key, $value ); + } + } + } + + $info = [ + 'globals' => $globals, + 'callbacks' => [], + 'defines' => [], + 'credits' => [], + 'attributes' => [], + 'autoloaderPaths' => [] + ]; + $registry = new ExtensionRegistry(); + $class = new ReflectionClass( ExtensionRegistry::class ); + $method = $class->getMethod( 'exportExtractedData' ); + $method->setAccessible( true ); + $method->invokeArgs( $registry, [ $info ] ); + foreach ( $expected as $name => $value ) { + $this->assertArrayHasKey( $name, $GLOBALS, $desc ); + $this->assertEquals( $value, $GLOBALS[$name], $desc ); + } + + // Remove mw prefixed globals + if ( $before ) { + foreach ( $before as $key => $value ) { + if ( substr( $key, 0, 2 ) == 'mw' ) { + unset( $GLOBALS[$key] ); + } + } + } + } + + public static function provideExportExtractedDataGlobals() { + // "mwtest" prefix used instead of "$wg" to avoid potential conflicts + return [ + [ + 'Simple non-array values', + [ + 'mwtestFooBarConfig' => true, + 'mwtestFooBarConfig2' => 'string', + ], + [ + 'mwtestFooBarDefault' => 1234, + 'mwtestFooBarConfig' => false, + ], + [ + 'mwtestFooBarConfig' => true, + 'mwtestFooBarConfig2' => 'string', + 'mwtestFooBarDefault' => 1234, + ], + ], + [ + 'No global already set, simple array', + null, + [ + 'mwtestDefaultOptions' => [ + 'foobar' => true, + ] + ], + [ + 'mwtestDefaultOptions' => [ + 'foobar' => true, + ] + ], + ], + [ + 'Global already set, simple array', + [ + 'mwtestDefaultOptions' => [ + 'foobar' => true, + 'foo' => 'string' + ], + ], + [ + 'mwtestDefaultOptions' => [ + 'barbaz' => 12345, + 'foobar' => false, + ], + ], + [ + 'mwtestDefaultOptions' => [ + 'barbaz' => 12345, + 'foo' => 'string', + 'foobar' => true, + ], + ] + ], + [ + 'Global already set, 1d array that appends', + [ + 'mwAvailableRights' => [ + 'foobar', + 'foo' + ], + ], + [ + 'mwAvailableRights' => [ + 'barbaz', + ], + ], + [ + 'mwAvailableRights' => [ + 'barbaz', + 'foobar', + 'foo', + ], + ] + ], + [ + 'Global already set, array with integer keys', + [ + 'mwNamespacesFoo' => [ + 100 => true, + 102 => false + ], + ], + [ + 'mwNamespacesFoo' => [ + 100 => false, + 500 => true, + ExtensionRegistry::MERGE_STRATEGY => 'array_plus', + ], + ], + [ + 'mwNamespacesFoo' => [ + 100 => true, + 102 => false, + 500 => true, + ], + ] + ], + [ + 'No global already set, $wgHooks', + [ + 'wgHooks' => [], + ], + [ + 'wgHooks' => [ + 'FooBarEvent' => [ + 'FooBarClass::onFooBarEvent' + ], + ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' + ], + ], + [ + 'wgHooks' => [ + 'FooBarEvent' => [ + 'FooBarClass::onFooBarEvent' + ], + ], + ], + ], + [ + 'Global already set, $wgHooks', + [ + 'wgHooks' => [ + 'FooBarEvent' => [ + 'FooBarClass::onFooBarEvent' + ], + 'BazBarEvent' => [ + 'FooBarClass::onBazBarEvent', + ], + ], + ], + [ + 'wgHooks' => [ + 'FooBarEvent' => [ + 'BazBarClass::onFooBarEvent', + ], + ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive', + ], + ], + [ + 'wgHooks' => [ + 'FooBarEvent' => [ + 'FooBarClass::onFooBarEvent', + 'BazBarClass::onFooBarEvent', + ], + 'BazBarEvent' => [ + 'FooBarClass::onBazBarEvent', + ], + ], + ], + ], + [ + 'Global already set, $wgGroupPermissions', + [ + 'wgGroupPermissions' => [ + 'sysop' => [ + 'something' => true, + ], + 'user' => [ + 'somethingtwo' => true, + ] + ], + ], + [ + 'wgGroupPermissions' => [ + 'customgroup' => [ + 'right' => true, + ], + 'user' => [ + 'right' => true, + 'somethingtwo' => false, + 'nonduplicated' => true, + ], + ExtensionRegistry::MERGE_STRATEGY => 'array_plus_2d', + ], + ], + [ + 'wgGroupPermissions' => [ + 'customgroup' => [ + 'right' => true, + ], + 'sysop' => [ + 'something' => true, + ], + 'user' => [ + 'somethingtwo' => true, + 'right' => true, + 'nonduplicated' => true, + ] + ], + ], + ], + [ + 'False local setting should not be overridden (T100767)', + [ + 'mwtestT100767' => false, + ], + [ + 'mwtestT100767' => true, + ], + [ + 'mwtestT100767' => false, + ], + ], + [ + 'test array_replace_recursive', + [ + 'mwtestJsonConfigs' => [ + 'JsonZeroConfig' => [ + 'namespace' => 480, + 'nsName' => 'Zero', + 'isLocal' => true, + ], + ], + ], + [ + 'mwtestJsonConfigs' => [ + 'JsonZeroConfig' => [ + 'isLocal' => false, + 'remote' => [ + 'username' => 'foo', + ], + ], + ExtensionRegistry::MERGE_STRATEGY => 'array_replace_recursive', + ], + ], + [ + 'mwtestJsonConfigs' => [ + 'JsonZeroConfig' => [ + 'namespace' => 480, + 'nsName' => 'Zero', + 'isLocal' => false, + 'remote' => [ + 'username' => 'foo', + ], + ], + ], + ], + ], + [ + 'global is null before', + [ + 'NullGlobal' => null, + ], + [ + 'NullGlobal' => 'not-null' + ], + [ + 'NullGlobal' => null + ], + ], + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php b/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php new file mode 100644 index 00000000..b668a9ad --- /dev/null +++ b/www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php @@ -0,0 +1,207 @@ +<?php + +/** + * @covers VersionChecker + */ +class VersionCheckerTest extends PHPUnit\Framework\TestCase { + + use MediaWikiCoversValidator; + use PHPUnit4And6Compat; + + /** + * @dataProvider provideCheck + */ + public function testCheck( $coreVersion, $constraint, $expected ) { + $checker = new VersionChecker( $coreVersion ); + $this->assertEquals( $expected, !(bool)$checker->checkArray( [ + 'FakeExtension' => [ + 'MediaWiki' => $constraint, + ], + ] ) ); + } + + public static function provideCheck() { + return [ + // [ $wgVersion, constraint, expected ] + [ '1.25alpha', '>= 1.26', false ], + [ '1.25.0', '>= 1.26', false ], + [ '1.26alpha', '>= 1.26', true ], + [ '1.26alpha', '>= 1.26.0', true ], + [ '1.26alpha', '>= 1.26.0-stable', false ], + [ '1.26.0', '>= 1.26.0-stable', true ], + [ '1.26.1', '>= 1.26.0-stable', true ], + [ '1.27.1', '>= 1.26.0-stable', true ], + [ '1.26alpha', '>= 1.26.1', false ], + [ '1.26alpha', '>= 1.26alpha', true ], + [ '1.26alpha', '>= 1.25', true ], + [ '1.26.0-alpha.14', '>= 1.26.0-alpha.15', false ], + [ '1.26.0-alpha.14', '>= 1.26.0-alpha.10', true ], + [ '1.26.1', '>= 1.26.2, <=1.26.0', false ], + [ '1.26.1', '^1.26.2', false ], + // Accept anything for un-parsable version strings + [ '1.26mwf14', '== 1.25alpha', true ], + [ 'totallyinvalid', '== 1.0', true ], + ]; + } + + /** + * @dataProvider provideType + */ + public function testType( $given, $expected ) { + $checker = new VersionChecker( '1.0.0' ); + $checker->setLoadedExtensionsAndSkins( [ + 'FakeDependency' => [ + 'version' => '1.0.0', + ], + 'NoVersionGiven' => [], + ] ); + $this->assertEquals( $expected, $checker->checkArray( [ + 'FakeExtension' => $given, + ] ) ); + } + + public static function provideType() { + return [ + // valid type + [ + [ + 'extensions' => [ + 'FakeDependency' => '1.0.0', + ], + ], + [], + ], + [ + [ + 'MediaWiki' => '1.0.0', + ], + [], + ], + [ + [ + 'extensions' => [ + 'NoVersionGiven' => '*', + ], + ], + [], + ], + [ + [ + 'extensions' => [ + 'NoVersionGiven' => '1.0', + ], + ], + [ + [ + 'incompatible' => 'FakeExtension', + 'type' => 'incompatible-extensions', + 'msg' => 'NoVersionGiven does not expose its version, but FakeExtension requires: 1.0.', + ], + ], + ], + [ + [ + 'extensions' => [ + 'Missing' => '*', + ], + ], + [ + [ + 'missing' => 'Missing', + 'type' => 'missing-extensions', + 'msg' => 'FakeExtension requires Missing to be installed.', + ], + ], + ], + [ + [ + 'extensions' => [ + 'FakeDependency' => '2.0.0', + ], + ], + [ + [ + 'incompatible' => 'FakeExtension', + 'type' => 'incompatible-extensions', + // phpcs:ignore Generic.Files.LineLength.TooLong + 'msg' => 'FakeExtension is not compatible with the current installed version of FakeDependency (1.0.0), it requires: 2.0.0.', + ], + ], + ], + [ + [ + 'skins' => [ + 'FakeSkin' => '*', + ], + ], + [ + [ + 'missing' => 'FakeSkin', + 'type' => 'missing-skins', + 'msg' => 'FakeExtension requires FakeSkin to be installed.', + ], + ], + ], + ]; + } + + /** + * Check, if a non-parsable version constraint does not throw an exception or + * returns any error message. + */ + public function testInvalidConstraint() { + $checker = new VersionChecker( '1.0.0' ); + $checker->setLoadedExtensionsAndSkins( [ + 'FakeDependency' => [ + 'version' => 'not really valid', + ], + ] ); + $this->assertEquals( [ + [ + 'type' => 'invalid-version', + 'msg' => "FakeDependency does not have a valid version string.", + ], + ], $checker->checkArray( [ + 'FakeExtension' => [ + 'extensions' => [ + 'FakeDependency' => '1.24.3', + ], + ], + ] ) ); + + $checker = new VersionChecker( '1.0.0' ); + $checker->setLoadedExtensionsAndSkins( [ + 'FakeDependency' => [ + 'version' => '1.24.3', + ], + ] ); + + $this->setExpectedException( UnexpectedValueException::class ); + $checker->checkArray( [ + 'FakeExtension' => [ + 'FakeDependency' => 'not really valid', + ], + ] ); + } + + /** + * T197478 + */ + public function testInvalidDependency() { + $checker = new VersionChecker( '1.0.0' ); + $this->setExpectedException( UnexpectedValueException::class, + 'Dependency type skin unknown in FakeExtension' ); + $this->assertEquals( [ + [ + 'type' => 'invalid-version', + 'msg' => 'FakeDependency does not have a valid version string.', + ], + ], $checker->checkArray( [ + 'FakeExtension' => [ + 'skin' => [ + 'FakeSkin' => '*', + ], + ], + ] ) ); + } +} |