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