Add PasswordFactory to MediaWikiServices
[lhc/web/wiklou.git] / tests / phpunit / includes / user / BotPasswordTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use MediaWiki\Session\SessionManager;
5 use Wikimedia\ScopedCallback;
6 use Wikimedia\TestingAccessWrapper;
7
8 /**
9 * @covers BotPassword
10 * @group Database
11 */
12 class BotPasswordTest extends MediaWikiTestCase {
13
14 /** @var TestUser */
15 private $testUser;
16
17 /** @var string */
18 private $testUserName;
19
20 protected function setUp() {
21 parent::setUp();
22
23 $this->setMwGlobals( [
24 'wgEnableBotPasswords' => true,
25 'wgBotPasswordsDatabase' => false,
26 'wgCentralIdLookupProvider' => 'BotPasswordTest OkMock',
27 'wgGrantPermissions' => [
28 'test' => [ 'read' => true ],
29 ],
30 'wgUserrightsInterwikiDelimiter' => '@',
31 ] );
32
33 $this->testUser = $this->getMutableTestUser();
34 $this->testUserName = $this->testUser->getUser()->getName();
35
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' );
42
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' );
49
50 $this->mergeMwGlobalArrayValue( 'wgCentralIdLookupProviders', [
51 'BotPasswordTest OkMock' => [ 'factory' => function () use ( $mock1 ) {
52 return $mock1;
53 } ],
54 'BotPasswordTest FailMock' => [ 'factory' => function () use ( $mock2 ) {
55 return $mock2;
56 } ],
57 ] );
58
59 CentralIdLookup::resetCache();
60 }
61
62 public function addDBData() {
63 $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
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::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() );
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::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() );
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::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() );
146
147 $user = $this->testUser->getUser();
148 $bp = BotPassword::newUnsaved( [
149 'centralId' => 45,
150 'appId' => 'DoesNotExist'
151 ] );
152 $this->assertInstanceOf( BotPassword::class, $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::class, $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::class, $password );
191 $this->assertTrue( $password->equals( 'foobaz' ) );
192
193 $bp->centralId = 44;
194 $password = $bp->getPassword();
195 $this->assertInstanceOf( InvalidPassword::class, $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::class, $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::class, $bp1->getPassword(), 'sanity check' );
214 $this->assertNotInstanceOf( InvalidPassword::class, $bp2->getPassword(), 'sanity check' );
215 BotPassword::invalidateAllPasswordsForUser( $this->testUserName );
216 $this->assertInstanceOf( InvalidPassword::class, $bp1->getPassword() );
217 $this->assertNotInstanceOf( InvalidPassword::class, $bp2->getPassword() );
218
219 $bp = TestingAccessWrapper::newFromObject( BotPassword::newFromCentralId( 42, 'BotPassword' ) );
220 $this->assertInstanceOf( InvalidPassword::class, $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::class )
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::class, $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 = MediaWikiServices::getInstance()->getPasswordFactory();
354
355 $bp = BotPassword::newUnsaved( [
356 'centralId' => 42,
357 'appId' => 'TestSave',
358 'restrictions' => MWRestrictions::newFromJson( '{"IPAddresses":["127.0.0.0/8"]}' ),
359 'grants' => [ 'test' ],
360 ] );
361 $this->assertFalse( $bp->isSaved(), 'sanity check' );
362 $this->assertNull(
363 BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ), 'sanity check'
364 );
365
366 $passwordHash = $password ? $passwordFactory->newFromPlaintext( $password ) : null;
367 $this->assertFalse( $bp->save( 'update', $passwordHash ) );
368 $this->assertTrue( $bp->save( 'insert', $passwordHash ) );
369 $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
370 $this->assertInstanceOf( BotPassword::class, $bp2 );
371 $this->assertEquals( $bp->getUserCentralId(), $bp2->getUserCentralId() );
372 $this->assertEquals( $bp->getAppId(), $bp2->getAppId() );
373 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
374 $this->assertEquals( $bp->getRestrictions(), $bp2->getRestrictions() );
375 $this->assertEquals( $bp->getGrants(), $bp2->getGrants() );
376 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
377 if ( $password === null ) {
378 $this->assertInstanceOf( InvalidPassword::class, $pw );
379 } else {
380 $this->assertTrue( $pw->equals( $password ) );
381 }
382
383 $token = $bp->getToken();
384 $this->assertEquals( 42, $bp->getUserCentralId() );
385 $this->assertEquals( 'TestSave', $bp->getAppId() );
386 $this->assertFalse( $bp->save( 'insert' ) );
387 $this->assertTrue( $bp->save( 'update' ) );
388 $this->assertNotEquals( $token, $bp->getToken() );
389 $bp2 = BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST );
390 $this->assertInstanceOf( BotPassword::class, $bp2 );
391 $this->assertEquals( $bp->getToken(), $bp2->getToken() );
392 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
393 if ( $password === null ) {
394 $this->assertInstanceOf( InvalidPassword::class, $pw );
395 } else {
396 $this->assertTrue( $pw->equals( $password ) );
397 }
398
399 $passwordHash = $passwordFactory->newFromPlaintext( 'XXX' );
400 $token = $bp->getToken();
401 $this->assertTrue( $bp->save( 'update', $passwordHash ) );
402 $this->assertNotEquals( $token, $bp->getToken() );
403 $pw = TestingAccessWrapper::newFromObject( $bp )->getPassword();
404 $this->assertTrue( $pw->equals( 'XXX' ) );
405
406 $this->assertTrue( $bp->delete() );
407 $this->assertFalse( $bp->isSaved() );
408 $this->assertNull( BotPassword::newFromCentralId( 42, 'TestSave', BotPassword::READ_LATEST ) );
409
410 $this->assertFalse( $bp->save( 'foobar' ) );
411 }
412
413 public static function provideSave() {
414 return [
415 [ null ],
416 [ 'foobar' ],
417 ];
418 }
419 }