3 define( 'NS_UNITTEST', 5600 );
4 define( 'NS_UNITTEST_TALK', 5601 );
9 class UserTest
extends MediaWikiTestCase
{
15 protected function setUp() {
18 $this->setMwGlobals( array(
19 'wgGroupPermissions' => array(),
20 'wgRevokePermissions' => array(),
23 $this->setUpPermissionGlobals();
25 $this->user
= new User
;
26 $this->user
->addGroup( 'unittesters' );
29 private function setUpPermissionGlobals() {
30 global $wgGroupPermissions, $wgRevokePermissions;
32 # Data for regular $wgGroupPermissions test
33 $wgGroupPermissions['unittesters'] = array(
39 $wgGroupPermissions['testwriters'] = array(
45 # Data for regular $wgRevokePermissions test
46 $wgRevokePermissions['formertesters'] = array(
50 # For the options test
51 $wgGroupPermissions['*'] = array(
52 'editmyoptions' => true,
57 * @covers User::getGroupPermissions
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 );
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 );
74 * @covers User::getGroupPermissions
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 );
85 * @covers User::getRights
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 );
96 * @dataProvider provideGetGroupsWithPermission
97 * @covers User::getGroupsWithPermission
99 public function testGetGroupsWithPermission( $expected, $right ) {
100 $result = User
::getGroupsWithPermission( $right );
104 $this->assertEquals( $expected, $result, "Groups with permission $right" );
107 public static function provideGetGroupsWithPermission() {
110 array( 'unittesters', 'testwriters' ),
114 array( 'unittesters' ),
118 array( 'testwriters' ),
122 array( 'testwriters' ),
129 * @dataProvider provideIPs
132 public function testIsIP( $value, $result, $message ) {
133 $this->assertEquals( $this->user
->isIP( $value ), $result, $message );
136 public static function provideIPs() {
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' ),
154 * @dataProvider provideUserNames
155 * @covers User::isValidUserName
157 public function testIsValidUserName( $username, $result, $message ) {
158 $this->assertEquals( $this->user
->isValidUserName( $username ), $result, $message );
161 public static function provideUserNames() {
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' ),
183 * Test, if for all rights a right- message exist,
184 * which is used on Special:ListGroupRights as help text
185 * Extensions and core
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' );
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-' ) );
201 sort( $rightsWithMessage );
206 'Each user rights (core/extensions) has a corresponding right- message.'
211 * Test User::editCount
213 * @covers User::getEditCount
215 public function testEditCount() {
216 $user = User
::newFromName( 'UnitTestUser' );
218 if ( !$user->getId() ) {
219 $user->addToDatabase();
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 );
228 $user->clearInstanceCache();
231 $user->getEditCount(),
232 'After three edits, the user edit count should be 3'
235 // increase the edit count and clear the cache
236 $user->incEditCount();
238 $user->clearInstanceCache();
241 $user->getEditCount(),
242 'After increasing the edit count manually, the user edit count should be 4'
247 * Test changing user options.
248 * @covers User::setOption
249 * @covers User::getOption
251 public function testOptions() {
252 $user = User
::newFromName( 'UnitTestUser' );
254 if ( !$user->getId() ) {
255 $user->addToDatabase();
258 $user->setOption( 'userjs-someoption', 'test' );
259 $user->setOption( 'cols', 200 );
260 $user->saveSettings();
262 $user = User
::newFromName( 'UnitTestUser' );
263 $this->assertEquals( 'test', $user->getOption( 'userjs-someoption' ) );
264 $this->assertEquals( 200, $user->getOption( 'cols' ) );
269 * Make sure defaults are loaded when setOption is called.
270 * @covers User::loadOptions
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' ) );
280 * Test password validity checks. There are 3 checks in core,
281 * - ensure the password meets the minimal length
282 * - ensure the password is not the same as the username
283 * - ensure the username/password combo isn't forbidden
284 * @covers User::checkPasswordValidity()
285 * @covers User::getPasswordValidity()
286 * @covers User::isValidPassword()
288 public function testCheckPasswordValidity() {
289 $this->setMwGlobals( array(
290 'wgPasswordPolicy' => array(
293 'MinimalPasswordLength' => 8,
294 'MinimumPasswordLengthToLogin' => 1,
295 'PasswordCannotMatchUsername' => 1,
298 'MinimalPasswordLength' => 6,
299 'PasswordCannotMatchUsername' => true,
300 'PasswordCannotMatchBlacklist' => true,
301 'MaximalPasswordLength' => 30,
305 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength',
306 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
307 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
308 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
309 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
314 $user = User
::newFromName( 'Useruser' );
316 $this->assertTrue( $user->isValidPassword( 'Password1234' ) );
319 $this->assertFalse( $user->isValidPassword( 'a' ) );
320 $this->assertFalse( $user->checkPasswordValidity( 'a' )->isGood() );
321 $this->assertTrue( $user->checkPasswordValidity( 'a' )->isOK() );
322 $this->assertEquals( 'passwordtooshort', $user->getPasswordValidity( 'a' ) );
325 $longPass = str_repeat( 'a', 31 );
326 $this->assertFalse( $user->isValidPassword( $longPass ) );
327 $this->assertFalse( $user->checkPasswordValidity( $longPass )->isGood() );
328 $this->assertFalse( $user->checkPasswordValidity( $longPass )->isOK() );
329 $this->assertEquals( 'passwordtoolong', $user->getPasswordValidity( $longPass ) );
332 $this->assertFalse( $user->checkPasswordValidity( 'Useruser' )->isGood() );
333 $this->assertTrue( $user->checkPasswordValidity( 'Useruser' )->isOK() );
334 $this->assertEquals( 'password-name-match', $user->getPasswordValidity( 'Useruser' ) );
336 // On the forbidden list
337 $this->assertFalse( $user->checkPasswordValidity( 'Passpass' )->isGood() );
338 $this->assertEquals( 'password-login-forbidden', $user->getPasswordValidity( 'Passpass' ) );
342 * @covers User::getCanonicalName()
343 * @dataProvider provideGetCanonicalName
345 public function testGetCanonicalName( $name, $expectedArray, $msg ) {
346 foreach ( $expectedArray as $validate => $expected ) {
349 User
::getCanonicalName( $name, $validate === 'false' ?
false : $validate ),
350 $msg . ' (' . $validate . ')'
355 public static function provideGetCanonicalName() {
357 array( ' Trailing space ', array( 'creatable' => 'Trailing space' ), 'Trailing spaces' ),
358 // @todo FIXME: Maybe the creatable name should be 'Talk:Username' or false to reject?
359 array( 'Talk:Username', array( 'creatable' => 'Username', 'usable' => 'Username',
360 'valid' => 'Username', 'false' => 'Talk:Username' ), 'Namespace prefix' ),
361 array( ' name with # hash', array( 'creatable' => false, 'usable' => false ), 'With hash' ),
362 array( 'Multi spaces', array( 'creatable' => 'Multi spaces',
363 'usable' => 'Multi spaces' ), 'Multi spaces' ),
364 array( 'lowercase', array( 'creatable' => 'Lowercase' ), 'Lowercase' ),
365 array( 'in[]valid', array( 'creatable' => false, 'usable' => false, 'valid' => false,
366 'false' => 'In[]valid' ), 'Invalid' ),
367 array( 'with / slash', array( 'creatable' => false, 'usable' => false, 'valid' => false,
368 'false' => 'With / slash' ), 'With slash' ),
373 * @covers User::equals
375 public function testEquals() {
376 $first = User
::newFromName( 'EqualUser' );
377 $second = User
::newFromName( 'EqualUser' );
379 $this->assertTrue( $first->equals( $first ) );
380 $this->assertTrue( $first->equals( $second ) );
381 $this->assertTrue( $second->equals( $first ) );
383 $third = User
::newFromName( '0' );
384 $fourth = User
::newFromName( '000' );
386 $this->assertFalse( $third->equals( $fourth ) );
387 $this->assertFalse( $fourth->equals( $third ) );
389 // Test users loaded from db with id
390 $user = User
::newFromName( 'EqualUnitTestUser' );
391 if ( !$user->getId() ) {
392 $user->addToDatabase();
395 $id = $user->getId();
397 $fifth = User
::newFromId( $id );
398 $sixth = User
::newFromName( 'EqualUnitTestUser' );
399 $this->assertTrue( $fifth->equals( $sixth ) );
403 * @covers User::getId
405 public function testGetId() {
406 $user = User
::newFromName( 'UTSysop' );
407 $this->assertTrue( $user->getId() > 0 );
412 * @covers User::isLoggedIn
413 * @covers User::isAnon
415 public function testLoggedIn() {
416 $user = User
::newFromName( 'UTSysop' );
417 $this->assertTrue( $user->isLoggedIn() );
418 $this->assertFalse( $user->isAnon() );
420 // Non-existent users are perceived as anonymous
421 $user = User
::newFromName( 'UTNonexistent' );
422 $this->assertFalse( $user->isLoggedIn() );
423 $this->assertTrue( $user->isAnon() );
426 $this->assertFalse( $user->isLoggedIn() );
427 $this->assertTrue( $user->isAnon() );
431 * @covers User::checkAndSetTouched
433 public function testCheckAndSetTouched() {
434 $user = TestingAccessWrapper
::newFromObject( User
::newFromName( 'UTSysop' ) );
435 $this->assertTrue( $user->isLoggedIn() );
437 $touched = $user->getDBTouched();
439 $user->checkAndSetTouched(), "checkAndSetTouched() succeded" );
440 $this->assertGreaterThan(
441 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched()" );
443 $touched = $user->getDBTouched();
445 $user->checkAndSetTouched(), "checkAndSetTouched() succeded #2" );
446 $this->assertGreaterThan(
447 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
450 public static function setExtendedLoginCookieDataProvider() {
454 $secondsInDay = 86400;
456 // Arbitrary durations, in units of days, to ensure it chooses the
457 // right one. There is a 5-minute grace period (see testSetExtendedLoginCookie)
458 // to work around slow tests, since we're not currently mocking time() for PHP.
460 $durationOne = $secondsInDay * 5;
461 $durationTwo = $secondsInDay * 29;
462 $durationThree = $secondsInDay * 17;
464 // If $wgExtendedLoginCookieExpiration is null, then the expiry passed to
465 // set cookie is time() + $wgCookieExpiration
472 // If $wgExtendedLoginCookieExpiration isn't null, then the expiry passed to
473 // set cookie is $now + $wgExtendedLoginCookieExpiration
484 * @dataProvider setExtendedLoginCookieDataProvider
485 * @covers User::getRequest
486 * @covers User::setCookie
487 * @backupGlobals enabled
489 public function testSetExtendedLoginCookie(
490 $extendedLoginCookieExpiration,
494 $this->setMwGlobals( array(
495 'wgExtendedLoginCookieExpiration' => $extendedLoginCookieExpiration,
496 'wgCookieExpiration' => $cookieExpiration,
499 $response = $this->getMock( 'WebResponse' );
500 $setcookieSpy = $this->any();
501 $response->expects( $setcookieSpy )
502 ->method( 'setcookie' );
504 $request = new MockWebRequest( $response );
505 $user = new UserProxy( User
::newFromSession( $request ) );
506 $user->setExtendedLoginCookie( 'name', 'value', true );
508 $setcookieInvocations = $setcookieSpy->getInvocations();
509 $setcookieInvocation = end( $setcookieInvocations );
510 $actualExpiry = $setcookieInvocation->parameters
[2];
512 // TODO: ± 600 seconds compensates for
513 // slow-running tests. However, the dependency on the time
514 // function should be removed. This requires some way
515 // to mock/isolate User->setExtendedLoginCookie's call to time()
516 $this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 );
520 class UserProxy
extends User
{
527 public function __construct( User
$user ) {
531 public function setExtendedLoginCookie( $name, $value, $secure ) {
532 $this->user
->setExtendedLoginCookie( $name, $value, $secure );