wiki.techinc.nl/tests/phpunit/includes/registration/ExtensionProcessorTest.php
Florianschmidtwelzow 29a5fc72e3 registration: Only allow one extension to set a specific config setting
ExtensionProcessor would previously just blindly overwrite duplicate
config settings, which ends up depending upon load order.

It's relatively hard to debug since it is silently overwritten. This now
throws exceptions in case of duplicate config settings.

This will also have some side-effects of catching people putting things
like "ResourceModules" in their "config" section when it should be a
top-level item.

Bug: T152929
Depends-On: I4c5eaf87657f5dc07787480a2f1a56a1db8c714f
Change-Id: Ieeb26011e42c741041d2c3252238ca0823b99eb4
2017-08-28 15:03:47 +00:00

640 lines
16 KiB
PHP

<?php
use Wikimedia\TestingAccessWrapper;
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',
];
/**
* @covers ExtensionProcessor::extractInfo
*/
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' ],
], 1 );
$extracted = $processor->getExtractedInfo();
$attributes = $extracted['attributes'];
$this->assertArrayHasKey( 'AnAttribute', $attributes );
$this->assertArrayNotHasKey( '@metadata', $attributes );
$this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
}
/**
* @covers ExtensionProcessor::extractInfo
*/
public function testExtractInfo_namespaces() {
// 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',
'content' => 'TestModel'
],
[ // Test_X will use ID 123456 not 334400
'id' => 334400,
'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
'name' => 'Test_X',
'content' => '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] );
}
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,
],
];
}
/**
* @covers ExtensionProcessor::extractHooks
* @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'] );
}
/**
* @covers ExtensionProcessor::extractConfig1
*/
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'] );
}
/**
* @covers ExtensionProcessor::extractConfig2
*/
public function testExtractConfig2() {
$processor = new ExtensionProcessor;
$info = [
'config' => [
'Bar' => [ 'value' => 'somevalue' ],
'Foo' => [ 'value' => 10 ],
'Path' => [ 'value' => 'foo.txt', 'path' => true ],
],
] + 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'] );
}
/**
* @covers ExtensionProcessor::addConfigGlobal()
* @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 );
}
/**
* @covers ExtensionProcessor::addConfigGlobal()
* @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',
],
],
],
];
}
/**
* @covers ExtensionProcessor::extractExtensionMessagesFiles
* @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' ] ] ]
],
];
}
/**
* @covers ExtensionProcessor::extractMessagesDirs
* @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] );
}
}
/**
* @covers ExtensionProcessor::extractCredits
*/
public function testExtractCredits() {
$processor = new ExtensionProcessor();
$processor->extractInfo( $this->dir, self::$default, 1 );
$this->setExpectedException( 'Exception' );
$processor->extractInfo( $this->dir, self::$default, 1 );
}
/**
* @covers ExtensionProcessor::extractResourceLoaderModules
* @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' => '',
'remoteExtPath' => 'FooBar',
],
'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,
'remoteExtPath' => 'FooBar',
],
'test.bar' => [
'styles' => 'bar.js',
'localBasePath' => "$dir/subdir",
'remoteExtPath' => 'FooBar/subdir',
],
'test.class' => [
'class' => 'FooBarModule',
'extra' => 'argument',
'localBasePath' => $dir,
'remoteExtPath' => 'FooBar',
],
'test.class.with.path' => [
'class' => 'FooBarPathModule',
'extra' => 'argument',
'localBasePath' => $dir,
'remoteExtPath' => 'FooBar',
]
],
],
],
// 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
*
* @covers ExtensionProcessor::extractAttributes
* @covers ExtensionProcessor::getExtractedInfo
*/
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
*
* @covers ExtensionProcessor::extractInfo
*/
public function testAttributes1() {
$processor = new ExtensionProcessor();
$processor->extractInfo(
$this->dir,
[
'name' => 'FooBar',
'FooBarPlugins' => [
'ext.baz.foobar',
],
'FizzBuzzMorePlugins' => [
'ext.baz.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' ], $info['attributes']['FizzBuzzMorePlugins'] );
}
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;
}
}