Merge "Revert "selenium: add new message banner test to user spec""
[lhc/web/wiklou.git] / tests / phpunit / includes / registration / ExtensionProcessorTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ExtensionProcessorTest extends MediaWikiTestCase {
6
7 private $dir, $dirname;
8
9 public function setUp() {
10 parent::setUp();
11 $this->dir = __DIR__ . '/FooBar/extension.json';
12 $this->dirname = dirname( $this->dir );
13 }
14
15 /**
16 * 'name' is absolutely required
17 *
18 * @var array
19 */
20 public static $default = [
21 'name' => 'FooBar',
22 ];
23
24 /**
25 * @covers ExtensionProcessor::extractInfo
26 */
27 public function testExtractInfo() {
28 // Test that attributes that begin with @ are ignored
29 $processor = new ExtensionProcessor();
30 $processor->extractInfo( $this->dir, self::$default + [
31 '@metadata' => [ 'foobarbaz' ],
32 'AnAttribute' => [ 'omg' ],
33 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
34 ], 1 );
35
36 $extracted = $processor->getExtractedInfo();
37 $attributes = $extracted['attributes'];
38 $this->assertArrayHasKey( 'AnAttribute', $attributes );
39 $this->assertArrayNotHasKey( '@metadata', $attributes );
40 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
41 }
42
43 /**
44 * @covers ExtensionProcessor::extractInfo
45 */
46 public function testExtractInfo_namespaces() {
47 // Test that namespace IDs can be overwritten
48 if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
49 define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
50 }
51
52 $processor = new ExtensionProcessor();
53 $processor->extractInfo( $this->dir, self::$default + [
54 'namespaces' => [
55 [
56 'id' => 332200,
57 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
58 'name' => 'Test_A',
59 'content' => 'TestModel'
60 ],
61 [ // Test_X will use ID 123456 not 334400
62 'id' => 334400,
63 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
64 'name' => 'Test_X',
65 'content' => 'TestModel'
66 ],
67 ]
68 ], 1 );
69
70 $extracted = $processor->getExtractedInfo();
71
72 $this->assertArrayHasKey(
73 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
74 $extracted['defines']
75 );
76 $this->assertArrayNotHasKey(
77 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
78 $extracted['defines']
79 );
80
81 $this->assertSame(
82 $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
83 332200
84 );
85
86 $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
87 $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
88 $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
89 $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
90
91 $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
92 $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
93 }
94
95 public static function provideRegisterHooks() {
96 $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
97 // Format:
98 // Current $wgHooks
99 // Content in extension.json
100 // Expected value of $wgHooks
101 return [
102 // No hooks
103 [
104 [],
105 self::$default,
106 $merge,
107 ],
108 // No current hooks, adding one for "FooBaz" in string format
109 [
110 [],
111 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
112 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
113 ],
114 // Hook for "FooBaz", adding another one
115 [
116 [ 'FooBaz' => [ 'PriorCallback' ] ],
117 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
118 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
119 ],
120 // No current hooks, adding one for "FooBaz" in verbose array format
121 [
122 [],
123 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
124 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
125 ],
126 // Hook for "BarBaz", adding one for "FooBaz"
127 [
128 [ 'BarBaz' => [ 'BarBazCallback' ] ],
129 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
130 [
131 'BarBaz' => [ 'BarBazCallback' ],
132 'FooBaz' => [ 'FooBazCallback' ],
133 ] + $merge,
134 ],
135 // Callbacks for FooBaz wrapped in an array
136 [
137 [],
138 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
139 [
140 'FooBaz' => [ 'Callback1' ],
141 ] + $merge,
142 ],
143 // Multiple callbacks for FooBaz hook
144 [
145 [],
146 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
147 [
148 'FooBaz' => [ 'Callback1', 'Callback2' ],
149 ] + $merge,
150 ],
151 ];
152 }
153
154 /**
155 * @covers ExtensionProcessor::extractHooks
156 * @dataProvider provideRegisterHooks
157 */
158 public function testRegisterHooks( $pre, $info, $expected ) {
159 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
160 $processor->extractInfo( $this->dir, $info, 1 );
161 $extracted = $processor->getExtractedInfo();
162 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
163 }
164
165 /**
166 * @covers ExtensionProcessor::extractConfig1
167 */
168 public function testExtractConfig1() {
169 $processor = new ExtensionProcessor;
170 $info = [
171 'config' => [
172 'Bar' => 'somevalue',
173 'Foo' => 10,
174 '@IGNORED' => 'yes',
175 ],
176 ] + self::$default;
177 $info2 = [
178 'config' => [
179 '_prefix' => 'eg',
180 'Bar' => 'somevalue'
181 ],
182 'name' => 'FooBar2',
183 ];
184 $processor->extractInfo( $this->dir, $info, 1 );
185 $processor->extractInfo( $this->dir, $info2, 1 );
186 $extracted = $processor->getExtractedInfo();
187 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
188 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
189 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
190 // Custom prefix:
191 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
192 }
193
194 /**
195 * @covers ExtensionProcessor::extractConfig2
196 */
197 public function testExtractConfig2() {
198 $processor = new ExtensionProcessor;
199 $info = [
200 'config' => [
201 'Bar' => [ 'value' => 'somevalue' ],
202 'Foo' => [ 'value' => 10 ],
203 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
204 ],
205 ] + self::$default;
206 $info2 = [
207 'config' => [
208 'Bar' => [ 'value' => 'somevalue' ],
209 ],
210 'config_prefix' => 'eg',
211 'name' => 'FooBar2',
212 ];
213 $processor->extractInfo( $this->dir, $info, 2 );
214 $processor->extractInfo( $this->dir, $info2, 2 );
215 $extracted = $processor->getExtractedInfo();
216 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
217 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
218 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
219 // Custom prefix:
220 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
221 }
222
223 /**
224 * @covers ExtensionProcessor::addConfigGlobal()
225 * @expectedException RuntimeException
226 */
227 public function testDuplicateConfigKey1() {
228 $processor = new ExtensionProcessor;
229 $info = [
230 'config' => [
231 'Bar' => '',
232 ]
233 ] + self::$default;
234 $info2 = [
235 'config' => [
236 'Bar' => 'g',
237 ],
238 'name' => 'FooBar2',
239 ];
240 $processor->extractInfo( $this->dir, $info, 1 );
241 $processor->extractInfo( $this->dir, $info2, 1 );
242 }
243
244 /**
245 * @covers ExtensionProcessor::addConfigGlobal()
246 * @expectedException RuntimeException
247 */
248 public function testDuplicateConfigKey2() {
249 $processor = new ExtensionProcessor;
250 $info = [
251 'config' => [
252 'Bar' => [ 'value' => 'somevalue' ],
253 ]
254 ] + self::$default;
255 $info2 = [
256 'config' => [
257 'Bar' => [ 'value' => 'somevalue' ],
258 ],
259 'name' => 'FooBar2',
260 ];
261 $processor->extractInfo( $this->dir, $info, 2 );
262 $processor->extractInfo( $this->dir, $info2, 2 );
263 }
264
265 public static function provideExtractExtensionMessagesFiles() {
266 $dir = __DIR__ . '/FooBar/';
267 return [
268 [
269 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
270 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
271 ],
272 [
273 [
274 'ExtensionMessagesFiles' => [
275 'FooBarAlias' => 'FooBar.alias.php',
276 'FooBarMagic' => 'FooBar.magic.i18n.php',
277 ],
278 ],
279 [
280 'wgExtensionMessagesFiles' => [
281 'FooBarAlias' => $dir . 'FooBar.alias.php',
282 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
283 ],
284 ],
285 ],
286 ];
287 }
288
289 /**
290 * @covers ExtensionProcessor::extractExtensionMessagesFiles
291 * @dataProvider provideExtractExtensionMessagesFiles
292 */
293 public function testExtractExtensionMessagesFiles( $input, $expected ) {
294 $processor = new ExtensionProcessor();
295 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
296 $out = $processor->getExtractedInfo();
297 foreach ( $expected as $key => $value ) {
298 $this->assertEquals( $value, $out['globals'][$key] );
299 }
300 }
301
302 public static function provideExtractMessagesDirs() {
303 $dir = __DIR__ . '/FooBar/';
304 return [
305 [
306 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
307 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
308 ],
309 [
310 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
311 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
312 ],
313 ];
314 }
315
316 /**
317 * @covers ExtensionProcessor::extractMessagesDirs
318 * @dataProvider provideExtractMessagesDirs
319 */
320 public function testExtractMessagesDirs( $input, $expected ) {
321 $processor = new ExtensionProcessor();
322 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
323 $out = $processor->getExtractedInfo();
324 foreach ( $expected as $key => $value ) {
325 $this->assertEquals( $value, $out['globals'][$key] );
326 }
327 }
328
329 /**
330 * @covers ExtensionProcessor::extractCredits
331 */
332 public function testExtractCredits() {
333 $processor = new ExtensionProcessor();
334 $processor->extractInfo( $this->dir, self::$default, 1 );
335 $this->setExpectedException( Exception::class );
336 $processor->extractInfo( $this->dir, self::$default, 1 );
337 }
338
339 /**
340 * @covers ExtensionProcessor::extractResourceLoaderModules
341 * @dataProvider provideExtractResourceLoaderModules
342 */
343 public function testExtractResourceLoaderModules( $input, $expected ) {
344 $processor = new ExtensionProcessor();
345 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
346 $out = $processor->getExtractedInfo();
347 foreach ( $expected as $key => $value ) {
348 $this->assertEquals( $value, $out['globals'][$key] );
349 }
350 }
351
352 public static function provideExtractResourceLoaderModules() {
353 $dir = __DIR__ . '/FooBar';
354 return [
355 // Generic module with localBasePath/remoteExtPath specified
356 [
357 // Input
358 [
359 'ResourceModules' => [
360 'test.foo' => [
361 'styles' => 'foobar.js',
362 'localBasePath' => '',
363 'remoteExtPath' => 'FooBar',
364 ],
365 ],
366 ],
367 // Expected
368 [
369 'wgResourceModules' => [
370 'test.foo' => [
371 'styles' => 'foobar.js',
372 'localBasePath' => $dir,
373 'remoteExtPath' => 'FooBar',
374 ],
375 ],
376 ],
377 ],
378 // ResourceFileModulePaths specified:
379 [
380 // Input
381 [
382 'ResourceFileModulePaths' => [
383 'localBasePath' => '',
384 'remoteExtPath' => 'FooBar',
385 ],
386 'ResourceModules' => [
387 // No paths
388 'test.foo' => [
389 'styles' => 'foo.js',
390 ],
391 // Different paths set
392 'test.bar' => [
393 'styles' => 'bar.js',
394 'localBasePath' => 'subdir',
395 'remoteExtPath' => 'FooBar/subdir',
396 ],
397 // Custom class with no paths set
398 'test.class' => [
399 'class' => 'FooBarModule',
400 'extra' => 'argument',
401 ],
402 // Custom class with a localBasePath
403 'test.class.with.path' => [
404 'class' => 'FooBarPathModule',
405 'extra' => 'argument',
406 'localBasePath' => '',
407 ]
408 ],
409 ],
410 // Expected
411 [
412 'wgResourceModules' => [
413 'test.foo' => [
414 'styles' => 'foo.js',
415 'localBasePath' => $dir,
416 'remoteExtPath' => 'FooBar',
417 ],
418 'test.bar' => [
419 'styles' => 'bar.js',
420 'localBasePath' => "$dir/subdir",
421 'remoteExtPath' => 'FooBar/subdir',
422 ],
423 'test.class' => [
424 'class' => 'FooBarModule',
425 'extra' => 'argument',
426 'localBasePath' => $dir,
427 'remoteExtPath' => 'FooBar',
428 ],
429 'test.class.with.path' => [
430 'class' => 'FooBarPathModule',
431 'extra' => 'argument',
432 'localBasePath' => $dir,
433 'remoteExtPath' => 'FooBar',
434 ]
435 ],
436 ],
437 ],
438 // ResourceModuleSkinStyles with file module paths
439 [
440 // Input
441 [
442 'ResourceFileModulePaths' => [
443 'localBasePath' => '',
444 'remoteSkinPath' => 'FooBar',
445 ],
446 'ResourceModuleSkinStyles' => [
447 'foobar' => [
448 'test.foo' => 'foo.css',
449 ]
450 ],
451 ],
452 // Expected
453 [
454 'wgResourceModuleSkinStyles' => [
455 'foobar' => [
456 'test.foo' => 'foo.css',
457 'localBasePath' => $dir,
458 'remoteSkinPath' => 'FooBar',
459 ],
460 ],
461 ],
462 ],
463 // ResourceModuleSkinStyles with file module paths and an override
464 [
465 // Input
466 [
467 'ResourceFileModulePaths' => [
468 'localBasePath' => '',
469 'remoteSkinPath' => 'FooBar',
470 ],
471 'ResourceModuleSkinStyles' => [
472 'foobar' => [
473 'test.foo' => 'foo.css',
474 'remoteSkinPath' => 'BarFoo'
475 ],
476 ],
477 ],
478 // Expected
479 [
480 'wgResourceModuleSkinStyles' => [
481 'foobar' => [
482 'test.foo' => 'foo.css',
483 'localBasePath' => $dir,
484 'remoteSkinPath' => 'BarFoo',
485 ],
486 ],
487 ],
488 ],
489 ];
490 }
491
492 public static function provideSetToGlobal() {
493 return [
494 [
495 [ 'wgAPIModules', 'wgAvailableRights' ],
496 [],
497 [
498 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
499 'AvailableRights' => [ 'foobar', 'unfoobar' ],
500 ],
501 [
502 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
503 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
504 ],
505 ],
506 [
507 [ 'wgAPIModules', 'wgAvailableRights' ],
508 [
509 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
510 'wgAvailableRights' => [ 'barbaz' ]
511 ],
512 [
513 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
514 'AvailableRights' => [ 'foobar', 'unfoobar' ],
515 ],
516 [
517 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
518 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
519 ],
520 ],
521 [
522 [ 'wgGroupPermissions' ],
523 [
524 'wgGroupPermissions' => [
525 'sysop' => [ 'delete' ]
526 ],
527 ],
528 [
529 'GroupPermissions' => [
530 'sysop' => [ 'undelete' ],
531 'user' => [ 'edit' ]
532 ],
533 ],
534 [
535 'wgGroupPermissions' => [
536 'sysop' => [ 'delete', 'undelete' ],
537 'user' => [ 'edit' ]
538 ],
539 ]
540 ]
541 ];
542 }
543
544 /**
545 * Attributes under manifest_version 2
546 *
547 * @covers ExtensionProcessor::extractAttributes
548 * @covers ExtensionProcessor::getExtractedInfo
549 */
550 public function testExtractAttributes() {
551 $processor = new ExtensionProcessor();
552 // Load FooBar extension
553 $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
554 $processor->extractInfo(
555 $this->dir,
556 [
557 'name' => 'Baz',
558 'attributes' => [
559 // Loaded
560 'FooBar' => [
561 'Plugins' => [
562 'ext.baz.foobar',
563 ],
564 ],
565 // Not loaded
566 'FizzBuzz' => [
567 'MorePlugins' => [
568 'ext.baz.fizzbuzz',
569 ],
570 ],
571 ],
572 ],
573 2
574 );
575
576 $info = $processor->getExtractedInfo();
577 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
578 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
579 $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
580 }
581
582 /**
583 * Attributes under manifest_version 1
584 *
585 * @covers ExtensionProcessor::extractInfo
586 */
587 public function testAttributes1() {
588 $processor = new ExtensionProcessor();
589 $processor->extractInfo(
590 $this->dir,
591 [
592 'name' => 'FooBar',
593 'FooBarPlugins' => [
594 'ext.baz.foobar',
595 ],
596 'FizzBuzzMorePlugins' => [
597 'ext.baz.fizzbuzz',
598 ],
599 ],
600 1
601 );
602
603 $info = $processor->getExtractedInfo();
604 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
605 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
606 $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
607 $this->assertSame( [ 'ext.baz.fizzbuzz' ], $info['attributes']['FizzBuzzMorePlugins'] );
608 }
609
610 /**
611 * Verify that extension.schema.json is in sync with ExtensionProcessor
612 *
613 * @coversNothing
614 */
615 public function testGlobalSettingsDocumentedInSchema() {
616 global $IP;
617 $globalSettings = TestingAccessWrapper::newFromClass(
618 ExtensionProcessor::class )->globalSettings;
619
620 $version = ExtensionRegistry::MANIFEST_VERSION;
621 $schema = FormatJson::decode(
622 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
623 true
624 );
625 $missing = [];
626 foreach ( $globalSettings as $global ) {
627 if ( !isset( $schema['properties'][$global] ) ) {
628 $missing[] = $global;
629 }
630 }
631
632 $this->assertEquals( [], $missing,
633 "The following global settings are not documented in docs/extension.schema.json" );
634 }
635 }
636
637 /**
638 * Allow overriding the default value of $this->globals
639 * so we can test merging
640 */
641 class MockExtensionProcessor extends ExtensionProcessor {
642 public function __construct( $globals = [] ) {
643 $this->globals = $globals + $this->globals;
644 }
645 }