Merge "Don't check namespace in SpecialWantedtemplates"
[lhc/web/wiklou.git] / tests / phpunit / includes / UserTest.php
1 <?php
2
3 define( 'NS_UNITTEST', 5600 );
4 define( 'NS_UNITTEST_TALK', 5601 );
5
6 /**
7 * @group Database
8 */
9 class UserTest extends MediaWikiTestCase {
10 /**
11 * @var User
12 */
13 protected $user;
14
15 protected function setUp() {
16 parent::setUp();
17
18 $this->setMwGlobals( array(
19 'wgGroupPermissions' => array(),
20 'wgRevokePermissions' => array(),
21 ) );
22
23 $this->setUpPermissionGlobals();
24
25 $this->user = new User;
26 $this->user->addGroup( 'unittesters' );
27 }
28
29 private function setUpPermissionGlobals() {
30 global $wgGroupPermissions, $wgRevokePermissions;
31
32 # Data for regular $wgGroupPermissions test
33 $wgGroupPermissions['unittesters'] = array(
34 'test' => true,
35 'runtest' => true,
36 'writetest' => false,
37 'nukeworld' => false,
38 );
39 $wgGroupPermissions['testwriters'] = array(
40 'test' => true,
41 'writetest' => true,
42 'modifytest' => true,
43 );
44
45 # Data for regular $wgRevokePermissions test
46 $wgRevokePermissions['formertesters'] = array(
47 'runtest' => true,
48 );
49
50 # For the options test
51 $wgGroupPermissions['*'] = array(
52 'editmyoptions' => true,
53 );
54 }
55
56 /**
57 * @covers User::getGroupPermissions
58 */
59 public function testGroupPermissions() {
60 $rights = User::getGroupPermissions( array( 'unittesters' ) );
61 $this->assertContains( 'runtest', $rights );
62 $this->assertNotContains( 'writetest', $rights );
63 $this->assertNotContains( 'modifytest', $rights );
64 $this->assertNotContains( 'nukeworld', $rights );
65
66 $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) );
67 $this->assertContains( 'runtest', $rights );
68 $this->assertContains( 'writetest', $rights );
69 $this->assertContains( 'modifytest', $rights );
70 $this->assertNotContains( 'nukeworld', $rights );
71 }
72
73 /**
74 * @covers User::getGroupPermissions
75 */
76 public function testRevokePermissions() {
77 $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) );
78 $this->assertNotContains( 'runtest', $rights );
79 $this->assertNotContains( 'writetest', $rights );
80 $this->assertNotContains( 'modifytest', $rights );
81 $this->assertNotContains( 'nukeworld', $rights );
82 }
83
84 /**
85 * @covers User::getRights
86 */
87 public function testUserPermissions() {
88 $rights = $this->user->getRights();
89 $this->assertContains( 'runtest', $rights );
90 $this->assertNotContains( 'writetest', $rights );
91 $this->assertNotContains( 'modifytest', $rights );
92 $this->assertNotContains( 'nukeworld', $rights );
93 }
94
95 /**
96 * @dataProvider provideGetGroupsWithPermission
97 * @covers User::getGroupsWithPermission
98 */
99 public function testGetGroupsWithPermission( $expected, $right ) {
100 $result = User::getGroupsWithPermission( $right );
101 sort( $result );
102 sort( $expected );
103
104 $this->assertEquals( $expected, $result, "Groups with permission $right" );
105 }
106
107 public static function provideGetGroupsWithPermission() {
108 return array(
109 array(
110 array( 'unittesters', 'testwriters' ),
111 'test'
112 ),
113 array(
114 array( 'unittesters' ),
115 'runtest'
116 ),
117 array(
118 array( 'testwriters' ),
119 'writetest'
120 ),
121 array(
122 array( 'testwriters' ),
123 'modifytest'
124 ),
125 );
126 }
127
128 /**
129 * @dataProvider provideIPs
130 * @covers User::isIP
131 */
132 public function testIsIP( $value, $result, $message ) {
133 $this->assertEquals( $this->user->isIP( $value ), $result, $message );
134 }
135
136 public static function provideIPs() {
137 return array(
138 array( '', false, 'Empty string' ),
139 array( ' ', false, 'Blank space' ),
140 array( '10.0.0.0', true, 'IPv4 private 10/8' ),
141 array( '10.255.255.255', true, 'IPv4 private 10/8' ),
142 array( '192.168.1.1', true, 'IPv4 private 192.168/16' ),
143 array( '203.0.113.0', true, 'IPv4 example' ),
144 array( '2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff', true, 'IPv6 example' ),
145 // Not valid IPs but classified as such by MediaWiki for negated asserting
146 // of whether this might be the identifier of a logged-out user or whether
147 // to allow usernames like it.
148 array( '300.300.300.300', true, 'Looks too much like an IPv4 address' ),
149 array( '203.0.113.xxx', true, 'Assigned by UseMod to cloaked logged-out users' ),
150 );
151 }
152
153 /**
154 * @dataProvider provideUserNames
155 * @covers User::isValidUserName
156 */
157 public function testIsValidUserName( $username, $result, $message ) {
158 $this->assertEquals( $this->user->isValidUserName( $username ), $result, $message );
159 }
160
161 public static function provideUserNames() {
162 return array(
163 array( '', false, 'Empty string' ),
164 array( ' ', false, 'Blank space' ),
165 array( 'abcd', false, 'Starts with small letter' ),
166 array( 'Ab/cd', false, 'Contains slash' ),
167 array( 'Ab cd', true, 'Whitespace' ),
168 array( '192.168.1.1', false, 'IP' ),
169 array( 'User:Abcd', false, 'Reserved Namespace' ),
170 array( '12abcd232', true, 'Starts with Numbers' ),
171 array( '?abcd', true, 'Start with ? mark' ),
172 array( '#abcd', false, 'Start with #' ),
173 array( 'Abcdകഖഗഘ', true, ' Mixed scripts' ),
174 array( 'ജോസ്‌തോമസ്', false, 'ZWNJ- Format control character' ),
175 array( 'Ab cd', false, ' Ideographic space' ),
176 array( '300.300.300.300', false, 'Looks too much like an IPv4 address' ),
177 array( '302.113.311.900', false, 'Looks too much like an IPv4 address' ),
178 array( '203.0.113.xxx', false, 'Reserved for usage by UseMod for cloaked logged-out users' ),
179 );
180 }
181
182 /**
183 * Test, if for all rights a right- message exist,
184 * which is used on Special:ListGroupRights as help text
185 * Extensions and core
186 */
187 public function testAllRightsWithMessage() {
188 // Getting all user rights, for core: User::$mCoreRights, for extensions: $wgAvailableRights
189 $allRights = User::getAllRights();
190 $allMessageKeys = Language::getMessageKeysFor( 'en' );
191
192 $rightsWithMessage = array();
193 foreach ( $allMessageKeys as $message ) {
194 // === 0: must be at beginning of string (position 0)
195 if ( strpos( $message, 'right-' ) === 0 ) {
196 $rightsWithMessage[] = substr( $message, strlen( 'right-' ) );
197 }
198 }
199
200 sort( $allRights );
201 sort( $rightsWithMessage );
202
203 $this->assertEquals(
204 $allRights,
205 $rightsWithMessage,
206 'Each user rights (core/extensions) has a corresponding right- message.'
207 );
208 }
209
210 /**
211 * Test User::editCount
212 * @group medium
213 * @covers User::getEditCount
214 */
215 public function testEditCount() {
216 $user = User::newFromName( 'UnitTestUser' );
217
218 if ( !$user->getId() ) {
219 $user->addToDatabase();
220 }
221
222 // let the user have a few (3) edits
223 $page = WikiPage::factory( Title::newFromText( 'Help:UserTest_EditCount' ) );
224 for ( $i = 0; $i < 3; $i++ ) {
225 $page->doEdit( (string)$i, 'test', 0, false, $user );
226 }
227
228 $user->clearInstanceCache();
229 $this->assertEquals(
230 3,
231 $user->getEditCount(),
232 'After three edits, the user edit count should be 3'
233 );
234
235 // increase the edit count and clear the cache
236 $user->incEditCount();
237
238 $user->clearInstanceCache();
239 $this->assertEquals(
240 4,
241 $user->getEditCount(),
242 'After increasing the edit count manually, the user edit count should be 4'
243 );
244 }
245
246 /**
247 * Test changing user options.
248 * @covers User::setOption
249 * @covers User::getOption
250 */
251 public function testOptions() {
252 $user = User::newFromName( 'UnitTestUser' );
253
254 if ( !$user->getId() ) {
255 $user->addToDatabase();
256 }
257
258 $user->setOption( 'userjs-someoption', 'test' );
259 $user->setOption( 'cols', 200 );
260 $user->saveSettings();
261
262 $user = User::newFromName( 'UnitTestUser' );
263 $this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) );
264 $this->assertEquals( 200, $user->getOption( 'cols' ) );
265 }
266
267 /**
268 * Bug 37963
269 * Make sure defaults are loaded when setOption is called.
270 * @covers User::loadOptions
271 */
272 public function testAnonOptions() {
273 global $wgDefaultUserOptions;
274 $this->user->setOption( 'userjs-someoption', 'test' );
275 $this->assertEquals( $wgDefaultUserOptions['cols'], $this->user->getOption( 'cols' ) );
276 $this->assertEquals( 'test', $this->user->getOption( 'userjs-someoption' ) );
277 }
278
279 /**
280 * Test password expiration.
281 * @covers User::getPasswordExpired()
282 */
283 public function testPasswordExpire() {
284 $this->setMwGlobals( 'wgPasswordExpireGrace', 3600 * 24 * 7 ); // 7 days
285
286 $user = User::newFromName( 'UnitTestUser' );
287 $user->loadDefaults( 'UnitTestUser' );
288 $this->assertEquals( false, $user->getPasswordExpired() );
289
290 $ts = time() - ( 3600 * 24 * 1 ); // 1 day ago
291 $user->expirePassword( $ts );
292 $this->assertEquals( 'soft', $user->getPasswordExpired() );
293
294 $ts = time() - ( 3600 * 24 * 10 ); // 10 days ago
295 $user->expirePassword( $ts );
296 $this->assertEquals( 'hard', $user->getPasswordExpired() );
297 }
298
299 /**
300 * Test password validity checks. There are 3 checks in core,
301 * - ensure the password meets the minimal length
302 * - ensure the password is not the same as the username
303 * - ensure the username/password combo isn't forbidden
304 * @covers User::checkPasswordValidity()
305 * @covers User::getPasswordValidity()
306 * @covers User::isValidPassword()
307 */
308 public function testCheckPasswordValidity() {
309 $this->setMwGlobals( array(
310 'wgPasswordPolicy' => array(
311 'policies' => array(
312 'sysop' => array(
313 'MinimalPasswordLength' => 8,
314 'MinimumPasswordLengthToLogin' => 1,
315 'PasswordCannotMatchUsername' => 1,
316 ),
317 'default' => array(
318 'MinimalPasswordLength' => 6,
319 'PasswordCannotMatchUsername' => true,
320 'PasswordCannotMatchBlacklist' => true,
321 'MaximalPasswordLength' => 30,
322 ),
323 ),
324 'checks' => array(
325 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength',
326 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
327 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
328 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
329 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
330 ),
331 ),
332 ) );
333
334 $user = User::newFromName( 'Useruser' );
335 // Sanity
336 $this->assertTrue( $user->isValidPassword( 'Password1234' ) );
337
338 // Minimum length
339 $this->assertFalse( $user->isValidPassword( 'a' ) );
340 $this->assertFalse( $user->checkPasswordValidity( 'a' )->isGood() );
341 $this->assertTrue( $user->checkPasswordValidity( 'a' )->isOK() );
342 $this->assertEquals( 'passwordtooshort', $user->getPasswordValidity( 'a' ) );
343
344 // Maximum length
345 $longPass = str_repeat( 'a', 31 );
346 $this->assertFalse( $user->isValidPassword( $longPass ) );
347 $this->assertFalse( $user->checkPasswordValidity( $longPass )->isGood() );
348 $this->assertFalse( $user->checkPasswordValidity( $longPass )->isOK() );
349 $this->assertEquals( 'passwordtoolong', $user->getPasswordValidity( $longPass ) );
350
351 // Matches username
352 $this->assertFalse( $user->checkPasswordValidity( 'Useruser' )->isGood() );
353 $this->assertTrue( $user->checkPasswordValidity( 'Useruser' )->isOK() );
354 $this->assertEquals( 'password-name-match', $user->getPasswordValidity( 'Useruser' ) );
355
356 // On the forbidden list
357 $this->assertFalse( $user->checkPasswordValidity( 'Passpass' )->isGood() );
358 $this->assertEquals( 'password-login-forbidden', $user->getPasswordValidity( 'Passpass' ) );
359 }
360
361 /**
362 * @covers User::getCanonicalName()
363 * @dataProvider provideGetCanonicalName
364 */
365 public function testGetCanonicalName( $name, $expectedArray, $msg ) {
366 foreach ( $expectedArray as $validate => $expected ) {
367 $this->assertEquals(
368 $expected,
369 User::getCanonicalName( $name, $validate === 'false' ? false : $validate ),
370 $msg . ' (' . $validate . ')'
371 );
372 }
373 }
374
375 public static function provideGetCanonicalName() {
376 return array(
377 array( ' Trailing space ', array( 'creatable' => 'Trailing space' ), 'Trailing spaces' ),
378 // @todo FIXME: Maybe the creatable name should be 'Talk:Username' or false to reject?
379 array( 'Talk:Username', array( 'creatable' => 'Username', 'usable' => 'Username',
380 'valid' => 'Username', 'false' => 'Talk:Username' ), 'Namespace prefix' ),
381 array( ' name with # hash', array( 'creatable' => false, 'usable' => false ), 'With hash' ),
382 array( 'Multi spaces', array( 'creatable' => 'Multi spaces',
383 'usable' => 'Multi spaces' ), 'Multi spaces' ),
384 array( 'lowercase', array( 'creatable' => 'Lowercase' ), 'Lowercase' ),
385 array( 'in[]valid', array( 'creatable' => false, 'usable' => false, 'valid' => false,
386 'false' => 'In[]valid' ), 'Invalid' ),
387 array( 'with / slash', array( 'creatable' => false, 'usable' => false, 'valid' => false,
388 'false' => 'With / slash' ), 'With slash' ),
389 );
390 }
391
392 /**
393 * @covers User::equals
394 */
395 public function testEquals() {
396 $first = User::newFromName( 'EqualUser' );
397 $second = User::newFromName( 'EqualUser' );
398
399 $this->assertTrue( $first->equals( $first ) );
400 $this->assertTrue( $first->equals( $second ) );
401 $this->assertTrue( $second->equals( $first ) );
402
403 $third = User::newFromName( '0' );
404 $fourth = User::newFromName( '000' );
405
406 $this->assertFalse( $third->equals( $fourth ) );
407 $this->assertFalse( $fourth->equals( $third ) );
408
409 // Test users loaded from db with id
410 $user = User::newFromName( 'EqualUnitTestUser' );
411 if ( !$user->getId() ) {
412 $user->addToDatabase();
413 }
414
415 $id = $user->getId();
416
417 $fifth = User::newFromId( $id );
418 $sixth = User::newFromName( 'EqualUnitTestUser' );
419 $this->assertTrue( $fifth->equals( $sixth ) );
420 }
421
422 /**
423 * @covers User::getId
424 */
425 public function testGetId() {
426 $user = User::newFromName( 'UTSysop' );
427 $this->assertTrue( $user->getId() > 0 );
428
429 }
430
431 /**
432 * @covers User::isLoggedIn
433 * @covers User::isAnon
434 */
435 public function testLoggedIn() {
436 $user = User::newFromName( 'UTSysop' );
437 $this->assertTrue( $user->isLoggedIn() );
438 $this->assertFalse( $user->isAnon() );
439
440 // Non-existent users are perceived as anonymous
441 $user = User::newFromName( 'UTNonexistent' );
442 $this->assertFalse( $user->isLoggedIn() );
443 $this->assertTrue( $user->isAnon() );
444
445 $user = new User;
446 $this->assertFalse( $user->isLoggedIn() );
447 $this->assertTrue( $user->isAnon() );
448 }
449
450 /**
451 * @covers User::checkAndSetTouched
452 */
453 public function testCheckAndSetTouched() {
454 $user = TestingAccessWrapper::newFromObject( User::newFromName( 'UTSysop' ) );
455 $this->assertTrue( $user->isLoggedIn() );
456
457 $touched = $user->getDBTouched();
458 $this->assertTrue(
459 $user->checkAndSetTouched(), "checkAndSetTouched() succeded" );
460 $this->assertGreaterThan(
461 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched()" );
462
463 $touched = $user->getDBTouched();
464 $this->assertTrue(
465 $user->checkAndSetTouched(), "checkAndSetTouched() succeded #2" );
466 $this->assertGreaterThan(
467 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
468 }
469
470 public static function setExtendedLoginCookieDataProvider() {
471 $data = array();
472 $now = time();
473
474 $secondsInDay = 86400;
475
476 // Arbitrary durations, in units of days, to ensure it chooses the
477 // right one. There is a 5-minute grace period (see testSetExtendedLoginCookie)
478 // to work around slow tests, since we're not currently mocking time() for PHP.
479
480 $durationOne = $secondsInDay * 5;
481 $durationTwo = $secondsInDay * 29;
482 $durationThree = $secondsInDay * 17;
483
484 // If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
485 // set cookie is time() + $wgCookieExpiration
486 $data[] = array(
487 null,
488 $durationOne,
489 $now + $durationOne,
490 );
491
492 // If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
493 // set cookie is $now + $wgExtendedLoginCookieExpiration
494 $data[] = array(
495 $durationTwo,
496 $durationThree,
497 $now + $durationTwo,
498 );
499
500 return $data;
501 }
502
503 /**
504 * @dataProvider setExtendedLoginCookieDataProvider
505 * @covers User::getRequest
506 * @covers User::setCookie
507 * @backupGlobals enabled
508 */
509 public function testSetExtendedLoginCookie(
510 $extendedLoginCookieExpiration,
511 $cookieExpiration,
512 $expectedExpiry
513 ) {
514 $this->setMwGlobals( array(
515 'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
516 'wgCookieExpiration' => $cookieExpiration,
517 ) );
518
519 $response = $this->getMock( 'WebResponse' );
520 $setcookieSpy = $this->any();
521 $response->expects( $setcookieSpy )
522 ->method( 'setcookie' );
523
524 $request = new MockWebRequest( $response );
525 $user = new UserProxy( User::newFromSession( $request ) );
526 $user->setExtendedLoginCookie( 'name', 'value', true );
527
528 $setcookieInvocations = $setcookieSpy->getInvocations();
529 $setcookieInvocation = end( $setcookieInvocations );
530 $actualExpiry = $setcookieInvocation->parameters[ 2 ];
531
532 // TODO: ± 300 seconds compensates for
533 // slow-running tests. However, the dependency on the time
534 // function should be removed. This requires some way
535 // to mock/isolate User->setExtendedLoginCookie's call to time()
536 $this->assertEquals( $expectedExpiry, $actualExpiry, '', 300 );
537 }
538 }
539
540 class UserProxy extends User {
541
542 /**
543 * @var User
544 */
545 protected $user;
546
547 public function __construct( User $user ) {
548 $this->user = $user;
549 }
550
551 public function setExtendedLoginCookie( $name, $value, $secure ) {
552 $this->user->setExtendedLoginCookie( $name, $value, $secure );
553 }
554 }