Merge "Update docs/hooks.txt for ShowSearchHitTitle"
[mediawiki.git] / tests / phpunit / includes / registration / ExtensionProcessorTest.php
blob71f760dced80c072d33cfed8897653860bd24d8f
1 <?php
3 class ExtensionProcessorTest extends MediaWikiTestCase {
5 private $dir, $dirname;
7 public function setUp() {
8 parent::setUp();
9 $this->dir = __DIR__ . '/FooBar/extension.json';
10 $this->dirname = dirname( $this->dir );
13 /**
14 * 'name' is absolutely required
16 * @var array
18 public static $default = [
19 'name' => 'FooBar',
22 /**
23 * @covers ExtensionProcessor::extractInfo
25 public function testExtractInfo() {
26 // Test that attributes that begin with @ are ignored
27 $processor = new ExtensionProcessor();
28 $processor->extractInfo( $this->dir, self::$default + [
29 '@metadata' => [ 'foobarbaz' ],
30 'AnAttribute' => [ 'omg' ],
31 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
32 ], 1 );
34 $extracted = $processor->getExtractedInfo();
35 $attributes = $extracted['attributes'];
36 $this->assertArrayHasKey( 'AnAttribute', $attributes );
37 $this->assertArrayNotHasKey( '@metadata', $attributes );
38 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
41 public static function provideRegisterHooks() {
42 $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
43 // Format:
44 // Current $wgHooks
45 // Content in extension.json
46 // Expected value of $wgHooks
47 return [
48 // No hooks
50 [],
51 self::$default,
52 $merge,
54 // No current hooks, adding one for "FooBaz" in string format
56 [],
57 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
58 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
60 // Hook for "FooBaz", adding another one
62 [ 'FooBaz' => [ 'PriorCallback' ] ],
63 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
64 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
66 // No current hooks, adding one for "FooBaz" in verbose array format
68 [],
69 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
70 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
72 // Hook for "BarBaz", adding one for "FooBaz"
74 [ 'BarBaz' => [ 'BarBazCallback' ] ],
75 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
77 'BarBaz' => [ 'BarBazCallback' ],
78 'FooBaz' => [ 'FooBazCallback' ],
79 ] + $merge,
81 // Callbacks for FooBaz wrapped in an array
83 [],
84 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
86 'FooBaz' => [ 'Callback1' ],
87 ] + $merge,
89 // Multiple callbacks for FooBaz hook
91 [],
92 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
94 'FooBaz' => [ 'Callback1', 'Callback2' ],
95 ] + $merge,
101 * @covers ExtensionProcessor::extractHooks
102 * @dataProvider provideRegisterHooks
104 public function testRegisterHooks( $pre, $info, $expected ) {
105 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
106 $processor->extractInfo( $this->dir, $info, 1 );
107 $extracted = $processor->getExtractedInfo();
108 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
112 * @covers ExtensionProcessor::extractConfig1
114 public function testExtractConfig1() {
115 $processor = new ExtensionProcessor;
116 $info = [
117 'config' => [
118 'Bar' => 'somevalue',
119 'Foo' => 10,
120 '@IGNORED' => 'yes',
122 ] + self::$default;
123 $info2 = [
124 'config' => [
125 '_prefix' => 'eg',
126 'Bar' => 'somevalue'
128 'name' => 'FooBar2',
130 $processor->extractInfo( $this->dir, $info, 1 );
131 $processor->extractInfo( $this->dir, $info2, 1 );
132 $extracted = $processor->getExtractedInfo();
133 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
134 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
135 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
136 // Custom prefix:
137 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
141 * @covers ExtensionProcessor::extractConfig2
143 public function testExtractConfig2() {
144 $processor = new ExtensionProcessor;
145 $info = [
146 'config' => [
147 'Bar' => [ 'value' => 'somevalue' ],
148 'Foo' => [ 'value' => 10 ],
149 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
151 ] + self::$default;
152 $info2 = [
153 'config' => [
154 'Bar' => [ 'value' => 'somevalue' ],
156 'config_prefix' => 'eg',
157 'name' => 'FooBar2',
159 $processor->extractInfo( $this->dir, $info, 2 );
160 $processor->extractInfo( $this->dir, $info2, 2 );
161 $extracted = $processor->getExtractedInfo();
162 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
163 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
164 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
165 // Custom prefix:
166 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
169 public static function provideExtractExtensionMessagesFiles() {
170 $dir = __DIR__ . '/FooBar/';
171 return [
173 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
174 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
178 'ExtensionMessagesFiles' => [
179 'FooBarAlias' => 'FooBar.alias.php',
180 'FooBarMagic' => 'FooBar.magic.i18n.php',
184 'wgExtensionMessagesFiles' => [
185 'FooBarAlias' => $dir . 'FooBar.alias.php',
186 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
194 * @covers ExtensionProcessor::extractExtensionMessagesFiles
195 * @dataProvider provideExtractExtensionMessagesFiles
197 public function testExtractExtensionMessagesFiles( $input, $expected ) {
198 $processor = new ExtensionProcessor();
199 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
200 $out = $processor->getExtractedInfo();
201 foreach ( $expected as $key => $value ) {
202 $this->assertEquals( $value, $out['globals'][$key] );
206 public static function provideExtractMessagesDirs() {
207 $dir = __DIR__ . '/FooBar/';
208 return [
210 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
211 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
214 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
215 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
221 * @covers ExtensionProcessor::extractMessagesDirs
222 * @dataProvider provideExtractMessagesDirs
224 public function testExtractMessagesDirs( $input, $expected ) {
225 $processor = new ExtensionProcessor();
226 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
227 $out = $processor->getExtractedInfo();
228 foreach ( $expected as $key => $value ) {
229 $this->assertEquals( $value, $out['globals'][$key] );
234 * @covers ExtensionProcessor::extractCredits
236 public function testExtractCredits() {
237 $processor = new ExtensionProcessor();
238 $processor->extractInfo( $this->dir, self::$default, 1 );
239 $this->setExpectedException( 'Exception' );
240 $processor->extractInfo( $this->dir, self::$default, 1 );
244 * @covers ExtensionProcessor::extractResourceLoaderModules
245 * @dataProvider provideExtractResourceLoaderModules
247 public function testExtractResourceLoaderModules( $input, $expected ) {
248 $processor = new ExtensionProcessor();
249 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
250 $out = $processor->getExtractedInfo();
251 foreach ( $expected as $key => $value ) {
252 $this->assertEquals( $value, $out['globals'][$key] );
256 public static function provideExtractResourceLoaderModules() {
257 $dir = __DIR__ . '/FooBar';
258 return [
259 // Generic module with localBasePath/remoteExtPath specified
261 // Input
263 'ResourceModules' => [
264 'test.foo' => [
265 'styles' => 'foobar.js',
266 'localBasePath' => '',
267 'remoteExtPath' => 'FooBar',
271 // Expected
273 'wgResourceModules' => [
274 'test.foo' => [
275 'styles' => 'foobar.js',
276 'localBasePath' => $dir,
277 'remoteExtPath' => 'FooBar',
282 // ResourceFileModulePaths specified:
284 // Input
286 'ResourceFileModulePaths' => [
287 'localBasePath' => '',
288 'remoteExtPath' => 'FooBar',
290 'ResourceModules' => [
291 // No paths
292 'test.foo' => [
293 'styles' => 'foo.js',
295 // Different paths set
296 'test.bar' => [
297 'styles' => 'bar.js',
298 'localBasePath' => 'subdir',
299 'remoteExtPath' => 'FooBar/subdir',
301 // Custom class with no paths set
302 'test.class' => [
303 'class' => 'FooBarModule',
304 'extra' => 'argument',
306 // Custom class with a localBasePath
307 'test.class.with.path' => [
308 'class' => 'FooBarPathModule',
309 'extra' => 'argument',
310 'localBasePath' => '',
314 // Expected
316 'wgResourceModules' => [
317 'test.foo' => [
318 'styles' => 'foo.js',
319 'localBasePath' => $dir,
320 'remoteExtPath' => 'FooBar',
322 'test.bar' => [
323 'styles' => 'bar.js',
324 'localBasePath' => "$dir/subdir",
325 'remoteExtPath' => 'FooBar/subdir',
327 'test.class' => [
328 'class' => 'FooBarModule',
329 'extra' => 'argument',
330 'localBasePath' => $dir,
331 'remoteExtPath' => 'FooBar',
333 'test.class.with.path' => [
334 'class' => 'FooBarPathModule',
335 'extra' => 'argument',
336 'localBasePath' => $dir,
337 'remoteExtPath' => 'FooBar',
342 // ResourceModuleSkinStyles with file module paths
344 // Input
346 'ResourceFileModulePaths' => [
347 'localBasePath' => '',
348 'remoteSkinPath' => 'FooBar',
350 'ResourceModuleSkinStyles' => [
351 'foobar' => [
352 'test.foo' => 'foo.css',
356 // Expected
358 'wgResourceModuleSkinStyles' => [
359 'foobar' => [
360 'test.foo' => 'foo.css',
361 'localBasePath' => $dir,
362 'remoteSkinPath' => 'FooBar',
367 // ResourceModuleSkinStyles with file module paths and an override
369 // Input
371 'ResourceFileModulePaths' => [
372 'localBasePath' => '',
373 'remoteSkinPath' => 'FooBar',
375 'ResourceModuleSkinStyles' => [
376 'foobar' => [
377 'test.foo' => 'foo.css',
378 'remoteSkinPath' => 'BarFoo'
382 // Expected
384 'wgResourceModuleSkinStyles' => [
385 'foobar' => [
386 'test.foo' => 'foo.css',
387 'localBasePath' => $dir,
388 'remoteSkinPath' => 'BarFoo',
396 public static function provideSetToGlobal() {
397 return [
399 [ 'wgAPIModules', 'wgAvailableRights' ],
402 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
403 'AvailableRights' => [ 'foobar', 'unfoobar' ],
406 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
407 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
411 [ 'wgAPIModules', 'wgAvailableRights' ],
413 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
414 'wgAvailableRights' => [ 'barbaz' ]
417 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
418 'AvailableRights' => [ 'foobar', 'unfoobar' ],
421 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
422 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
426 [ 'wgGroupPermissions' ],
428 'wgGroupPermissions' => [
429 'sysop' => [ 'delete' ]
433 'GroupPermissions' => [
434 'sysop' => [ 'undelete' ],
435 'user' => [ 'edit' ]
439 'wgGroupPermissions' => [
440 'sysop' => [ 'delete', 'undelete' ],
441 'user' => [ 'edit' ]
448 public function testGlobalSettingsDocumentedInSchema() {
449 global $IP;
450 $globalSettings = TestingAccessWrapper::newFromClass(
451 ExtensionProcessor::class )->globalSettings;
453 $version = ExtensionRegistry::MANIFEST_VERSION;
454 $schema = FormatJson::decode(
455 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
456 true
458 $missing = [];
459 foreach ( $globalSettings as $global ) {
460 if ( !isset( $schema['properties'][$global] ) ) {
461 $missing[] = $global;
465 $this->assertEquals( [], $missing,
466 "The following global settings are not documented in docs/extension.schema.json" );
471 * Allow overriding the default value of $this->globals
472 * so we can test merging
474 class MockExtensionProcessor extends ExtensionProcessor {
475 public function __construct( $globals = [] ) {
476 $this->globals = $globals + $this->globals;