summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/registration
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/registration')
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionJsonValidatorTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionProcessorTest.php742
-rw-r--r--www/wiki/tests/phpunit/includes/registration/ExtensionRegistryTest.php352
-rw-r--r--www/wiki/tests/phpunit/includes/registration/VersionCheckerTest.php207
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' => '*',
+ ],
+ ],
+ ] ) );
+ }
+}