createMock( ExtensionRegistry::class ), $params['configBuilder'] ?? new ArrayConfigBuilder(), $params['phpIniSink'] ?? $this->createMock( PhpIniSink::class ), $params['cache'] ?? null ); } public function testLoadingFromFile() { $phpIniSinkMock = $this->createMock( PhpIniSink::class ); $phpIniSinkMock->expects( $this->once() )->method( 'set' )->with( 'foo', 'bar' ); $setting = $this->newSettingsBuilder( [ 'phpIniSink' => $phpIniSinkMock ] ); $setting->loadFile( 'fixtures/settings.json' )->apply(); $config = $setting->getConfig(); $this->assertSame( 'TEST', $config->get( 'Something' ) ); } public function testLoadingIncludesRecursiveFromFile() { $configBuilder = new ArrayConfigBuilder(); $phpIniSinkMock = $this->createMock( PhpIniSink::class ); $phpIniSinkMock->expects( $this->once() )->method( 'set' )->with( 'foo', 'bar' ); // Make sure the cache is hit twice, once for each file! $mockCache = $this->createMock( BagOStuff::class ); $key = 'foo'; $mockCache ->expects( $this->atLeastOnce() ) ->method( 'makeGlobalKey' ) ->willReturn( $key ); $mockCache ->expects( $this->exactly( 2 ) ) ->method( 'get' ) ->willReturn( false ); $mockCache ->expects( $this->exactly( 2 ) ) ->method( 'lock' ) ->willReturn( true ); $mockCache ->expects( $this->exactly( 2 ) ) ->method( 'unlock' ) ->willReturn( true ); $setting = $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder, 'phpIniSink' => $phpIniSinkMock, 'cache' => $mockCache, ] ); $setting->loadFile( 'fixtures/settings-with-includes.json' )->apply(); $config = $configBuilder->build(); $this->assertSame( 'TEST', $config->get( 'Something' ) ); } /** * Ensure that includes work in a source that is not a SettingsIncludeLocator */ public function testLoadingIncludesRecursiveFromArray() { $configBuilder = new ArrayConfigBuilder(); $phpIniSinkMock = $this->createMock( PhpIniSink::class ); $phpIniSinkMock->expects( $this->once() )->method( 'set' )->with( 'foo', 'bar' ); $setting = $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder, 'phpIniSink' => $phpIniSinkMock, ] ); $setting->loadArray( [ 'includes' => [ 'fixtures/settings.json' ] ] )->apply(); $config = $configBuilder->build(); $this->assertSame( 'TEST', $config->get( 'Something' ) ); } public function testLoadingIncludesRecursiveLoop() { $setting = $this->newSettingsBuilder(); $this->expectException( SettingsBuilderException::class ); $setting->loadFile( 'fixtures/settings-with-self-includes.json' )->apply(); } public function testLoadingAndApplyingFromFileAfterFinalize() { $configBuilder = new ArrayConfigBuilder(); $phpIniSinkMock = $this->createMock( PhpIniSink::class ); $phpIniSinkMock->expects( $this->once() )->method( 'set' )->with( 'foo', 'bar' ); $setting = $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder, 'phpIniSink' => $phpIniSinkMock ] ); $setting->loadFile( 'fixtures/settings.json' )->apply(); $config = $configBuilder->build(); $this->assertSame( 'TEST', $config->get( 'Something' ) ); // Check that after enterRegistrationStage(), we can still override config, // but we can't load settings sources. $setting->enterRegistrationStage(); $setting->overrideConfigValue( 'Foo', 'bar' ); $this->expectException( SettingsBuilderException::class ); $setting->loadFile( 'fixtures/settings.json' )->apply(); } public function testSettingConfigAfterLock() { $setting = $this->newSettingsBuilder(); // Check that after enterOperationStage(), we can't override config. $setting->enterReadOnlyStage(); $this->expectException( SettingsBuilderException::class ); $setting->overrideConfigValue( 'Foo', 'bar' ); } public function testLoadingExtensions() { $extensionRegistryMock = $this->createMock( ExtensionRegistry::class ); $extensionRegistryMock ->expects( $this->exactly( 3 ) ) ->method( 'queue' )->withConsecutive( [ '/test/extensions/Foo/extension.json' ], [ '/test/extensions/Bar/extension.json' ], [ '/test/skins/Quux/skin.json' ] ); $setting = $this->newSettingsBuilder( [ 'extensionRegistry' => $extensionRegistryMock, ] ); $setting->loadFile( 'fixtures/default-schema.json' ); $setting->loadFile( 'fixtures/settings.json' ); $setting->apply(); } public static function provideConfigDefaults() { yield 'sets a value from a single settings file' => [ 'settingsBatches' => [ [ 'config' => [ 'MySetting' => 'MyValue', ], ], ], 'expected' => [ 'MySetting' => 'MyValue', ], ]; yield 'merges different values from multiple settings files' => [ 'settingsBatches' => [ [ 'config' => [ 'MySetting' => 'MyValue', ], ], [ 'config' => [ 'MyOtherSetting' => 'MyOtherValue', ], ], ], 'expected' => [ 'MySetting' => 'MyValue', 'MyOtherSetting' => 'MyOtherValue', ], ]; yield 'overrides value in config' => [ 'settingsBatches' => [ [ 'config' => [ 'MySetting' => 'MyValue', ], ], [ 'config' => [ 'MySetting' => 'MyOtherValue', ], ], ], 'expected' => [ 'MySetting' => 'MyOtherValue', ], ]; yield 'sets a default from schema' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'default' => 'MyDefault', ], ], ], ], 'expected' => [ 'MySetting' => 'MyDefault', ], ]; yield 'value in config overrides default from schema' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'default' => 'MyDefault', ], ], 'config' => [ 'MySetting' => 'MyValue', ], ], ], 'expected' => [ 'MySetting' => 'MyValue', ], ]; yield 'default null is applied' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'default' => null, ], ], ], ], 'expected' => [ 'MySetting' => null, ], ]; yield 'null value can override default' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default', ], ], 'config' => [ 'MySetting' => null, ], ], ], 'expected' => [ 'MySetting' => null, ], ]; yield 'merge strategy is applied when setting config' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE_RECURSIVE ], ], 'config' => [ 'MySetting' => [ 'a' => [ 'b' => 'c' ], ], ], ], [ 'config' => [ 'MySetting' => [ 'a' => [ 'b' => 'd' ], ], ], ] ], 'expected' => [ 'MySetting' => [ 'a' => [ 'b' => [ 'c', 'd' ], ], ], ], ]; yield 'merge strategy is ignored when using config-overrides' => [ 'settingsBatches' => [ [ 'config-schema' => [ 'MySetting' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE_RECURSIVE ], ], 'config-overrides' => [ 'MySetting' => [ 'a' => [ 'b' => 'c' ], ], ], ], [ 'config-overrides' => [ 'MySetting' => [ 'a' => [ 'b' => 'd' ], ], ], ] ], 'expected' => [ 'MySetting' => [ 'a' => [ 'b' => 'd' ], ], ], ]; yield 'config value is merged into default when setting both in the same settings file' => [ 'settingsBatches' => [ [ 'config' => [ 'MySetting' => [ 'a' => [ 'b' => 'd' ], ], ], 'config-schema' => [ 'MySetting' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE_RECURSIVE, 'default' => [ 'a' => [ 'b' => 'c' ], ], ], ], ] ], 'expected' => [ 'MySetting' => [ 'a' => [ 'b' => [ 'c', 'd' ], ], ], ], ]; yield 'default is merged backwards into config value when set later' => [ 'settingsBatches' => [ [ 'config' => [ 'MySetting' => [ 'a' => [ 'b' => 'd' ], ], ], ], [ 'config-schema' => [ 'MySetting' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE_RECURSIVE, 'default' => [ 'a' => [ 'b' => 'c' ], ], ], ], ] ], 'expected' => [ 'MySetting' => [ 'a' => [ 'b' => [ 'c', 'd' ], ], ], ], ]; } /** * @dataProvider provideConfigDefaults */ public function testConfigDefaults( array $settingsBatches, array $expected ) { $configBuilder = new ArrayConfigBuilder(); $setting = $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder ] ); foreach ( $settingsBatches as $batch ) { $setting->loadArray( $batch ); } $setting->apply(); foreach ( $expected as $key => $value ) { $this->assertSame( $value, $configBuilder->build()->get( $key ) ); } } public function testSetConfig() { $setting = $this->newSettingsBuilder(); $setting->loadArray( [ 'config-schema' => [ 'a' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE ] ] ] ); $setting->putConfigValues( [ 'a' => [ 1 ], 'b' => 2 ] ); $config = $setting->getConfig(); $this->assertSame( [ 1 ], $config->get( 'a' ) ); $this->assertSame( 2, $config->get( 'b' ) ); $setting->putConfigValues( [ 'a' => [ 11 ], 'b' => 22 ] ); $config = $setting->getConfig(); $this->assertSame( [ 1, 11 ], $config->get( 'a' ) ); $this->assertSame( 22, $config->get( 'b' ) ); $setting->putConfigValue( 'a', [ 111 ] ); $config = $setting->getConfig(); $this->assertSame( [ 1, 11, 111 ], $config->get( 'a' ) ); $this->assertSame( 22, $config->get( 'b' ) ); } public function testOverrideConfig() { $setting = $this->newSettingsBuilder(); $setting->loadArray( [ 'config-schema' => [ 'a' => [ 'mergeStrategy' => MergeStrategy::ARRAY_MERGE ] ] ] ); $setting->overrideConfigValues( [ 'a' => [ 1 ], 'b' => 2 ] ); $config = $setting->getConfig(); $this->assertSame( [ 1 ], $config->get( 'a' ) ); $this->assertSame( 2, $config->get( 'b' ) ); $setting->overrideConfigValues( [ 'a' => [ 11 ], 'b' => 22 ] ); $config = $setting->getConfig(); $this->assertSame( [ 11 ], $config->get( 'a' ) ); $this->assertSame( 22, $config->get( 'b' ) ); $setting->overrideConfigValue( 'a', [ 111 ] ); $config = $setting->getConfig(); $this->assertSame( [ 111 ], $config->get( 'a' ) ); $this->assertSame( 22, $config->get( 'b' ) ); } public function testRegisterHookHandler() { $setting = $this->newSettingsBuilder(); $hookName = 'TestHookForSettingsBuilderTest'; $setting->registerHookHandlers( [ $hookName => [ 'strtolower' ] ] ); $config = $setting->getConfig(); $this->assertArrayHasKey( $hookName, $config->get( MainConfigNames::Hooks ) ); } public function testApplyPurgesState() { $configBuilder = new ArrayConfigBuilder(); $setting = $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder ] ); $setting->putConfigValue( 'MySetting', 'MyValue' ) ->apply(); $this->assertSame( 'MyValue', $configBuilder->build()->get( 'MySetting' ) ); $configBuilder->set( 'MySetting', 'MyOtherValue' ); // Calling apply a second time should not redefine the global // since the state should be cleared $setting->apply(); $this->assertSame( 'MyOtherValue', $configBuilder->build()->get( 'MySetting' ) ); } public function testApplyDefaultDoesNotOverwriteExisting() { $configBuilder = new ArrayConfigBuilder(); // If we set a value via SettingsBuilder, defining a default later // should not override it. $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder ] ) ->putConfigValue( 'MySetting', 'existing' ) ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default' ], ], ] ) ->apply(); $this->assertSame( 'existing', $configBuilder->build()->get( 'MySetting' ) ); } public function testApplyDefaultDoesNotOverwritePreexisting() { $configBuilder = new ArrayConfigBuilder(); // If we set a value in the config directly, defining a default later // would override it, unless we call assumeDirtyConfig(). $configBuilder->set( 'MySetting', 'existing' ); $this->newSettingsBuilder( [ 'configBuilder' => $configBuilder ] ) ->assumeDirtyConfig() ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default' ], ], ] ) ->apply(); $this->assertSame( 'existing', $configBuilder->build()->get( 'MySetting' ) ); } public function testConfigSchemaOverrideNotAllowed() { $this->expectException( SettingsBuilderException::class ); $this->newSettingsBuilder() ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default' ], ], ] ) ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'override' ], ], ] ) ->apply(); } public function testConfigSchemaDefaultsEvaluation() { $settingsBuilder = $this->newSettingsBuilder(); $settingsBuilder->loadArray( [ 'config-schema' => [ 'A' => [ 'default' => [ 'a' ], 'type' => 'array' ], 'B' => [ 'default' => [ 'b' ], 'type' => 'array' ], ], 'config' => [ 'B' => [ 'b2' ], 'C' => [ 'c2' ], ] ] ); $settingsBuilder->loadArray( [ 'config-schema' => [ 'X' => [ 'default' => [ 'x' ], 'type' => 'array' ], ], 'config' => [ 'A' => [ 'a3' ], 'B' => [ 'b3' ], ], 'config-overrides' => [ 'C' => [ 'c4' ], ] ] ); $config = $settingsBuilder->getConfig(); $this->assertSame( [ 'a', 'a3' ], $config->get( 'A' ) ); $this->assertSame( [ 'b', 'b2', 'b3' ], $config->get( 'B' ) ); $this->assertSame( [ 'c4' ], $config->get( 'C' ) ); $this->assertSame( [ 'x' ], $config->get( 'X' ) ); } public static function provideValidate() { yield 'all good' => [ 'settings' => [ 'config-schema' => [ 'foo' => [ 'type' => 'string', ], ], 'config' => [ 'foo' => 'bar', ], ], 'valid' => true, ]; yield 'missing key' => [ 'settings' => [ 'config-schema' => [ 'foo' => [ 'type' => 'string', ], ], 'config' => [ 'bar' => 'bar' ], ], 'valid' => false, ]; yield 'invalid config' => [ 'settings' => [ 'config-schema' => [ 'foo' => [ 'type' => 'string', ], ], 'config' => [ 'foo' => 1 ], ], 'valid' => false, ]; yield 'no schema was added' => [ 'settings' => [ 'config-schema' => [], 'config' => [ 'foo' => 'bar', ], ], 'valid' => true, ]; yield 'key is in config but has no schema' => [ 'settings' => [ 'config-schema' => [ 'foo' => [ 'type' => 'array', 'mergeStrategy' => MergeStrategy::ARRAY_MERGE ], ], 'config' => [ 'foo' => [], 'baz' => false, ], ], 'valid' => true, ]; } /** * @dataProvider provideValidate */ public function testValidate( array $settings, bool $valid ) { $status = $this->newSettingsBuilder() ->loadArray( $settings ) ->apply() ->validate(); $this->assertSame( $valid, $status->isOK() ); } public function testValidateInvalidSchema() { $this->expectException( InvalidArgumentException::class ); $this->newSettingsBuilder() ->loadArray( [ 'config-schema' => [ 'foo' => [ 'type' => 1 ] ], 'config' => [ 'foo' => 'bar' ], ] ) ->apply() ->validate(); } public function testLoadsCacheableSource() { $mockSource = $this->createMock( CacheableSource::class ); $mockCache = $this->createMock( BagOStuff::class ); $configBuilder = new ArrayConfigBuilder(); $builder = $this ->newSettingsBuilder( [ 'configBuilder' => $configBuilder, 'cache' => $mockCache ] ) ->load( $mockSource ); $hashKey = 'abc123'; $key = 'global:MediaWiki\Tests\Unit\Settings\Cache\CachedSourceTest:' . $hashKey; // Mock a cache miss $mockSource ->expects( $this->once() ) ->method( 'getHashKey' ) ->willReturn( $hashKey ); $mockCache ->expects( $this->once() ) ->method( 'makeGlobalKey' ) ->with( 'MediaWiki\Settings\Cache\CachedSource', $hashKey ) ->willReturn( $key ); $mockCache ->expects( $this->once() ) ->method( 'get' ) ->with( $key ) ->willReturn( false ); $mockCache ->expects( $this->once() ) ->method( 'lock' ) ->willReturn( true ); $mockCache ->expects( $this->once() ) ->method( 'unlock' ) ->willReturn( true ); $mockSource ->expects( $this->once() ) ->method( 'load' ) ->willReturn( [ 'config' => [ 'MySetting' => 'BlaBla' ] ] ); $builder->apply(); $this->assertSame( 'BlaBla', $configBuilder->build()->get( 'MySetting' ) ); } public function testGetDefaultConfig() { $defaultConfig = $this->newSettingsBuilder() ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'bla' ], 'OtherSetting' => [ 'type' => 'number' ], // no default ] ] ) ->apply() ->getDefaultConfig(); $this->assertTrue( $defaultConfig->has( 'MySetting' ) ); $this->assertSame( 'bla', $defaultConfig->get( 'MySetting' ) ); $this->assertTrue( $defaultConfig->has( 'OtherSetting' ) ); $this->assertNull( $defaultConfig->get( 'OtherSetting' ) ); } public function testGetConfigSchema() { $configSchema = $this->newSettingsBuilder() ->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'bla' ], ], ] ) ->apply() ->getConfigSchema(); $this->assertTrue( $configSchema->hasSchemaFor( 'MySetting' ) ); $this->assertSame( 'bla', $configSchema->getDefaultFor( 'MySetting' ) ); } /** * Make sure that the 'config-schema' and 'config-schema-inverse' keys * are fully processed and combined appropriately. */ public function testSchemaLoading() { $settings = $this->newSettingsBuilder(); $settings->loadFile( 'fixtures/default-schema.json' ); $schema = $settings->getConfigSchema(); $this->assertSame( '/DEFAULT/', $schema->getDefaultFor( 'StyleDirectory' ) ); $this->assertSame( [ 'callback' => [ MainConfigSchema::class, 'getDefaultUsePathInfo' ] ], $schema->getDynamicDefaultDeclarationFor( 'UsePathInfo' ) ); $this->assertSame( 'replace', $schema->getMergeStrategyFor( 'LBFactoryConf' )->getName() ); $this->assertSame( '/DEFAULT/', $schema->getDefaultFor( 'ExtensionDirectory' ) ); $this->assertSame( [ 'use' => [ 'ScriptPath' ], 'callback' => [ MainConfigSchema::class, 'getDefaultRestPath' ] ], $schema->getDynamicDefaultDeclarationFor( 'RestPath' ) ); $this->assertSame( [ 'use' => [ 'ScriptPath' ], 'callback' => [ MainConfigSchema::class, 'getDefaultRestPath' ] ], $schema->getDynamicDefaultDeclarationFor( 'RestPath' ) ); $this->assertSame( 'replace', $schema->getMergeStrategyFor( 'TiffThumbnailType' )->getName() ); } }