8 * @covers ApiUserrights
10 class ApiUserrightsTest
extends ApiTestCase
{
12 protected function setUp() {
14 $this->tablesUsed
= array_merge(
16 [ 'change_tag', 'change_tag_def', 'logging' ]
21 * Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets
22 * $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the
25 * @param array|bool $add Groups bureaucrats should be allowed to add, true for all
26 * @param array|bool $remove Groups bureaucrats should be allowed to remove, true for all
28 protected function setPermissions( $add = [], $remove = [] ) {
29 global $wgAddGroups, $wgRemoveGroups;
31 $this->setGroupPermissions( 'bureaucrat', 'userrights', false );
34 $this->stashMwGlobals( 'wgAddGroups' );
35 $wgAddGroups['bureaucrat'] = $add;
38 $this->stashMwGlobals( 'wgRemoveGroups' );
39 $wgRemoveGroups['bureaucrat'] = $remove;
44 * Perform an API userrights request that's expected to be successful.
46 * @param array|string $expectedGroups Group(s) that the user is expected
47 * to have after the API request
48 * @param array $params Array to pass to doApiRequestWithToken(). 'action'
49 * => 'userrights' is implicit. If no 'user' or 'userid' is specified,
50 * we add a 'user' parameter. If no 'add' or 'remove' is specified, we
51 * add 'add' => 'sysop'.
52 * @param User|null $user The user that we're modifying. The user must be
53 * mutable, because we're going to change its groups! null means that
54 * we'll make up our own user to modify, and doesn't make sense if 'user'
55 * or 'userid' is specified in $params.
57 protected function doSuccessfulRightsChange(
58 $expectedGroups = 'sysop', array $params = [], User
$user = null
60 $expectedGroups = (array)$expectedGroups;
61 $params['action'] = 'userrights';
64 $user = $this->getMutableTestUser()->getUser();
67 $this->assertTrue( TestUserRegistry
::isMutable( $user ),
68 'Immutable user passed to doSuccessfulRightsChange!' );
70 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
71 $params['user'] = $user->getName();
73 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
74 $params['add'] = 'sysop';
77 $res = $this->doApiRequestWithToken( $params );
79 $user->clearInstanceCache();
80 $this->assertSame( $expectedGroups, $user->getGroups() );
82 $this->assertArrayNotHasKey( 'warnings', $res[0] );
86 * Perform an API userrights request that's expected to fail.
88 * @param string $expectedException Expected exception text
89 * @param array $params As for doSuccessfulRightsChange()
90 * @param User|null $user As for doSuccessfulRightsChange(). If there's no
91 * user who will possibly be affected (such as if an invalid username is
92 * provided in $params), pass null.
94 protected function doFailedRightsChange(
95 $expectedException, array $params = [], User
$user = null
97 $params['action'] = 'userrights';
99 $this->setExpectedException( ApiUsageException
::class, $expectedException );
102 // If 'user' or 'userid' is specified and $user was not specified,
103 // the user we're creating now will have nothing to do with the API
104 // request, but that's okay, since we're just testing that it has
106 $user = $this->getMutableTestUser()->getUser();
109 $this->assertTrue( TestUserRegistry
::isMutable( $user ),
110 'Immutable user passed to doFailedRightsChange!' );
112 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
113 $params['user'] = $user->getName();
115 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
116 $params['add'] = 'sysop';
118 $expectedGroups = $user->getGroups();
121 $this->doApiRequestWithToken( $params );
123 $user->clearInstanceCache();
124 $this->assertSame( $expectedGroups, $user->getGroups() );
128 public function testAdd() {
129 $this->doSuccessfulRightsChange();
132 public function testBlockedWithUserrights() {
135 $block = new Block( [ 'address' => $wgUser, 'by' => $wgUser->getId(), ] );
139 $this->doSuccessfulRightsChange();
142 $wgUser->clearInstanceCache();
146 public function testBlockedWithoutUserrights() {
147 $user = $this->getTestSysop()->getUser();
149 $this->setPermissions( true, true );
151 $block = new Block( [ 'address' => $user, 'by' => $user->getId() ] );
155 $this->doFailedRightsChange( 'You have been blocked from editing.' );
158 $user->clearInstanceCache();
162 public function testAddMultiple() {
163 $this->doSuccessfulRightsChange(
164 [ 'bureaucrat', 'sysop' ],
165 [ 'add' => 'bureaucrat|sysop' ]
169 public function testTooFewExpiries() {
170 $this->doFailedRightsChange(
171 '2 expiry timestamps were provided where 3 were needed.',
172 [ 'add' => 'sysop|bureaucrat|bot', 'expiry' => 'infinity|tomorrow' ]
176 public function testTooManyExpiries() {
177 $this->doFailedRightsChange(
178 '3 expiry timestamps were provided where 2 were needed.',
179 [ 'add' => 'sysop|bureaucrat', 'expiry' => 'infinity|tomorrow|never' ]
183 public function testInvalidExpiry() {
184 $this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] );
187 public function testMultipleInvalidExpiries() {
188 $this->doFailedRightsChange(
189 'Invalid expiry time "foo".',
190 [ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ]
194 public function testWithTag() {
195 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH
);
196 ChangeTags
::defineTag( 'custom tag' );
198 $user = $this->getMutableTestUser()->getUser();
200 $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
202 $dbr = wfGetDB( DB_REPLICA
);
206 [ 'change_tag', 'logging' ],
209 'ct_log_id = log_id',
210 'log_namespace' => NS_USER
,
211 'log_title' => strtr( $user->getName(), ' ', '_' )
218 public function testWithTagNewBackend() {
219 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW
);
220 ChangeTags
::defineTag( 'custom tag' );
222 $user = $this->getMutableTestUser()->getUser();
224 $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
226 $dbr = wfGetDB( DB_REPLICA
);
230 [ 'change_tag', 'logging', 'change_tag_def' ],
233 'ct_log_id = log_id',
234 'log_namespace' => NS_USER
,
235 'log_title' => strtr( $user->getName(), ' ', '_' )
238 [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ]
243 public function testWithoutTagPermission() {
244 global $wgGroupPermissions;
246 ChangeTags
::defineTag( 'custom tag' );
248 $this->stashMwGlobals( 'wgGroupPermissions' );
249 $wgGroupPermissions['user']['applychangetags'] = false;
251 $this->doFailedRightsChange(
252 'You do not have permission to apply change tags along with your changes.',
253 [ 'tags' => 'custom tag' ]
257 public function testNonexistentUser() {
258 $this->doFailedRightsChange(
259 'There is no user by the name "Nonexistent user". Check your spelling.',
260 [ 'user' => 'Nonexistent user' ]
264 public function testWebToken() {
265 $sysop = $this->getTestSysop()->getUser();
266 $user = $this->getMutableTestUser()->getUser();
268 $token = $sysop->getEditToken( $user->getName() );
270 $res = $this->doApiRequest( [
271 'action' => 'userrights',
272 'user' => $user->getName(),
277 $user->clearInstanceCache();
278 $this->assertSame( [ 'sysop' ], $user->getGroups() );
280 $this->assertArrayNotHasKey( 'warnings', $res[0] );
284 * Helper for testCanProcessExpiries that returns a mock ApiUserrights that either can or cannot
285 * process expiries. Although the regular page can process expiries, we use a mock here to
286 * ensure that it's the result of canProcessExpiries() that makes a difference, and not some
287 * error in the way we construct the mock.
289 * @param bool $canProcessExpiries
291 private function getMockForProcessingExpiries( $canProcessExpiries ) {
292 $sysop = $this->getTestSysop()->getUser();
293 $user = $this->getMutableTestUser()->getUser();
295 $token = $sysop->getEditToken( 'userrights' );
297 $main = new ApiMain( new FauxRequest( [
298 'action' => 'userrights',
299 'user' => $user->getName(),
304 $mockUserRightsPage = $this->getMockBuilder( UserrightsPage
::class )
305 ->setMethods( [ 'canProcessExpiries' ] )
307 $mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries );
309 $mockApi = $this->getMockBuilder( ApiUserrights
::class )
310 ->setConstructorArgs( [ $main, 'userrights' ] )
311 ->setMethods( [ 'getUserRightsPage' ] )
313 $mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage );
318 public function testCanProcessExpiries() {
319 $mock1 = $this->getMockForProcessingExpiries( true );
320 $this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() );
322 $mock2 = $this->getMockForProcessingExpiries( false );
323 $this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() );
327 * Tests adding and removing various groups with various permissions.
329 * @dataProvider addAndRemoveGroupsProvider
330 * @param array|null $permissions [ [ $wgAddGroups, $wgRemoveGroups ] ] or null for 'userrights'
331 * to be set in $wgGroupPermissions
332 * @param array $groupsToChange [ [ groups to add ], [ groups to remove ] ]
333 * @param array $expectedGroups Array of expected groups
335 public function testAddAndRemoveGroups(
336 array $permissions = null, array $groupsToChange, array $expectedGroups
338 if ( $permissions !== null ) {
339 $this->setPermissions( $permissions[0], $permissions[1] );
343 'add' => implode( '|', $groupsToChange[0] ),
344 'remove' => implode( '|', $groupsToChange[1] ),
347 // We'll take a bot so we have a group to remove
348 $user = $this->getMutableTestUser( [ 'bot' ] )->getUser();
350 $this->doSuccessfulRightsChange( $expectedGroups, $params, $user );
353 public function addAndRemoveGroupsProvider() {
359 ], 'Add with only remove permission' => [
363 ], 'Add with global remove permission' => [
367 ], 'Simple remove' => [
371 ], 'Remove with only add permission' => [
375 ], 'Remove with global add permission' => [
379 ], 'Add and remove same new group' => [
381 [ [ 'sysop' ], [ 'sysop' ] ],
382 // The userrights code does removals before adds, so it doesn't remove the sysop
383 // group here and only adds it.
385 ], 'Add and remove same existing group' => [
387 [ [ 'bot' ], [ 'bot' ] ],
388 // But here it first removes the existing group and then re-adds it.