3 use MediaWiki\MediaWikiServices
;
4 use MediaWiki\Session\SessionManager
;
5 use Wikimedia\ScopedCallback
;
6 use Wikimedia\TestingAccessWrapper
;
12 class BotPasswordTest
extends MediaWikiTestCase
{
18 private $testUserName;
20 protected function setUp() {
23 $this->setMwGlobals( [
24 'wgEnableBotPasswords' => true,
25 'wgBotPasswordsDatabase' => false,
26 'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
27 'wgGrantPermissions' => [
28 'test' => [ 'read' => true ],
30 'wgUserrightsInterwikiDelimiter' => '@',
33 $this->testUser
= $this->getMutableTestUser();
34 $this->testUserName
= $this->testUser
->getUser()->getName();
36 $mock1 = $this->getMockForAbstractClass( CentralIdLookup
::class );
37 $mock1->expects( $this->any() )->method( 'isAttached' )
38 ->will( $this->returnValue( true ) );
39 $mock1->expects( $this->any() )->method( 'lookupUserNames' )
40 ->will( $this->returnValue( [ $this->testUserName
=> 42, 'UTDummy' => 43, 'UTInvalid' => 0 ] ) );
41 $mock1->expects( $this->never() )->method( 'lookupCentralIds' );
43 $mock2 = $this->getMockForAbstractClass( CentralIdLookup
::class );
44 $mock2->expects( $this->any() )->method( 'isAttached' )
45 ->will( $this->returnValue( false ) );
46 $mock2->expects( $this->any() )->method( 'lookupUserNames' )
47 ->will( $this->returnArgument( 0 ) );
48 $mock2->expects( $this->never() )->method( 'lookupCentralIds' );
50 $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', [
51 'BotPasswordTest OkMock' => [ 'factory' => function () use ( $mock1 ) {
54 'BotPasswordTest FailMock' => [ 'factory' => function () use ( $mock2 ) {
59 CentralIdLookup
::resetCache();
62 public function addDBData() {
63 $passwordFactory = MediaWikiServices
::getInstance()->getPasswordFactory();
64 $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
66 $dbw = wfGetDB( DB_MASTER
);
69 [ 'bp_user' => [ 42, 43 ], 'bp_app_id' => 'BotPassword' ],
77 'bp_app_id' => 'BotPassword',
78 'bp_password' => $passwordHash->toString(),
79 'bp_token' => 'token!',
80 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
81 'bp_grants' => '["test"]',
85 'bp_app_id' => 'BotPassword',
86 'bp_password' => $passwordHash->toString(),
87 'bp_token' => 'token!',
88 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
89 'bp_grants' => '["test"]',
96 public function testBasics() {
97 $user = $this->testUser
->getUser();
98 $bp = BotPassword
::newFromUser( $user, 'BotPassword' );
99 $this->assertInstanceOf( BotPassword
::class, $bp );
100 $this->assertTrue( $bp->isSaved() );
101 $this->assertSame( 42, $bp->getUserCentralId() );
102 $this->assertSame( 'BotPassword', $bp->getAppId() );
103 $this->assertSame( 'token!', trim( $bp->getToken(), " \0" ) );
104 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
105 $this->assertSame( [ 'test' ], $bp->getGrants() );
107 $this->assertNull( BotPassword
::newFromUser( $user, 'DoesNotExist' ) );
109 $this->setMwGlobals( [
110 'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock'
112 $this->assertNull( BotPassword
::newFromUser( $user, 'BotPassword' ) );
114 $this->assertSame( '@', BotPassword
::getSeparator() );
115 $this->setMwGlobals( [
116 'wgUserrightsInterwikiDelimiter' => '#',
118 $this->assertSame( '#', BotPassword
::getSeparator() );
121 public function testUnsaved() {
122 $user = $this->testUser
->getUser();
123 $bp = BotPassword
::newUnsaved( [
125 'appId' => 'DoesNotExist'
127 $this->assertInstanceOf( BotPassword
::class, $bp );
128 $this->assertFalse( $bp->isSaved() );
129 $this->assertSame( 42, $bp->getUserCentralId() );
130 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
131 $this->assertEquals( MWRestrictions
::newDefault(), $bp->getRestrictions() );
132 $this->assertSame( [], $bp->getGrants() );
134 $bp = BotPassword
::newUnsaved( [
135 'username' => 'UTDummy',
136 'appId' => 'DoesNotExist2',
137 'restrictions' => MWRestrictions
::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
138 'grants' => [ 'test' ],
140 $this->assertInstanceOf( BotPassword
::class, $bp );
141 $this->assertFalse( $bp->isSaved() );
142 $this->assertSame( 43, $bp->getUserCentralId() );
143 $this->assertSame( 'DoesNotExist2', $bp->getAppId() );
144 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
145 $this->assertSame( [ 'test' ], $bp->getGrants() );
147 $user = $this->testUser
->getUser();
148 $bp = BotPassword
::newUnsaved( [
150 'appId' => 'DoesNotExist'
152 $this->assertInstanceOf( BotPassword
::class, $bp );
153 $this->assertFalse( $bp->isSaved() );
154 $this->assertSame( 45, $bp->getUserCentralId() );
155 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
157 $user = $this->testUser
->getUser();
158 $bp = BotPassword
::newUnsaved( [
160 'appId' => 'BotPassword'
162 $this->assertInstanceOf( BotPassword
::class, $bp );
163 $this->assertFalse( $bp->isSaved() );
165 $this->assertNull( BotPassword
::newUnsaved( [
169 $this->assertNull( BotPassword
::newUnsaved( [
171 'appId' => str_repeat( 'X', BotPassword
::APPID_MAXLENGTH +
1 ),
173 $this->assertNull( BotPassword
::newUnsaved( [
174 'user' => $this->testUserName
,
177 $this->assertNull( BotPassword
::newUnsaved( [
178 'username' => 'UTInvalid',
181 $this->assertNull( BotPassword
::newUnsaved( [
186 public function testGetPassword() {
187 /** @var BotPassword $bp */
188 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
190 $password = $bp->getPassword();
191 $this->assertInstanceOf( Password
::class, $password );
192 $this->assertTrue( $password->verify( 'foobaz' ) );
195 $password = $bp->getPassword();
196 $this->assertInstanceOf( InvalidPassword
::class, $password );
198 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
199 $dbw = wfGetDB( DB_MASTER
);
202 [ 'bp_password' => 'garbage' ],
203 [ 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ],
206 $password = $bp->getPassword();
207 $this->assertInstanceOf( InvalidPassword
::class, $password );
210 public function testInvalidateAllPasswordsForUser() {
211 $bp1 = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
212 $bp2 = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 43, 'BotPassword' ) );
214 $this->assertNotInstanceOf( InvalidPassword
::class, $bp1->getPassword(), 'sanity check' );
215 $this->assertNotInstanceOf( InvalidPassword
::class, $bp2->getPassword(), 'sanity check' );
216 BotPassword
::invalidateAllPasswordsForUser( $this->testUserName
);
217 $this->assertInstanceOf( InvalidPassword
::class, $bp1->getPassword() );
218 $this->assertNotInstanceOf( InvalidPassword
::class, $bp2->getPassword() );
220 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
221 $this->assertInstanceOf( InvalidPassword
::class, $bp->getPassword() );
224 public function testRemoveAllPasswordsForUser() {
225 $this->assertNotNull( BotPassword
::newFromCentralId( 42, 'BotPassword' ), 'sanity check' );
226 $this->assertNotNull( BotPassword
::newFromCentralId( 43, 'BotPassword' ), 'sanity check' );
228 BotPassword
::removeAllPasswordsForUser( $this->testUserName
);
230 $this->assertNull( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
231 $this->assertNotNull( BotPassword
::newFromCentralId( 43, 'BotPassword' ) );
235 * @dataProvider provideCanonicalizeLoginData
237 public function testCanonicalizeLoginData( $username, $password, $expectedResult ) {
238 $result = BotPassword
::canonicalizeLoginData( $username, $password );
239 if ( is_array( $expectedResult ) ) {
240 $this->assertArrayEquals( $expectedResult, $result, true, true );
242 $this->assertSame( $expectedResult, $result );
246 public function provideCanonicalizeLoginData() {
248 [ 'user', 'pass', false ],
249 [ 'user', 'abc@def', false ],
250 [ 'legacy@user', 'pass', false ],
251 [ 'user@bot', '12345678901234567890123456789012',
252 [ 'user@bot', '12345678901234567890123456789012' ] ],
253 [ 'user', 'bot@12345678901234567890123456789012',
254 [ 'user@bot', '12345678901234567890123456789012' ] ],
255 [ 'user', 'bot@12345678901234567890123456789012345',
256 [ 'user@bot', '12345678901234567890123456789012345' ] ],
257 [ 'user', 'bot@x@12345678901234567890123456789012',
258 [ 'user@bot@x', '12345678901234567890123456789012' ] ],
262 public function testLogin() {
263 // Test failure when bot passwords aren't enabled
264 $this->setMwGlobals( 'wgEnableBotPasswords', false );
265 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest
);
266 $this->assertEquals( Status
::newFatal( 'botpasswords-disabled' ), $status );
267 $this->setMwGlobals( 'wgEnableBotPasswords', true );
269 // Test failure when BotPasswordSessionProvider isn't configured
270 $manager = new SessionManager( [
271 'logger' => new Psr\Log\NullLogger
,
272 'store' => new EmptyBagOStuff
,
274 $reset = MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
276 $manager->getProvider( MediaWiki\Session\BotPasswordSessionProvider
::class ),
279 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest
);
280 $this->assertEquals( Status
::newFatal( 'botpasswords-no-provider' ), $status );
281 ScopedCallback
::consume( $reset );
283 // Now configure BotPasswordSessionProvider for further tests...
284 $mainConfig = RequestContext
::getMain()->getConfig();
285 $config = new HashConfig( [
286 'SessionProviders' => $mainConfig->get( 'SessionProviders' ) +
[
287 MediaWiki\Session\BotPasswordSessionProvider
::class => [
288 'class' => MediaWiki\Session\BotPasswordSessionProvider
::class,
289 'args' => [ [ 'priority' => 40 ] ],
293 $manager = new SessionManager( [
294 'config' => new MultiConfig( [ $config, RequestContext
::getMain()->getConfig() ] ),
295 'logger' => new Psr\Log\NullLogger
,
296 'store' => new EmptyBagOStuff
,
298 $reset = MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
300 // No "@"-thing in the username
301 $status = BotPassword
::login( $this->testUserName
, 'foobaz', new FauxRequest
);
302 $this->assertEquals( Status
::newFatal( 'botpasswords-invalid-name', '@' ), $status );
305 $status = BotPassword
::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest
);
306 $this->assertEquals( Status
::newFatal( 'nosuchuser', 'UTDummy' ), $status );
309 $status = BotPassword
::login( "{$this->testUserName}@DoesNotExist", 'foobaz', new FauxRequest
);
311 Status
::newFatal( 'botpasswords-not-exist', $this->testUserName
, 'DoesNotExist' ),
315 // Failed restriction
316 $request = $this->getMockBuilder( FauxRequest
::class )
317 ->setMethods( [ 'getIP' ] )
319 $request->expects( $this->any() )->method( 'getIP' )
320 ->will( $this->returnValue( '10.0.0.1' ) );
321 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
322 $this->assertEquals( Status
::newFatal( 'botpasswords-restriction-failed' ), $status );
325 $status = BotPassword
::login(
326 "{$this->testUserName}@BotPassword", $this->testUser
->getPassword(), new FauxRequest
);
327 $this->assertEquals( Status
::newFatal( 'wrongpassword' ), $status );
330 $request = new FauxRequest
;
331 $this->assertNotInstanceOf(
332 MediaWiki\Session\BotPasswordSessionProvider
::class,
333 $request->getSession()->getProvider(),
336 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
337 $this->assertInstanceOf( Status
::class, $status );
338 $this->assertTrue( $status->isGood() );
339 $session = $status->getValue();
340 $this->assertInstanceOf( MediaWiki\Session\Session
::class, $session );
341 $this->assertInstanceOf(
342 MediaWiki\Session\BotPasswordSessionProvider
::class, $session->getProvider()
344 $this->assertSame( $session->getId(), $request->getSession()->getId() );
346 ScopedCallback
::consume( $reset );
350 * @dataProvider provideSave
351 * @param string|null $password
353 public function testSave( $password ) {
354 $passwordFactory = MediaWikiServices
::getInstance()->getPasswordFactory();
356 $bp = BotPassword
::newUnsaved( [
358 'appId' => 'TestSave',
359 'restrictions' => MWRestrictions
::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
360 'grants' => [ 'test' ],
362 $this->assertFalse( $bp->isSaved(), 'sanity check' );
364 BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
), 'sanity check'
367 $passwordHash = $password ?
$passwordFactory->newFromPlaintext( $password ) : null;
368 $this->assertFalse( $bp->save( 'update', $passwordHash ) );
369 $this->assertTrue( $bp->save( 'insert', $passwordHash ) );
370 $bp2 = BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
);
371 $this->assertInstanceOf( BotPassword
::class, $bp2 );
372 $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
373 $this->assertEquals( $bp->getAppId(), $bp2->getAppId() );
374 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
375 $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() );
376 $this->assertEquals( $bp->getGrants(), $bp2->getGrants() );
377 /** @var Password $pw */
378 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
379 if ( $password === null ) {
380 $this->assertInstanceOf( InvalidPassword
::class, $pw );
382 $this->assertTrue( $pw->verify( $password ) );
385 $token = $bp->getToken();
386 $this->assertEquals( 42, $bp->getUserCentralId() );
387 $this->assertEquals( 'TestSave', $bp->getAppId() );
388 $this->assertFalse( $bp->save( 'insert' ) );
389 $this->assertTrue( $bp->save( 'update' ) );
390 $this->assertNotEquals( $token, $bp->getToken() );
391 $bp2 = BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
);
392 $this->assertInstanceOf( BotPassword
::class, $bp2 );
393 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
394 /** @var Password $pw */
395 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
396 if ( $password === null ) {
397 $this->assertInstanceOf( InvalidPassword
::class, $pw );
399 $this->assertTrue( $pw->verify( $password ) );
402 $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' );
403 $token = $bp->getToken();
404 $this->assertTrue( $bp->save( 'update', $passwordHash ) );
405 $this->assertNotEquals( $token, $bp->getToken() );
406 /** @var Password $pw */
407 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
408 $this->assertTrue( $pw->verify( 'XXX' ) );
410 $this->assertTrue( $bp->delete() );
411 $this->assertFalse( $bp->isSaved() );
412 $this->assertNull( BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
) );
414 $this->assertFalse( $bp->save( 'foobar' ) );
417 public static function provideSave() {