3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
21 namespace MediaWiki\Tests\User
;
23 use InvalidArgumentException
;
26 use MediaWiki\Block\DatabaseBlock
;
27 use MediaWiki\Config\ServiceOptions
;
28 use MediaWiki\Config\SiteConfiguration
;
29 use MediaWiki\Context\RequestContext
;
30 use MediaWiki\MainConfigNames
;
31 use MediaWiki\Permissions\SimpleAuthority
;
32 use MediaWiki\Request\WebRequest
;
33 use MediaWiki\Session\PHPSessionHandler
;
34 use MediaWiki\Session\SessionManager
;
35 use MediaWiki\User\TempUser\RealTempUserConfig
;
36 use MediaWiki\User\User
;
37 use MediaWiki\User\UserEditTracker
;
38 use MediaWiki\User\UserGroupManager
;
39 use MediaWiki\User\UserIdentity
;
40 use MediaWiki\User\UserIdentityValue
;
41 use MediaWiki\Utils\MWTimestamp
;
42 use MediaWikiIntegrationTestCase
;
43 use PHPUnit\Framework\MockObject\MockObject
;
44 use PHPUnit\Framework\MockObject\Rule\InvokedCount
;
46 use Wikimedia\Assert\PreconditionException
;
47 use Wikimedia\Rdbms\IDBAccessObject
;
50 * @covers \MediaWiki\User\UserGroupManager
53 class UserGroupManagerTest
extends MediaWikiIntegrationTestCase
{
55 private const GROUP
= 'user_group_manager_test_group';
61 * @param array $configOverrides
62 * @param UserEditTracker|null $userEditTrackerOverride
63 * @param callable|null $callback
64 * @return UserGroupManager
66 private function getManager(
67 array $configOverrides = [],
68 ?UserEditTracker
$userEditTrackerOverride = null,
69 ?callable
$callback = null
71 $services = $this->getServiceContainer();
72 return new UserGroupManager(
74 UserGroupManager
::CONSTRUCTOR_OPTIONS
,
77 MainConfigNames
::AddGroups
=> [],
78 MainConfigNames
::AutoConfirmAge
=> 0,
79 MainConfigNames
::AutoConfirmCount
=> 0,
80 MainConfigNames
::Autopromote
=> [
81 'autoconfirmed' => [ APCOND_EDITCOUNT
, 0 ]
83 MainConfigNames
::AutopromoteOnce
=> [],
84 MainConfigNames
::GroupPermissions
=> [
89 MainConfigNames
::GroupsAddToSelf
=> [],
90 MainConfigNames
::GroupsRemoveFromSelf
=> [],
91 MainConfigNames
::ImplicitGroups
=> [ '*', 'user', 'autoconfirmed' ],
92 MainConfigNames
::RemoveGroups
=> [],
93 MainConfigNames
::RevokePermissions
=> [],
95 $services->getMainConfig()
97 $services->getReadOnlyMode(),
98 $services->getDBLoadBalancerFactory(),
99 $services->getHookContainer(),
100 $userEditTrackerOverride ??
$services->getUserEditTracker(),
101 $services->getGroupPermissionsLookup(),
102 $services->getJobQueueGroup(),
104 new RealTempUserConfig( [
106 'expireAfterDays' => null,
107 'actions' => [ 'edit' ],
108 'serialProvider' => [ 'type' => 'local' ],
109 'serialMapping' => [ 'type' => 'plain-numeric' ],
110 'matchPattern' => '*Unregistered $1',
111 'genPattern' => '*Unregistered $1'
113 $callback ?
[ $callback ] : []
117 protected function setUp(): void
{
120 $this->expiryTime
= wfTimestamp( TS_MW
, time() +
100500 );
125 * Returns a callable that must be called exactly $invokedCount times.
126 * @param InvokedCount $invokedCount
127 * @return callable|MockObject
129 private function countPromise( $invokedCount ) {
130 $mockHandler = $this->getMockBuilder( \stdClass
::class )
131 ->addMethods( [ '__invoke' ] )
133 $mockHandler->expects( $invokedCount )
134 ->method( '__invoke' );
139 * @param UserGroupManager $manager
140 * @param UserIdentity $user
141 * @param string $group
142 * @param string|null $expiry
144 private function assertMembership(
145 UserGroupManager
$manager,
148 ?
string $expiry = null
150 $this->assertContains( $group, $manager->getUserGroups( $user ) );
151 $memberships = $manager->getUserGroupMemberships( $user );
152 $this->assertArrayHasKey( $group, $memberships );
153 $membership = $memberships[$group];
154 $this->assertSame( $group, $membership->getGroup() );
155 $this->assertSame( $user->getId(), $membership->getUserId() );
156 $this->assertSame( $expiry, $membership->getExpiry() );
160 * @covers \MediaWiki\User\UserGroupManager::newGroupMembershipFromRow
162 public function testNewGroupMembershipFromRow() {
163 $row = new \
stdClass();
165 $row->ug_group
= __METHOD__
;
166 $row->ug_expiry
= null;
167 $membership = $this->getManager()->newGroupMembershipFromRow( $row );
168 $this->assertSame( 1, $membership->getUserId() );
169 $this->assertSame( __METHOD__
, $membership->getGroup() );
170 $this->assertNull( $membership->getExpiry() );
174 * @covers \MediaWiki\User\UserGroupManager::newGroupMembershipFromRow
176 public function testNewGroupMembershipFromRowExpiring() {
177 $row = new \
stdClass();
179 $row->ug_group
= __METHOD__
;
180 $row->ug_expiry
= $this->expiryTime
;
181 $membership = $this->getManager()->newGroupMembershipFromRow( $row );
182 $this->assertSame( 1, $membership->getUserId() );
183 $this->assertSame( __METHOD__
, $membership->getGroup() );
184 $this->assertSame( $this->expiryTime
, $membership->getExpiry() );
188 * @covers \MediaWiki\User\UserGroupManager::getUserImplicitGroups
190 public function testGetImplicitGroups() {
191 $manager = $this->getManager();
192 $user = $this->getTestUser( 'unittesters' )->getUser();
193 $this->assertArrayEquals(
194 [ '*', 'user', 'autoconfirmed' ],
195 $manager->getUserImplicitGroups( $user )
198 $user = $this->getTestUser( [ 'bureaucrat', 'test' ] )->getUser();
199 $this->assertArrayEquals(
200 [ '*', 'user', 'autoconfirmed' ],
201 $manager->getUserImplicitGroups( $user )
205 $manager->addUserToGroup( $user, self
::GROUP
),
206 'added user to group'
208 $this->assertArrayEquals(
209 [ '*', 'user', 'autoconfirmed' ],
210 $manager->getUserImplicitGroups( $user )
213 $user = User
::newFromName( 'UTUser1' );
214 $this->assertSame( [ '*' ], $manager->getUserImplicitGroups( $user ) );
216 $manager = $this->getManager( [ MainConfigNames
::Autopromote
=> [
217 'dummy' => APCOND_EMAILCONFIRMED
219 $user = $this->getTestUser()->getUser();
220 $this->assertArrayEquals(
222 $manager->getUserImplicitGroups( $user )
224 $this->assertArrayEquals(
226 $manager->getUserEffectiveGroups( $user )
228 $user->confirmEmail();
229 $this->assertArrayEquals(
230 [ '*', 'user', 'dummy' ],
231 $manager->getUserImplicitGroups( $user, IDBAccessObject
::READ_NORMAL
, true )
233 $this->assertArrayEquals(
234 [ '*', 'user', 'dummy' ],
235 $manager->getUserEffectiveGroups( $user )
238 $user = $this->getTestUser( [ 'dummy' ] )->getUser();
239 $user->confirmEmail();
240 $this->assertArrayEquals(
241 [ '*', 'user', 'dummy' ],
242 $manager->getUserImplicitGroups( $user )
246 $user->setName( '*Unregistered 1234' );
247 $this->assertArrayEquals(
249 $manager->getUserImplicitGroups( $user )
253 public static function provideGetEffectiveGroups() {
254 yield
[ [], [ '*', 'user', 'autoconfirmed' ] ];
255 yield
[ [ 'bureaucrat', 'test' ], [ '*', 'user', 'autoconfirmed', 'bureaucrat', 'test' ] ];
256 yield
[ [ 'autoconfirmed', 'test' ], [ '*', 'user', 'autoconfirmed', 'test' ] ];
260 * @dataProvider provideGetEffectiveGroups
261 * @covers \MediaWiki\User\UserGroupManager::getUserEffectiveGroups
263 public function testGetEffectiveGroups( $userGroups, $effectiveGroups ) {
264 $manager = $this->getManager();
265 $user = $this->getTestUser( $userGroups )->getUser();
266 $this->assertArrayEquals( $effectiveGroups, $manager->getUserEffectiveGroups( $user ) );
270 * @covers \MediaWiki\User\UserGroupManager::getUserEffectiveGroups
272 public function testGetEffectiveGroupsHook() {
273 $manager = $this->getManager();
274 $user = $this->getTestUser()->getUser();
275 $this->setTemporaryHook(
276 'UserEffectiveGroups',
277 function ( UserIdentity
$hookUser, array &$groups ) use ( $user ) {
278 $this->assertTrue( $hookUser->equals( $user ) );
279 $groups[] = 'from_hook';
282 $this->assertContains( 'from_hook', $manager->getUserEffectiveGroups( $user ) );
286 * @covers \MediaWiki\User\UserGroupManager::addUserToGroup
287 * @covers \MediaWiki\User\UserGroupManager::getUserGroups
288 * @covers \MediaWiki\User\UserGroupManager::getUserGroupMemberships
290 public function testAddUserToGroup() {
291 $manager = $this->getManager();
292 $user = $this->getMutableTestUser()->getUser();
294 $result = $manager->addUserToGroup( $user, self
::GROUP
);
295 $this->assertTrue( $result );
296 $this->assertMembership( $manager, $user, self
::GROUP
);
297 $manager->clearCache( $user );
298 $this->assertMembership( $manager, $user, self
::GROUP
);
300 // try updating without allowUpdate. Should fail
301 $result = $manager->addUserToGroup( $user, self
::GROUP
, $this->expiryTime
);
302 $this->assertFalse( $result );
304 // now try updating with allowUpdate
305 $result = $manager->addUserToGroup( $user, self
::GROUP
, $this->expiryTime
, true );
306 $this->assertTrue( $result );
307 $this->assertMembership( $manager, $user, self
::GROUP
, $this->expiryTime
);
308 $manager->clearCache( $user );
309 $this->assertMembership( $manager, $user, self
::GROUP
, $this->expiryTime
);
313 * @covers \MediaWiki\User\UserGroupManager::addUserToGroup
315 public function testAddUserToGroupReadonly() {
316 $user = $this->getTestUser()->getUser();
317 $this->getServiceContainer()->getReadOnlyMode()->setReason( 'TEST' );
318 $manager = $this->getManager();
319 $this->assertFalse( $manager->addUserToGroup( $user, 'test' ) );
320 $this->assertNotContains( 'test', $manager->getUserGroups( $user ) );
324 * @covers \MediaWiki\User\UserGroupManager::addUserToGroup
326 public function testAddUserToGroupAnon() {
327 $manager = $this->getManager();
328 $anon = new UserIdentityValue( 0, 'Anon' );
329 $this->expectException( InvalidArgumentException
::class );
330 $manager->addUserToGroup( $anon, 'test' );
334 * @covers \MediaWiki\User\UserGroupManager::addUserToGroup
336 public function testAddUserToGroupHookAbort() {
337 $manager = $this->getManager();
338 $user = $this->getTestUser()->getUser();
339 $originalGroups = $manager->getUserGroups( $user );
340 $this->setTemporaryHook(
342 function ( UserIdentity
$hookUser ) use ( $user ) {
343 $this->assertTrue( $hookUser->equals( $user ) );
347 $this->assertFalse( $manager->addUserToGroup( $user, 'test_group' ) );
348 $this->assertArrayEquals( $originalGroups, $manager->getUserGroups( $user ) );
352 * @covers \MediaWiki\User\UserGroupManager::addUserToGroup
354 public function testAddUserToGroupHookModify() {
355 $manager = $this->getManager();
356 $user = $this->getTestUser()->getUser();
357 $this->setTemporaryHook(
359 function ( UserIdentity
$hookUser, &$group, &$hookExp ) use ( $user ) {
360 $this->assertTrue( $hookUser->equals( $user ) );
361 $this->assertSame( self
::GROUP
, $group );
362 $this->assertSame( $this->expiryTime
, $hookExp );
363 $group = 'from_hook';
368 $this->assertTrue( $manager->addUserToGroup( $user, self
::GROUP
, $this->expiryTime
) );
369 $this->assertContains( 'from_hook', $manager->getUserGroups( $user ) );
370 $this->assertNotContains( self
::GROUP
, $manager->getUserGroups( $user ) );
371 $this->assertNull( $manager->getUserGroupMemberships( $user )['from_hook']->getExpiry() );
375 * @covers \MediaWiki\User\UserGroupManager::addUserToMultipleGroups
377 public function testAddUserToMultipleGroups() {
378 $manager = $this->getManager();
379 $user = $this->getMutableTestUser()->getUser();
381 $manager->addUserToMultipleGroups( $user, [ self
::GROUP
, self
::GROUP
. '1' ] );
382 $this->assertMembership( $manager, $user, self
::GROUP
);
383 $this->assertMembership( $manager, $user, self
::GROUP
. '1' );
385 $anon = new UserIdentityValue( 0, 'Anon' );
386 $this->expectException( InvalidArgumentException
::class );
387 $manager->addUserToMultipleGroups( $anon, [ self
::GROUP
, self
::GROUP
. '1' ] );
391 * @covers \MediaWiki\User\UserGroupManager::getUserGroupMemberships
393 public function testGetUserGroupMembershipsForAnon() {
394 $manager = $this->getManager();
395 $anon = new UserIdentityValue( 0, 'Anon' );
397 $this->assertSame( [], $manager->getUserGroupMemberships( $anon ) );
401 * @covers \MediaWiki\User\UserGroupManager::getUserFormerGroups
403 public function testGetUserFormerGroupsForAnon() {
404 $manager = $this->getManager();
405 $anon = new UserIdentityValue( 0, 'Anon' );
407 $this->assertSame( [], $manager->getUserFormerGroups( $anon ) );
411 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
412 * @covers \MediaWiki\User\UserGroupManager::getUserFormerGroups
413 * @covers \MediaWiki\User\UserGroupManager::getUserGroups
414 * @covers \MediaWiki\User\UserGroupManager::getUserGroupMemberships
416 public function testRemoveUserFromGroup() {
417 $manager = $this->getManager();
418 $user = $this->getMutableTestUser( [ self
::GROUP
] )->getUser();
419 $this->assertMembership( $manager, $user, self
::GROUP
);
421 $result = $manager->removeUserFromGroup( $user, self
::GROUP
);
422 $this->assertTrue( $result );
423 $this->assertNotContains( self
::GROUP
,
424 $manager->getUserGroups( $user ) );
425 $this->assertArrayNotHasKey( self
::GROUP
,
426 $manager->getUserGroupMemberships( $user ) );
427 $this->assertContains( self
::GROUP
,
428 $manager->getUserFormerGroups( $user ) );
429 $manager->clearCache( $user );
430 $this->assertNotContains( self
::GROUP
,
431 $manager->getUserGroups( $user ) );
432 $this->assertArrayNotHasKey( self
::GROUP
,
433 $manager->getUserGroupMemberships( $user ) );
434 $this->assertContains( self
::GROUP
,
435 $manager->getUserFormerGroups( $user ) );
436 $this->assertContains( self
::GROUP
,
437 $manager->getUserFormerGroups( $user ) ); // From cache
441 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
443 public function testRemoveUserToGroupHookAbort() {
444 $manager = $this->getManager();
445 $user = $this->getTestUser( [ self
::GROUP
] )->getUser();
446 $originalGroups = $manager->getUserGroups( $user );
447 $this->setTemporaryHook(
449 function ( UserIdentity
$hookUser ) use ( $user ) {
450 $this->assertTrue( $hookUser->equals( $user ) );
454 $this->assertFalse( $manager->removeUserFromGroup( $user, self
::GROUP
) );
455 $this->assertArrayEquals( $originalGroups, $manager->getUserGroups( $user ) );
459 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
461 public function testRemoveUserFromGroupHookModify() {
462 $manager = $this->getManager();
463 $user = $this->getTestUser( [ self
::GROUP
, 'from_hook' ] )->getUser();
464 $this->setTemporaryHook(
466 function ( UserIdentity
$hookUser, &$group ) use ( $user ) {
467 $this->assertTrue( $hookUser->equals( $user ) );
468 $this->assertSame( self
::GROUP
, $group );
469 $group = 'from_hook';
473 $this->assertTrue( $manager->removeUserFromGroup( $user, self
::GROUP
) );
474 $this->assertNotContains( 'from_hook', $manager->getUserGroups( $user ) );
475 $this->assertContains( self
::GROUP
, $manager->getUserGroups( $user ) );
479 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
481 public function testRemoveUserFromGroupReadOnly() {
482 $user = $this->getTestUser( [ 'test' ] )->getUser();
483 $this->getServiceContainer()->getReadOnlyMode()->setReason( 'TEST' );
484 $manager = $this->getManager();
485 $this->assertFalse( $manager->removeUserFromGroup( $user, 'test' ) );
486 $this->assertContains( 'test', $manager->getUserGroups( $user ) );
490 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
492 public function testRemoveUserFromGroupAnon() {
493 $manager = $this->getManager();
494 $anon = new UserIdentityValue( 0, 'Anon' );
495 $this->expectException( InvalidArgumentException
::class );
496 $manager->removeUserFromGroup( $anon, 'test' );
500 * @covers \MediaWiki\User\UserGroupManager::removeUserFromGroup
502 public function testRemoveUserFromGroupCallback() {
503 $user = $this->getTestUser( [ 'test' ] )->getUser();
505 $callback = function ( UserIdentity
$callbackUser ) use ( $user, &$calledCount ) {
506 $this->assertTrue( $callbackUser->equals( $user ) );
509 $manager = $this->getManager( [], null, $callback );
510 $this->assertTrue( $manager->removeUserFromGroup( $user, 'test' ) );
511 $this->assertNotContains( 'test', $manager->getUserGroups( $user ) );
512 $this->assertSame( 1, $calledCount );
513 $this->assertFalse( $manager->removeUserFromGroup( $user, 'test' ) );
514 $this->assertSame( 1, $calledCount );
518 * @covers \MediaWiki\User\UserGroupManager::purgeExpired
520 public function testPurgeExpired() {
521 $manager = $this->getManager();
522 $user = $this->getTestUser()->getUser();
523 $expiryInPast = wfTimestamp( TS_MW
, time() - 100500 );
525 $manager->addUserToGroup( $user, 'expired', $expiryInPast ),
526 'can add expired group'
528 $manager->purgeExpired();
529 $this->assertNotContains( 'expired', $manager->getUserGroups( $user ) );
530 $this->assertArrayNotHasKey( 'expired', $manager->getUserGroupMemberships( $user ) );
531 $this->assertContains( 'expired', $manager->getUserFormerGroups( $user ) );
535 * @covers \MediaWiki\User\UserGroupManager::purgeExpired
537 public function testPurgeExpiredReadOnly() {
538 $this->getServiceContainer()->getReadOnlyMode()->setReason( 'TEST' );
539 $manager = $this->getManager();
540 $this->assertFalse( $manager->purgeExpired() );
544 * @covers \MediaWiki\User\UserGroupManager::listAllGroups
546 public function testGetAllGroups() {
547 $manager = $this->getManager( [
548 MainConfigNames
::GroupPermissions
=> [
549 __METHOD__
=> [ 'test' => true ],
550 'implicit' => [ 'test' => true ]
552 MainConfigNames
::RevokePermissions
=> [
553 'revoked' => [ 'test' => true ]
555 MainConfigNames
::ImplicitGroups
=> [ 'implicit' ]
557 $this->assertArrayEquals( [ __METHOD__
, 'revoked' ], $manager->listAllGroups() );
561 * @covers \MediaWiki\User\UserGroupManager::listAllImplicitGroups
563 public function testGetAllImplicitGroups() {
564 $manager = $this->getManager( [ MainConfigNames
::ImplicitGroups
=> [ __METHOD__
] ] );
565 $this->assertArrayEquals( [ __METHOD__
], $manager->listAllImplicitGroups() );
569 * @covers \MediaWiki\User\UserGroupManager::loadGroupMembershipsFromArray
571 public function testLoadGroupMembershipsFromArray() {
572 $manager = $this->getManager();
573 $user = $this->getTestUser()->getUser();
574 $row = new \
stdClass();
575 $row->ug_user
= $user->getId();
576 $row->ug_group
= 'test';
577 $row->ug_expiry
= null;
578 $manager->loadGroupMembershipsFromArray( $user, [ $row ], IDBAccessObject
::READ_NORMAL
);
579 $memberships = $manager->getUserGroupMemberships( $user );
580 $this->assertCount( 1, $memberships );
581 $this->assertArrayHasKey( 'test', $memberships );
582 $this->assertSame( $user->getId(), $memberships['test']->getUserId() );
583 $this->assertSame( 'test', $memberships['test']->getGroup() );
586 public function provideGetUserAutopromoteEmailConfirmed() {
587 $successUserMock = $this->createNoOpMock(
588 User
::class, [ 'getEmail', 'getEmailAuthenticationTimestamp', 'isTemp', 'assertWiki' ]
590 $successUserMock->method( 'assertWiki' )->willReturn( true );
591 $successUserMock->expects( $this->once() )
592 ->method( 'getEmail' )
593 ->willReturn( 'test@test.com' );
594 $successUserMock->expects( $this->once() )
595 ->method( 'getEmailAuthenticationTimestamp' )
596 ->willReturn( wfTimestampNow() );
597 yield
'Successful autopromote' => [
598 true, $successUserMock, [ 'test_autoconfirmed' ]
600 $emailAuthMock = $this->createNoOpMock( User
::class, [ 'getEmail', 'isTemp', 'assertWiki' ] );
601 $emailAuthMock->method( 'assertWiki' )->willReturn( true );
602 $emailAuthMock->expects( $this->once() )
603 ->method( 'getEmail' )
604 ->willReturn( 'test@test.com' );
605 yield
'wgEmailAuthentication is false' => [
606 false, $emailAuthMock, [ 'test_autoconfirmed' ]
608 $invalidEmailMock = $this->createNoOpMock( User
::class, [ 'getEmail', 'isTemp', 'assertWiki' ] );
609 $invalidEmailMock->method( 'assertWiki' )->willReturn( true );
611 ->expects( $this->once() )
612 ->method( 'getEmail' )
613 ->willReturn( 'INVALID!' );
614 yield
'Invalid email' => [
615 true, $invalidEmailMock, []
617 $nullTimestampMock = $this->createNoOpMock(
618 User
::class, [ 'getEmail', 'getEmailAuthenticationTimestamp', 'isTemp', 'assertWiki' ]
620 $nullTimestampMock->method( 'assertWiki' )->willReturn( true );
621 $nullTimestampMock->expects( $this->once() )
622 ->method( 'getEmail' )
623 ->willReturn( 'test@test.com' );
624 $nullTimestampMock->expects( $this->once() )
625 ->method( 'getEmailAuthenticationTimestamp' )
626 ->willReturn( null );
627 yield
'Invalid email auth timestamp' => [
628 true, $nullTimestampMock, []
633 * @dataProvider provideGetUserAutopromoteEmailConfirmed
634 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
635 * @covers \MediaWiki\User\UserGroupManager::checkCondition
636 * @param bool $emailAuthentication
638 * @param array $expected
640 public function testGetUserAutopromoteEmailConfirmed(
641 bool $emailAuthentication,
645 $manager = $this->getManager( [
646 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => [ APCOND_EMAILCONFIRMED
] ],
647 MainConfigNames
::EmailAuthentication
=> $emailAuthentication
649 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
652 public static function provideGetUserAutopromoteEditCount() {
653 yield
'Successful promote' => [
654 [ APCOND_EDITCOUNT
, 5 ], true, 10, [ 'test_autoconfirmed' ]
656 yield
'Required edit count negative' => [
657 [ APCOND_EDITCOUNT
, -1 ], true, 10, [ 'test_autoconfirmed' ]
659 yield
'No edit count, use AutoConfirmCount = 11' => [
660 [ APCOND_EDITCOUNT
], true, 10, []
662 yield
'Null edit count, use AutoConfirmCount = 11' => [
663 [ APCOND_EDITCOUNT
, null ], true, 13, [ 'test_autoconfirmed' ]
666 [ APCOND_EDITCOUNT
, 5 ], false, 100, []
668 yield
'Not enough edits' => [
669 [ APCOND_EDITCOUNT
, 100 ], true, 10, []
674 * @dataProvider provideGetUserAutopromoteEditCount
675 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
676 * @covers \MediaWiki\User\UserGroupManager::checkCondition
678 public function testGetUserAutopromoteEditCount(
680 bool $userRegistered,
684 $userEditTrackerMock = $this->createNoOpMock(
685 UserEditTracker
::class,
686 [ 'getUserEditCount' ]
688 if ( $userRegistered ) {
689 $user = $this->getTestUser()->getUser();
690 $userEditTrackerMock->method( 'getUserEditCount' )
692 ->willReturn( $userEditCount );
694 $user = User
::newFromName( 'UTUser1' );
696 $manager = $this->getManager(
698 MainConfigNames
::AutoConfirmCount
=> 11,
699 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => $requiredCond ]
703 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
706 public static function provideGetUserAutopromoteAge() {
707 yield
'Successful promote' => [
708 [ APCOND_AGE
, 1000 ],
709 MWTimestamp
::convert( TS_MW
, time() - 1000000 ),
710 [ 'test_autoconfirmed' ]
712 yield
'Not old enough' => [
713 [ APCOND_AGE
, 10000000 ], MWTimestamp
::now(), []
715 yield
'Not old enough, using AutoConfirmAge via unset' => [
716 [ APCOND_AGE
], MWTimestamp
::now(), []
718 yield
'Not old enough, using AutoConfirmAge via null' => [
719 [ APCOND_AGE
, null ], MWTimestamp
::now(), []
724 * @dataProvider provideGetUserAutopromoteAge
725 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
726 * @covers \MediaWiki\User\UserGroupManager::checkCondition
727 * @param array $requiredCondition
728 * @param string $registrationTs
729 * @param array $expected
731 public function testGetUserAutopromoteAge(
732 array $requiredCondition,
733 string $registrationTs,
736 $manager = $this->getManager( [
737 MainConfigNames
::AutoConfirmAge
=> 10000000,
738 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => $requiredCondition ]
740 $user = $this->createNoOpMock( User
::class, [ 'getRegistration', 'isTemp', 'assertWiki' ] );
741 $user->method( 'assertWiki' )->willReturn( true );
742 $user->method( 'getRegistration' )
743 ->willReturn( $registrationTs );
744 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
747 public static function provideGetUserAutopromoteEditAge() {
748 yield
'Successful promote' => [
749 [ APCOND_AGE_FROM_EDIT
, 1000 ],
750 MWTimestamp
::convert( TS_MW
, time() - 1000000 ),
751 [ 'test_autoconfirmed' ]
753 yield
'Not old enough' => [
754 [ APCOND_AGE_FROM_EDIT
, 10000000 ], MWTimestamp
::now(), []
759 * @dataProvider provideGetUserAutopromoteEditAge
760 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
761 * @covers \MediaWiki\User\UserGroupManager::checkCondition
762 * @param array $requiredCondition
763 * @param string $firstEditTs
764 * @param array $expected
766 public function testGetUserAutopromoteEditAge(
767 array $requiredCondition,
771 $user = $this->getTestUser()->getUser();
772 $mockUserEditTracker = $this->createNoOpMock( UserEditTracker
::class, [ 'getFirstEditTimestamp' ] );
773 $mockUserEditTracker->expects( $this->once() )
774 ->method( 'getFirstEditTimestamp' )
776 ->willReturn( $firstEditTs );
777 $manager = $this->getManager( [
778 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => $requiredCondition ]
779 ], $mockUserEditTracker );
780 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
783 public static function provideGetUserAutopromoteGroups() {
784 yield
'Successful promote' => [
785 [ 'group1', 'group2' ], [ 'group1', 'group2' ], [ 'test_autoconfirmed' ]
787 yield
'Not enough groups to promote' => [
788 [ 'group1', 'group2' ], [ 'group1' ], []
793 * @dataProvider provideGetUserAutopromoteGroups
794 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
795 * @covers \MediaWiki\User\UserGroupManager::checkCondition
797 public function testGetUserAutopromoteGroups(
798 array $requiredGroups,
802 $user = $this->getTestUser( $userGroups )->getUser();
803 $manager = $this->getManager( [
804 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => array_merge( [ APCOND_INGROUPS
], $requiredGroups ) ]
806 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
809 public static function provideGetUserAutopromoteIP() {
810 yield
'Individual ip, success' => [
811 [ APCOND_ISIP
, '123.123.123.123' ], '123.123.123.123', [ 'test_autoconfirmed' ]
813 yield
'Individual ip, failed' => [
814 [ APCOND_ISIP
, '123.123.123.123' ], '124.124.124.124', []
816 yield
'Range ip, success' => [
817 [ APCOND_IPINRANGE
, '123.123.123.1/24' ], '123.123.123.123', [ 'test_autoconfirmed' ]
819 yield
'Range ip, failed' => [
820 [ APCOND_IPINRANGE
, '123.123.123.1/24' ], '124.124.124.124', []
825 * @dataProvider provideGetUserAutopromoteIP
826 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
827 * @covers \MediaWiki\User\UserGroupManager::checkCondition
828 * @param array $condition
829 * @param string $userIp
830 * @param array $expected
832 public function testGetUserAutopromoteIP(
837 $manager = $this->getManager( [
838 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => $condition ]
840 $requestMock = $this->createNoOpMock( WebRequest
::class, [ 'getIP' ] );
841 $requestMock->expects( $this->once() )
843 ->willReturn( $userIp );
844 $user = $this->createNoOpMock( User
::class, [ 'getRequest', 'isTemp', 'assertWiki' ] );
845 $user->method( 'assertWiki' )->willReturn( true );
846 $user->expects( $this->once() )
847 ->method( 'getRequest' )
848 ->willReturn( $requestMock );
849 $this->assertArrayEquals( $expected, $manager->getUserAutopromoteGroups( $user ) );
853 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
855 public function testGetUserAutopromoteGroupsHook() {
856 $manager = $this->getManager( [ MainConfigNames
::Autopromote
=> [] ] );
857 $user = $this->getTestUser()->getUser();
858 $this->setTemporaryHook(
859 'GetAutoPromoteGroups',
860 function ( User
$hookUser, array &$promote ) use ( $user ){
861 $this->assertTrue( $user->equals( $hookUser ) );
862 $this->assertSame( [], $promote );
863 $promote[] = 'from_hook';
866 $this->assertArrayEquals( [ 'from_hook' ], $manager->getUserAutopromoteGroups( $user ) );
870 * @covers \MediaWiki\User\UserGroupManager::checkCondition
871 * @covers \MediaWiki\User\UserGroupManager::recCheckCondition
873 public function testGetUserAutopromoteComplexCondition() {
874 $manager = $this->getManager( [
875 MainConfigNames
::Autopromote
=> [
876 'test_autoconfirmed' => [ '&',
877 [ APCOND_INGROUPS
, 'group1' ],
878 [ '!', [ APCOND_INGROUPS
, 'group2' ] ],
879 [ '^', [ APCOND_INGROUPS
, 'group3' ], [ APCOND_INGROUPS
, 'group4' ] ],
880 [ '|', [ APCOND_INGROUPS
, 'group5' ], [ APCOND_INGROUPS
, 'group6' ] ]
884 $this->assertSame( [], $manager->getUserAutopromoteGroups(
885 $this->getTestUser( [ 'group1' ] )->getUser() )
887 $this->assertSame( [], $manager->getUserAutopromoteGroups(
888 $this->getTestUser( [ 'group1', 'group2' ] )->getUser() )
890 $this->assertSame( [], $manager->getUserAutopromoteGroups(
891 $this->getTestUser( [ 'group1', 'group3', 'group4' ] )->getUser() )
893 $this->assertSame( [], $manager->getUserAutopromoteGroups(
894 $this->getTestUser( [ 'group1', 'group3' ] )->getUser() )
896 $this->assertArrayEquals(
897 [ 'test_autoconfirmed' ],
898 $manager->getUserAutopromoteGroups( $this->getTestUser( [ 'group1', 'group3', 'group5' ] )->getUser() )
903 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
904 * @covers \MediaWiki\User\UserGroupManager::checkCondition
906 public function testGetUserAutopromoteBot() {
907 $manager = $this->getManager( [
908 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => [ APCOND_ISBOT
] ]
910 $notBot = $this->getTestUser()->getUser();
911 $this->assertSame( [], $manager->getUserAutopromoteGroups( $notBot ) );
912 $bot = $this->getTestUser( [ 'bot' ] )->getUser();
913 $this->assertArrayEquals( [ 'test_autoconfirmed' ],
914 $manager->getUserAutopromoteGroups( $bot ) );
918 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
919 * @covers \MediaWiki\User\UserGroupManager::checkCondition
921 public function testGetUserAutopromoteBlocked() {
922 $manager = $this->getManager( [
923 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => [ APCOND_BLOCKED
] ]
925 $nonBlockedUser = $this->getTestUser()->getUser();
926 $this->assertSame( [], $manager->getUserAutopromoteGroups( $nonBlockedUser ) );
927 $blockedUser = $this->getTestUser( [ 'blocked' ] )->getUser();
928 $block = new DatabaseBlock();
929 $block->setTarget( $blockedUser );
930 $block->setBlocker( $this->getTestSysop()->getUser() );
931 $block->isSitewide( true );
932 $this->getServiceContainer()->getDatabaseBlockStore()->insertBlock( $block );
933 $this->assertArrayEquals( [ 'test_autoconfirmed' ],
934 $manager->getUserAutopromoteGroups( $blockedUser ) );
938 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
939 * @covers \MediaWiki\User\UserGroupManager::checkCondition
941 public function testGetUserAutopromoteBlockedDoesNotRecurse() {
942 // Make sure session handling is started
943 if ( !PHPSessionHandler
::isInstalled() ) {
944 PHPSessionHandler
::install(
945 SessionManager
::singleton()
948 $oldSessionId = session_id();
950 $context = RequestContext
::getMain();
951 // Variables are unused but needed to reproduce the failure
952 $oInfo = $context->exportSession();
954 $user = User
::newFromName( 'UnitTestContextUser' );
955 $user->addToDatabase();
958 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
959 'userId' => $user->getId(),
962 'USER-AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0'
965 $this->overrideConfigValue(
966 MainConfigNames
::Autopromote
,
967 [ 'test_autoconfirmed' => [ '&', APCOND_BLOCKED
] ]
969 // Variables are unused but needed to reproduce the failure
970 $sc = RequestContext
::importScopedSession( $sinfo ); // load new context
971 $info = $context->exportSession();
973 $this->assertNull( $user->getBlock() );
977 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
978 * @covers \MediaWiki\User\UserGroupManager::checkCondition
980 public function testGetUserAutopromoteBlockedDoesNotRecurseWithHook() {
981 $this->overrideConfigValue(
982 MainConfigNames
::Autopromote
,
983 [ 'test_autoconfirmed' => [ '&', APCOND_BLOCKED
] ]
986 // Make sure session handling is started
987 if ( !PHPSessionHandler
::isInstalled() ) {
988 PHPSessionHandler
::install(
989 SessionManager
::singleton()
992 $permissionManager = $this->getServiceContainer()->getPermissionManager();
993 $permissionManager->invalidateUsersRightsCache();
995 $oldSessionId = session_id();
997 $context = RequestContext
::getMain();
998 // Variables are unused but needed to reproduce the failure
999 $oInfo = $context->exportSession();
1001 $user = User
::newFromName( 'UnitTestContextUser' );
1002 $user->addToDatabase();
1005 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
1006 'userId' => $user->getId(),
1007 'ip' => '192.0.2.0',
1009 'USER-AGENT' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:18.0) Gecko/20100101 Firefox/18.0'
1013 $onGetUserBlockCalled = false;
1014 $this->setTemporaryHook(
1016 static function ( $user, $ip, &$block ) use ( $permissionManager, &$onGetUserBlockCalled ) {
1017 $onGetUserBlockCalled = true;
1020 if ( $permissionManager->userHasAnyRight( $user, 'ipblock-exempt', 'globalblock-exempt' ) ) {
1023 } catch ( LogicException
$e ) {
1024 // We expect an uncaught LogicException from UserGroupManager::checkCondition here
1025 // otherwise there's something else wrong!
1026 if ( !str_starts_with( $e->getMessage(), "Unexpected recursion!" ) ) {
1035 // Variables are unused but needed to reproduce the failure
1036 $sc = RequestContext
::importScopedSession( $sinfo ); // load new context
1037 $info = $context->exportSession();
1039 $this->assertNull( $user->getBlock() );
1042 $onGetUserBlockCalled,
1043 'Check that HookRunner::onGetUserBlock was called'
1048 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
1050 public function testGetUserAutopromoteInvalid() {
1051 $manager = $this->getManager( [
1052 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => [ 999 ] ]
1054 $user = $this->getTestUser()->getUser();
1055 $this->expectException( InvalidArgumentException
::class );
1056 $manager->getUserAutopromoteGroups( $user );
1060 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteGroups
1061 * @covers \MediaWiki\User\UserGroupManager::checkCondition
1063 public function testGetUserAutopromoteConditionHook() {
1064 $user = $this->getTestUser()->getUser();
1065 $this->setTemporaryHook(
1066 'AutopromoteCondition',
1067 function ( $type, array $arg, User
$hookUser, &$result ) use ( $user ){
1068 $this->assertTrue( $user->equals( $hookUser ) );
1069 $this->assertSame( 999, $type );
1070 $this->assertSame( 'ARGUMENT', $arg[0] );
1074 $manager = $this->getManager( [
1075 MainConfigNames
::Autopromote
=> [ 'test_autoconfirmed' => [ 999, 'ARGUMENT' ] ]
1077 $this->assertArrayEquals( [ 'test_autoconfirmed' ], $manager->getUserAutopromoteGroups( $user ) );
1080 public static function provideGetUserAutopromoteOnce() {
1081 yield
'Events are not matching' => [
1082 [ 'NOT_EVENT' => [ 'autopromoteonce' => [ APCOND_EDITCOUNT
, 0 ] ] ], [], [], []
1084 yield
'Empty config' => [
1085 [ 'EVENT' => [] ], [], [], []
1087 yield
'Simple case, not user groups, not former groups' => [
1088 [ 'EVENT' => [ 'autopromoteonce' => [ APCOND_EDITCOUNT
, 0 ] ] ], [], [], [ 'autopromoteonce' ]
1090 yield
'User already in the group' => [
1091 [ 'EVENT' => [ 'autopromoteonce' => [ APCOND_EDITCOUNT
, 0 ] ] ], [], [ 'autopromoteonce' ], []
1093 yield
'User used to be in the group' => [
1094 [ 'EVENT' => [ 'autopromoteonce' => [ APCOND_EDITCOUNT
, 0 ] ] ], [ 'autopromoteonce' ], [], []
1099 * @dataProvider provideGetUserAutopromoteOnce
1100 * @covers \MediaWiki\User\UserGroupManager::getUserAutopromoteOnceGroups
1101 * @param array $config
1102 * @param array $formerGroups
1103 * @param array $userGroups
1104 * @param array $expected
1106 public function testGetUserAutopromoteOnce(
1108 array $formerGroups,
1112 $manager = $this->getManager( [ MainConfigNames
::AutopromoteOnce
=> $config ] );
1113 $user = $this->getTestUser()->getUser();
1114 $manager->addUserToMultipleGroups( $user, $userGroups );
1115 foreach ( $formerGroups as $formerGroup ) {
1116 $manager->addUserToGroup( $user, $formerGroup );
1117 $manager->removeUserFromGroup( $user, $formerGroup );
1119 $this->assertArrayEquals( $userGroups, $manager->getUserGroups( $user ),
1120 false, 'user groups are correct ' );
1121 $this->assertArrayEquals( $formerGroups, $manager->getUserFormerGroups( $user ),
1122 false, 'user former groups are correct ' );
1123 $this->assertArrayEquals(
1125 $manager->getUserAutopromoteOnceGroups( $user, 'EVENT' )
1130 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1132 public function testAddUserToAutopromoteOnceGroupsForeignDomain() {
1133 $siteConfig = new SiteConfiguration();
1134 $siteConfig->wikis
= [ 'TEST_DOMAIN' ];
1135 $this->setMwGlobals( 'wgConf', $siteConfig );
1137 $this->overrideConfigValue( MainConfigNames
::LocalDatabases
, [ 'TEST_DOMAIN' ] );
1139 $manager = $this->getServiceContainer()
1140 ->getUserGroupManagerFactory()
1141 ->getUserGroupManager( 'TEST_DOMAIN' );
1142 $user = $this->getTestUser()->getUser();
1143 $this->expectException( PreconditionException
::class );
1144 $this->assertSame( [], $manager->addUserToAutopromoteOnceGroups( $user, 'TEST' ) );
1148 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1150 public function testAddUserToAutopromoteOnceGroupsAnon() {
1151 $manager = $this->getManager();
1152 $anon = new UserIdentityValue( 0, 'TEST' );
1153 $this->assertSame( [], $manager->addUserToAutopromoteOnceGroups( $anon, 'TEST' ) );
1157 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1159 public function testAddUserToAutopromoteOnceGroupsReadOnly() {
1160 $manager = $this->getManager();
1161 $user = $this->getTestUser()->getUser();
1162 $this->getServiceContainer()->getReadOnlyMode()->setReason( 'TEST' );
1163 $this->assertSame( [], $manager->addUserToAutopromoteOnceGroups( $user, 'TEST' ) );
1167 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1169 public function testAddUserToAutopromoteOnceGroupsNoGroups() {
1170 $manager = $this->getManager();
1171 $user = $this->getTestUser()->getUser();
1172 $this->assertSame( [], $manager->addUserToAutopromoteOnceGroups( $user, 'TEST' ) );
1176 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1178 public function testAddUserToAutopromoteOnceGroupsSuccess() {
1179 $user = $this->getTestUser()->getUser();
1180 $manager = $this->getManager( [
1181 MainConfigNames
::AutopromoteOnce
=> [ 'EVENT' => [ 'autopromoteonce' => [ APCOND_EDITCOUNT
, 0 ] ] ]
1183 $this->assertNotContains( 'autopromoteonce', $manager->getUserGroups( $user ) );
1184 $hookCalled = false;
1185 $this->setTemporaryHook(
1186 'UserGroupsChanged',
1187 function ( User
$hookUser, array $added, array $removed ) use ( $user, &$hookCalled ) {
1188 $this->assertTrue( $user->equals( $hookUser ) );
1189 $this->assertArrayEquals( [ 'autopromoteonce' ], $added );
1190 $this->assertSame( [], $removed );
1194 $manager->addUserToAutopromoteOnceGroups( $user, 'EVENT' );
1195 $this->assertContains( 'autopromoteonce', $manager->getUserGroups( $user ) );
1196 $this->assertTrue( $hookCalled );
1197 $this->newSelectQueryBuilder()
1198 ->select( [ 'log_type', 'log_action', 'log_params' ] )
1200 ->where( [ 'log_type' => 'rights' ] )
1201 ->assertResultSet( [ [ 'rights',
1203 LogEntryBase
::makeParamBlob( [
1204 '4::oldgroups' => [],
1205 '5::newgroups' => [ 'autopromoteonce' ],
1211 * @covers \MediaWiki\User\UserGroupManager::addUserToAutopromoteOnceGroups
1212 * @dataProvider provideAutopromoteOnceGroupsRecentChanges
1214 public function testAddUserToAutopromoteOnceGroupsRecentChanges( array $autoPromoteOnceGroups ) {
1215 $user = $this->getTestUser()->getUser();
1217 // Setup one-shot autopromote conditions for the groups we would like to trigger autopromotion into
1218 $autoPromoteOnce = [];
1219 foreach ( $autoPromoteOnceGroups as $groupName ) {
1220 $autoPromoteOnce[$groupName] = [ APCOND_EDITCOUNT
, 0 ];
1223 $rcExcludedGroups = [ 'autopromoteonce-excluded' ];
1225 $manager = $this->getManager( [
1226 MainConfigNames
::AutopromoteOnce
=> [
1227 'EVENT' => $autoPromoteOnce
1229 MainConfigNames
::AutopromoteOnceLogInRC
=> true,
1230 MainConfigNames
::AutopromoteOnceRCExcludedGroups
=> $rcExcludedGroups
1233 // Add the test user to an unrelated group to verify autopromote RC exclusion ignores these.
1234 $manager->addUserToGroup( $user, 'sysop' );
1235 $preAutopromoteGroups = $manager->getUserGroups( $user );
1237 foreach ( $autoPromoteOnceGroups as $groupName ) {
1238 $this->assertNotContains( $groupName, $manager->getUserGroups( $user ) );
1241 $manager->addUserToAutopromoteOnceGroups( $user, 'EVENT' );
1243 foreach ( $autoPromoteOnceGroups as $groupName ) {
1244 $this->assertContains( $groupName, $manager->getUserGroups( $user ) );
1247 $logQueryBuilder = $this->newSelectQueryBuilder()
1248 ->select( [ 'log_type', 'log_action', 'log_params' ] )
1250 ->where( [ 'log_type' => 'rights' ] );
1252 $logQueryBuilder->assertRowValue( [ 'rights',
1254 LogEntryBase
::makeParamBlob( [
1255 '4::oldgroups' => $preAutopromoteGroups,
1256 '5::newgroups' => $manager->getUserGroups( $user ),
1260 $logId = $logQueryBuilder
1265 if ( !array_diff( $autoPromoteOnceGroups, $rcExcludedGroups ) ) {
1266 $this->newSelectQueryBuilder()
1267 ->select( [ 'rc_logid' ] )
1268 ->from( 'recentchanges' )
1269 ->where( [ 'rc_type' => RC_LOG
] )
1270 ->assertEmptyResult();
1274 $this->newSelectQueryBuilder()
1275 ->select( [ 'rc_logid' ] )
1276 ->from( 'recentchanges' )
1277 ->where( [ 'rc_type' => RC_LOG
] )
1278 ->assertFieldValue( $logId );
1281 public static function provideAutopromoteOnceGroupsRecentChanges(): iterable
{
1282 yield
'autopromotion into excluded group' => [ [ 'autopromoteonce-excluded' ] ];
1283 yield
'autopromotion into excluded and non-excluded group' => [ [ 'autopromoteonce', 'autopromoteonce-excluded' ] ];
1284 yield
'autopromotion into non-excluded group' => [ [ 'autopromoteonce' ] ];
1287 private const CHANGEABLE_GROUPS_TEST_CONFIG
= [
1288 MainConfigNames
::GroupPermissions
=> [],
1289 MainConfigNames
::AddGroups
=> [
1290 'sysop' => [ 'rollback' ],
1291 'bureaucrat' => [ 'sysop', 'bureaucrat' ],
1293 MainConfigNames
::RemoveGroups
=> [
1294 'sysop' => [ 'rollback' ],
1295 'bureaucrat' => [ 'sysop' ],
1297 MainConfigNames
::GroupsAddToSelf
=> [
1298 'sysop' => [ 'flood' ],
1300 MainConfigNames
::GroupsRemoveFromSelf
=> [
1301 'flood' => [ 'flood' ],
1305 private function assertGroupsEquals( array $expected, array $actual ) {
1306 // assertArrayEquals can compare without requiring the same order,
1307 // but the elements of an array are still required to be in the same order,
1308 // so just compare each element
1309 $this->assertArrayEquals( $expected['add'], $actual['add'], 'Add must match' );
1310 $this->assertArrayEquals( $expected['remove'], $actual['remove'], 'Remove must match' );
1311 $this->assertArrayEquals( $expected['add-self'], $actual['add-self'], 'Add-self must match' );
1312 $this->assertArrayEquals( $expected['remove-self'], $actual['remove-self'], 'Remove-self must match' );
1316 * @covers \MediaWiki\User\UserGroupManager::getGroupsChangeableBy
1318 public function testChangeableGroups() {
1319 $manager = $this->getManager( self
::CHANGEABLE_GROUPS_TEST_CONFIG
);
1320 $allGroups = $manager->listAllGroups();
1322 $user = $this->getTestUser()->getUser();
1323 $changeableGroups = $manager->getGroupsChangeableBy( new SimpleAuthority( $user, [ 'userrights' ] ) );
1324 $this->assertGroupsEquals(
1326 'add' => $allGroups,
1327 'remove' => $allGroups,
1329 'remove-self' => [],
1334 $user = $this->getTestUser( [ 'bureaucrat', 'sysop' ] )->getUser();
1335 $changeableGroups = $manager->getGroupsChangeableBy( new SimpleAuthority( $user, [] ) );
1336 $this->assertGroupsEquals(
1338 'add' => [ 'sysop', 'bureaucrat', 'rollback' ],
1339 'remove' => [ 'sysop', 'rollback' ],
1340 'add-self' => [ 'flood' ],
1341 'remove-self' => [],
1346 $user = $this->getTestUser( [ 'flood' ] )->getUser();
1347 $changeableGroups = $manager->getGroupsChangeableBy( new SimpleAuthority( $user, [] ) );
1348 $this->assertGroupsEquals(
1353 'remove-self' => [ 'flood' ],
1359 public static function provideChangeableByGroup() {
1360 yield
'sysop' => [ 'sysop', [
1361 'add' => [ 'rollback' ],
1362 'remove' => [ 'rollback' ],
1363 'add-self' => [ 'flood' ],
1364 'remove-self' => [],
1366 yield
'flood' => [ 'flood', [
1370 'remove-self' => [ 'flood' ],
1375 * @dataProvider provideChangeableByGroup
1376 * @covers \MediaWiki\User\UserGroupManager::getGroupsChangeableByGroup
1377 * @param string $group
1378 * @param array $expected
1380 public function testChangeableByGroup( string $group, array $expected ) {
1381 $manager = $this->getManager( self
::CHANGEABLE_GROUPS_TEST_CONFIG
);
1382 $this->assertGroupsEquals( $expected, $manager->getGroupsChangeableByGroup( $group ) );
1386 * @covers \MediaWiki\User\UserGroupManager::getUserPrivilegedGroups()
1388 public function testGetUserPrivilegedGroups() {
1389 $this->overrideConfigValue( MainConfigNames
::PrivilegedGroups
, [ 'sysop', 'interface-admin', 'bar', 'baz' ] );
1390 $makeHook = function ( $invocationCount, User
$userToMatch, array $groupsToAdd ) {
1391 return function ( $u, &$groups ) use ( $userToMatch, $invocationCount, $groupsToAdd ) {
1393 $this->assertTrue( $userToMatch->equals( $u ) );
1394 $groups = array_merge( $groups, $groupsToAdd );
1398 $manager = $this->getManager();
1401 $user->setName( '*Unregistered 1234' );
1403 $this->assertArrayEquals(
1405 $manager->getUserPrivilegedGroups( $user )
1408 $user = $this->getTestUser( [ 'sysop', 'bot', 'interface-admin' ] )->getUser();
1410 $this->setTemporaryHook( 'UserPrivilegedGroups',
1411 $makeHook( $this->countPromise( $this->once() ), $user, [ 'foo' ] ) );
1412 $this->setTemporaryHook( 'UserEffectiveGroups',
1413 $makeHook( $this->countPromise( $this->once() ), $user, [ 'bar', 'boom' ] ) );
1414 $this->assertArrayEquals(
1415 [ 'sysop', 'interface-admin', 'foo', 'bar' ],
1416 $manager->getUserPrivilegedGroups( $user )
1418 $this->assertArrayEquals(
1419 [ 'sysop', 'interface-admin', 'foo', 'bar' ],
1420 $manager->getUserPrivilegedGroups( $user )
1423 $this->setTemporaryHook( 'UserPrivilegedGroups',
1424 $makeHook( $this->countPromise( $this->once() ), $user, [ 'baz' ] ) );
1425 $this->setTemporaryHook( 'UserEffectiveGroups',
1426 $makeHook( $this->countPromise( $this->once() ), $user, [ 'baz' ] ) );
1427 $this->assertArrayEquals(
1428 [ 'sysop', 'interface-admin', 'foo', 'bar' ],
1429 $manager->getUserPrivilegedGroups( $user )
1431 $this->assertArrayEquals(
1432 [ 'sysop', 'interface-admin', 'baz' ],
1433 $manager->getUserPrivilegedGroups( $user, IDBAccessObject
::READ_NORMAL
, true )
1435 $this->assertArrayEquals(
1436 [ 'sysop', 'interface-admin', 'baz' ],
1437 $manager->getUserPrivilegedGroups( $user )
1440 $this->setTemporaryHook( 'UserPrivilegedGroups', static function () {
1442 $this->setTemporaryHook( 'UserEffectiveGroups', static function () {
1444 $user = $this->getTestUser( [] )->getUser();
1445 $this->assertArrayEquals(
1447 $manager->getUserPrivilegedGroups( $user, IDBAccessObject
::READ_NORMAL
, true )