wiki.techinc.nl/tests/phpunit/unit/includes/Settings/SettingsBuilderTest.php
Ebrahim Byagowi 697e19e461 Add MediaWiki\Registration namespace to registration classes
Bug: T353458
Change-Id: Ifa3b6a6e0353bb4ce21a3f4456f1fc696c8d377c
2024-08-10 10:08:22 +00:00

643 lines
19 KiB
PHP

<?php
namespace MediaWiki\Tests\Unit\Settings;
use InvalidArgumentException;
use MediaWiki\MainConfigNames;
use MediaWiki\MainConfigSchema;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Settings\Cache\CacheableSource;
use MediaWiki\Settings\Cache\CachedSource;
use MediaWiki\Settings\Config\ArrayConfigBuilder;
use MediaWiki\Settings\Config\MergeStrategy;
use MediaWiki\Settings\Config\PhpIniSink;
use MediaWiki\Settings\SettingsBuilder;
use MediaWiki\Settings\SettingsBuilderException;
use PHPUnit\Framework\TestCase;
use Wikimedia\ObjectCache\BagOStuff;
/**
* @covers \MediaWiki\Settings\SettingsBuilder
*/
class SettingsBuilderTest extends TestCase {
/**
* @param array $params
* @return SettingsBuilder
*/
private function newSettingsBuilder( $params = [] ): SettingsBuilder {
return new SettingsBuilder(
__DIR__,
$params['extensionRegistry'] ?? $this->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 );
$expectedQueuePaths = [
'/test/extensions/Foo/extension.json',
'/test/extensions/Bar/extension.json',
'/test/skins/Quux/skin.json',
];
$extensionRegistryMock
->expects( $this->exactly( 3 ) )
->method( 'queue' )
->willReturnCallback( function ( $path ) use ( &$expectedQueuePaths ) {
$this->assertContains( $path, $expectedQueuePaths );
$pathIdx = array_search( $path, $expectedQueuePaths, true );
unset( $expectedQueuePaths[$pathIdx] );
} );
$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:' . self::class . ':' . $hashKey;
// Mock a cache miss
$mockSource
->expects( $this->once() )
->method( 'getHashKey' )
->willReturn( $hashKey );
$mockCache
->expects( $this->once() )
->method( 'makeGlobalKey' )
->with( CachedSource::class, $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()
);
}
}