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