diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php')
-rw-r--r-- | www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php | 742 |
1 files changed, 742 insertions, 0 deletions
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; + } +} |