Merge "mediawiki.content.json: Remove file and author annotations"
[mediawiki.git] / tests / phpunit / includes / user / UserGroupManagerTest.php
blob2cfd3c89ae6ff3b8844496295bf8a3dc9e8933f6
1 <?php
2 /**
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
18 * @file
21 namespace MediaWiki\Tests\User;
23 use InvalidArgumentException;
24 use LogEntryBase;
25 use LogicException;
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;
45 use TestLogger;
46 use Wikimedia\Assert\PreconditionException;
47 use Wikimedia\Rdbms\IDBAccessObject;
49 /**
50 * @covers \MediaWiki\User\UserGroupManager
51 * @group Database
53 class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
55 private const GROUP = 'user_group_manager_test_group';
57 /** @var string */
58 private $expiryTime;
60 /**
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
70 ): UserGroupManager {
71 $services = $this->getServiceContainer();
72 return new UserGroupManager(
73 new ServiceOptions(
74 UserGroupManager::CONSTRUCTOR_OPTIONS,
75 $configOverrides,
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 => [
85 self::GROUP => [
86 'runtest' => true,
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(),
103 new TestLogger(),
104 new RealTempUserConfig( [
105 'enabled' => true,
106 'expireAfterDays' => null,
107 'actions' => [ 'edit' ],
108 'serialProvider' => [ 'type' => 'local' ],
109 'serialMapping' => [ 'type' => 'plain-numeric' ],
110 'matchPattern' => '*Unregistered $1',
111 'genPattern' => '*Unregistered $1'
112 ] ),
113 $callback ? [ $callback ] : []
117 protected function setUp(): void {
118 parent::setUp();
120 $this->expiryTime = wfTimestamp( TS_MW, time() + 100500 );
121 $this->clearHooks();
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' ] )
132 ->getMock();
133 $mockHandler->expects( $invokedCount )
134 ->method( '__invoke' );
135 return $mockHandler;
139 * @param UserGroupManager $manager
140 * @param UserIdentity $user
141 * @param string $group
142 * @param string|null $expiry
144 private function assertMembership(
145 UserGroupManager $manager,
146 UserIdentity $user,
147 string $group,
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();
164 $row->ug_user = '1';
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();
178 $row->ug_user = '1';
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 )
204 $this->assertTrue(
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
218 ] ] );
219 $user = $this->getTestUser()->getUser();
220 $this->assertArrayEquals(
221 [ '*', 'user' ],
222 $manager->getUserImplicitGroups( $user )
224 $this->assertArrayEquals(
225 [ '*', 'user' ],
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 )
245 $user = new User;
246 $user->setName( '*Unregistered 1234' );
247 $this->assertArrayEquals(
248 [ '*', 'temp' ],
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(
341 'UserAddGroup',
342 function ( UserIdentity $hookUser ) use ( $user ) {
343 $this->assertTrue( $hookUser->equals( $user ) );
344 return false;
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(
358 'UserAddGroup',
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';
364 $hookExp = null;
365 return true;
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(
448 'UserRemoveGroup',
449 function ( UserIdentity $hookUser ) use ( $user ) {
450 $this->assertTrue( $hookUser->equals( $user ) );
451 return false;
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(
465 'UserRemoveGroup',
466 function ( UserIdentity $hookUser, &$group ) use ( $user ) {
467 $this->assertTrue( $hookUser->equals( $user ) );
468 $this->assertSame( self::GROUP, $group );
469 $group = 'from_hook';
470 return true;
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();
504 $calledCount = 0;
505 $callback = function ( UserIdentity $callbackUser ) use ( $user, &$calledCount ) {
506 $this->assertTrue( $callbackUser->equals( $user ) );
507 $calledCount++;
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 );
524 $this->assertTrue(
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' ]
556 ] );
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 );
610 $invalidEmailMock
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
637 * @param User $user
638 * @param array $expected
640 public function testGetUserAutopromoteEmailConfirmed(
641 bool $emailAuthentication,
642 User $user,
643 array $expected
645 $manager = $this->getManager( [
646 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => [ APCOND_EMAILCONFIRMED ] ],
647 MainConfigNames::EmailAuthentication => $emailAuthentication
648 ] );
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' ]
665 yield 'Anon' => [
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(
679 array $requiredCond,
680 bool $userRegistered,
681 int $userEditCount,
682 array $expected
684 $userEditTrackerMock = $this->createNoOpMock(
685 UserEditTracker::class,
686 [ 'getUserEditCount' ]
688 if ( $userRegistered ) {
689 $user = $this->getTestUser()->getUser();
690 $userEditTrackerMock->method( 'getUserEditCount' )
691 ->with( $user )
692 ->willReturn( $userEditCount );
693 } else {
694 $user = User::newFromName( 'UTUser1' );
696 $manager = $this->getManager(
698 MainConfigNames::AutoConfirmCount => 11,
699 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => $requiredCond ]
701 $userEditTrackerMock
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,
734 array $expected
736 $manager = $this->getManager( [
737 MainConfigNames::AutoConfirmAge => 10000000,
738 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => $requiredCondition ]
739 ] );
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,
768 string $firstEditTs,
769 array $expected
771 $user = $this->getTestUser()->getUser();
772 $mockUserEditTracker = $this->createNoOpMock( UserEditTracker::class, [ 'getFirstEditTimestamp' ] );
773 $mockUserEditTracker->expects( $this->once() )
774 ->method( 'getFirstEditTimestamp' )
775 ->with( $user )
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,
799 array $userGroups,
800 array $expected
802 $user = $this->getTestUser( $userGroups )->getUser();
803 $manager = $this->getManager( [
804 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => array_merge( [ APCOND_INGROUPS ], $requiredGroups ) ]
805 ] );
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(
833 array $condition,
834 string $userIp,
835 array $expected
837 $manager = $this->getManager( [
838 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => $condition ]
839 ] );
840 $requestMock = $this->createNoOpMock( WebRequest::class, [ 'getIP' ] );
841 $requestMock->expects( $this->once() )
842 ->method( 'getIP' )
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' ] ]
883 ] );
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 ] ]
909 ] );
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 ] ]
924 ] );
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();
957 $sinfo = [
958 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
959 'userId' => $user->getId(),
960 'ip' => '192.0.2.0',
961 'headers' => [
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();
1004 $sinfo = [
1005 'sessionId' => 'd612ee607c87e749ef14da4983a702cd',
1006 'userId' => $user->getId(),
1007 'ip' => '192.0.2.0',
1008 'headers' => [
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(
1015 'GetUserBlock',
1016 static function ( $user, $ip, &$block ) use ( $permissionManager, &$onGetUserBlockCalled ) {
1017 $onGetUserBlockCalled = true;
1019 try {
1020 if ( $permissionManager->userHasAnyRight( $user, 'ipblock-exempt', 'globalblock-exempt' ) ) {
1021 return true;
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!" ) ) {
1027 throw $e;
1031 return true;
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() );
1041 $this->assertTrue(
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 ] ]
1053 ] );
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] );
1071 $result = true;
1074 $manager = $this->getManager( [
1075 MainConfigNames::Autopromote => [ 'test_autoconfirmed' => [ 999, 'ARGUMENT' ] ]
1076 ] );
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(
1107 array $config,
1108 array $formerGroups,
1109 array $userGroups,
1110 array $expected
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(
1124 $expected,
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 ] ] ]
1182 ] );
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 );
1191 $hookCalled = true;
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' ] )
1199 ->from( 'logging' )
1200 ->where( [ 'log_type' => 'rights' ] )
1201 ->assertResultSet( [ [ 'rights',
1202 'autopromote',
1203 LogEntryBase::makeParamBlob( [
1204 '4::oldgroups' => [],
1205 '5::newgroups' => [ 'autopromoteonce' ],
1207 ] ] );
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
1231 ] );
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' ] )
1249 ->from( 'logging' )
1250 ->where( [ 'log_type' => 'rights' ] );
1252 $logQueryBuilder->assertRowValue( [ 'rights',
1253 'autopromote',
1254 LogEntryBase::makeParamBlob( [
1255 '4::oldgroups' => $preAutopromoteGroups,
1256 '5::newgroups' => $manager->getUserGroups( $user ),
1258 ] );
1260 $logId = $logQueryBuilder
1261 ->clearFields()
1262 ->field( 'log_id' )
1263 ->fetchField();
1265 if ( !array_diff( $autoPromoteOnceGroups, $rcExcludedGroups ) ) {
1266 $this->newSelectQueryBuilder()
1267 ->select( [ 'rc_logid' ] )
1268 ->from( 'recentchanges' )
1269 ->where( [ 'rc_type' => RC_LOG ] )
1270 ->assertEmptyResult();
1271 return;
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,
1328 'add-self' => [],
1329 'remove-self' => [],
1331 $changeableGroups
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' => [],
1343 $changeableGroups
1346 $user = $this->getTestUser( [ 'flood' ] )->getUser();
1347 $changeableGroups = $manager->getGroupsChangeableBy( new SimpleAuthority( $user, [] ) );
1348 $this->assertGroupsEquals(
1350 'add' => [],
1351 'remove' => [],
1352 'add-self' => [],
1353 'remove-self' => [ 'flood' ],
1355 $changeableGroups
1359 public static function provideChangeableByGroup() {
1360 yield 'sysop' => [ 'sysop', [
1361 'add' => [ 'rollback' ],
1362 'remove' => [ 'rollback' ],
1363 'add-self' => [ 'flood' ],
1364 'remove-self' => [],
1365 ] ];
1366 yield 'flood' => [ 'flood', [
1367 'add' => [],
1368 'remove' => [],
1369 'add-self' => [],
1370 'remove-self' => [ 'flood' ],
1371 ] ];
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 ) {
1392 $invocationCount();
1393 $this->assertTrue( $userToMatch->equals( $u ) );
1394 $groups = array_merge( $groups, $groupsToAdd );
1398 $manager = $this->getManager();
1400 $user = new User;
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 () {
1441 } );
1442 $this->setTemporaryHook( 'UserEffectiveGroups', static function () {
1443 } );
1444 $user = $this->getTestUser( [] )->getUser();
1445 $this->assertArrayEquals(
1447 $manager->getUserPrivilegedGroups( $user, IDBAccessObject::READ_NORMAL, true )