Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / structure / SettingsTest.php
blobe2a8d18db024b86e7de7170211f12699fe088d87
1 <?php
3 namespace MediaWiki\Tests\Structure;
5 use MediaWiki\MainConfigNames;
6 use MediaWiki\MainConfigSchema;
7 use MediaWiki\Registration\ExtensionRegistry;
8 use MediaWiki\Settings\Config\ArrayConfigBuilder;
9 use MediaWiki\Settings\Config\PhpIniSink;
10 use MediaWiki\Settings\SettingsBuilder;
11 use MediaWiki\Settings\Source\FileSource;
12 use MediaWiki\Settings\Source\JsonSchemaTrait;
13 use MediaWiki\Settings\Source\PhpSettingsSource;
14 use MediaWiki\Settings\Source\ReflectionSchemaSource;
15 use MediaWiki\Settings\Source\SettingsSource;
16 use MediaWiki\Shell\Shell;
17 use MediaWikiIntegrationTestCase;
19 /**
20 * @coversNothing
22 class SettingsTest extends MediaWikiIntegrationTestCase {
23 use JsonSchemaTrait;
25 /**
26 * Returns the main configuration schema as a settings array.
28 * @return array
30 private static function getSchemaData(): array {
31 $source = new ReflectionSchemaSource( MainConfigSchema::class, true );
32 $settings = $source->load();
33 return $settings;
36 /**
37 * @return SettingsBuilder
39 private function getSettingsBuilderWithSchema(): SettingsBuilder {
40 $configBuilder = new ArrayConfigBuilder();
41 $settingsBuilder = new SettingsBuilder(
42 __DIR__ . '/../../..',
43 $this->createNoOpMock( ExtensionRegistry::class ),
44 $configBuilder,
45 $this->createNoOpMock( PhpIniSink::class )
47 $settingsBuilder->loadArray( self::getSchemaData() );
48 return $settingsBuilder;
51 public function testConfigSchemaIsLoadable() {
52 $settingsBuilder = $this->getSettingsBuilderWithSchema();
53 $settingsBuilder->apply();
55 // Assert we've read some random config value
56 $this->assertTrue( $settingsBuilder->getConfig()->has( MainConfigNames::Server ) );
59 /**
60 * Check that core default settings validate against the schema
62 public function testConfigSchemaDefaultsValidate() {
63 $settingsBuilder = $this->getSettingsBuilderWithSchema();
64 $validationResult = $settingsBuilder->apply()->validate();
65 $this->assertStatusOK( $validationResult );
68 /**
69 * Check that currently loaded settings validate against the schema.
71 public function testCurrentSettingsValidate() {
72 $validationResult = SettingsBuilder::getInstance()->validate();
73 $this->assertStatusOK( $validationResult );
76 /**
77 * Check that currently loaded config does not use deprecated settings.
79 public function testCurrentSettingsNotDeprecated() {
80 $deprecations = SettingsBuilder::getInstance()->detectDeprecatedConfig();
81 $this->assertEquals( [], $deprecations );
84 /**
85 * Check that currently loaded config does not use obsolete settings.
87 public function testCurrentSettingsNotObsolete() {
88 $obsolete = SettingsBuilder::getInstance()->detectObsoleteConfig();
89 $this->assertEquals( [], $obsolete );
92 /**
93 * Check that currently loaded config does not have warnings.
95 public function testCurrentSettingsHaveNoWarnings() {
96 $deprecations = SettingsBuilder::getInstance()->getWarnings();
97 $this->assertEquals( [], $deprecations );
100 public static function provideConfigGeneration() {
101 yield 'includes/config-schema.php' => [
102 'option' => '--schema',
103 'expectedFile' => MW_INSTALL_PATH . '/includes/config-schema.php',
105 yield 'docs/config-vars.php' => [
106 'option' => '--vars',
107 'expectedFile' => MW_INSTALL_PATH . '/docs/config-vars.php',
109 yield 'docs/config-schema.yaml' => [
110 'option' => '--yaml',
111 'expectedFile' => MW_INSTALL_PATH . '/docs/config-schema.yaml',
113 yield 'includes/MainConfigNames.php' => [
114 'option' => '--names',
115 'expectedFile' => MW_INSTALL_PATH . '/includes/MainConfigNames.php',
120 * @dataProvider provideConfigGeneration
122 public function testConfigGeneration( string $option, string $expectedFile ) {
123 $script = 'GenerateConfigSchema';
124 $schemaGenerator = Shell::makeScriptCommand( $script, [ $option, 'php://stdout' ] );
125 $result = $schemaGenerator->execute();
126 $this->assertSame(
128 $result->getExitCode(),
129 'Config generation must finish successfully.' . "\n" . $result->getStderr()
132 $errors = $result->getStderr();
133 $errors = preg_replace( '/^Xdebug:.*\n/m', '', $errors );
134 $this->assertSame( '', $errors, 'Config generation must not have errors' );
136 $oldGeneratedSchema = file_get_contents( $expectedFile );
137 $relativePath = wfRelativePath( $script, MW_INSTALL_PATH );
139 $this->assertEquals(
140 $oldGeneratedSchema,
141 $result->getStdout(),
142 "Configuration schema was changed. Rerun $relativePath script!"
146 public static function provideDefaultSettingsConsistency() {
147 yield 'YAML' => [ new FileSource( MW_INSTALL_PATH . '/docs/config-schema.yaml' ) ];
148 yield 'PHP' => [ new PhpSettingsSource( MW_INSTALL_PATH . '/includes/config-schema.php' ) ];
152 * Check that the result of loading config-schema.yaml is the same as DefaultSettings.php
153 * This test can be removed when DefaultSettings.php is removed.
154 * @dataProvider provideDefaultSettingsConsistency
156 public function testDefaultSettingsConsistency( SettingsSource $source ) {
157 $this->expectDeprecationAndContinue( '/DefaultSettings\\.php/' );
158 $defaultSettingsProps = ( static function () {
159 require MW_INSTALL_PATH . '/includes/DefaultSettings.php';
160 $vars = get_defined_vars();
161 unset( $vars['input'] );
162 $result = [];
163 foreach ( $vars as $key => $value ) {
164 $result[substr( $key, 2 )] = $value;
166 return $result;
167 } )();
169 $configBuilder = new ArrayConfigBuilder();
170 $settingsBuilder = new SettingsBuilder(
171 __DIR__ . '/../../..',
172 $this->createNoOpMock( ExtensionRegistry::class ),
173 $configBuilder,
174 $this->createNoOpMock( PhpIniSink::class )
176 $settingsBuilder->load( $source );
177 $defaults = iterator_to_array( $settingsBuilder->getDefaultConfig() );
179 foreach ( $defaultSettingsProps as $key => $value ) {
180 if ( in_array( $key, [
181 'Version', // deprecated alias to MW_VERSION
182 'Conf', // instance of SiteConfiguration
183 'AutoloadClasses', // conditionally initialized
184 ] ) ) {
185 continue;
187 $this->assertArrayHasKey( $key, $defaults, "Missing $key from $source" );
188 $this->assertEquals( $value, $defaults[ $key ], "Wrong value for $key\n" );
191 $missingKeys = array_diff_key( $defaults, $defaultSettingsProps );
192 $this->assertSame( [], $missingKeys, 'Keys missing from DefaultSettings.php' );
195 public static function provideArraysHaveMergeStrategy() {
196 [ 'config-schema' => $allSchemas ] = self::getSchemaData();
198 foreach ( $allSchemas as $name => $schema ) {
199 yield "Schema for $name" => [ $schema ];
204 * Check that the schema for each config variable contains all necessary information.
205 * @dataProvider provideArraysHaveMergeStrategy
207 public function testSchemaCompleteness( $schema ) {
208 $type = $schema['type'] ?? null;
209 $type = (array)$type;
211 $this->assertArrayNotHasKey( 'obsolete', $schema, 'Obsolete schemas should have been filtered out' );
213 if ( isset( $schema['properties'] ) ) {
214 $this->assertContains(
215 'object', $type,
216 'must be of type "object", since it defines properties'
219 $defaults = $schema['default'] ?? [];
220 foreach ( $schema['properties'] as $key => $sch ) {
221 // must have a default in the schema, or in the top level default
222 if ( !array_key_exists( 'default', $sch ) ) {
223 $this->assertArrayHasKey( $key, $defaults, "property $key must have a default" );
224 } else {
225 $defaults[$key] = $sch['default'];
228 } else {
229 $this->assertArrayHasKey(
230 'default',
231 $schema,
232 'should specify a default value'
234 $defaults = $schema['default'];
237 // If the default is an array, the type must be declared, so we know whether
238 // it's a list (JS "array") or a map (JS "object").
239 if ( is_array( $defaults ) ) {
240 $this->assertTrue(
241 in_array( 'array', $type ) || in_array( 'object', $type ),
242 'must be of type "array" or "object", since the default is an array'
246 // If the default value of a list is not empty, check that it is an indexed array,
247 // not an associative array.
248 if ( in_array( 'array', $type ) && !empty( $defaults ) ) {
249 $this->assertArrayHasKey(
251 $schema['default'],
252 'should have a default value starting with index 0, since its type is "array".'
256 $mergeStrategy = $schema['mergeStrategy'] ?? null;
258 // If a merge strategy is defined, make sure it makes sense for the given type.
259 if ( $mergeStrategy ) {
260 if ( in_array( 'array', $type ) ) {
261 $this->assertNotSame(
262 'array_merge',
263 $mergeStrategy,
264 'should not specify redundant mergeStrategy "array_merge" since '
265 . 'it is implied by the type being "array"'
268 $this->assertNotSame(
269 'array_plus',
270 $mergeStrategy,
271 'should not specify mergeStrategy "array_plus" since its type is "array"'
274 $this->assertNotSame(
275 'array_plus_2d',
276 $mergeStrategy,
277 'should not specify mergeStrategy "array_plus_2d" since its type is "array"'
279 } elseif ( in_array( 'object', $type ) ) {
280 $this->assertNotSame(
281 'array_plus',
282 $mergeStrategy,
283 'should not specify redundant mergeStrategy "array_plus" since '
284 . 'it is implied by the type being "object"'
287 $this->assertNotSame(
288 'array_merge',
289 $mergeStrategy,
290 'should not specify mergeStrategy "array_merge" since its type is "object"'
295 if ( isset( $schema['items'] ) ) {
296 $this->assertContains(
297 'array',
298 $type,
299 'should be declared to be an array if an "items" schema is defined'
303 if ( isset( $schema['additionalProperties'] ) || isset( $schema['properties'] ) ) {
304 $this->assertContains(
305 'object',
306 $type,
307 'should be declared to be an object if schemas are defined for "properties" ' .
308 'or "additionalProperties"'
313 public static function provideConfigStructureHandling() {
314 yield 'NamespacesWithSubpages' => [
315 MainConfigNames::NamespacesWithSubpages,
316 [ 0 => true, 1 => false,
317 2 => true, 3 => true, 4 => true, 5 => true, 7 => true,
318 8 => true, 9 => true, 10 => true, 11 => true, 12 => true,
319 13 => true, 15 => true
321 [ 0 => true, 1 => false ]
323 yield 'InterwikiCache array' => [
324 MainConfigNames::InterwikiCache,
325 [ 'x' => [ 'foo' => 1 ] ],
326 [ 'x' => [ 'foo' => 1 ] ],
328 yield 'InterwikiCache string' => [
329 MainConfigNames::InterwikiCache,
330 'interwiki.map',
331 'interwiki.map',
333 yield 'InterwikiCache string over array' => [
334 MainConfigNames::InterwikiCache,
335 'interwiki.map',
336 [ 'x' => [ 'foo' => 1 ] ],
337 'interwiki.map',
339 yield 'ProxyList array' => [
340 MainConfigNames::ProxyList,
341 [ 'a', 'b', 'c' ],
342 [ 'a', 'b', 'c' ],
344 yield 'ProxyList string' => [
345 MainConfigNames::ProxyList,
346 'interwiki.map',
347 'interwiki.map',
349 yield 'ProxyList string over array' => [
350 MainConfigNames::ProxyList,
351 'interwiki.map',
352 [ 'a', 'b', 'c' ],
353 'interwiki.map',
355 yield 'ProxyList array over array' => [
356 MainConfigNames::ProxyList,
357 [ 'a', 'b', 'c', 'd' ],
358 [ 'a', 'b' ],
359 [ 'c', 'd' ],
361 yield 'Logos' => [
362 MainConfigNames::Logos,
363 [ '1x' => 'Logo1', '2x' => 'Logo2' ],
364 [ '1x' => 'Logo1', '2x' => 'Logo2' ],
366 yield 'Logos clear' => [
367 MainConfigNames::Logos,
368 false,
369 [ '1x' => 'Logo1', '2x' => 'Logo2' ],
370 false
372 yield 'RevokePermissions' => [
373 MainConfigNames::RevokePermissions,
374 [ '*' => [ 'read' => true, 'edit' => true, ] ],
375 [ '*' => [ 'edit' => true ] ],
376 [ '*' => [ 'read' => true ] ]
381 * Ensure that some of the more complex/problematic config structures are handled
382 * correctly.
384 * @dataProvider provideConfigStructureHandling
386 public function testConfigStructureHandling( $key, $expected, $value, $value2 = null ) {
387 $settingsBuilder = $this->getSettingsBuilderWithSchema();
388 $settingsBuilder->apply();
390 $settingsBuilder->putConfigValue( $key, $value );
392 if ( $value2 !== null ) {
393 $settingsBuilder->putConfigValue( $key, $value2 );
396 $config = $settingsBuilder->getConfig();
398 $this->assertSame( $expected, $config->get( $key ) );
401 public static function provideConfigStructurePartialReplacement() {
402 yield 'GroupPermissions' => [
403 'GroupPermissions',
404 [ // permissions for each group should be merged
405 'autoconfirmed' => [
406 'autoconfirmed' => true,
407 'editsemiprotected' => false,
408 'patrol' => true,
410 'mygroup' => [ 'test' => true ],
413 'autoconfirmed' => [
414 'patrol' => true,
415 'editsemiprotected' => false
417 'mygroup' => [ 'test' => true ],
420 yield 'RateLimits' => [
421 'RateLimits',
422 [ // limits for each action should be merged, limits for each group get replaced
423 'move' => [ 'newbie' => [ 1, 80 ], 'user' => [ 8, 60 ], 'ip' => [ 1, 60 ] ],
424 'test' => [ 'ip' => [ 1, 60 ] ],
427 'move' => [ 'ip' => [ 1, 60 ], 'newbie' => [ 1, 80 ], 'user' => [ 8, 60 ] ],
428 'test' => [ 'ip' => [ 1, 60 ] ],
434 * Ensure that some of the more complex/problematic config structures are
435 * correctly replacing parts of a complex default.
437 * @dataProvider provideConfigStructurePartialReplacement
439 public function testConfigStructurePartialReplacement( $key, $expectedValue, $newValue ) {
440 $settingsBuilder = $this->getSettingsBuilderWithSchema();
441 $defaultValue = $settingsBuilder->getConfig()->get( $key );
443 $settingsBuilder->putConfigValue( $key, $newValue );
444 $mergedValue = $settingsBuilder->getConfig()->get( $key );
446 // Check that the keys in $mergedValue that are also present
447 // in $newValue now match $expectedValue.
448 $updatedValue = array_intersect_key( $mergedValue, $newValue );
449 $this->assertArrayEquals( $expectedValue, $updatedValue, false, true );
451 // Check that the other keys in $mergedValue are still the same
452 // as in $defaultValue.
453 $mergedValue = array_diff_key( $mergedValue, $newValue );
454 $defaultValue = array_diff_key( $defaultValue, $newValue );
455 $this->assertArrayEquals( $defaultValue, $mergedValue, false, true );
459 * Ensure that hook handlers are merged correctly.
461 public function testHooksMerge() {
462 $settingsBuilder = $this->getSettingsBuilderWithSchema();
464 $f1 = static function () {
465 // noop
468 $hooks = [
469 'TestHook' => [
470 'TestHookHandler1',
471 [ 'TestHookHandler1', 'handler data' ],
472 $f1,
475 $settingsBuilder->putConfigValue( MainConfigNames::Hooks, $hooks );
477 $f2 = static function () {
478 // noop
481 $hooks = [
482 'TestHook' => [
483 'TestHookHandler2',
484 [ 'TestHookHandler2', 'more handler data' ],
485 $f2,
488 $settingsBuilder->putConfigValue( MainConfigNames::Hooks, $hooks );
490 $config = $settingsBuilder->getConfig();
492 $hooks = [
493 'TestHook' => [
494 'TestHookHandler1',
495 [ 'TestHookHandler1', 'handler data' ],
496 $f1,
497 'TestHookHandler2',
498 [ 'TestHookHandler2', 'more handler data' ],
499 $f2,
502 $this->assertSame( $hooks, $config->get( MainConfigNames::Hooks ) );
506 * Ensure that PasswordPolicy are merged correctly.
508 public function testPasswordPolicyMerge() {
509 $settingsBuilder = $this->getSettingsBuilderWithSchema();
510 $defaultPolicies = $settingsBuilder->getConfig()->get( MainConfigNames::PasswordPolicy );
512 $newPolicies = [
513 'policies' => [
514 'sysop' => [
515 'MinimalPasswordLength' => [
516 'value' => 10,
517 'suggestChangeOnLogin' => false,
520 'bot' => [
521 'MinimumPasswordLengthToLogin' => 2,
524 'checks' => [
525 'MinimalPasswordLength' => 'myLengthCheck',
526 'SomeOtherCheck' => 'myOtherCheck',
529 $settingsBuilder->putConfigValue( MainConfigNames::PasswordPolicy, $newPolicies );
530 $mergedPolicies = $settingsBuilder->getConfig()->get( MainConfigNames::PasswordPolicy );
532 // check that the new policies have been applied
533 $this->assertSame(
535 'MinimalPasswordLength' => [
536 'value' => 10,
537 'suggestChangeOnLogin' => false,
539 'MinimumPasswordLengthToLogin' => 1, // from defaults
541 $mergedPolicies['policies']['sysop']
543 $this->assertSame(
545 'MinimalPasswordLength' => 10, // from defaults
546 'MinimumPasswordLengthToLogin' => 2,
548 $mergedPolicies['policies']['bot']
550 $this->assertSame(
551 'myLengthCheck',
552 $mergedPolicies['checks']['MinimalPasswordLength']
554 $this->assertSame(
555 'myOtherCheck',
556 $mergedPolicies['checks']['SomeOtherCheck']
559 // check that other stuff wasn't changed
560 $this->assertSame(
561 $defaultPolicies['checks']['PasswordCannotMatchDefaults'],
562 $mergedPolicies['checks']['PasswordCannotMatchDefaults']
564 $this->assertSame(
565 $defaultPolicies['policies']['bureaucrat'],
566 $mergedPolicies['policies']['bureaucrat']
568 $this->assertSame(
569 $defaultPolicies['policies']['default'],
570 $mergedPolicies['policies']['default']
575 * @covers \MediaWiki\MainConfigSchema::listDefaultValues
576 * @covers \MediaWiki\MainConfigSchema::getDefaultValue
578 public function testMainConfigSchemaDefaults() {
579 $defaults = iterator_to_array( MainConfigSchema::listDefaultValues() );
580 $prefixed = iterator_to_array( MainConfigSchema::listDefaultValues( 'wg' ) );
582 $schema = self::getSchemaData();
583 foreach ( $schema['config-schema'] as $name => $sch ) {
584 $this->assertArrayHasKey( $name, $defaults );
585 $this->assertArrayHasKey( "wg$name", $prefixed );
587 $expected = self::getDefaultFromJsonSchema( $sch );
589 $this->assertSame( $expected, $defaults[$name] );
590 $this->assertSame( $expected, $prefixed["wg$name"] );
592 $this->assertSame( $expected, MainConfigSchema::getDefaultValue( $name ) );
597 * @coversNothing Only covers code in global scope, no way to annotate that?
599 public function testSetLocaltimezone(): void {
600 // Make sure the configured timezone ewas applied to the PHP runtime.
601 $tz = $this->getConfVar( MainConfigNames::Localtimezone );
602 $this->assertSame( $tz, date_default_timezone_get() );