]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - tests/phpunit/includes/user/BotPasswordTest.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / tests / phpunit / includes / user / BotPasswordTest.php
1 <?php
2
3 use MediaWiki\Session\SessionManager;
4 use Wikimedia\ScopedCallback;
5 use Wikimedia\TestingAccessWrapper;
6
7 /**
8  * @covers BotPassword
9  * @group Database
10  */
11 class BotPasswordTest extends MediaWikiTestCase {
12
13         /** @var TestUser */
14         private $testUser;
15
16         /** @var string */
17         private $testUserName;
18
19         protected function setUp() {
20                 parent::setUp();
21
22                 $this->setMwGlobals( [
23                         'wgEnableBotPasswords' => true,
24                         'wgBotPasswordsDatabase' => false,
25                         'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
26                         'wgGrantPermissions' => [
27                                 'test' => [ 'read' => true ],
28                         ],
29                         'wgUserrightsInterwikiDelimiter' => '@',
30                 ] );
31
32                 $this->testUser = $this->getMutableTestUser();
33                 $this->testUserName = $this->testUser->getUser()->getName();
34
35                 $mock1 = $this->getMockForAbstractClass( 'CentralIdLookup' );
36                 $mock1->expects( $this->any() )->method( 'isAttached' )
37                         ->will( $this->returnValue( true ) );
38                 $mock1->expects( $this->any() )->method( 'lookupUserNames' )
39                         ->will( $this->returnValue( [ $this->testUserName => 42, 'UTDummy' => 43, 'UTInvalid' => 0 ] ) );
40                 $mock1->expects( $this->never() )->method( 'lookupCentralIds' );
41
42                 $mock2 = $this->getMockForAbstractClass( 'CentralIdLookup' );
43                 $mock2->expects( $this->any() )->method( 'isAttached' )
44                         ->will( $this->returnValue( false ) );
45                 $mock2->expects( $this->any() )->method( 'lookupUserNames' )
46                         ->will( $this->returnArgument( 0 ) );
47                 $mock2->expects( $this->never() )->method( 'lookupCentralIds' );
48
49                 $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', [
50                         'BotPasswordTest OkMock' => [ 'factory' => function () use ( $mock1 ) {
51                                 return $mock1;
52                         } ],
53                         'BotPasswordTest FailMock' => [ 'factory' => function () use ( $mock2 ) {
54                                 return $mock2;
55                         } ],
56                 ] );
57
58                 CentralIdLookup::resetCache();
59         }
60
61         public function addDBData() {
62                 $passwordFactory = new \PasswordFactory();
63                 $passwordFactory->init( \RequestContext::getMain()->getConfig() );
64                 $passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
65
66                 $dbw = wfGetDB( DB_MASTER );
67                 $dbw->delete(
68                         'bot_passwords',
69                         [ 'bp_user' => [ 42, 43 ], 'bp_app_id' => 'BotPassword' ],
70                         __METHOD__
71                 );
72                 $dbw->insert(
73                         'bot_passwords',
74                         [
75                                 [
76                                         'bp_user' => 42,
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"]',
82                                 ],
83                                 [
84                                         'bp_user' => 43,
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"]',
90                                 ],
91                         ],
92                         __METHOD__
93                 );
94         }
95
96         public function testBasics() {
97                 $user = $this->testUser->getUser();
98                 $bp = BotPassword::newFromUser( $user, 'BotPassword' );
99                 $this->assertInstanceOf( 'BotPassword', $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() );
106
107                 $this->assertNull( BotPassword::newFromUser( $user, 'DoesNotExist' ) );
108
109                 $this->setMwGlobals( [
110                         'wgCentralIdLookupProvider' => 'BotPasswordTest FailMock'
111                 ] );
112                 $this->assertNull( BotPassword::newFromUser( $user, 'BotPassword' ) );
113
114                 $this->assertSame( '@', BotPassword::getSeparator() );
115                 $this->setMwGlobals( [
116                         'wgUserrightsInterwikiDelimiter' => '#',
117                 ] );
118                 $this->assertSame( '#', BotPassword::getSeparator() );
119         }
120
121         public function testUnsaved() {
122                 $user = $this->testUser->getUser();
123                 $bp = BotPassword::newUnsaved( [
124                         'user' => $user,
125                         'appId' => 'DoesNotExist'
126                 ] );
127                 $this->assertInstanceOf( 'BotPassword', $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() );
133
134                 $bp = BotPassword::newUnsaved( [
135                         'username' => 'UTDummy',
136                         'appId' => 'DoesNotExist2',
137                         'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
138                         'grants' => [ 'test' ],
139                 ] );
140                 $this->assertInstanceOf( 'BotPassword', $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() );
146
147                 $user = $this->testUser->getUser();
148                 $bp = BotPassword::newUnsaved( [
149                         'centralId' => 45,
150                         'appId' => 'DoesNotExist'
151                 ] );
152                 $this->assertInstanceOf( 'BotPassword', $bp );
153                 $this->assertFalse( $bp->isSaved() );
154                 $this->assertSame( 45, $bp->getUserCentralId() );
155                 $this->assertSame( 'DoesNotExist', $bp->getAppId() );
156
157                 $user = $this->testUser->getUser();
158                 $bp = BotPassword::newUnsaved( [
159                         'user' => $user,
160                         'appId' => 'BotPassword'
161                 ] );
162                 $this->assertInstanceOf( 'BotPassword', $bp );
163                 $this->assertFalse( $bp->isSaved() );
164
165                 $this->assertNull( BotPassword::newUnsaved( [
166                         'user' => $user,
167                         'appId' => '',
168                 ] ) );
169                 $this->assertNull( BotPassword::newUnsaved( [
170                         'user' => $user,
171                         'appId' => str_repeat( 'X', BotPassword::APPID_MAXLENGTH + 1 ),
172                 ] ) );
173                 $this->assertNull( BotPassword::newUnsaved( [
174                         'user' => $this->testUserName,
175                         'appId' => 'Ok',
176                 ] ) );
177                 $this->assertNull( BotPassword::newUnsaved( [
178                         'username' => 'UTInvalid',
179                         'appId' => 'Ok',
180                 ] ) );
181                 $this->assertNull( BotPassword::newUnsaved( [
182                         'appId' => 'Ok',
183                 ] ) );
184         }
185
186         public function testGetPassword() {
187                 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
188
189                 $password = $bp->getPassword();
190                 $this->assertInstanceOf( 'Password', $password );
191                 $this->assertTrue( $password->equals( 'foobaz' ) );
192
193                 $bp->centralId = 44;
194                 $password = $bp->getPassword();
195                 $this->assertInstanceOf( 'InvalidPassword', $password );
196
197                 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
198                 $dbw = wfGetDB( DB_MASTER );
199                 $dbw->update(
200                         'bot_passwords',
201                         [ 'bp_password' => 'garbage' ],
202                         [ 'bp_user' => 42, 'bp_app_id' => 'BotPassword' ],
203                         __METHOD__
204                 );
205                 $password = $bp->getPassword();
206                 $this->assertInstanceOf( 'InvalidPassword', $password );
207         }
208
209         public function testInvalidateAllPasswordsForUser() {
210                 $bp1 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
211                 $bp2 = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
212
213                 $this->assertNotInstanceOf( 'InvalidPassword', $bp1->getPassword(), 'sanity check' );
214                 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword(), 'sanity check' );
215                 BotPassword::invalidateAllPasswordsForUser( $this->testUserName );
216                 $this->assertInstanceOf( 'InvalidPassword', $bp1->getPassword() );
217                 $this->assertNotInstanceOf( 'InvalidPassword', $bp2->getPassword() );
218
219                 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
220                 $this->assertInstanceOf( 'InvalidPassword', $bp->getPassword() );
221         }
222
223         public function testRemoveAllPasswordsForUser() {
224                 $this->assertNotNull( BotPassword::newFromCentralId( 42, 'BotPassword' ), 'sanity check' );
225                 $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ), 'sanity check' );
226
227                 BotPassword::removeAllPasswordsForUser( $this->testUserName );
228
229                 $this->assertNull( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
230                 $this->assertNotNull( BotPassword::newFromCentralId( 43, 'BotPassword' ) );
231         }
232
233         /**
234          * @dataProvider provideCanonicalizeLoginData
235          */
236         public function testCanonicalizeLoginData( $username, $password, $expectedResult ) {
237                 $result = BotPassword::canonicalizeLoginData( $username, $password );
238                 if ( is_array( $expectedResult ) ) {
239                         $this->assertArrayEquals( $expectedResult, $result, true, true );
240                 } else {
241                         $this->assertSame( $expectedResult, $result );
242                 }
243         }
244
245         public function provideCanonicalizeLoginData() {
246                 return [
247                         [ 'user', 'pass', false ],
248                         [ 'user', 'abc@def', false ],
249                         [ 'legacy@user', 'pass', false ],
250                         [ 'user@bot', '12345678901234567890123456789012',
251                                 [ 'user@bot', '12345678901234567890123456789012', true ] ],
252                         [ 'user', 'bot@12345678901234567890123456789012',
253                                 [ 'user@bot', '12345678901234567890123456789012', true ] ],
254                         [ 'user', 'bot@12345678901234567890123456789012345',
255                                 [ 'user@bot', '12345678901234567890123456789012345', true ] ],
256                         [ 'user', 'bot@x@12345678901234567890123456789012',
257                                 [ 'user@bot@x', '12345678901234567890123456789012', true ] ],
258                 ];
259         }
260
261         public function testLogin() {
262                 // Test failure when bot passwords aren't enabled
263                 $this->setMwGlobals( 'wgEnableBotPasswords', false );
264                 $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest );
265                 $this->assertEquals( Status::newFatal( 'botpasswords-disabled' ), $status );
266                 $this->setMwGlobals( 'wgEnableBotPasswords', true );
267
268                 // Test failure when BotPasswordSessionProvider isn't configured
269                 $manager = new SessionManager( [
270                         'logger' => new Psr\Log\NullLogger,
271                         'store' => new EmptyBagOStuff,
272                 ] );
273                 $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
274                 $this->assertNull(
275                         $manager->getProvider( MediaWiki\Session\BotPasswordSessionProvider::class ),
276                         'sanity check'
277                 );
278                 $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', new FauxRequest );
279                 $this->assertEquals( Status::newFatal( 'botpasswords-no-provider' ), $status );
280                 ScopedCallback::consume( $reset );
281
282                 // Now configure BotPasswordSessionProvider for further tests...
283                 $mainConfig = RequestContext::getMain()->getConfig();
284                 $config = new HashConfig( [
285                         'SessionProviders' => $mainConfig->get( 'SessionProviders' ) + [
286                                 MediaWiki\Session\BotPasswordSessionProvider::class => [
287                                         'class' => MediaWiki\Session\BotPasswordSessionProvider::class,
288                                         'args' => [ [ 'priority' => 40 ] ],
289                                 ]
290                         ],
291                 ] );
292                 $manager = new SessionManager( [
293                         'config' => new MultiConfig( [ $config, RequestContext::getMain()->getConfig() ] ),
294                         'logger' => new Psr\Log\NullLogger,
295                         'store' => new EmptyBagOStuff,
296                 ] );
297                 $reset = MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
298
299                 // No "@"-thing in the username
300                 $status = BotPassword::login( $this->testUserName, 'foobaz', new FauxRequest );
301                 $this->assertEquals( Status::newFatal( 'botpasswords-invalid-name', '@' ), $status );
302
303                 // No base user
304                 $status = BotPassword::login( 'UTDummy@BotPassword', 'foobaz', new FauxRequest );
305                 $this->assertEquals( Status::newFatal( 'nosuchuser', 'UTDummy' ), $status );
306
307                 // No bot password
308                 $status = BotPassword::login( "{$this->testUserName}@DoesNotExist", 'foobaz', new FauxRequest );
309                 $this->assertEquals(
310                         Status::newFatal( 'botpasswords-not-exist', $this->testUserName, 'DoesNotExist' ),
311                         $status
312                 );
313
314                 // Failed restriction
315                 $request = $this->getMockBuilder( 'FauxRequest' )
316                         ->setMethods( [ 'getIP' ] )
317                         ->getMock();
318                 $request->expects( $this->any() )->method( 'getIP' )
319                         ->will( $this->returnValue( '10.0.0.1' ) );
320                 $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
321                 $this->assertEquals( Status::newFatal( 'botpasswords-restriction-failed' ), $status );
322
323                 // Wrong password
324                 $status = BotPassword::login(
325                         "{$this->testUserName}@BotPassword", $this->testUser->getPassword(), new FauxRequest );
326                 $this->assertEquals( Status::newFatal( 'wrongpassword' ), $status );
327
328                 // Success!
329                 $request = new FauxRequest;
330                 $this->assertNotInstanceOf(
331                         MediaWiki\Session\BotPasswordSessionProvider::class,
332                         $request->getSession()->getProvider(),
333                         'sanity check'
334                 );
335                 $status = BotPassword::login( "{$this->testUserName}@BotPassword", 'foobaz', $request );
336                 $this->assertInstanceOf( 'Status', $status );
337                 $this->assertTrue( $status->isGood() );
338                 $session = $status->getValue();
339                 $this->assertInstanceOf( MediaWiki\Session\Session::class, $session );
340                 $this->assertInstanceOf(
341                         MediaWiki\Session\BotPasswordSessionProvider::class, $session->getProvider()
342                 );
343                 $this->assertSame( $session->getId(), $request->getSession()->getId() );
344
345                 ScopedCallback::consume( $reset );
346         }
347
348         /**
349          * @dataProvider provideSave
350          * @param string|null $password
351          */
352         public function testSave( $password ) {
353                 $passwordFactory = new \PasswordFactory();
354                 $passwordFactory->init( \RequestContext::getMain()->getConfig() );
355
356                 $bp = BotPassword::newUnsaved( [
357                         'centralId' => 42,
358                         'appId' => 'TestSave',
359                         'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
360                         'grants' => [ 'test' ],
361                 ] );
362                 $this->assertFalse( $bp->isSaved(), 'sanity check' );
363                 $this->assertNull(
364                         BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check'
365                 );
366
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', $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                 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
378                 if ( $password === null ) {
379                         $this->assertInstanceOf( 'InvalidPassword', $pw );
380                 } else {
381                         $this->assertTrue( $pw->equals( $password ) );
382                 }
383
384                 $token = $bp->getToken();
385                 $this->assertEquals( 42, $bp->getUserCentralId() );
386                 $this->assertEquals( 'TestSave', $bp->getAppId() );
387                 $this->assertFalse( $bp->save( 'insert' ) );
388                 $this->assertTrue( $bp->save( 'update' ) );
389                 $this->assertNotEquals( $token, $bp->getToken() );
390                 $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
391                 $this->assertInstanceOf( 'BotPassword', $bp2 );
392                 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
393                 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
394                 if ( $password === null ) {
395                         $this->assertInstanceOf( 'InvalidPassword', $pw );
396                 } else {
397                         $this->assertTrue( $pw->equals( $password ) );
398                 }
399
400                 $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' );
401                 $token = $bp->getToken();
402                 $this->assertTrue( $bp->save( 'update', $passwordHash ) );
403                 $this->assertNotEquals( $token, $bp->getToken() );
404                 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
405                 $this->assertTrue( $pw->equals( 'XXX' ) );
406
407                 $this->assertTrue( $bp->delete() );
408                 $this->assertFalse( $bp->isSaved() );
409                 $this->assertNull( BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ) );
410
411                 $this->assertFalse( $bp->save( 'foobar' ) );
412         }
413
414         public static function provideSave() {
415                 return [
416                         [ null ],
417                         [ 'foobar' ],
418                 ];
419         }
420 }