Merge "Make sure all functions in Database.php are documented"
[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 public static function provideRegisterHooks() {
44 $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
45 // Format:
46 // Current $wgHooks
47 // Content in extension.json
48 // Expected value of $wgHooks
49 return [
50 // No hooks
51 [
52 [],
53 self::$default,
54 $merge,
55 ],
56 // No current hooks, adding one for "FooBaz" in string format
57 [
58 [],
59 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
60 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
61 ],
62 // Hook for "FooBaz", adding another one
63 [
64 [ 'FooBaz' => [ 'PriorCallback' ] ],
65 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
66 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
67 ],
68 // No current hooks, adding one for "FooBaz" in verbose array format
69 [
70 [],
71 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
72 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
73 ],
74 // Hook for "BarBaz", adding one for "FooBaz"
75 [
76 [ 'BarBaz' => [ 'BarBazCallback' ] ],
77 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
78 [
79 'BarBaz' => [ 'BarBazCallback' ],
80 'FooBaz' => [ 'FooBazCallback' ],
81 ] + $merge,
82 ],
83 // Callbacks for FooBaz wrapped in an array
84 [
85 [],
86 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
87 [
88 'FooBaz' => [ 'Callback1' ],
89 ] + $merge,
90 ],
91 // Multiple callbacks for FooBaz hook
92 [
93 [],
94 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
95 [
96 'FooBaz' => [ 'Callback1', 'Callback2' ],
97 ] + $merge,
98 ],
99 ];
100 }
101
102 /**
103 * @covers ExtensionProcessor::extractHooks
104 * @dataProvider provideRegisterHooks
105 */
106 public function testRegisterHooks( $pre, $info, $expected ) {
107 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
108 $processor->extractInfo( $this->dir, $info, 1 );
109 $extracted = $processor->getExtractedInfo();
110 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
111 }
112
113 /**
114 * @covers ExtensionProcessor::extractConfig1
115 */
116 public function testExtractConfig1() {
117 $processor = new ExtensionProcessor;
118 $info = [
119 'config' => [
120 'Bar' => 'somevalue',
121 'Foo' => 10,
122 '@IGNORED' => 'yes',
123 ],
124 ] + self::$default;
125 $info2 = [
126 'config' => [
127 '_prefix' => 'eg',
128 'Bar' => 'somevalue'
129 ],
130 'name' => 'FooBar2',
131 ];
132 $processor->extractInfo( $this->dir, $info, 1 );
133 $processor->extractInfo( $this->dir, $info2, 1 );
134 $extracted = $processor->getExtractedInfo();
135 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
136 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
137 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
138 // Custom prefix:
139 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
140 }
141
142 /**
143 * @covers ExtensionProcessor::extractConfig2
144 */
145 public function testExtractConfig2() {
146 $processor = new ExtensionProcessor;
147 $info = [
148 'config' => [
149 'Bar' => [ 'value' => 'somevalue' ],
150 'Foo' => [ 'value' => 10 ],
151 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
152 ],
153 ] + self::$default;
154 $info2 = [
155 'config' => [
156 'Bar' => [ 'value' => 'somevalue' ],
157 ],
158 'config_prefix' => 'eg',
159 'name' => 'FooBar2',
160 ];
161 $processor->extractInfo( $this->dir, $info, 2 );
162 $processor->extractInfo( $this->dir, $info2, 2 );
163 $extracted = $processor->getExtractedInfo();
164 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
165 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
166 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
167 // Custom prefix:
168 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
169 }
170
171 public static function provideExtractExtensionMessagesFiles() {
172 $dir = __DIR__ . '/FooBar/';
173 return [
174 [
175 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
176 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
177 ],
178 [
179 [
180 'ExtensionMessagesFiles' => [
181 'FooBarAlias' => 'FooBar.alias.php',
182 'FooBarMagic' => 'FooBar.magic.i18n.php',
183 ],
184 ],
185 [
186 'wgExtensionMessagesFiles' => [
187 'FooBarAlias' => $dir . 'FooBar.alias.php',
188 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
189 ],
190 ],
191 ],
192 ];
193 }
194
195 /**
196 * @covers ExtensionProcessor::extractExtensionMessagesFiles
197 * @dataProvider provideExtractExtensionMessagesFiles
198 */
199 public function testExtractExtensionMessagesFiles( $input, $expected ) {
200 $processor = new ExtensionProcessor();
201 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
202 $out = $processor->getExtractedInfo();
203 foreach ( $expected as $key => $value ) {
204 $this->assertEquals( $value, $out['globals'][$key] );
205 }
206 }
207
208 public static function provideExtractMessagesDirs() {
209 $dir = __DIR__ . '/FooBar/';
210 return [
211 [
212 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
213 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
214 ],
215 [
216 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
217 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
218 ],
219 ];
220 }
221
222 /**
223 * @covers ExtensionProcessor::extractMessagesDirs
224 * @dataProvider provideExtractMessagesDirs
225 */
226 public function testExtractMessagesDirs( $input, $expected ) {
227 $processor = new ExtensionProcessor();
228 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
229 $out = $processor->getExtractedInfo();
230 foreach ( $expected as $key => $value ) {
231 $this->assertEquals( $value, $out['globals'][$key] );
232 }
233 }
234
235 /**
236 * @covers ExtensionProcessor::extractCredits
237 */
238 public function testExtractCredits() {
239 $processor = new ExtensionProcessor();
240 $processor->extractInfo( $this->dir, self::$default, 1 );
241 $this->setExpectedException( 'Exception' );
242 $processor->extractInfo( $this->dir, self::$default, 1 );
243 }
244
245 /**
246 * @covers ExtensionProcessor::extractResourceLoaderModules
247 * @dataProvider provideExtractResourceLoaderModules
248 */
249 public function testExtractResourceLoaderModules( $input, $expected ) {
250 $processor = new ExtensionProcessor();
251 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
252 $out = $processor->getExtractedInfo();
253 foreach ( $expected as $key => $value ) {
254 $this->assertEquals( $value, $out['globals'][$key] );
255 }
256 }
257
258 public static function provideExtractResourceLoaderModules() {
259 $dir = __DIR__ . '/FooBar';
260 return [
261 // Generic module with localBasePath/remoteExtPath specified
262 [
263 // Input
264 [
265 'ResourceModules' => [
266 'test.foo' => [
267 'styles' => 'foobar.js',
268 'localBasePath' => '',
269 'remoteExtPath' => 'FooBar',
270 ],
271 ],
272 ],
273 // Expected
274 [
275 'wgResourceModules' => [
276 'test.foo' => [
277 'styles' => 'foobar.js',
278 'localBasePath' => $dir,
279 'remoteExtPath' => 'FooBar',
280 ],
281 ],
282 ],
283 ],
284 // ResourceFileModulePaths specified:
285 [
286 // Input
287 [
288 'ResourceFileModulePaths' => [
289 'localBasePath' => '',
290 'remoteExtPath' => 'FooBar',
291 ],
292 'ResourceModules' => [
293 // No paths
294 'test.foo' => [
295 'styles' => 'foo.js',
296 ],
297 // Different paths set
298 'test.bar' => [
299 'styles' => 'bar.js',
300 'localBasePath' => 'subdir',
301 'remoteExtPath' => 'FooBar/subdir',
302 ],
303 // Custom class with no paths set
304 'test.class' => [
305 'class' => 'FooBarModule',
306 'extra' => 'argument',
307 ],
308 // Custom class with a localBasePath
309 'test.class.with.path' => [
310 'class' => 'FooBarPathModule',
311 'extra' => 'argument',
312 'localBasePath' => '',
313 ]
314 ],
315 ],
316 // Expected
317 [
318 'wgResourceModules' => [
319 'test.foo' => [
320 'styles' => 'foo.js',
321 'localBasePath' => $dir,
322 'remoteExtPath' => 'FooBar',
323 ],
324 'test.bar' => [
325 'styles' => 'bar.js',
326 'localBasePath' => "$dir/subdir",
327 'remoteExtPath' => 'FooBar/subdir',
328 ],
329 'test.class' => [
330 'class' => 'FooBarModule',
331 'extra' => 'argument',
332 'localBasePath' => $dir,
333 'remoteExtPath' => 'FooBar',
334 ],
335 'test.class.with.path' => [
336 'class' => 'FooBarPathModule',
337 'extra' => 'argument',
338 'localBasePath' => $dir,
339 'remoteExtPath' => 'FooBar',
340 ]
341 ],
342 ],
343 ],
344 // ResourceModuleSkinStyles with file module paths
345 [
346 // Input
347 [
348 'ResourceFileModulePaths' => [
349 'localBasePath' => '',
350 'remoteSkinPath' => 'FooBar',
351 ],
352 'ResourceModuleSkinStyles' => [
353 'foobar' => [
354 'test.foo' => 'foo.css',
355 ]
356 ],
357 ],
358 // Expected
359 [
360 'wgResourceModuleSkinStyles' => [
361 'foobar' => [
362 'test.foo' => 'foo.css',
363 'localBasePath' => $dir,
364 'remoteSkinPath' => 'FooBar',
365 ],
366 ],
367 ],
368 ],
369 // ResourceModuleSkinStyles with file module paths and an override
370 [
371 // Input
372 [
373 'ResourceFileModulePaths' => [
374 'localBasePath' => '',
375 'remoteSkinPath' => 'FooBar',
376 ],
377 'ResourceModuleSkinStyles' => [
378 'foobar' => [
379 'test.foo' => 'foo.css',
380 'remoteSkinPath' => 'BarFoo'
381 ],
382 ],
383 ],
384 // Expected
385 [
386 'wgResourceModuleSkinStyles' => [
387 'foobar' => [
388 'test.foo' => 'foo.css',
389 'localBasePath' => $dir,
390 'remoteSkinPath' => 'BarFoo',
391 ],
392 ],
393 ],
394 ],
395 ];
396 }
397
398 public static function provideSetToGlobal() {
399 return [
400 [
401 [ 'wgAPIModules', 'wgAvailableRights' ],
402 [],
403 [
404 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
405 'AvailableRights' => [ 'foobar', 'unfoobar' ],
406 ],
407 [
408 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
409 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
410 ],
411 ],
412 [
413 [ 'wgAPIModules', 'wgAvailableRights' ],
414 [
415 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
416 'wgAvailableRights' => [ 'barbaz' ]
417 ],
418 [
419 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
420 'AvailableRights' => [ 'foobar', 'unfoobar' ],
421 ],
422 [
423 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
424 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
425 ],
426 ],
427 [
428 [ 'wgGroupPermissions' ],
429 [
430 'wgGroupPermissions' => [
431 'sysop' => [ 'delete' ]
432 ],
433 ],
434 [
435 'GroupPermissions' => [
436 'sysop' => [ 'undelete' ],
437 'user' => [ 'edit' ]
438 ],
439 ],
440 [
441 'wgGroupPermissions' => [
442 'sysop' => [ 'delete', 'undelete' ],
443 'user' => [ 'edit' ]
444 ],
445 ]
446 ]
447 ];
448 }
449
450 /**
451 * Attributes under manifest_version 2
452 *
453 * @covers ExtensionProcessor::extractAttributes
454 * @covers ExtensionProcessor::getExtractedInfo
455 */
456 public function testExtractAttributes() {
457 $processor = new ExtensionProcessor();
458 // Load FooBar extension
459 $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
460 $processor->extractInfo(
461 $this->dir,
462 [
463 'name' => 'Baz',
464 'attributes' => [
465 // Loaded
466 'FooBar' => [
467 'Plugins' => [
468 'ext.baz.foobar',
469 ],
470 ],
471 // Not loaded
472 'FizzBuzz' => [
473 'MorePlugins' => [
474 'ext.baz.fizzbuzz',
475 ],
476 ],
477 ],
478 ],
479 2
480 );
481
482 $info = $processor->getExtractedInfo();
483 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
484 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
485 $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
486 }
487
488 /**
489 * Attributes under manifest_version 1
490 *
491 * @covers ExtensionProcessor::extractInfo
492 */
493 public function testAttributes1() {
494 $processor = new ExtensionProcessor();
495 $processor->extractInfo(
496 $this->dir,
497 [
498 'name' => 'FooBar',
499 'FooBarPlugins' => [
500 'ext.baz.foobar',
501 ],
502 'FizzBuzzMorePlugins' => [
503 'ext.baz.fizzbuzz',
504 ],
505 ],
506 1
507 );
508
509 $info = $processor->getExtractedInfo();
510 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
511 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
512 $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
513 $this->assertSame( [ 'ext.baz.fizzbuzz' ], $info['attributes']['FizzBuzzMorePlugins'] );
514 }
515
516 public function testGlobalSettingsDocumentedInSchema() {
517 global $IP;
518 $globalSettings = TestingAccessWrapper::newFromClass(
519 ExtensionProcessor::class )->globalSettings;
520
521 $version = ExtensionRegistry::MANIFEST_VERSION;
522 $schema = FormatJson::decode(
523 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
524 true
525 );
526 $missing = [];
527 foreach ( $globalSettings as $global ) {
528 if ( !isset( $schema['properties'][$global] ) ) {
529 $missing[] = $global;
530 }
531 }
532
533 $this->assertEquals( [], $missing,
534 "The following global settings are not documented in docs/extension.schema.json" );
535 }
536 }
537
538 /**
539 * Allow overriding the default value of $this->globals
540 * so we can test merging
541 */
542 class MockExtensionProcessor extends ExtensionProcessor {
543 public function __construct( $globals = [] ) {
544 $this->globals = $globals + $this->globals;
545 }
546 }