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