Update API tests to comply with the new backend for change tags
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiUserrightsTest.php
1 <?php
2
3 /**
4 * @group API
5 * @group Database
6 * @group medium
7 *
8 * @covers ApiUserrights
9 */
10 class ApiUserrightsTest extends ApiTestCase {
11
12 protected function setUp() {
13 parent::setUp();
14 $this->tablesUsed = array_merge(
15 $this->tablesUsed,
16 [ 'change_tag', 'change_tag_def', 'logging' ]
17 );
18 }
19
20 /**
21 * Unsets $wgGroupPermissions['bureaucrat']['userrights'], and sets
22 * $wgAddGroups['bureaucrat'] and $wgRemoveGroups['bureaucrat'] to the
23 * specified values.
24 *
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
27 */
28 protected function setPermissions( $add = [], $remove = [] ) {
29 global $wgAddGroups, $wgRemoveGroups;
30
31 $this->setGroupPermissions( 'bureaucrat', 'userrights', false );
32
33 if ( $add ) {
34 $this->stashMwGlobals( 'wgAddGroups' );
35 $wgAddGroups['bureaucrat'] = $add;
36 }
37 if ( $remove ) {
38 $this->stashMwGlobals( 'wgRemoveGroups' );
39 $wgRemoveGroups['bureaucrat'] = $remove;
40 }
41 }
42
43 /**
44 * Perform an API userrights request that's expected to be successful.
45 *
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.
56 */
57 protected function doSuccessfulRightsChange(
58 $expectedGroups = 'sysop', array $params = [], User $user = null
59 ) {
60 $expectedGroups = (array)$expectedGroups;
61 $params['action'] = 'userrights';
62
63 if ( !$user ) {
64 $user = $this->getMutableTestUser()->getUser();
65 }
66
67 $this->assertTrue( TestUserRegistry::isMutable( $user ),
68 'Immutable user passed to doSuccessfulRightsChange!' );
69
70 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
71 $params['user'] = $user->getName();
72 }
73 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
74 $params['add'] = 'sysop';
75 }
76
77 $res = $this->doApiRequestWithToken( $params );
78
79 $user->clearInstanceCache();
80 $this->assertSame( $expectedGroups, $user->getGroups() );
81
82 $this->assertArrayNotHasKey( 'warnings', $res[0] );
83 }
84
85 /**
86 * Perform an API userrights request that's expected to fail.
87 *
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.
93 */
94 protected function doFailedRightsChange(
95 $expectedException, array $params = [], User $user = null
96 ) {
97 $params['action'] = 'userrights';
98
99 $this->setExpectedException( ApiUsageException::class, $expectedException );
100
101 if ( !$user ) {
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
105 // no groups.
106 $user = $this->getMutableTestUser()->getUser();
107 }
108
109 $this->assertTrue( TestUserRegistry::isMutable( $user ),
110 'Immutable user passed to doFailedRightsChange!' );
111
112 if ( !isset( $params['user'] ) && !isset( $params['userid'] ) ) {
113 $params['user'] = $user->getName();
114 }
115 if ( !isset( $params['add'] ) && !isset( $params['remove'] ) ) {
116 $params['add'] = 'sysop';
117 }
118 $expectedGroups = $user->getGroups();
119
120 try {
121 $this->doApiRequestWithToken( $params );
122 } finally {
123 $user->clearInstanceCache();
124 $this->assertSame( $expectedGroups, $user->getGroups() );
125 }
126 }
127
128 public function testAdd() {
129 $this->doSuccessfulRightsChange();
130 }
131
132 public function testBlockedWithUserrights() {
133 global $wgUser;
134
135 $block = new Block( [ 'address' => $wgUser, 'by' => $wgUser->getId(), ] );
136 $block->insert();
137
138 try {
139 $this->doSuccessfulRightsChange();
140 } finally {
141 $block->delete();
142 $wgUser->clearInstanceCache();
143 }
144 }
145
146 public function testBlockedWithoutUserrights() {
147 $user = $this->getTestSysop()->getUser();
148
149 $this->setPermissions( true, true );
150
151 $block = new Block( [ 'address' => $user, 'by' => $user->getId() ] );
152 $block->insert();
153
154 try {
155 $this->doFailedRightsChange( 'You have been blocked from editing.' );
156 } finally {
157 $block->delete();
158 $user->clearInstanceCache();
159 }
160 }
161
162 public function testAddMultiple() {
163 $this->doSuccessfulRightsChange(
164 [ 'bureaucrat', 'sysop' ],
165 [ 'add' => 'bureaucrat|sysop' ]
166 );
167 }
168
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' ]
173 );
174 }
175
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' ]
180 );
181 }
182
183 public function testInvalidExpiry() {
184 $this->doFailedRightsChange( 'Invalid expiry time', [ 'expiry' => 'yummy lollipops!' ] );
185 }
186
187 public function testMultipleInvalidExpiries() {
188 $this->doFailedRightsChange(
189 'Invalid expiry time "foo".',
190 [ 'add' => 'sysop|bureaucrat', 'expiry' => 'foo|bar' ]
191 );
192 }
193
194 public function testWithTag() {
195 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH );
196 ChangeTags::defineTag( 'custom tag' );
197
198 $user = $this->getMutableTestUser()->getUser();
199
200 $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
201
202 $dbr = wfGetDB( DB_REPLICA );
203 $this->assertSame(
204 'custom tag',
205 $dbr->selectField(
206 [ 'change_tag', 'logging' ],
207 'ct_tag',
208 [
209 'ct_log_id = log_id',
210 'log_namespace' => NS_USER,
211 'log_title' => strtr( $user->getName(), ' ', '_' )
212 ],
213 __METHOD__
214 )
215 );
216 }
217
218 public function testWithTagNewBackend() {
219 $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW );
220 ChangeTags::defineTag( 'custom tag' );
221
222 $user = $this->getMutableTestUser()->getUser();
223
224 $this->doSuccessfulRightsChange( 'sysop', [ 'tags' => 'custom tag' ], $user );
225
226 $dbr = wfGetDB( DB_REPLICA );
227 $this->assertSame(
228 'custom tag',
229 $dbr->selectField(
230 [ 'change_tag', 'logging', 'change_tag_def' ],
231 'ctd_name',
232 [
233 'ct_log_id = log_id',
234 'log_namespace' => NS_USER,
235 'log_title' => strtr( $user->getName(), ' ', '_' )
236 ],
237 __METHOD__,
238 [ 'change_tag_def' => [ 'INNER JOIN', 'ctd_id = ct_tag_id' ] ]
239 )
240 );
241 }
242
243 public function testWithoutTagPermission() {
244 global $wgGroupPermissions;
245
246 ChangeTags::defineTag( 'custom tag' );
247
248 $this->stashMwGlobals( 'wgGroupPermissions' );
249 $wgGroupPermissions['user']['applychangetags'] = false;
250
251 $this->doFailedRightsChange(
252 'You do not have permission to apply change tags along with your changes.',
253 [ 'tags' => 'custom tag' ]
254 );
255 }
256
257 public function testNonexistentUser() {
258 $this->doFailedRightsChange(
259 'There is no user by the name "Nonexistent user". Check your spelling.',
260 [ 'user' => 'Nonexistent user' ]
261 );
262 }
263
264 public function testWebToken() {
265 $sysop = $this->getTestSysop()->getUser();
266 $user = $this->getMutableTestUser()->getUser();
267
268 $token = $sysop->getEditToken( $user->getName() );
269
270 $res = $this->doApiRequest( [
271 'action' => 'userrights',
272 'user' => $user->getName(),
273 'add' => 'sysop',
274 'token' => $token,
275 ] );
276
277 $user->clearInstanceCache();
278 $this->assertSame( [ 'sysop' ], $user->getGroups() );
279
280 $this->assertArrayNotHasKey( 'warnings', $res[0] );
281 }
282
283 /**
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.
288 *
289 * @param bool $canProcessExpiries
290 */
291 private function getMockForProcessingExpiries( $canProcessExpiries ) {
292 $sysop = $this->getTestSysop()->getUser();
293 $user = $this->getMutableTestUser()->getUser();
294
295 $token = $sysop->getEditToken( 'userrights' );
296
297 $main = new ApiMain( new FauxRequest( [
298 'action' => 'userrights',
299 'user' => $user->getName(),
300 'add' => 'sysop',
301 'token' => $token,
302 ] ) );
303
304 $mockUserRightsPage = $this->getMockBuilder( UserrightsPage::class )
305 ->setMethods( [ 'canProcessExpiries' ] )
306 ->getMock();
307 $mockUserRightsPage->method( 'canProcessExpiries' )->willReturn( $canProcessExpiries );
308
309 $mockApi = $this->getMockBuilder( ApiUserrights::class )
310 ->setConstructorArgs( [ $main, 'userrights' ] )
311 ->setMethods( [ 'getUserRightsPage' ] )
312 ->getMock();
313 $mockApi->method( 'getUserRightsPage' )->willReturn( $mockUserRightsPage );
314
315 return $mockApi;
316 }
317
318 public function testCanProcessExpiries() {
319 $mock1 = $this->getMockForProcessingExpiries( true );
320 $this->assertArrayHasKey( 'expiry', $mock1->getAllowedParams() );
321
322 $mock2 = $this->getMockForProcessingExpiries( false );
323 $this->assertArrayNotHasKey( 'expiry', $mock2->getAllowedParams() );
324 }
325
326 /**
327 * Tests adding and removing various groups with various permissions.
328 *
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
334 */
335 public function testAddAndRemoveGroups(
336 array $permissions = null, array $groupsToChange, array $expectedGroups
337 ) {
338 if ( $permissions !== null ) {
339 $this->setPermissions( $permissions[0], $permissions[1] );
340 }
341
342 $params = [
343 'add' => implode( '|', $groupsToChange[0] ),
344 'remove' => implode( '|', $groupsToChange[1] ),
345 ];
346
347 // We'll take a bot so we have a group to remove
348 $user = $this->getMutableTestUser( [ 'bot' ] )->getUser();
349
350 $this->doSuccessfulRightsChange( $expectedGroups, $params, $user );
351 }
352
353 public function addAndRemoveGroupsProvider() {
354 return [
355 'Simple add' => [
356 [ [ 'sysop' ], [] ],
357 [ [ 'sysop' ], [] ],
358 [ 'bot', 'sysop' ]
359 ], 'Add with only remove permission' => [
360 [ [], [ 'sysop' ] ],
361 [ [ 'sysop' ], [] ],
362 [ 'bot' ],
363 ], 'Add with global remove permission' => [
364 [ [], true ],
365 [ [ 'sysop' ], [] ],
366 [ 'bot' ],
367 ], 'Simple remove' => [
368 [ [], [ 'bot' ] ],
369 [ [], [ 'bot' ] ],
370 [],
371 ], 'Remove with only add permission' => [
372 [ [ 'bot' ], [] ],
373 [ [], [ 'bot' ] ],
374 [ 'bot' ],
375 ], 'Remove with global add permission' => [
376 [ true, [] ],
377 [ [], [ 'bot' ] ],
378 [ 'bot' ],
379 ], 'Add and remove same new group' => [
380 null,
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.
384 [ 'bot', 'sysop' ],
385 ], 'Add and remove same existing group' => [
386 null,
387 [ [ 'bot' ], [ 'bot' ] ],
388 // But here it first removes the existing group and then re-adds it.
389 [ 'bot' ],
390 ],
391 ];
392 }
393 }