3 use MediaWiki\Session\SessionManager
;
9 class BotPasswordTest
extends MediaWikiTestCase
{
15 private $testUserName;
17 protected function setUp() {
20 $this->setMwGlobals( [
21 'wgEnableBotPasswords' => true,
22 'wgBotPasswordsDatabase' => false,
23 'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
24 'wgGrantPermissions' => [
25 'test' => [ 'read' => true ],
27 'wgUserrightsInterwikiDelimiter' => '@',
30 $this->testUser
= $this->getMutableTestUser();
31 $this->testUserName
= $this->testUser
->getUser()->getName();
33 $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' );
34 $mock1->expects( $this->any() )->method( 'isAttached' )
35 ->will( $this->returnValue( true ) );
36 $mock1->expects( $this->any() )->method( 'lookupUserNames' )
37 ->will( $this->returnValue( [ $this->testUserName
=> 42, 'UTDummy' => 43, 'UTInvalid' => 0 ] ) );
38 $mock1->expects( $this->never() )->method( 'lookupCentralIds' );
40 $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' );
41 $mock2->expects( $this->any() )->method( 'isAttached' )
42 ->will( $this->returnValue( false ) );
43 $mock2->expects( $this->any() )->method( 'lookupUserNames' )
44 ->will( $this->returnArgument( 0 ) );
45 $mock2->expects( $this->never() )->method( 'lookupCentralIds' );
47 $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', [
48 'BotPasswordTest OkMock' => [ 'factory' => function () use ( $mock1 ) {
51 'BotPasswordTest FailMock' => [ 'factory' => function () use ( $mock2 ) {
56 CentralIdLookup
::resetCache();
59 public function addDBData() {
60 $passwordFactory = new \
PasswordFactory();
61 $passwordFactory->init( \RequestContext
::getMain()->getConfig() );
62 $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
64 $dbw = wfGetDB( DB_MASTER
);
67 [ 'bp_user' => [ 42, 43 ], 'bp_app_id' => 'BotPassword' ],
75 'bp_app_id' => 'BotPassword',
76 'bp_password' => $passwordHash->toString(),
77 'bp_token' => 'token!',
78 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
79 'bp_grants' => '["test"]',
83 'bp_app_id' => 'BotPassword',
84 'bp_password' => $passwordHash->toString(),
85 'bp_token' => 'token!',
86 'bp_restrictions' => '{"IPAddresses":["127.0.0.0/8"]}',
87 'bp_grants' => '["test"]',
94 public function testBasics() {
95 $user = $this->testUser
->getUser();
96 $bp = BotPassword
::newFromUser( $user, 'BotPassword' );
97 $this->assertInstanceOf( 'BotPassword', $bp );
98 $this->assertTrue( $bp->isSaved() );
99 $this->assertSame( 42, $bp->getUserCentralId() );
100 $this->assertSame( 'BotPassword', $bp->getAppId() );
101 $this->assertSame( 'token!', trim( $bp->getToken(), " \0" ) );
102 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
103 $this->assertSame( [ 'test' ], $bp->getGrants() );
105 $this->assertNull( BotPassword
::newFromUser( $user, 'DoesNotExist' ) );
107 $this->setMwGlobals( [
108 'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock'
110 $this->assertNull( BotPassword
::newFromUser( $user, 'BotPassword' ) );
112 $this->assertSame( '@', BotPassword
::getSeparator() );
113 $this->setMwGlobals( [
114 'wgUserrightsInterwikiDelimiter' => '#',
116 $this->assertSame( '#', BotPassword
::getSeparator() );
119 public function testUnsaved() {
120 $user = $this->testUser
->getUser();
121 $bp = BotPassword
::newUnsaved( [
123 'appId' => 'DoesNotExist'
125 $this->assertInstanceOf( 'BotPassword', $bp );
126 $this->assertFalse( $bp->isSaved() );
127 $this->assertSame( 42, $bp->getUserCentralId() );
128 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
129 $this->assertEquals( MWRestrictions
::newDefault(), $bp->getRestrictions() );
130 $this->assertSame( [], $bp->getGrants() );
132 $bp = BotPassword
::newUnsaved( [
133 'username' => 'UTDummy',
134 'appId' => 'DoesNotExist2',
135 'restrictions' => MWRestrictions
::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
136 'grants' => [ 'test' ],
138 $this->assertInstanceOf( 'BotPassword', $bp );
139 $this->assertFalse( $bp->isSaved() );
140 $this->assertSame( 43, $bp->getUserCentralId() );
141 $this->assertSame( 'DoesNotExist2', $bp->getAppId() );
142 $this->assertEquals( '{"IPAddresses":["127.0.0.0/8"]}', $bp->getRestrictions()->toJson() );
143 $this->assertSame( [ 'test' ], $bp->getGrants() );
145 $user = $this->testUser
->getUser();
146 $bp = BotPassword
::newUnsaved( [
148 'appId' => 'DoesNotExist'
150 $this->assertInstanceOf( 'BotPassword', $bp );
151 $this->assertFalse( $bp->isSaved() );
152 $this->assertSame( 45, $bp->getUserCentralId() );
153 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
155 $user = $this->testUser
->getUser();
156 $bp = BotPassword
::newUnsaved( [
158 'appId' => 'BotPassword'
160 $this->assertInstanceOf( 'BotPassword', $bp );
161 $this->assertFalse( $bp->isSaved() );
163 $this->assertNull( BotPassword
::newUnsaved( [
167 $this->assertNull( BotPassword
::newUnsaved( [
169 'appId' => str_repeat( 'X', BotPassword
::APPID_MAXLENGTH +
1 ),
171 $this->assertNull( BotPassword
::newUnsaved( [
172 'user' => $this->testUserName
,
175 $this->assertNull( BotPassword
::newUnsaved( [
176 'username' => 'UTInvalid',
179 $this->assertNull( BotPassword
::newUnsaved( [
184 public function testGetPassword() {
185 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
187 $password = $bp->getPassword();
188 $this->assertInstanceOf( 'Password', $password );
189 $this->assertTrue( $password->equals( 'foobaz' ) );
192 $password = $bp->getPassword();
193 $this->assertInstanceOf( 'InvalidPassword', $password );
195 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
196 $dbw = wfGetDB( DB_MASTER
);
199 [ 'bp_password' => 'garbage' ],
200 [ 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ],
203 $password = $bp->getPassword();
204 $this->assertInstanceOf( 'InvalidPassword', $password );
207 public function testInvalidateAllPasswordsForUser() {
208 $bp1 = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
209 $bp2 = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 43, 'BotPassword' ) );
211 $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' );
212 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' );
213 BotPassword
::invalidateAllPasswordsForUser( $this->testUserName
);
214 $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() );
215 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() );
217 $bp = TestingAccessWrapper
::newFromObject( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
218 $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() );
221 public function testRemoveAllPasswordsForUser() {
222 $this->assertNotNull( BotPassword
::newFromCentralId( 42, 'BotPassword' ), 'sanity check' );
223 $this->assertNotNull( BotPassword
::newFromCentralId( 43, 'BotPassword' ), 'sanity check' );
225 BotPassword
::removeAllPasswordsForUser( $this->testUserName
);
227 $this->assertNull( BotPassword
::newFromCentralId( 42, 'BotPassword' ) );
228 $this->assertNotNull( BotPassword
::newFromCentralId( 43, 'BotPassword' ) );
232 * @dataProvider provideCanonicalizeLoginData
234 public function testCanonicalizeLoginData( $username, $password, $expectedResult ) {
235 $result = BotPassword
::canonicalizeLoginData( $username, $password );
236 if ( is_array( $expectedResult ) ) {
237 $this->assertArrayEquals( $expectedResult, $result, true, true );
239 $this->assertSame( $expectedResult, $result );
243 public function provideCanonicalizeLoginData() {
245 [ 'user', 'pass', false ],
246 [ 'user', 'abc@def', false ],
247 [ 'legacy@user', 'pass', false ],
248 [ 'user@bot', '12345678901234567890123456789012',
249 [ 'user@bot', '12345678901234567890123456789012', true ] ],
250 [ 'user', 'bot@12345678901234567890123456789012',
251 [ 'user@bot', '12345678901234567890123456789012', true ] ],
252 [ 'user', 'bot@12345678901234567890123456789012345',
253 [ 'user@bot', '12345678901234567890123456789012345', true ] ],
254 [ 'user', 'bot@x@12345678901234567890123456789012',
255 [ 'user@bot@x', '12345678901234567890123456789012', true ] ],
259 public function testLogin() {
260 // Test failure when bot passwords aren't enabled
261 $this->setMwGlobals( 'wgEnableBotPasswords', false );
262 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest
);
263 $this->assertEquals( Status
::newFatal( 'botpasswords-disabled' ), $status );
264 $this->setMwGlobals( 'wgEnableBotPasswords', true );
266 // Test failure when BotPasswordSessionProvider isn't configured
267 $manager = new SessionManager( [
268 'logger' => new Psr\Log\NullLogger
,
269 'store' => new EmptyBagOStuff
,
271 $reset = MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
273 $manager->getProvider( MediaWiki\Session\BotPasswordSessionProvider
::class ),
276 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest
);
277 $this->assertEquals( Status
::newFatal( 'botpasswords-no-provider' ), $status );
278 ScopedCallback
::consume( $reset );
280 // Now configure BotPasswordSessionProvider for further tests...
281 $mainConfig = RequestContext
::getMain()->getConfig();
282 $config = new HashConfig( [
283 'SessionProviders' => $mainConfig->get( 'SessionProviders' ) +
[
284 MediaWiki\Session\BotPasswordSessionProvider
::class => [
285 'class' => MediaWiki\Session\BotPasswordSessionProvider
::class,
286 'args' => [ [ 'priority' => 40 ] ],
290 $manager = new SessionManager( [
291 'config' => new MultiConfig( [ $config, RequestContext
::getMain()->getConfig() ] ),
292 'logger' => new Psr\Log\NullLogger
,
293 'store' => new EmptyBagOStuff
,
295 $reset = MediaWiki\Session\TestUtils
::setSessionManagerSingleton( $manager );
297 // No "@"-thing in the username
298 $status = BotPassword
::login( $this->testUserName
, 'foobaz', new FauxRequest
);
299 $this->assertEquals( Status
::newFatal( 'botpasswords-invalid-name', '@' ), $status );
302 $status = BotPassword
::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest
);
303 $this->assertEquals( Status
::newFatal( 'nosuchuser', 'UTDummy' ), $status );
306 $status = BotPassword
::login( "{$this->testUserName}@DoesNotExist", 'foobaz', new FauxRequest
);
308 Status
::newFatal( 'botpasswords-not-exist', $this->testUserName
, 'DoesNotExist' ),
312 // Failed restriction
313 $request = $this->getMock( 'FauxRequest', [ 'getIP' ] );
314 $request->expects( $this->any() )->method( 'getIP' )
315 ->will( $this->returnValue( '10.0.0.1' ) );
316 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
317 $this->assertEquals( Status
::newFatal( 'botpasswords-restriction-failed' ), $status );
320 $status = BotPassword
::login(
321 "{$this->testUserName}@BotPassword", $this->testUser
->getPassword(), new FauxRequest
);
322 $this->assertEquals( Status
::newFatal( 'wrongpassword' ), $status );
325 $request = new FauxRequest
;
326 $this->assertNotInstanceOf(
327 MediaWiki\Session\BotPasswordSessionProvider
::class,
328 $request->getSession()->getProvider(),
331 $status = BotPassword
::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
332 $this->assertInstanceOf( 'Status', $status );
333 $this->assertTrue( $status->isGood() );
334 $session = $status->getValue();
335 $this->assertInstanceOf( MediaWiki\Session\Session
::class, $session );
336 $this->assertInstanceOf(
337 MediaWiki\Session\BotPasswordSessionProvider
::class, $session->getProvider()
339 $this->assertSame( $session->getId(), $request->getSession()->getId() );
341 ScopedCallback
::consume( $reset );
345 * @dataProvider provideSave
346 * @param string|null $password
348 public function testSave( $password ) {
349 $passwordFactory = new \
PasswordFactory();
350 $passwordFactory->init( \RequestContext
::getMain()->getConfig() );
352 $bp = BotPassword
::newUnsaved( [
354 'appId' => 'TestSave',
355 'restrictions' => MWRestrictions
::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
356 'grants' => [ 'test' ],
358 $this->assertFalse( $bp->isSaved(), 'sanity check' );
360 BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
), 'sanity check'
363 $passwordHash = $password ?
$passwordFactory->newFromPlaintext( $password ) : null;
364 $this->assertFalse( $bp->save( 'update', $passwordHash ) );
365 $this->assertTrue( $bp->save( 'insert', $passwordHash ) );
366 $bp2 = BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
);
367 $this->assertInstanceOf( 'BotPassword', $bp2 );
368 $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
369 $this->assertEquals( $bp->getAppId(), $bp2->getAppId() );
370 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
371 $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() );
372 $this->assertEquals( $bp->getGrants(), $bp2->getGrants() );
373 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
374 if ( $password === null ) {
375 $this->assertInstanceOf( 'InvalidPassword', $pw );
377 $this->assertTrue( $pw->equals( $password ) );
380 $token = $bp->getToken();
381 $this->assertFalse( $bp->save( 'insert' ) );
382 $this->assertTrue( $bp->save( 'update' ) );
383 $this->assertNotEquals( $token, $bp->getToken() );
384 $bp2 = BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
);
385 $this->assertInstanceOf( 'BotPassword', $bp2 );
386 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
387 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
388 if ( $password === null ) {
389 $this->assertInstanceOf( 'InvalidPassword', $pw );
391 $this->assertTrue( $pw->equals( $password ) );
394 $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' );
395 $token = $bp->getToken();
396 $this->assertTrue( $bp->save( 'update', $passwordHash ) );
397 $this->assertNotEquals( $token, $bp->getToken() );
398 $pw = TestingAccessWrapper
::newFromObject( $bp )->getPassword();
399 $this->assertTrue( $pw->equals( 'XXX' ) );
401 $this->assertTrue( $bp->delete() );
402 $this->assertFalse( $bp->isSaved() );
403 $this->assertNull( BotPassword
::newFromCentralId( 42, 'TestSave', BotPassword
::READ_LATEST
) );
405 $this->assertFalse( $bp->save( 'foobar' ) );
408 public static function provideSave() {