Merge "phpunit: Avoid use of deprecated getMock for PHPUnit 5 compat"
[lhc/web/wiklou.git] / tests / phpunit / includes / session / SessionBackendTest.php
1 <?php
2
3 namespace MediaWiki\Session;
4
5 use MediaWikiTestCase;
6 use User;
7
8 /**
9 * @group Session
10 * @group Database
11 * @covers MediaWiki\Session\SessionBackend
12 */
13 class SessionBackendTest extends MediaWikiTestCase {
14 const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
15
16 protected $manager;
17 protected $config;
18 protected $provider;
19 protected $store;
20
21 protected $onSessionMetadataCalled = false;
22
23 /**
24 * Returns a non-persistent backend that thinks it has at least one session active
25 * @param User|null $user
26 * @param string $id
27 */
28 protected function getBackend( User $user = null, $id = null ) {
29 if ( !$this->config ) {
30 $this->config = new \HashConfig();
31 $this->manager = null;
32 }
33 if ( !$this->store ) {
34 $this->store = new TestBagOStuff();
35 $this->manager = null;
36 }
37
38 $logger = new \Psr\Log\NullLogger();
39 if ( !$this->manager ) {
40 $this->manager = new SessionManager( [
41 'store' => $this->store,
42 'logger' => $logger,
43 'config' => $this->config,
44 ] );
45 }
46
47 if ( !$this->provider ) {
48 $this->provider = new \DummySessionProvider();
49 }
50 $this->provider->setLogger( $logger );
51 $this->provider->setConfig( $this->config );
52 $this->provider->setManager( $this->manager );
53
54 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
55 'provider' => $this->provider,
56 'id' => $id ?: self::SESSIONID,
57 'persisted' => true,
58 'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
59 'idIsSafe' => true,
60 ] );
61 $id = new SessionId( $info->getId() );
62
63 $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
64 $priv = \TestingAccessWrapper::newFromObject( $backend );
65 $priv->persist = false;
66 $priv->requests = [ 100 => new \FauxRequest() ];
67 $priv->requests[100]->setSessionId( $id );
68 $priv->usePhpSessionHandling = false;
69
70 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
71 $manager->allSessionBackends = [ $backend->getId() => $backend ] + $manager->allSessionBackends;
72 $manager->allSessionIds = [ $backend->getId() => $id ] + $manager->allSessionIds;
73 $manager->sessionProviders = [ (string)$this->provider => $this->provider ];
74
75 return $backend;
76 }
77
78 public function testConstructor() {
79 // Set variables
80 $this->getBackend();
81
82 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
83 'provider' => $this->provider,
84 'id' => self::SESSIONID,
85 'persisted' => true,
86 'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
87 'idIsSafe' => true,
88 ] );
89 $id = new SessionId( $info->getId() );
90 $logger = new \Psr\Log\NullLogger();
91 try {
92 new SessionBackend( $id, $info, $this->store, $logger, 10 );
93 $this->fail( 'Expected exception not thrown' );
94 } catch ( \InvalidArgumentException $ex ) {
95 $this->assertSame(
96 "Refusing to create session for unverified user {$info->getUserInfo()}",
97 $ex->getMessage()
98 );
99 }
100
101 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
102 'id' => self::SESSIONID,
103 'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
104 'idIsSafe' => true,
105 ] );
106 $id = new SessionId( $info->getId() );
107 try {
108 new SessionBackend( $id, $info, $this->store, $logger, 10 );
109 $this->fail( 'Expected exception not thrown' );
110 } catch ( \InvalidArgumentException $ex ) {
111 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
112 }
113
114 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
115 'provider' => $this->provider,
116 'id' => self::SESSIONID,
117 'persisted' => true,
118 'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
119 'idIsSafe' => true,
120 ] );
121 $id = new SessionId( '!' . $info->getId() );
122 try {
123 new SessionBackend( $id, $info, $this->store, $logger, 10 );
124 $this->fail( 'Expected exception not thrown' );
125 } catch ( \InvalidArgumentException $ex ) {
126 $this->assertSame(
127 'SessionId and SessionInfo don\'t match',
128 $ex->getMessage()
129 );
130 }
131
132 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
133 'provider' => $this->provider,
134 'id' => self::SESSIONID,
135 'persisted' => true,
136 'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
137 'idIsSafe' => true,
138 ] );
139 $id = new SessionId( $info->getId() );
140 $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
141 $this->assertSame( self::SESSIONID, $backend->getId() );
142 $this->assertSame( $id, $backend->getSessionId() );
143 $this->assertSame( $this->provider, $backend->getProvider() );
144 $this->assertInstanceOf( 'User', $backend->getUser() );
145 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
146 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
147 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
148 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
149
150 $expire = time() + 100;
151 $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ], 2 );
152
153 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
154 'provider' => $this->provider,
155 'id' => self::SESSIONID,
156 'persisted' => true,
157 'forceHTTPS' => true,
158 'metadata' => [ 'foo' ],
159 'idIsSafe' => true,
160 ] );
161 $id = new SessionId( $info->getId() );
162 $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
163 $this->assertSame( self::SESSIONID, $backend->getId() );
164 $this->assertSame( $id, $backend->getSessionId() );
165 $this->assertSame( $this->provider, $backend->getProvider() );
166 $this->assertInstanceOf( 'User', $backend->getUser() );
167 $this->assertTrue( $backend->getUser()->isAnon() );
168 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
169 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
170 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
171 $this->assertSame( $expire, \TestingAccessWrapper::newFromObject( $backend )->expires );
172 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
173 }
174
175 public function testSessionStuff() {
176 $backend = $this->getBackend();
177 $priv = \TestingAccessWrapper::newFromObject( $backend );
178 $priv->requests = []; // Remove dummy session
179
180 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
181
182 $request1 = new \FauxRequest();
183 $session1 = $backend->getSession( $request1 );
184 $request2 = new \FauxRequest();
185 $session2 = $backend->getSession( $request2 );
186
187 $this->assertInstanceOf( Session::class, $session1 );
188 $this->assertInstanceOf( Session::class, $session2 );
189 $this->assertSame( 2, count( $priv->requests ) );
190
191 $index = \TestingAccessWrapper::newFromObject( $session1 )->index;
192
193 $this->assertSame( $request1, $backend->getRequest( $index ) );
194 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
195 $request1->setCookie( 'UserName', 'Example' );
196 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
197
198 $session1 = null;
199 $this->assertSame( 1, count( $priv->requests ) );
200 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
201 $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
202 try {
203 $backend->getRequest( $index );
204 $this->fail( 'Expected exception not thrown' );
205 } catch ( \InvalidArgumentException $ex ) {
206 $this->assertSame( 'Invalid session index', $ex->getMessage() );
207 }
208 try {
209 $backend->suggestLoginUsername( $index );
210 $this->fail( 'Expected exception not thrown' );
211 } catch ( \InvalidArgumentException $ex ) {
212 $this->assertSame( 'Invalid session index', $ex->getMessage() );
213 }
214
215 $session2 = null;
216 $this->assertSame( 0, count( $priv->requests ) );
217 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
218 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
219 }
220
221 public function testSetProviderMetadata() {
222 $backend = $this->getBackend();
223 $priv = \TestingAccessWrapper::newFromObject( $backend );
224 $priv->providerMetadata = [ 'dummy' ];
225
226 try {
227 $backend->setProviderMetadata( 'foo' );
228 $this->fail( 'Expected exception not thrown' );
229 } catch ( \InvalidArgumentException $ex ) {
230 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
231 }
232
233 try {
234 $backend->setProviderMetadata( (object)[] );
235 $this->fail( 'Expected exception not thrown' );
236 } catch ( \InvalidArgumentException $ex ) {
237 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
238 }
239
240 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
241 $backend->setProviderMetadata( [ 'dummy' ] );
242 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
243
244 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
245 $backend->setProviderMetadata( [ 'test' ] );
246 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
247 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
248 $this->store->deleteSession( self::SESSIONID );
249
250 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
251 $backend->setProviderMetadata( null );
252 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
253 $this->assertSame( null, $backend->getProviderMetadata() );
254 $this->store->deleteSession( self::SESSIONID );
255 }
256
257 public function testResetId() {
258 $id = session_id();
259
260 $builder = $this->getMockBuilder( 'DummySessionProvider' )
261 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
262
263 $this->provider = $builder->getMock();
264 $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
265 ->will( $this->returnValue( false ) );
266 $this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
267 $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
268 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
269 $sessionId = $backend->getSessionId();
270 $backend->resetId();
271 $this->assertSame( self::SESSIONID, $backend->getId() );
272 $this->assertSame( $backend->getId(), $sessionId->getId() );
273 $this->assertSame( $id, session_id() );
274 $this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
275
276 $this->provider = $builder->getMock();
277 $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
278 ->will( $this->returnValue( true ) );
279 $backend = $this->getBackend();
280 $this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
281 ->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
282 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
283 $sessionId = $backend->getSessionId();
284 $backend->resetId();
285 $this->assertNotEquals( self::SESSIONID, $backend->getId() );
286 $this->assertSame( $backend->getId(), $sessionId->getId() );
287 $this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) );
288 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
289 $this->assertSame( $id, session_id() );
290 $this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
291 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
292 $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
293 }
294
295 public function testPersist() {
296 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
297 ->setMethods( [ 'persistSession' ] )->getMock();
298 $this->provider->expects( $this->once() )->method( 'persistSession' );
299 $backend = $this->getBackend();
300 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
301 $backend->save(); // This one shouldn't call $provider->persistSession()
302
303 $backend->persist();
304 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
305
306 $this->provider = null;
307 $backend = $this->getBackend();
308 $wrap = \TestingAccessWrapper::newFromObject( $backend );
309 $wrap->persist = true;
310 $wrap->expires = 0;
311 $backend->persist();
312 $this->assertNotEquals( 0, $wrap->expires );
313 }
314
315 public function testUnpersist() {
316 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
317 ->setMethods( [ 'unpersistSession' ] )->getMock();
318 $this->provider->expects( $this->once() )->method( 'unpersistSession' );
319 $backend = $this->getBackend();
320 $wrap = \TestingAccessWrapper::newFromObject( $backend );
321 $wrap->store = new \CachedBagOStuff( $this->store );
322 $wrap->persist = true;
323 $wrap->dataDirty = true;
324
325 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
326 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
327 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
328
329 $backend->unpersist();
330 $this->assertFalse( $backend->isPersistent() );
331 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
332 $this->assertNotFalse( $wrap->store->get( wfMemcKey( 'MWSession', self::SESSIONID ) ) );
333 }
334
335 public function testRememberUser() {
336 $backend = $this->getBackend();
337
338 $remembered = $backend->shouldRememberUser();
339 $backend->setRememberUser( !$remembered );
340 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
341 $backend->setRememberUser( $remembered );
342 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
343 }
344
345 public function testForceHTTPS() {
346 $backend = $this->getBackend();
347
348 $force = $backend->shouldForceHTTPS();
349 $backend->setForceHTTPS( !$force );
350 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
351 $backend->setForceHTTPS( $force );
352 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
353 }
354
355 public function testLoggedOutTimestamp() {
356 $backend = $this->getBackend();
357
358 $backend->setLoggedOutTimestamp( 42 );
359 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
360 $backend->setLoggedOutTimestamp( '123' );
361 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
362 }
363
364 public function testSetUser() {
365 $user = static::getTestSysop()->getUser();
366
367 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
368 ->setMethods( [ 'canChangeUser' ] )->getMock();
369 $this->provider->expects( $this->any() )->method( 'canChangeUser' )
370 ->will( $this->returnValue( false ) );
371 $backend = $this->getBackend();
372 $this->assertFalse( $backend->canSetUser() );
373 try {
374 $backend->setUser( $user );
375 $this->fail( 'Expected exception not thrown' );
376 } catch ( \BadMethodCallException $ex ) {
377 $this->assertSame(
378 'Cannot set user on this session; check $session->canSetUser() first',
379 $ex->getMessage()
380 );
381 }
382 $this->assertNotSame( $user, $backend->getUser() );
383
384 $this->provider = null;
385 $backend = $this->getBackend();
386 $this->assertTrue( $backend->canSetUser() );
387 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
388 $backend->setUser( $user );
389 $this->assertSame( $user, $backend->getUser() );
390 }
391
392 public function testDirty() {
393 $backend = $this->getBackend();
394 $priv = \TestingAccessWrapper::newFromObject( $backend );
395 $priv->dataDirty = false;
396 $backend->dirty();
397 $this->assertTrue( $priv->dataDirty );
398 }
399
400 public function testGetData() {
401 $backend = $this->getBackend();
402 $data = $backend->getData();
403 $this->assertSame( [], $data );
404 $this->assertTrue( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
405 $data['???'] = '!!!';
406 $this->assertSame( [ '???' => '!!!' ], $data );
407
408 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
409 $this->store->setSessionData( self::SESSIONID, $testData );
410 $backend = $this->getBackend();
411 $this->assertSame( $testData, $backend->getData() );
412 $this->assertFalse( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
413 }
414
415 public function testAddData() {
416 $backend = $this->getBackend();
417 $priv = \TestingAccessWrapper::newFromObject( $backend );
418
419 $priv->data = [ 'foo' => 1 ];
420 $priv->dataDirty = false;
421 $backend->addData( [ 'foo' => 1 ] );
422 $this->assertSame( [ 'foo' => 1 ], $priv->data );
423 $this->assertFalse( $priv->dataDirty );
424
425 $priv->data = [ 'foo' => 1 ];
426 $priv->dataDirty = false;
427 $backend->addData( [ 'foo' => '1' ] );
428 $this->assertSame( [ 'foo' => '1' ], $priv->data );
429 $this->assertTrue( $priv->dataDirty );
430
431 $priv->data = [ 'foo' => 1 ];
432 $priv->dataDirty = false;
433 $backend->addData( [ 'bar' => 2 ] );
434 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data );
435 $this->assertTrue( $priv->dataDirty );
436 }
437
438 public function testDelaySave() {
439 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
440 $backend = $this->getBackend();
441 $priv = \TestingAccessWrapper::newFromObject( $backend );
442 $priv->persist = true;
443
444 // Saves happen normally when no delay is in effect
445 $this->onSessionMetadataCalled = false;
446 $priv->metaDirty = true;
447 $backend->save();
448 $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
449
450 $this->onSessionMetadataCalled = false;
451 $priv->metaDirty = true;
452 $priv->autosave();
453 $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
454
455 $delay = $backend->delaySave();
456
457 // Autosave doesn't happen when no delay is in effect
458 $this->onSessionMetadataCalled = false;
459 $priv->metaDirty = true;
460 $priv->autosave();
461 $this->assertFalse( $this->onSessionMetadataCalled );
462
463 // Save still does happen when no delay is in effect
464 $priv->save();
465 $this->assertTrue( $this->onSessionMetadataCalled );
466
467 // Save happens when delay is consumed
468 $this->onSessionMetadataCalled = false;
469 $priv->metaDirty = true;
470 \Wikimedia\ScopedCallback::consume( $delay );
471 $this->assertTrue( $this->onSessionMetadataCalled );
472
473 // Test multiple delays
474 $delay1 = $backend->delaySave();
475 $delay2 = $backend->delaySave();
476 $delay3 = $backend->delaySave();
477 $this->onSessionMetadataCalled = false;
478 $priv->metaDirty = true;
479 $priv->autosave();
480 $this->assertFalse( $this->onSessionMetadataCalled );
481 \Wikimedia\ScopedCallback::consume( $delay3 );
482 $this->assertFalse( $this->onSessionMetadataCalled );
483 \Wikimedia\ScopedCallback::consume( $delay1 );
484 $this->assertFalse( $this->onSessionMetadataCalled );
485 \Wikimedia\ScopedCallback::consume( $delay2 );
486 $this->assertTrue( $this->onSessionMetadataCalled );
487 }
488
489 public function testSave() {
490 $user = static::getTestSysop()->getUser();
491 $this->store = new TestBagOStuff();
492 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
493
494 $neverHook = $this->getMockBuilder( __CLASS__ )
495 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
496 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
497
498 $builder = $this->getMockBuilder( 'DummySessionProvider' )
499 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
500
501 $neverProvider = $builder->getMock();
502 $neverProvider->expects( $this->never() )->method( 'persistSession' );
503 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
504
505 // Not persistent or dirty
506 $this->provider = $neverProvider;
507 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
508 $this->store->setSessionData( self::SESSIONID, $testData );
509 $backend = $this->getBackend( $user );
510 $this->store->deleteSession( self::SESSIONID );
511 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
512 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
513 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
514 $backend->save();
515 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
516
517 // (but does unpersist if forced)
518 $this->provider = $builder->getMock();
519 $this->provider->expects( $this->never() )->method( 'persistSession' );
520 $this->provider->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
521 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
522 $this->store->setSessionData( self::SESSIONID, $testData );
523 $backend = $this->getBackend( $user );
524 $this->store->deleteSession( self::SESSIONID );
525 \TestingAccessWrapper::newFromObject( $backend )->persist = false;
526 \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
527 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
528 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
529 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
530 $backend->save();
531 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
532
533 // (but not to a WebRequest associated with a different session)
534 $this->provider = $neverProvider;
535 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
536 $this->store->setSessionData( self::SESSIONID, $testData );
537 $backend = $this->getBackend( $user );
538 \TestingAccessWrapper::newFromObject( $backend )->requests[100]
539 ->setSessionId( new SessionId( 'x' ) );
540 $this->store->deleteSession( self::SESSIONID );
541 \TestingAccessWrapper::newFromObject( $backend )->persist = false;
542 \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
543 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
544 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
545 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
546 $backend->save();
547 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
548
549 // Not persistent, but dirty
550 $this->provider = $neverProvider;
551 $this->onSessionMetadataCalled = false;
552 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
553 $this->store->setSessionData( self::SESSIONID, $testData );
554 $backend = $this->getBackend( $user );
555 $this->store->deleteSession( self::SESSIONID );
556 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
557 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
558 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
559 $backend->save();
560 $this->assertTrue( $this->onSessionMetadataCalled );
561 $blob = $this->store->getSession( self::SESSIONID );
562 $this->assertInternalType( 'array', $blob );
563 $this->assertArrayHasKey( 'metadata', $blob );
564 $metadata = $blob['metadata'];
565 $this->assertInternalType( 'array', $metadata );
566 $this->assertArrayHasKey( '???', $metadata );
567 $this->assertSame( '!!!', $metadata['???'] );
568 $this->assertFalse( $this->store->getSessionFromBackend( self::SESSIONID ),
569 'making sure it didn\'t save to backend' );
570
571 // Persistent, not dirty
572 $this->provider = $neverProvider;
573 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
574 $this->store->setSessionData( self::SESSIONID, $testData );
575 $backend = $this->getBackend( $user );
576 $this->store->deleteSession( self::SESSIONID );
577 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
578 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
579 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
580 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
581 $backend->save();
582 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
583
584 // (but will persist if forced)
585 $this->provider = $builder->getMock();
586 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
587 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
588 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
589 $this->store->setSessionData( self::SESSIONID, $testData );
590 $backend = $this->getBackend( $user );
591 $this->store->deleteSession( self::SESSIONID );
592 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
593 \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
594 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
595 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
596 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
597 $backend->save();
598 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
599
600 // Persistent and dirty
601 $this->provider = $neverProvider;
602 $this->onSessionMetadataCalled = false;
603 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
604 $this->store->setSessionData( self::SESSIONID, $testData );
605 $backend = $this->getBackend( $user );
606 $this->store->deleteSession( self::SESSIONID );
607 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
608 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
609 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
610 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
611 $backend->save();
612 $this->assertTrue( $this->onSessionMetadataCalled );
613 $blob = $this->store->getSession( self::SESSIONID );
614 $this->assertInternalType( 'array', $blob );
615 $this->assertArrayHasKey( 'metadata', $blob );
616 $metadata = $blob['metadata'];
617 $this->assertInternalType( 'array', $metadata );
618 $this->assertArrayHasKey( '???', $metadata );
619 $this->assertSame( '!!!', $metadata['???'] );
620 $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
621 'making sure it did save to backend' );
622
623 // (also persists if forced)
624 $this->provider = $builder->getMock();
625 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
626 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
627 $this->onSessionMetadataCalled = false;
628 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
629 $this->store->setSessionData( self::SESSIONID, $testData );
630 $backend = $this->getBackend( $user );
631 $this->store->deleteSession( self::SESSIONID );
632 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
633 \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
634 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
635 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
636 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
637 $backend->save();
638 $this->assertTrue( $this->onSessionMetadataCalled );
639 $blob = $this->store->getSession( self::SESSIONID );
640 $this->assertInternalType( 'array', $blob );
641 $this->assertArrayHasKey( 'metadata', $blob );
642 $metadata = $blob['metadata'];
643 $this->assertInternalType( 'array', $metadata );
644 $this->assertArrayHasKey( '???', $metadata );
645 $this->assertSame( '!!!', $metadata['???'] );
646 $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
647 'making sure it did save to backend' );
648
649 // (also persists if metadata dirty)
650 $this->provider = $builder->getMock();
651 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
652 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
653 $this->onSessionMetadataCalled = false;
654 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
655 $this->store->setSessionData( self::SESSIONID, $testData );
656 $backend = $this->getBackend( $user );
657 $this->store->deleteSession( self::SESSIONID );
658 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
659 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
660 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
661 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
662 $backend->save();
663 $this->assertTrue( $this->onSessionMetadataCalled );
664 $blob = $this->store->getSession( self::SESSIONID );
665 $this->assertInternalType( 'array', $blob );
666 $this->assertArrayHasKey( 'metadata', $blob );
667 $metadata = $blob['metadata'];
668 $this->assertInternalType( 'array', $metadata );
669 $this->assertArrayHasKey( '???', $metadata );
670 $this->assertSame( '!!!', $metadata['???'] );
671 $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
672 'making sure it did save to backend' );
673
674 // Not marked dirty, but dirty data
675 // (e.g. indirect modification from ArrayAccess::offsetGet)
676 $this->provider = $neverProvider;
677 $this->onSessionMetadataCalled = false;
678 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
679 $this->store->setSessionData( self::SESSIONID, $testData );
680 $backend = $this->getBackend( $user );
681 $this->store->deleteSession( self::SESSIONID );
682 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
683 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
684 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
685 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
686 \TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
687 $backend->save();
688 $this->assertTrue( $this->onSessionMetadataCalled );
689 $blob = $this->store->getSession( self::SESSIONID );
690 $this->assertInternalType( 'array', $blob );
691 $this->assertArrayHasKey( 'metadata', $blob );
692 $metadata = $blob['metadata'];
693 $this->assertInternalType( 'array', $metadata );
694 $this->assertArrayHasKey( '???', $metadata );
695 $this->assertSame( '!!!', $metadata['???'] );
696 $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
697 'making sure it did save to backend' );
698
699 // Bad hook
700 $this->provider = null;
701 $mockHook = $this->getMockBuilder( __CLASS__ )
702 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
703 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
704 ->will( $this->returnCallback(
705 function ( SessionBackend $backend, array &$metadata, array $requests ) {
706 $metadata['userId']++;
707 }
708 ) );
709 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
710 $this->store->setSessionData( self::SESSIONID, $testData );
711 $backend = $this->getBackend( $user );
712 $backend->dirty();
713 try {
714 $backend->save();
715 $this->fail( 'Expected exception not thrown' );
716 } catch ( \UnexpectedValueException $ex ) {
717 $this->assertSame(
718 'SessionMetadata hook changed metadata key "userId"',
719 $ex->getMessage()
720 );
721 }
722
723 // SessionManager::preventSessionsForUser
724 \TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = [
725 $user->getName() => true,
726 ];
727 $this->provider = $neverProvider;
728 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
729 $this->store->setSessionData( self::SESSIONID, $testData );
730 $backend = $this->getBackend( $user );
731 $this->store->deleteSession( self::SESSIONID );
732 \TestingAccessWrapper::newFromObject( $backend )->persist = true;
733 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
734 \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
735 \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
736 $backend->save();
737 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
738 }
739
740 public function testRenew() {
741 $user = static::getTestSysop()->getUser();
742 $this->store = new TestBagOStuff();
743 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
744
745 // Not persistent
746 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
747 ->setMethods( [ 'persistSession' ] )->getMock();
748 $this->provider->expects( $this->never() )->method( 'persistSession' );
749 $this->onSessionMetadataCalled = false;
750 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
751 $this->store->setSessionData( self::SESSIONID, $testData );
752 $backend = $this->getBackend( $user );
753 $this->store->deleteSession( self::SESSIONID );
754 $wrap = \TestingAccessWrapper::newFromObject( $backend );
755 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
756 $wrap->metaDirty = false;
757 $wrap->dataDirty = false;
758 $wrap->forcePersist = false;
759 $wrap->expires = 0;
760 $backend->renew();
761 $this->assertTrue( $this->onSessionMetadataCalled );
762 $blob = $this->store->getSession( self::SESSIONID );
763 $this->assertInternalType( 'array', $blob );
764 $this->assertArrayHasKey( 'metadata', $blob );
765 $metadata = $blob['metadata'];
766 $this->assertInternalType( 'array', $metadata );
767 $this->assertArrayHasKey( '???', $metadata );
768 $this->assertSame( '!!!', $metadata['???'] );
769 $this->assertNotEquals( 0, $wrap->expires );
770
771 // Persistent
772 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
773 ->setMethods( [ 'persistSession' ] )->getMock();
774 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
775 $this->onSessionMetadataCalled = false;
776 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
777 $this->store->setSessionData( self::SESSIONID, $testData );
778 $backend = $this->getBackend( $user );
779 $this->store->deleteSession( self::SESSIONID );
780 $wrap = \TestingAccessWrapper::newFromObject( $backend );
781 $wrap->persist = true;
782 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
783 $wrap->metaDirty = false;
784 $wrap->dataDirty = false;
785 $wrap->forcePersist = false;
786 $wrap->expires = 0;
787 $backend->renew();
788 $this->assertTrue( $this->onSessionMetadataCalled );
789 $blob = $this->store->getSession( self::SESSIONID );
790 $this->assertInternalType( 'array', $blob );
791 $this->assertArrayHasKey( 'metadata', $blob );
792 $metadata = $blob['metadata'];
793 $this->assertInternalType( 'array', $metadata );
794 $this->assertArrayHasKey( '???', $metadata );
795 $this->assertSame( '!!!', $metadata['???'] );
796 $this->assertNotEquals( 0, $wrap->expires );
797
798 // Not persistent, not expiring
799 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
800 ->setMethods( [ 'persistSession' ] )->getMock();
801 $this->provider->expects( $this->never() )->method( 'persistSession' );
802 $this->onSessionMetadataCalled = false;
803 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
804 $this->store->setSessionData( self::SESSIONID, $testData );
805 $backend = $this->getBackend( $user );
806 $this->store->deleteSession( self::SESSIONID );
807 $wrap = \TestingAccessWrapper::newFromObject( $backend );
808 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
809 $wrap->metaDirty = false;
810 $wrap->dataDirty = false;
811 $wrap->forcePersist = false;
812 $expires = time() + $wrap->lifetime + 100;
813 $wrap->expires = $expires;
814 $backend->renew();
815 $this->assertFalse( $this->onSessionMetadataCalled );
816 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
817 $this->assertEquals( $expires, $wrap->expires );
818 }
819
820 public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
821 $this->onSessionMetadataCalled = true;
822 $metadata['???'] = '!!!';
823 }
824
825 public function testTakeOverGlobalSession() {
826 if ( !PHPSessionHandler::isInstalled() ) {
827 PHPSessionHandler::install( SessionManager::singleton() );
828 }
829 if ( !PHPSessionHandler::isEnabled() ) {
830 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
831 $rProp->setAccessible( true );
832 $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
833 $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
834 session_write_close();
835 $handler->enable = false;
836 } );
837 $handler->enable = true;
838 }
839
840 $backend = $this->getBackend( static::getTestSysop()->getUser() );
841 \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
842
843 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
844
845 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
846 $request = \RequestContext::getMain()->getRequest();
847 $manager->globalSession = $backend->getSession( $request );
848 $manager->globalSessionRequest = $request;
849
850 session_id( '' );
851 \TestingAccessWrapper::newFromObject( $backend )->checkPHPSession();
852 $this->assertSame( $backend->getId(), session_id() );
853 session_write_close();
854
855 $backend2 = $this->getBackend(
856 User::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
857 );
858 \TestingAccessWrapper::newFromObject( $backend2 )->usePhpSessionHandling = true;
859
860 session_id( '' );
861 \TestingAccessWrapper::newFromObject( $backend2 )->checkPHPSession();
862 $this->assertSame( '', session_id() );
863 }
864
865 public function testResetIdOfGlobalSession() {
866 if ( !PHPSessionHandler::isInstalled() ) {
867 PHPSessionHandler::install( SessionManager::singleton() );
868 }
869 if ( !PHPSessionHandler::isEnabled() ) {
870 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
871 $rProp->setAccessible( true );
872 $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
873 $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
874 session_write_close();
875 $handler->enable = false;
876 } );
877 $handler->enable = true;
878 }
879
880 $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
881 \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
882
883 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
884
885 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
886 $request = \RequestContext::getMain()->getRequest();
887 $manager->globalSession = $backend->getSession( $request );
888 $manager->globalSessionRequest = $request;
889
890 session_id( self::SESSIONID );
891 \MediaWiki\quietCall( 'session_start' );
892 $_SESSION['foo'] = __METHOD__;
893 $backend->resetId();
894 $this->assertNotEquals( self::SESSIONID, $backend->getId() );
895 $this->assertSame( $backend->getId(), session_id() );
896 $this->assertArrayHasKey( 'foo', $_SESSION );
897 $this->assertSame( __METHOD__, $_SESSION['foo'] );
898 session_write_close();
899 }
900
901 public function testUnpersistOfGlobalSession() {
902 if ( !PHPSessionHandler::isInstalled() ) {
903 PHPSessionHandler::install( SessionManager::singleton() );
904 }
905 if ( !PHPSessionHandler::isEnabled() ) {
906 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
907 $rProp->setAccessible( true );
908 $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
909 $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
910 session_write_close();
911 $handler->enable = false;
912 } );
913 $handler->enable = true;
914 }
915
916 $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
917 $wrap = \TestingAccessWrapper::newFromObject( $backend );
918 $wrap->usePhpSessionHandling = true;
919 $wrap->persist = true;
920
921 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
922
923 $manager = \TestingAccessWrapper::newFromObject( $this->manager );
924 $request = \RequestContext::getMain()->getRequest();
925 $manager->globalSession = $backend->getSession( $request );
926 $manager->globalSessionRequest = $request;
927
928 session_id( self::SESSIONID . 'x' );
929 \MediaWiki\quietCall( 'session_start' );
930 $backend->unpersist();
931 $this->assertSame( self::SESSIONID . 'x', session_id() );
932
933 session_id( self::SESSIONID );
934 $wrap->persist = true;
935 $backend->unpersist();
936 $this->assertSame( '', session_id() );
937 }
938
939 public function testGetAllowedUserRights() {
940 $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
941 ->setMethods( [ 'getAllowedUserRights' ] )
942 ->getMock();
943 $this->provider->expects( $this->any() )->method( 'getAllowedUserRights' )
944 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
945
946 $backend = $this->getBackend();
947 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );
948 }
949
950 }