wiki.techinc.nl/tests/phpunit/includes/preferences/DefaultPreferencesFactoryTest.php
thiemowmde 8a9dd67139 Avoid calling overrideConfigValue() multiple times
Same as I7a82951.

overrideConfigValue() and overrideConfigValues() both call
setMwGlobals(), which calls resetServices(). This is surprisingly
expensive. It's much better to call it once with an array.

Change-Id: I4ff2f6b902b1a1e0b554ce6fc76f3b612f703fae
2023-07-20 14:59:42 +02:00

503 lines
17 KiB
PHP

<?php
use MediaWiki\Auth\AuthManager;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Languages\LanguageConverterFactory;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Preferences\DefaultPreferencesFactory;
use MediaWiki\Preferences\SignatureValidatorFactory;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Session\SessionId;
use MediaWiki\Session\TestUtils;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\Title\Title;
use MediaWiki\User\UserGroupManager;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserOptionsLookup;
use MediaWiki\User\UserOptionsManager;
use PHPUnit\Framework\MockObject\MockObject;
use Wikimedia\TestingAccessWrapper;
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* @group Preferences
* @coversDefaultClass MediaWiki\Preferences\DefaultPreferencesFactory
*/
class DefaultPreferencesFactoryTest extends \MediaWikiIntegrationTestCase {
use DummyServicesTrait;
use TestAllServiceOptionsUsed;
/** @var IContextSource */
protected $context;
/** @var Config */
protected $config;
protected function setUp(): void {
parent::setUp();
$this->context = new RequestContext();
$this->context->setTitle( Title::newFromText( self::class ) );
$this->overrideConfigValues( [
MainConfigNames::DisableLangConversion => false,
MainConfigNames::UsePigLatinVariant => false,
] );
$this->config = $this->getServiceContainer()->getMainConfig();
}
/**
* @covers ::__construct
*/
public function testConstruct() {
// Make sure if the optional services are not provided, stuff still works, so that
// the GlobalPreferences extension isn't broken
$params = [
$this->createMock( ServiceOptions::class ),
$this->createMock( Language::class ),
$this->createMock( AuthManager::class ),
$this->createMock( LinkRenderer::class ),
$this->createMock( NamespaceInfo::class ),
$this->createMock( PermissionManager::class ),
$this->createMock( ILanguageConverter::class ),
$this->createMock( LanguageNameUtils::class ),
$this->createMock( HookContainer::class ),
$this->createMock( UserOptionsLookup::class ),
];
$preferencesFactory = new DefaultPreferencesFactory( ...$params );
$this->assertInstanceOf(
DefaultPreferencesFactory::class,
$preferencesFactory,
'Created with some services missing'
);
// Now, make sure that MediaWikiServices isn't used
// Switch the UserOptionsLookup to a UserOptionsManager
$params[9] = $this->createMock( UserOptionsManager::class );
$params[] = $this->createMock( LanguageConverterFactory::class );
$params[] = $this->createMock( Parser::class );
$params[] = $this->createMock( SkinFactory::class );
$params[] = $this->createMock( UserGroupManager::class );
$params[] = $this->createMock( SignatureValidatorFactory::class );
$params[] = new HashConfig();
$oldMwServices = MediaWikiServices::forceGlobalInstance(
$this->createNoOpMock( MediaWikiServices::class )
);
// Wrap in a try-finally block to make sure the real MediaWikiServices is
// always put back even if something goes wrong
try {
$preferencesFactory = new DefaultPreferencesFactory( ...$params );
$this->assertInstanceOf(
DefaultPreferencesFactory::class,
$preferencesFactory,
'Created with all services, MediaWikiServices not used'
);
} finally {
// Put back the real MediaWikiServices
MediaWikiServices::forceGlobalInstance( $oldMwServices );
}
}
/**
* Get a basic PreferencesFactory for testing with.
* @param array $options Supported options are:
* 'language' - A Language object, falls back to `new Language()`
* 'userOptionsManager' - A UserOptionsManager service, falls back to using MediaWikiServices
* 'userGroupManager' - A UserGroupManager service, falls back to a mock where no users
* have any extra groups, just `*` and `user`
* @return DefaultPreferencesFactory
*/
protected function getPreferencesFactory( array $options = [] ) {
// DummyServicesTrait::getDummyNamespaceInfo
$nsInfo = $this->getDummyNamespaceInfo();
$services = $this->getServiceContainer();
// The PermissionManager should not be used for anything, its only a parameter
// until we figure out how to remove it without breaking the GlobalPreferences
// extension (GlobalPreferencesFactory extends DefaultPreferencesFactory)
$permissionManager = $this->createNoOpMock( PermissionManager::class );
$language = $options['language'] ?? new Language();
$userOptionsManager = $options['userOptionsManager'] ?? $services->getUserOptionsManager();
$userGroupManager = $options['userGroupManager'] ?? false;
if ( !$userGroupManager ) {
$userGroupManager = $this->createMock( UserGroupManager::class );
$userGroupManager->method( 'getUserGroupMemberships' )->willReturn( [] );
$userGroupManager->method( 'getUserEffectiveGroups' )->willReturnCallback(
static function ( UserIdentity $user ) {
return $user->isRegistered() ? [ '*', 'user' ] : [ '*' ];
}
);
}
return new DefaultPreferencesFactory(
new LoggedServiceOptions( self::$serviceOptionsAccessLog,
DefaultPreferencesFactory::CONSTRUCTOR_OPTIONS, $this->config ),
$language,
$services->getAuthManager(),
$services->getLinkRenderer(),
$nsInfo,
$permissionManager,
$services->getLanguageConverterFactory()->getLanguageConverter( $language ),
$services->getLanguageNameUtils(),
$services->getHookContainer(),
$userOptionsManager,
$services->getLanguageConverterFactory(),
$services->getParser(),
$services->getSkinFactory(),
$userGroupManager,
$services->getSignatureValidatorFactory(),
$services->getMainConfig()
);
}
/**
* @covers ::getForm
* @covers ::searchPreferences
*/
public function testGetForm() {
$this->setTemporaryHook( 'GetPreferences', HookContainer::NOOP );
$testUser = $this->getTestUser();
$prefFactory = $this->getPreferencesFactory();
$form = $prefFactory->getForm( $testUser->getUser(), $this->context );
$this->assertInstanceOf( PreferencesFormOOUI::class, $form );
$this->assertCount( 6, $form->getPreferenceSections() );
}
/**
* @covers ::sortSkinNames
*/
public function testSortSkinNames() {
/** @var DefaultPreferencesFactory $factory */
$factory = TestingAccessWrapper::newFromObject(
$this->getPreferencesFactory()
);
$validSkinNames = [
'minerva' => 'Minerva Neue',
'monobook' => 'Monobook',
'cologne-blue' => 'Cologne Blue',
'vector' => 'Vector',
'vector-2022' => 'Vector 2022',
'timeless' => 'Timeless',
];
$currentSkin = 'monobook';
$preferredSkins = [ 'vector-2022', 'invalid-skin', 'vector' ];
uksort( $validSkinNames, static function ( $a, $b ) use ( $factory, $currentSkin, $preferredSkins ) {
return $factory->sortSkinNames( $a, $b, $currentSkin, $preferredSkins );
} );
$this->assertArrayEquals( [
'monobook' => 'Monobook',
'vector-2022' => 'Vector 2022',
'vector' => 'Vector',
'cologne-blue' => 'Cologne Blue',
'minerva' => 'Minerva Neue',
'timeless' => 'Timeless',
], $validSkinNames );
}
/**
* CSS classes for emailauthentication preference field when there's no email.
* @see https://phabricator.wikimedia.org/T36302
*
* @covers ::profilePreferences
* @dataProvider emailAuthenticationProvider
*/
public function testEmailAuthentication( $user, $cssClass ) {
$this->overrideConfigValue( MainConfigNames::EmailAuthentication, true );
$prefs = $this->getPreferencesFactory()
->getFormDescriptor( $user, $this->context );
$this->assertArrayHasKey( 'cssclass', $prefs['emailauthentication'] );
$this->assertEquals( $cssClass, $prefs['emailauthentication']['cssclass'] );
}
/**
* @covers ::renderingPreferences
*/
public function testShowRollbackConfIsHiddenForUsersWithoutRollbackRights() {
$userMock = $this->createMock( User::class );
$userMock->method( 'isAllowed' )->willReturnCallback(
static function ( $permission ) {
return $permission === 'editmyoptions';
}
);
$userOptionsManagerMock = $this->createUserOptionsManagerMock( [ 'test' => 'yes' ], true );
$userMock = $this->getUserMockWithSession( $userMock );
$prefs = $this->getPreferencesFactory( [
'userOptionsManager' => $userOptionsManagerMock,
] )->getFormDescriptor( $userMock, $this->context );
$this->assertArrayNotHasKey( 'showrollbackconfirmation', $prefs );
}
/**
* @covers ::renderingPreferences
*/
public function testShowRollbackConfIsShownForUsersWithRollbackRights() {
$userMock = $this->createMock( User::class );
$userMock->method( 'isAllowed' )->willReturnCallback(
static function ( $permission ) {
return $permission === 'editmyoptions' || $permission === 'rollback';
}
);
$userMock = $this->getUserMockWithSession( $userMock );
$userOptionsManagerMock = $this->createUserOptionsManagerMock( [ 'test' => 'yes' ], true );
$prefs = $this->getPreferencesFactory( [
'userOptionsManager' => $userOptionsManagerMock,
] )->getFormDescriptor( $userMock, $this->context );
$this->assertArrayHasKey( 'showrollbackconfirmation', $prefs );
$this->assertEquals(
'rendering/advancedrendering',
$prefs['showrollbackconfirmation']['section']
);
}
public function emailAuthenticationProvider() {
$userNoEmail = new User;
$userEmailUnauthed = new User;
$userEmailUnauthed->setEmail( 'noauth@example.org' );
$userEmailAuthed = new User;
$userEmailAuthed->setEmail( 'noauth@example.org' );
$userEmailAuthed->setEmailAuthenticationTimestamp( wfTimestamp() );
return [
[ $userNoEmail, 'mw-email-none' ],
[ $userEmailUnauthed, 'mw-email-not-authenticated' ],
[ $userEmailAuthed, 'mw-email-authenticated' ],
];
}
/**
* Test that PreferencesFormPreSave hook has correct data:
* - user Object is passed
* - oldUserOptions contains previous user options (before save)
* - formData and User object have set up new properties
*
* @see https://phabricator.wikimedia.org/T169365
* @covers ::submitForm
*/
public function testPreferencesFormPreSaveHookHasCorrectData() {
$oldOptions = [
'test' => 'abc',
'option' => 'old'
];
$newOptions = [
'test' => 'abc',
'option' => 'new'
];
$configMock = new HashConfig( [
MainConfigNames::HiddenPrefs => []
] );
$form = $this->createMock( PreferencesFormOOUI::class );
$userMock = $this->createMock( User::class );
$userOptionsManagerMock = $this->createUserOptionsManagerMock( $oldOptions );
$userOptionsManagerMock->expects( $this->exactly( 2 ) )
->method( 'setOption' )
->withConsecutive(
[ $userMock, 'test', $newOptions[ 'test' ] ],
[ $userMock, 'option', $newOptions[ 'option' ] ]
);
$userMock->method( 'isAllowed' )->willReturnCallback(
static function ( $permission ) {
return $permission === 'editmyprivateinfo' || $permission === 'editmyoptions';
}
);
$userMock->method( 'isAllowedAny' )->willReturnCallback(
static function ( ...$permissions ) {
foreach ( $permissions as $perm ) {
if ( $perm === 'editmyprivateinfo' || $perm === 'editmyoptions' ) {
return true;
}
}
return false;
}
);
$form->method( 'getModifiedUser' )
->willReturn( $userMock );
$form->method( 'getContext' )
->willReturn( $this->context );
$form->method( 'getConfig' )
->willReturn( $configMock );
$this->setTemporaryHook( 'PreferencesFormPreSave',
function ( $formData, $form, $user, &$result, $oldUserOptions )
use ( $newOptions, $oldOptions, $userMock ) {
$this->assertSame( $userMock, $user );
foreach ( $newOptions as $option => $value ) {
$this->assertSame( $value, $formData[ $option ] );
}
foreach ( $oldOptions as $option => $value ) {
$this->assertSame( $value, $oldUserOptions[ $option ] );
}
$this->assertTrue( $result );
}
);
/** @var DefaultPreferencesFactory $factory */
$factory = TestingAccessWrapper::newFromObject(
$this->getPreferencesFactory( [ 'userOptionsManager' => $userOptionsManagerMock ] )
);
$factory->saveFormData( $newOptions, $form, [] );
}
/**
* The rclimit preference should accept non-integer input and filter it to become an integer.
*
* @covers ::saveFormData
*/
public function testIntvalFilter() {
// Test a string with leading zeros (i.e. not octal) and spaces.
$this->context->getRequest()->setVal( 'wprclimit', ' 0012 ' );
$user = new User;
$prefFactory = $this->getPreferencesFactory();
$form = $prefFactory->getForm( $user, $this->context );
$form->show();
$form->trySubmit();
$userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
$this->assertEquals( 12, $userOptionsLookup->getOption( $user, 'rclimit' ) );
}
/**
* @covers ::profilePreferences
*/
public function testVariantsSupport() {
$userMock = $this->createMock( User::class );
$userMock->method( 'isAllowed' )->willReturn( true );
$userMock = $this->getUserMockWithSession( $userMock );
$language = $this->createMock( Language::class );
$language->method( 'getCode' )
->willReturn( 'sr' );
$userOptionsManagerMock = $this->createUserOptionsManagerMock(
[ 'LanguageCode' => 'sr', 'variant' => 'sr' ], true
);
$prefs = $this->getPreferencesFactory( [
'language' => $language,
'userOptionsManager' => $userOptionsManagerMock,
] )->getFormDescriptor( $userMock, $this->context );
$this->assertArrayHasKey( 'default', $prefs['variant'] );
$this->assertEquals( 'sr', $prefs['variant']['default'] );
}
/**
* @covers ::profilePreferences
*/
public function testUserGroupMemberships() {
$userMock = $this->createMock( User::class );
$userMock->method( 'isAllowed' )->willReturn( true );
$userMock->method( 'isAllowedAny' )->willReturn( true );
$userMock->method( 'isRegistered' )->willReturn( true );
$userMock = $this->getUserMockWithSession( $userMock );
$language = $this->createMock( Language::class );
$language->method( 'getCode' )
->willReturn( 'en' );
$userOptionsManagerMock = $this->createUserOptionsManagerMock( [], true );
$prefs = $this->getPreferencesFactory( [
'language' => $language,
'userOptionsManager' => $userOptionsManagerMock,
] )->getFormDescriptor( $userMock, $this->context );
$this->assertArrayHasKey( 'default', $prefs['usergroups'] );
$this->assertEquals(
UserGroupMembership::getLinkHTML( 'user', $this->context ),
( $prefs['usergroups']['default'] )()
);
}
/**
* @coversNothing
*/
public function testAllServiceOptionsUsed() {
$this->assertAllServiceOptionsUsed( [
// Only used when $wgEnotifWatchlist or $wgEnotifUserTalk is true
'EnotifMinorEdits',
// Only used when $wgEnotifWatchlist or $wgEnotifUserTalk is true
'EnotifRevealEditorAddress',
// Only used when 'fancysig' preference is enabled
'SignatureValidation',
] );
}
/**
* @param array $userOptions
* @param bool $defaultOptions
* @return UserOptionsManager
*/
private function createUserOptionsManagerMock( array $userOptions, bool $defaultOptions = false ) {
$services = $this->getServiceContainer();
$defaults = $services->getMainConfig()->get( 'DefaultUserOptions' );
$defaults['language'] = $services->getContentLanguage()->getCode();
$defaults['skin'] = Skin::normalizeKey( $services->getMainConfig()->get( 'DefaultSkin' ) );
$userOptions += $defaults;
$mock = $this->createMock( UserOptionsManager::class );
$mock->method( 'getOptions' )->willReturn( $userOptions );
$mock->method( 'getOption' )->willReturnCallback(
static function ( $user, $option ) use ( $userOptions ) {
return $userOptions[$option] ?? null;
}
);
if ( $defaultOptions ) {
$mock->method( 'getDefaultOptions' )->willReturn( $defaults );
}
return $mock;
}
/**
* @param MockObject $userMock
* @return MockObject
*/
private function getUserMockWithSession( MockObject $userMock ): MockObject {
// We're mocking a stdClass because the Session class is final, and thus not mockable.
$mock = $this->getMockBuilder( stdClass::class )
->addMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
->getMock();
$mock->method( 'getSessionId' )->willReturn(
new SessionId( str_repeat( 'X', 32 ) )
);
$session = TestUtils::getDummySession( $mock );
$mockRequest = $this->getMockBuilder( FauxRequest::class )
->onlyMethods( [ 'getSession' ] )
->getMock();
$mockRequest->method( 'getSession' )->willReturn( $session );
$userMock->method( 'getRequest' )->willReturn( $mockRequest );
$userMock->method( 'getTitleKey' )->willReturn( '' );
return $userMock;
}
}