Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / tests / phpunit / includes / preferences / DefaultPreferencesFactoryTest.php
bloba3212cafa1ed2c6fd421915499082b5c3334da00
1 <?php
3 use MediaWiki\Auth\AuthManager;
4 use MediaWiki\Config\Config;
5 use MediaWiki\Config\ServiceOptions;
6 use MediaWiki\Context\IContextSource;
7 use MediaWiki\Context\RequestContext;
8 use MediaWiki\HookContainer\HookContainer;
9 use MediaWiki\HookContainer\HookRunner;
10 use MediaWiki\Language\ILanguageConverter;
11 use MediaWiki\Language\Language;
12 use MediaWiki\Languages\LanguageConverterFactory;
13 use MediaWiki\Languages\LanguageNameUtils;
14 use MediaWiki\Linker\LinkRenderer;
15 use MediaWiki\MainConfigNames;
16 use MediaWiki\MediaWikiServices;
17 use MediaWiki\Parser\ParserFactory;
18 use MediaWiki\Permissions\PermissionManager;
19 use MediaWiki\Preferences\DefaultPreferencesFactory;
20 use MediaWiki\Preferences\SignatureValidatorFactory;
21 use MediaWiki\Request\FauxRequest;
22 use MediaWiki\Session\SessionId;
23 use MediaWiki\Tests\Session\TestUtils;
24 use MediaWiki\Tests\Unit\DummyServicesTrait;
25 use MediaWiki\Title\NamespaceInfo;
26 use MediaWiki\Title\Title;
27 use MediaWiki\User\Options\UserOptionsLookup;
28 use MediaWiki\User\Options\UserOptionsManager;
29 use MediaWiki\User\User;
30 use MediaWiki\User\UserGroupManager;
31 use MediaWiki\User\UserGroupMembership;
32 use MediaWiki\User\UserIdentity;
33 use PHPUnit\Framework\MockObject\MockObject;
34 use Wikimedia\TestingAccessWrapper;
36 /**
37 * This program is free software; you can redistribute it and/or modify
38 * it under the terms of the GNU General Public License as published by
39 * the Free Software Foundation; either version 2 of the License, or
40 * (at your option) any later version.
42 * This program is distributed in the hope that it will be useful,
43 * but WITHOUT ANY WARRANTY; without even the implied warranty of
44 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
45 * GNU General Public License for more details.
47 * You should have received a copy of the GNU General Public License along
48 * with this program; if not, write to the Free Software Foundation, Inc.,
49 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
50 * http://www.gnu.org/copyleft/gpl.html
52 * @file
55 /**
56 * @group Preferences
57 * @group Database
58 * @coversDefaultClass \MediaWiki\Preferences\DefaultPreferencesFactory
60 class DefaultPreferencesFactoryTest extends \MediaWikiIntegrationTestCase {
61 use DummyServicesTrait;
62 use TestAllServiceOptionsUsed;
64 /** @var IContextSource */
65 protected $context;
67 /** @var Config */
68 protected $config;
70 protected function setUp(): void {
71 parent::setUp();
72 $this->context = new RequestContext();
73 $this->context->setTitle( Title::makeTitle( NS_MAIN, self::class ) );
75 $this->overrideConfigValues( [
76 MainConfigNames::DisableLangConversion => false,
77 MainConfigNames::UsePigLatinVariant => false,
78 ] );
79 $this->config = $this->getServiceContainer()->getMainConfig();
82 /**
83 * @covers ::__construct
85 public function testConstruct() {
86 // Make sure if the optional services are not provided, stuff still works, so that
87 // the GlobalPreferences extension isn't broken
88 $params = [
89 $this->createMock( ServiceOptions::class ),
90 $this->createMock( Language::class ),
91 $this->createMock( AuthManager::class ),
92 $this->createMock( LinkRenderer::class ),
93 $this->createMock( NamespaceInfo::class ),
94 $this->createMock( PermissionManager::class ),
95 $this->createMock( ILanguageConverter::class ),
96 $this->createMock( LanguageNameUtils::class ),
97 $this->createMock( HookContainer::class ),
98 $this->createMock( UserOptionsLookup::class ),
100 $preferencesFactory = new DefaultPreferencesFactory( ...$params );
101 $this->assertInstanceOf(
102 DefaultPreferencesFactory::class,
103 $preferencesFactory,
104 'Created with some services missing'
107 // Now, make sure that MediaWikiServices isn't used
108 // Switch the UserOptionsLookup to a UserOptionsManager
109 $params[9] = $this->createMock( UserOptionsManager::class );
110 $params[] = $this->createMock( LanguageConverterFactory::class );
111 $params[] = $this->createMock( ParserFactory::class );
112 $params[] = $this->createMock( SkinFactory::class );
113 $params[] = $this->createMock( UserGroupManager::class );
114 $params[] = $this->createMock( SignatureValidatorFactory::class );
115 $oldMwServices = MediaWikiServices::forceGlobalInstance(
116 $this->createNoOpMock( MediaWikiServices::class )
118 // Wrap in a try-finally block to make sure the real MediaWikiServices is
119 // always put back even if something goes wrong
120 try {
121 $preferencesFactory = new DefaultPreferencesFactory( ...$params );
122 $this->assertInstanceOf(
123 DefaultPreferencesFactory::class,
124 $preferencesFactory,
125 'Created with all services, MediaWikiServices not used'
127 } finally {
128 // Put back the real MediaWikiServices
129 MediaWikiServices::forceGlobalInstance( $oldMwServices );
134 * Get a basic PreferencesFactory for testing with.
135 * @param array $options Supported options are:
136 * 'language' - A Language object, falls back to content language
137 * 'userOptionsManager' - A UserOptionsManager service, falls back to using MediaWikiServices
138 * 'userGroupManager' - A UserGroupManager service, falls back to a mock where no users
139 * have any extra groups, just `*` and `user`
140 * @return DefaultPreferencesFactory
142 protected function getPreferencesFactory( array $options = [] ) {
143 $nsInfo = $this->getDummyNamespaceInfo();
145 $services = $this->getServiceContainer();
147 // The PermissionManager should not be used for anything, its only a parameter
148 // until we figure out how to remove it without breaking the GlobalPreferences
149 // extension (GlobalPreferencesFactory extends DefaultPreferencesFactory)
150 $permissionManager = $this->createNoOpMock( PermissionManager::class );
152 $language = $options['language'] ?? $services->getContentLanguage();
153 $userOptionsManager = $options['userOptionsManager'] ?? $services->getUserOptionsManager();
155 $userGroupManager = $options['userGroupManager'] ?? false;
156 if ( !$userGroupManager ) {
157 $userGroupManager = $this->createMock( UserGroupManager::class );
158 $userGroupManager->method( 'getUserGroupMemberships' )->willReturn( [] );
159 $userGroupManager->method( 'getUserEffectiveGroups' )->willReturnCallback(
160 static function ( UserIdentity $user ) {
161 return $user->isRegistered() ? [ '*', 'user' ] : [ '*' ];
166 return new DefaultPreferencesFactory(
167 new LoggedServiceOptions( self::$serviceOptionsAccessLog,
168 DefaultPreferencesFactory::CONSTRUCTOR_OPTIONS, $this->config ),
169 $language,
170 $services->getAuthManager(),
171 $services->getLinkRenderer(),
172 $nsInfo,
173 $permissionManager,
174 $services->getLanguageConverterFactory()->getLanguageConverter( $language ),
175 $services->getLanguageNameUtils(),
176 $services->getHookContainer(),
177 $userOptionsManager,
178 $services->getLanguageConverterFactory(),
179 $services->getParserFactory(),
180 $services->getSkinFactory(),
181 $userGroupManager,
182 $services->getSignatureValidatorFactory()
187 * @covers ::getForm
188 * @covers ::searchPreferences
190 public function testGetForm() {
191 $this->setTemporaryHook( 'GetPreferences', HookContainer::NOOP );
193 $testUser = $this->createMock( User::class );
194 $prefFactory = $this->getPreferencesFactory();
195 $form = $prefFactory->getForm( $testUser, $this->context );
196 $this->assertInstanceOf( PreferencesFormOOUI::class, $form );
197 $this->assertCount( 6, $form->getPreferenceSections() );
201 * @covers ::sortSkinNames
203 public function testSortSkinNames() {
204 /** @var DefaultPreferencesFactory $factory */
205 $factory = TestingAccessWrapper::newFromObject(
206 $this->getPreferencesFactory()
208 $validSkinNames = [
209 'minerva' => 'Minerva Neue',
210 'monobook' => 'Monobook',
211 'cologne-blue' => 'Cologne Blue',
212 'vector' => 'Vector',
213 'vector-2022' => 'Vector 2022',
214 'timeless' => 'Timeless',
216 $currentSkin = 'monobook';
217 $preferredSkins = [ 'vector-2022', 'invalid-skin', 'vector' ];
219 uksort( $validSkinNames, static function ( $a, $b ) use ( $factory, $currentSkin, $preferredSkins ) {
220 return $factory->sortSkinNames( $a, $b, $currentSkin, $preferredSkins );
221 } );
223 $this->assertArrayEquals( [
224 'monobook' => 'Monobook',
225 'vector-2022' => 'Vector 2022',
226 'vector' => 'Vector',
227 'cologne-blue' => 'Cologne Blue',
228 'minerva' => 'Minerva Neue',
229 'timeless' => 'Timeless',
230 ], $validSkinNames );
234 * CSS classes for emailauthentication preference field when there's no email.
235 * @see https://phabricator.wikimedia.org/T36302
237 * @covers ::profilePreferences
238 * @dataProvider emailAuthenticationProvider
240 public function testEmailAuthentication( $user, $cssClass ) {
241 $this->overrideConfigValue( MainConfigNames::EmailAuthentication, true );
243 $prefs = $this->getPreferencesFactory()
244 ->getFormDescriptor( $user, $this->context );
245 $this->assertArrayHasKey( 'cssclass', $prefs['emailauthentication'] );
246 $this->assertEquals( $cssClass, $prefs['emailauthentication']['cssclass'] );
250 * @covers ::renderingPreferences
252 public function testShowRollbackConfIsHiddenForUsersWithoutRollbackRights() {
253 $userMock = $this->createMock( User::class );
254 $userMock->method( 'isAllowed' )->willReturnCallback(
255 static function ( $permission ) {
256 return $permission === 'editmyoptions';
260 $userOptionsManagerMock = $this->createUserOptionsManagerMock( [ 'test' => 'yes' ], true );
261 $userMock = $this->getUserMockWithSession( $userMock );
262 $prefs = $this->getPreferencesFactory( [
263 'userOptionsManager' => $userOptionsManagerMock,
264 ] )->getFormDescriptor( $userMock, $this->context );
265 $this->assertArrayNotHasKey( 'showrollbackconfirmation', $prefs );
269 * @covers ::renderingPreferences
271 public function testShowRollbackConfIsShownForUsersWithRollbackRights() {
272 $userMock = $this->createMock( User::class );
273 $userMock->method( 'isAllowed' )->willReturnCallback(
274 static function ( $permission ) {
275 return $permission === 'editmyoptions' || $permission === 'rollback';
278 $userMock = $this->getUserMockWithSession( $userMock );
280 $userOptionsManagerMock = $this->createUserOptionsManagerMock( [ 'test' => 'yes' ], true );
281 $prefs = $this->getPreferencesFactory( [
282 'userOptionsManager' => $userOptionsManagerMock,
283 ] )->getFormDescriptor( $userMock, $this->context );
284 $this->assertArrayHasKey( 'showrollbackconfirmation', $prefs );
285 $this->assertEquals(
286 'rendering/advancedrendering',
287 $prefs['showrollbackconfirmation']['section']
291 public function emailAuthenticationProvider() {
292 $userNoEmail = new User;
293 $userEmailUnauthed = new User;
294 $userEmailUnauthed->setEmail( 'noauth@example.org' );
295 $userEmailAuthed = new User;
296 $userEmailAuthed->setEmail( 'noauth@example.org' );
297 $userEmailAuthed->setEmailAuthenticationTimestamp( wfTimestamp() );
298 return [
299 [ $userNoEmail, 'mw-email-none' ],
300 [ $userEmailUnauthed, 'mw-email-not-authenticated' ],
301 [ $userEmailAuthed, 'mw-email-authenticated' ],
306 * Test that PreferencesFormPreSave hook has correct data:
307 * - user Object is passed
308 * - oldUserOptions contains previous user options (before save)
309 * - formData and User object have set up new properties
311 * @see https://phabricator.wikimedia.org/T169365
312 * @covers ::submitForm
314 public function testPreferencesFormPreSaveHookHasCorrectData() {
315 $oldOptions = [
316 'test' => 'abc',
317 'option' => 'old'
319 $newOptions = [
320 'test' => 'abc',
321 'option' => 'new'
324 $this->overrideConfigValue( MainConfigNames::HiddenPrefs, [] );
326 $form = $this->createMock( PreferencesFormOOUI::class );
328 $userMock = $this->createMock( User::class );
330 $userOptionsManagerMock = $this->createUserOptionsManagerMock( $oldOptions );
331 $expectedOptions = $newOptions;
332 $userOptionsManagerMock->expects( $this->exactly( count( $newOptions ) ) )
333 ->method( 'setOption' )
334 ->willReturnCallback( function ( $user, $oname, $val ) use ( $userMock, &$expectedOptions ) {
335 $this->assertSame( $userMock, $user );
336 $this->assertArrayHasKey( $oname, $expectedOptions );
337 $this->assertSame( $expectedOptions[$oname], $val );
338 unset( $expectedOptions[$oname] );
339 } );
340 $userMock->method( 'isAllowed' )->willReturnCallback(
341 static function ( $permission ) {
342 return $permission === 'editmyprivateinfo' || $permission === 'editmyoptions';
345 $userMock->method( 'isAllowedAny' )->willReturnCallback(
346 static function ( ...$permissions ) {
347 foreach ( $permissions as $perm ) {
348 if ( $perm === 'editmyprivateinfo' || $perm === 'editmyoptions' ) {
349 return true;
352 return false;
356 $form->method( 'getModifiedUser' )
357 ->willReturn( $userMock );
359 $form->method( 'getContext' )
360 ->willReturn( $this->context );
362 $this->setTemporaryHook( 'PreferencesFormPreSave',
363 function (
364 $formData, $form, $user, &$result, $oldUserOptions
365 ) use (
366 $newOptions, $oldOptions, $userMock
368 $this->assertSame( $userMock, $user );
369 foreach ( $newOptions as $option => $value ) {
370 $this->assertSame( $value, $formData[ $option ] );
372 foreach ( $oldOptions as $option => $value ) {
373 $this->assertSame( $value, $oldUserOptions[ $option ] );
375 $this->assertTrue( $result );
379 /** @var DefaultPreferencesFactory $factory */
380 $factory = TestingAccessWrapper::newFromObject(
381 $this->getPreferencesFactory( [ 'userOptionsManager' => $userOptionsManagerMock ] )
383 $factory->saveFormData( $newOptions, $form, [] );
387 * The rclimit preference should accept non-integer input and filter it to become an integer.
389 * @covers ::saveFormData
391 public function testIntvalFilter() {
392 // Test a string with leading zeros (i.e. not octal) and spaces.
393 $this->context->getRequest()->setVal( 'wprclimit', ' 0012 ' );
394 $user = new User;
395 $prefFactory = $this->getPreferencesFactory();
396 $form = $prefFactory->getForm( $user, $this->context );
397 $form->show();
398 $form->trySubmit();
399 $userOptionsLookup = $this->getServiceContainer()->getUserOptionsLookup();
400 $this->assertEquals( 12, $userOptionsLookup->getOption( $user, 'rclimit' ) );
404 * @covers ::profilePreferences
406 public function testVariantsSupport() {
407 $userMock = $this->createMock( User::class );
408 $userMock->method( 'isAllowed' )->willReturn( true );
409 $userMock = $this->getUserMockWithSession( $userMock );
411 $language = $this->createMock( Language::class );
412 $language->method( 'getCode' )
413 ->willReturn( 'sr' );
415 $userOptionsManagerMock = $this->createUserOptionsManagerMock(
416 [ 'LanguageCode' => 'sr', 'variant' => 'sr' ], true
419 $prefs = $this->getPreferencesFactory( [
420 'language' => $language,
421 'userOptionsManager' => $userOptionsManagerMock,
422 ] )->getFormDescriptor( $userMock, $this->context );
423 $this->assertArrayHasKey( 'default', $prefs['variant'] );
424 $this->assertEquals( 'sr', $prefs['variant']['default'] );
428 * @covers ::profilePreferences
430 public function testUserGroupMemberships() {
431 $userMock = $this->createMock( User::class );
432 $userMock->method( 'isAllowed' )->willReturn( true );
433 $userMock->method( 'isAllowedAny' )->willReturn( true );
434 $userMock->method( 'isRegistered' )->willReturn( true );
435 $userMock = $this->getUserMockWithSession( $userMock );
437 $language = $this->createMock( Language::class );
438 $language->method( 'getCode' )
439 ->willReturn( 'en' );
441 $userOptionsManagerMock = $this->createUserOptionsManagerMock( [], true );
443 $prefs = $this->getPreferencesFactory( [
444 'language' => $language,
445 'userOptionsManager' => $userOptionsManagerMock,
446 ] )->getFormDescriptor( $userMock, $this->context );
447 $this->assertArrayHasKey( 'default', $prefs['usergroups'] );
448 $this->assertEquals(
449 UserGroupMembership::getLinkHTML( 'user', $this->context ),
450 ( $prefs['usergroups']['default'] )()
455 * @coversNothing
457 public function testAllServiceOptionsUsed() {
458 $this->assertAllServiceOptionsUsed( [
459 // Only used when $wgEnotifWatchlist or $wgEnotifUserTalk is true
460 'EnotifMinorEdits',
461 // Only used when $wgEnotifWatchlist or $wgEnotifUserTalk is true
462 'EnotifRevealEditorAddress',
463 // Only used when 'fancysig' preference is enabled
464 'SignatureValidation',
465 ] );
469 * @param array $userOptions
470 * @param bool $defaultOptions
471 * @return UserOptionsManager&MockObject
473 private function createUserOptionsManagerMock( array $userOptions, bool $defaultOptions = false ) {
474 $services = $this->getServiceContainer();
475 $defaults = $services->getMainConfig()->get( MainConfigNames::DefaultUserOptions );
476 $defaults['language'] = $services->getContentLanguageCode()->toString();
477 $defaults['skin'] = Skin::normalizeKey( $services->getMainConfig()->get( MainConfigNames::DefaultSkin ) );
478 ( new HookRunner( $services->getHookContainer() ) )->onUserGetDefaultOptions( $defaults );
479 $userOptions += $defaults;
481 $mock = $this->createMock( UserOptionsManager::class );
482 $mock->method( 'getOptions' )->willReturn( $userOptions );
483 $mock->method( 'getOption' )->willReturnCallback(
484 static function ( $user, $option ) use ( $userOptions ) {
485 return $userOptions[$option] ?? null;
488 if ( $defaultOptions ) {
489 $mock->method( 'getDefaultOptions' )->willReturn( $defaults );
491 return $mock;
495 * @param MockObject $userMock
496 * @return MockObject
498 private function getUserMockWithSession( MockObject $userMock ): MockObject {
499 // We're mocking a stdClass because the Session class is final, and thus not mockable.
500 $mock = $this->getMockBuilder( stdClass::class )
501 ->addMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
502 ->getMock();
503 $mock->method( 'getSessionId' )->willReturn(
504 new SessionId( str_repeat( 'X', 32 ) )
506 $session = TestUtils::getDummySession( $mock );
507 $mockRequest = $this->getMockBuilder( FauxRequest::class )
508 ->onlyMethods( [ 'getSession' ] )
509 ->getMock();
510 $mockRequest->method( 'getSession' )->willReturn( $session );
511 $userMock->method( 'getRequest' )->willReturn( $mockRequest );
512 $userMock->method( 'getTitleKey' )->willReturn( '' );
513 return $userMock;