]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - tests/phpunit/includes/session/SessionManagerTest.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / tests / phpunit / includes / session / SessionManagerTest.php
1 <?php
2
3 namespace MediaWiki\Session;
4
5 use MediaWikiTestCase;
6 use Psr\Log\LogLevel;
7 use User;
8 use Wikimedia\TestingAccessWrapper;
9
10 /**
11  * @group Session
12  * @group Database
13  * @covers MediaWiki\Session\SessionManager
14  */
15 class SessionManagerTest extends MediaWikiTestCase {
16
17         protected $config, $logger, $store;
18
19         protected function getManager() {
20                 \ObjectCache::$instances['testSessionStore'] = new TestBagOStuff();
21                 $this->config = new \HashConfig( [
22                         'LanguageCode' => 'en',
23                         'SessionCacheType' => 'testSessionStore',
24                         'ObjectCacheSessionExpiry' => 100,
25                         'SessionProviders' => [
26                                 [ 'class' => 'DummySessionProvider' ],
27                         ]
28                 ] );
29                 $this->logger = new \TestLogger( false, function ( $m ) {
30                         return substr( $m, 0, 15 ) === 'SessionBackend ' ? null : $m;
31                 } );
32                 $this->store = new TestBagOStuff();
33
34                 return new SessionManager( [
35                         'config' => $this->config,
36                         'logger' => $this->logger,
37                         'store' => $this->store,
38                 ] );
39         }
40
41         protected function objectCacheDef( $object ) {
42                 return [ 'factory' => function () use ( $object ) {
43                         return $object;
44                 } ];
45         }
46
47         public function testSingleton() {
48                 $reset = TestUtils::setSessionManagerSingleton( null );
49
50                 $singleton = SessionManager::singleton();
51                 $this->assertInstanceOf( SessionManager::class, $singleton );
52                 $this->assertSame( $singleton, SessionManager::singleton() );
53         }
54
55         public function testGetGlobalSession() {
56                 $context = \RequestContext::getMain();
57
58                 if ( !PHPSessionHandler::isInstalled() ) {
59                         PHPSessionHandler::install( SessionManager::singleton() );
60                 }
61                 $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
62                 $rProp->setAccessible( true );
63                 $handler = TestingAccessWrapper::newFromObject( $rProp->getValue() );
64                 $oldEnable = $handler->enable;
65                 $reset[] = new \Wikimedia\ScopedCallback( function () use ( $handler, $oldEnable ) {
66                         if ( $handler->enable ) {
67                                 session_write_close();
68                         }
69                         $handler->enable = $oldEnable;
70                 } );
71                 $reset[] = TestUtils::setSessionManagerSingleton( $this->getManager() );
72
73                 $handler->enable = true;
74                 $request = new \FauxRequest();
75                 $context->setRequest( $request );
76                 $id = $request->getSession()->getId();
77
78                 session_id( '' );
79                 $session = SessionManager::getGlobalSession();
80                 $this->assertSame( $id, $session->getId() );
81
82                 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
83                 $session = SessionManager::getGlobalSession();
84                 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() );
85                 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() );
86
87                 session_write_close();
88                 $handler->enable = false;
89                 $request = new \FauxRequest();
90                 $context->setRequest( $request );
91                 $id = $request->getSession()->getId();
92
93                 session_id( '' );
94                 $session = SessionManager::getGlobalSession();
95                 $this->assertSame( $id, $session->getId() );
96
97                 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
98                 $session = SessionManager::getGlobalSession();
99                 $this->assertSame( $id, $session->getId() );
100                 $this->assertSame( $id, $request->getSession()->getId() );
101         }
102
103         public function testConstructor() {
104                 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
105                 $this->assertSame( $this->config, $manager->config );
106                 $this->assertSame( $this->logger, $manager->logger );
107                 $this->assertSame( $this->store, $manager->store );
108
109                 $manager = TestingAccessWrapper::newFromObject( new SessionManager() );
110                 $this->assertSame( \RequestContext::getMain()->getConfig(), $manager->config );
111
112                 $manager = TestingAccessWrapper::newFromObject( new SessionManager( [
113                         'config' => $this->config,
114                 ] ) );
115                 $this->assertSame( \ObjectCache::$instances['testSessionStore'], $manager->store );
116
117                 foreach ( [
118                         'config' => '$options[\'config\'] must be an instance of Config',
119                         'logger' => '$options[\'logger\'] must be an instance of LoggerInterface',
120                         'store' => '$options[\'store\'] must be an instance of BagOStuff',
121                 ] as $key => $error ) {
122                         try {
123                                 new SessionManager( [ $key => new \stdClass ] );
124                                 $this->fail( 'Expected exception not thrown' );
125                         } catch ( \InvalidArgumentException $ex ) {
126                                 $this->assertSame( $error, $ex->getMessage() );
127                         }
128                 }
129         }
130
131         public function testGetSessionForRequest() {
132                 $manager = $this->getManager();
133                 $request = new \FauxRequest();
134                 $request->unpersist1 = false;
135                 $request->unpersist2 = false;
136
137                 $id1 = '';
138                 $id2 = '';
139                 $idEmpty = 'empty-session-------------------';
140
141                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
142                         ->setMethods(
143                                 [ 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe', 'unpersistSession' ]
144                         );
145
146                 $provider1 = $providerBuilder->getMock();
147                 $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
148                         ->with( $this->identicalTo( $request ) )
149                         ->will( $this->returnCallback( function ( $request ) {
150                                 return $request->info1;
151                         } ) );
152                 $provider1->expects( $this->any() )->method( 'newSessionInfo' )
153                         ->will( $this->returnCallback( function () use ( $idEmpty, $provider1 ) {
154                                 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
155                                         'provider' => $provider1,
156                                         'id' => $idEmpty,
157                                         'persisted' => true,
158                                         'idIsSafe' => true,
159                                 ] );
160                         } ) );
161                 $provider1->expects( $this->any() )->method( '__toString' )
162                         ->will( $this->returnValue( 'Provider1' ) );
163                 $provider1->expects( $this->any() )->method( 'describe' )
164                         ->will( $this->returnValue( '#1 sessions' ) );
165                 $provider1->expects( $this->any() )->method( 'unpersistSession' )
166                         ->will( $this->returnCallback( function ( $request ) {
167                                 $request->unpersist1 = true;
168                         } ) );
169
170                 $provider2 = $providerBuilder->getMock();
171                 $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
172                         ->with( $this->identicalTo( $request ) )
173                         ->will( $this->returnCallback( function ( $request ) {
174                                 return $request->info2;
175                         } ) );
176                 $provider2->expects( $this->any() )->method( '__toString' )
177                         ->will( $this->returnValue( 'Provider2' ) );
178                 $provider2->expects( $this->any() )->method( 'describe' )
179                         ->will( $this->returnValue( '#2 sessions' ) );
180                 $provider2->expects( $this->any() )->method( 'unpersistSession' )
181                         ->will( $this->returnCallback( function ( $request ) {
182                                 $request->unpersist2 = true;
183                         } ) );
184
185                 $this->config->set( 'SessionProviders', [
186                         $this->objectCacheDef( $provider1 ),
187                         $this->objectCacheDef( $provider2 ),
188                 ] );
189
190                 // No provider returns info
191                 $request->info1 = null;
192                 $request->info2 = null;
193                 $session = $manager->getSessionForRequest( $request );
194                 $this->assertInstanceOf( Session::class, $session );
195                 $this->assertSame( $idEmpty, $session->getId() );
196                 $this->assertFalse( $request->unpersist1 );
197                 $this->assertFalse( $request->unpersist2 );
198
199                 // Both providers return info, picks best one
200                 $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
201                         'provider' => $provider1,
202                         'id' => ( $id1 = $manager->generateSessionId() ),
203                         'persisted' => true,
204                         'idIsSafe' => true,
205                 ] );
206                 $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
207                         'provider' => $provider2,
208                         'id' => ( $id2 = $manager->generateSessionId() ),
209                         'persisted' => true,
210                         'idIsSafe' => true,
211                 ] );
212                 $session = $manager->getSessionForRequest( $request );
213                 $this->assertInstanceOf( Session::class, $session );
214                 $this->assertSame( $id2, $session->getId() );
215                 $this->assertFalse( $request->unpersist1 );
216                 $this->assertFalse( $request->unpersist2 );
217
218                 $request->info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
219                         'provider' => $provider1,
220                         'id' => ( $id1 = $manager->generateSessionId() ),
221                         'persisted' => true,
222                         'idIsSafe' => true,
223                 ] );
224                 $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
225                         'provider' => $provider2,
226                         'id' => ( $id2 = $manager->generateSessionId() ),
227                         'persisted' => true,
228                         'idIsSafe' => true,
229                 ] );
230                 $session = $manager->getSessionForRequest( $request );
231                 $this->assertInstanceOf( Session::class, $session );
232                 $this->assertSame( $id1, $session->getId() );
233                 $this->assertFalse( $request->unpersist1 );
234                 $this->assertFalse( $request->unpersist2 );
235
236                 // Tied priorities
237                 $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
238                         'provider' => $provider1,
239                         'id' => ( $id1 = $manager->generateSessionId() ),
240                         'persisted' => true,
241                         'userInfo' => UserInfo::newAnonymous(),
242                         'idIsSafe' => true,
243                 ] );
244                 $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
245                         'provider' => $provider2,
246                         'id' => ( $id2 = $manager->generateSessionId() ),
247                         'persisted' => true,
248                         'userInfo' => UserInfo::newAnonymous(),
249                         'idIsSafe' => true,
250                 ] );
251                 try {
252                         $manager->getSessionForRequest( $request );
253                         $this->fail( 'Expcected exception not thrown' );
254                 } catch ( \OverflowException $ex ) {
255                         $this->assertStringStartsWith(
256                                 'Multiple sessions for this request tied for top priority: ',
257                                 $ex->getMessage()
258                         );
259                         $this->assertCount( 2, $ex->sessionInfos );
260                         $this->assertContains( $request->info1, $ex->sessionInfos );
261                         $this->assertContains( $request->info2, $ex->sessionInfos );
262                 }
263                 $this->assertFalse( $request->unpersist1 );
264                 $this->assertFalse( $request->unpersist2 );
265
266                 // Bad provider
267                 $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
268                         'provider' => $provider2,
269                         'id' => ( $id1 = $manager->generateSessionId() ),
270                         'persisted' => true,
271                         'idIsSafe' => true,
272                 ] );
273                 $request->info2 = null;
274                 try {
275                         $manager->getSessionForRequest( $request );
276                         $this->fail( 'Expcected exception not thrown' );
277                 } catch ( \UnexpectedValueException $ex ) {
278                         $this->assertSame(
279                                 'Provider1 returned session info for a different provider: ' . $request->info1,
280                                 $ex->getMessage()
281                         );
282                 }
283                 $this->assertFalse( $request->unpersist1 );
284                 $this->assertFalse( $request->unpersist2 );
285
286                 // Unusable session info
287                 $this->logger->setCollect( true );
288                 $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
289                         'provider' => $provider1,
290                         'id' => ( $id1 = $manager->generateSessionId() ),
291                         'persisted' => true,
292                         'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
293                         'idIsSafe' => true,
294                 ] );
295                 $request->info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
296                         'provider' => $provider2,
297                         'id' => ( $id2 = $manager->generateSessionId() ),
298                         'persisted' => true,
299                         'idIsSafe' => true,
300                 ] );
301                 $session = $manager->getSessionForRequest( $request );
302                 $this->assertInstanceOf( Session::class, $session );
303                 $this->assertSame( $id2, $session->getId() );
304                 $this->logger->setCollect( false );
305                 $this->assertTrue( $request->unpersist1 );
306                 $this->assertFalse( $request->unpersist2 );
307                 $request->unpersist1 = false;
308
309                 $this->logger->setCollect( true );
310                 $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
311                         'provider' => $provider1,
312                         'id' => ( $id1 = $manager->generateSessionId() ),
313                         'persisted' => true,
314                         'idIsSafe' => true,
315                 ] );
316                 $request->info2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
317                         'provider' => $provider2,
318                         'id' => ( $id2 = $manager->generateSessionId() ),
319                         'persisted' => true,
320                         'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
321                         'idIsSafe' => true,
322                 ] );
323                 $session = $manager->getSessionForRequest( $request );
324                 $this->assertInstanceOf( Session::class, $session );
325                 $this->assertSame( $id1, $session->getId() );
326                 $this->logger->setCollect( false );
327                 $this->assertFalse( $request->unpersist1 );
328                 $this->assertTrue( $request->unpersist2 );
329                 $request->unpersist2 = false;
330
331                 // Unpersisted session ID
332                 $request->info1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
333                         'provider' => $provider1,
334                         'id' => ( $id1 = $manager->generateSessionId() ),
335                         'persisted' => false,
336                         'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
337                         'idIsSafe' => true,
338                 ] );
339                 $request->info2 = null;
340                 $session = $manager->getSessionForRequest( $request );
341                 $this->assertInstanceOf( Session::class, $session );
342                 $this->assertSame( $id1, $session->getId() );
343                 $this->assertTrue( $request->unpersist1 ); // The saving of the session does it
344                 $this->assertFalse( $request->unpersist2 );
345                 $session->persist();
346                 $this->assertTrue( $session->isPersistent(), 'sanity check' );
347         }
348
349         public function testGetSessionById() {
350                 $manager = $this->getManager();
351                 try {
352                         $manager->getSessionById( 'bad' );
353                         $this->fail( 'Expected exception not thrown' );
354                 } catch ( \InvalidArgumentException $ex ) {
355                         $this->assertSame( 'Invalid session ID', $ex->getMessage() );
356                 }
357
358                 // Unknown session ID
359                 $id = $manager->generateSessionId();
360                 $session = $manager->getSessionById( $id, true );
361                 $this->assertInstanceOf( Session::class, $session );
362                 $this->assertSame( $id, $session->getId() );
363
364                 $id = $manager->generateSessionId();
365                 $this->assertNull( $manager->getSessionById( $id, false ) );
366
367                 // Known but unloadable session ID
368                 $this->logger->setCollect( true );
369                 $id = $manager->generateSessionId();
370                 $this->store->setSession( $id, [ 'metadata' => [
371                         'userId' => User::idFromName( 'UTSysop' ),
372                         'userToken' => 'bad',
373                 ] ] );
374
375                 $this->assertNull( $manager->getSessionById( $id, true ) );
376                 $this->assertNull( $manager->getSessionById( $id, false ) );
377                 $this->logger->setCollect( false );
378
379                 // Known session ID
380                 $this->store->setSession( $id, [] );
381                 $session = $manager->getSessionById( $id, false );
382                 $this->assertInstanceOf( Session::class, $session );
383                 $this->assertSame( $id, $session->getId() );
384
385                 // Store isn't checked if the session is already loaded
386                 $this->store->setSession( $id, [ 'metadata' => [
387                         'userId' => User::idFromName( 'UTSysop' ),
388                         'userToken' => 'bad',
389                 ] ] );
390                 $session2 = $manager->getSessionById( $id, false );
391                 $this->assertInstanceOf( Session::class, $session2 );
392                 $this->assertSame( $id, $session2->getId() );
393                 unset( $session, $session2 );
394                 $this->logger->setCollect( true );
395                 $this->assertNull( $manager->getSessionById( $id, true ) );
396                 $this->logger->setCollect( false );
397
398                 // Failure to create an empty session
399                 $manager = $this->getManager();
400                 $provider = $this->getMockBuilder( 'DummySessionProvider' )
401                         ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] )
402                         ->getMock();
403                 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
404                         ->will( $this->returnValue( null ) );
405                 $provider->expects( $this->any() )->method( 'newSessionInfo' )
406                         ->will( $this->returnValue( null ) );
407                 $provider->expects( $this->any() )->method( '__toString' )
408                         ->will( $this->returnValue( 'MockProvider' ) );
409                 $this->config->set( 'SessionProviders', [
410                         $this->objectCacheDef( $provider ),
411                 ] );
412                 $this->logger->setCollect( true );
413                 $this->assertNull( $manager->getSessionById( $id, true ) );
414                 $this->logger->setCollect( false );
415                 $this->assertSame( [
416                         [ LogLevel::ERROR, 'Failed to create empty session: {exception}' ]
417                 ], $this->logger->getBuffer() );
418         }
419
420         public function testGetEmptySession() {
421                 $manager = $this->getManager();
422                 $pmanager = TestingAccessWrapper::newFromObject( $manager );
423                 $request = new \FauxRequest();
424
425                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
426                         ->setMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] );
427
428                 $expectId = null;
429                 $info1 = null;
430                 $info2 = null;
431
432                 $provider1 = $providerBuilder->getMock();
433                 $provider1->expects( $this->any() )->method( 'provideSessionInfo' )
434                         ->will( $this->returnValue( null ) );
435                 $provider1->expects( $this->any() )->method( 'newSessionInfo' )
436                         ->with( $this->callback( function ( $id ) use ( &$expectId ) {
437                                 return $id === $expectId;
438                         } ) )
439                         ->will( $this->returnCallback( function () use ( &$info1 ) {
440                                 return $info1;
441                         } ) );
442                 $provider1->expects( $this->any() )->method( '__toString' )
443                         ->will( $this->returnValue( 'MockProvider1' ) );
444
445                 $provider2 = $providerBuilder->getMock();
446                 $provider2->expects( $this->any() )->method( 'provideSessionInfo' )
447                         ->will( $this->returnValue( null ) );
448                 $provider2->expects( $this->any() )->method( 'newSessionInfo' )
449                         ->with( $this->callback( function ( $id ) use ( &$expectId ) {
450                                 return $id === $expectId;
451                         } ) )
452                         ->will( $this->returnCallback( function () use ( &$info2 ) {
453                                 return $info2;
454                         } ) );
455                 $provider1->expects( $this->any() )->method( '__toString' )
456                         ->will( $this->returnValue( 'MockProvider2' ) );
457
458                 $this->config->set( 'SessionProviders', [
459                         $this->objectCacheDef( $provider1 ),
460                         $this->objectCacheDef( $provider2 ),
461                 ] );
462
463                 // No info
464                 $expectId = null;
465                 $info1 = null;
466                 $info2 = null;
467                 try {
468                         $manager->getEmptySession();
469                         $this->fail( 'Expected exception not thrown' );
470                 } catch ( \UnexpectedValueException $ex ) {
471                         $this->assertSame(
472                                 'No provider could provide an empty session!',
473                                 $ex->getMessage()
474                         );
475                 }
476
477                 // Info
478                 $expectId = null;
479                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
480                         'provider' => $provider1,
481                         'id' => 'empty---------------------------',
482                         'persisted' => true,
483                         'idIsSafe' => true,
484                 ] );
485                 $info2 = null;
486                 $session = $manager->getEmptySession();
487                 $this->assertInstanceOf( Session::class, $session );
488                 $this->assertSame( 'empty---------------------------', $session->getId() );
489
490                 // Info, explicitly
491                 $expectId = 'expected------------------------';
492                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
493                         'provider' => $provider1,
494                         'id' => $expectId,
495                         'persisted' => true,
496                         'idIsSafe' => true,
497                 ] );
498                 $info2 = null;
499                 $session = $pmanager->getEmptySessionInternal( null, $expectId );
500                 $this->assertInstanceOf( Session::class, $session );
501                 $this->assertSame( $expectId, $session->getId() );
502
503                 // Wrong ID
504                 $expectId = 'expected-----------------------2';
505                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
506                         'provider' => $provider1,
507                         'id' => "un$expectId",
508                         'persisted' => true,
509                         'idIsSafe' => true,
510                 ] );
511                 $info2 = null;
512                 try {
513                         $pmanager->getEmptySessionInternal( null, $expectId );
514                         $this->fail( 'Expected exception not thrown' );
515                 } catch ( \UnexpectedValueException $ex ) {
516                         $this->assertSame(
517                                 'MockProvider1 returned empty session info with a wrong id: ' .
518                                         "un$expectId != $expectId",
519                                 $ex->getMessage()
520                         );
521                 }
522
523                 // Unsafe ID
524                 $expectId = 'expected-----------------------2';
525                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
526                         'provider' => $provider1,
527                         'id' => $expectId,
528                         'persisted' => true,
529                 ] );
530                 $info2 = null;
531                 try {
532                         $pmanager->getEmptySessionInternal( null, $expectId );
533                         $this->fail( 'Expected exception not thrown' );
534                 } catch ( \UnexpectedValueException $ex ) {
535                         $this->assertSame(
536                                 'MockProvider1 returned empty session info with id flagged unsafe',
537                                 $ex->getMessage()
538                         );
539                 }
540
541                 // Wrong provider
542                 $expectId = null;
543                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
544                         'provider' => $provider2,
545                         'id' => 'empty---------------------------',
546                         'persisted' => true,
547                         'idIsSafe' => true,
548                 ] );
549                 $info2 = null;
550                 try {
551                         $manager->getEmptySession();
552                         $this->fail( 'Expected exception not thrown' );
553                 } catch ( \UnexpectedValueException $ex ) {
554                         $this->assertSame(
555                                 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
556                                 $ex->getMessage()
557                         );
558                 }
559
560                 // Highest priority wins
561                 $expectId = null;
562                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
563                         'provider' => $provider1,
564                         'id' => 'empty1--------------------------',
565                         'persisted' => true,
566                         'idIsSafe' => true,
567                 ] );
568                 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
569                         'provider' => $provider2,
570                         'id' => 'empty2--------------------------',
571                         'persisted' => true,
572                         'idIsSafe' => true,
573                 ] );
574                 $session = $manager->getEmptySession();
575                 $this->assertInstanceOf( Session::class, $session );
576                 $this->assertSame( 'empty1--------------------------', $session->getId() );
577
578                 $expectId = null;
579                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
580                         'provider' => $provider1,
581                         'id' => 'empty1--------------------------',
582                         'persisted' => true,
583                         'idIsSafe' => true,
584                 ] );
585                 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
586                         'provider' => $provider2,
587                         'id' => 'empty2--------------------------',
588                         'persisted' => true,
589                         'idIsSafe' => true,
590                 ] );
591                 $session = $manager->getEmptySession();
592                 $this->assertInstanceOf( Session::class, $session );
593                 $this->assertSame( 'empty2--------------------------', $session->getId() );
594
595                 // Tied priorities throw an exception
596                 $expectId = null;
597                 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
598                         'provider' => $provider1,
599                         'id' => 'empty1--------------------------',
600                         'persisted' => true,
601                         'userInfo' => UserInfo::newAnonymous(),
602                         'idIsSafe' => true,
603                 ] );
604                 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
605                         'provider' => $provider2,
606                         'id' => 'empty2--------------------------',
607                         'persisted' => true,
608                         'userInfo' => UserInfo::newAnonymous(),
609                         'idIsSafe' => true,
610                 ] );
611                 try {
612                         $manager->getEmptySession();
613                         $this->fail( 'Expected exception not thrown' );
614                 } catch ( \UnexpectedValueException $ex ) {
615                         $this->assertStringStartsWith(
616                                 'Multiple empty sessions tied for top priority: ',
617                                 $ex->getMessage()
618                         );
619                 }
620
621                 // Bad id
622                 try {
623                         $pmanager->getEmptySessionInternal( null, 'bad' );
624                         $this->fail( 'Expected exception not thrown' );
625                 } catch ( \InvalidArgumentException $ex ) {
626                         $this->assertSame( 'Invalid session ID', $ex->getMessage() );
627                 }
628
629                 // Session already exists
630                 $expectId = 'expected-----------------------3';
631                 $this->store->setSessionMeta( $expectId, [
632                         'provider' => 'MockProvider2',
633                         'userId' => 0,
634                         'userName' => null,
635                         'userToken' => null,
636                 ] );
637                 try {
638                         $pmanager->getEmptySessionInternal( null, $expectId );
639                         $this->fail( 'Expected exception not thrown' );
640                 } catch ( \InvalidArgumentException $ex ) {
641                         $this->assertSame( 'Session ID already exists', $ex->getMessage() );
642                 }
643         }
644
645         public function testInvalidateSessionsForUser() {
646                 $user = User::newFromName( 'UTSysop' );
647                 $manager = $this->getManager();
648
649                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
650                         ->setMethods( [ 'invalidateSessionsForUser', '__toString' ] );
651
652                 $provider1 = $providerBuilder->getMock();
653                 $provider1->expects( $this->once() )->method( 'invalidateSessionsForUser' )
654                         ->with( $this->identicalTo( $user ) );
655                 $provider1->expects( $this->any() )->method( '__toString' )
656                         ->will( $this->returnValue( 'MockProvider1' ) );
657
658                 $provider2 = $providerBuilder->getMock();
659                 $provider2->expects( $this->once() )->method( 'invalidateSessionsForUser' )
660                         ->with( $this->identicalTo( $user ) );
661                 $provider2->expects( $this->any() )->method( '__toString' )
662                         ->will( $this->returnValue( 'MockProvider2' ) );
663
664                 $this->config->set( 'SessionProviders', [
665                         $this->objectCacheDef( $provider1 ),
666                         $this->objectCacheDef( $provider2 ),
667                 ] );
668
669                 $oldToken = $user->getToken( true );
670                 $manager->invalidateSessionsForUser( $user );
671                 $this->assertNotEquals( $oldToken, $user->getToken() );
672         }
673
674         public function testGetVaryHeaders() {
675                 $manager = $this->getManager();
676
677                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
678                         ->setMethods( [ 'getVaryHeaders', '__toString' ] );
679
680                 $provider1 = $providerBuilder->getMock();
681                 $provider1->expects( $this->once() )->method( 'getVaryHeaders' )
682                         ->will( $this->returnValue( [
683                                 'Foo' => null,
684                                 'Bar' => [ 'X', 'Bar1' ],
685                                 'Quux' => null,
686                         ] ) );
687                 $provider1->expects( $this->any() )->method( '__toString' )
688                         ->will( $this->returnValue( 'MockProvider1' ) );
689
690                 $provider2 = $providerBuilder->getMock();
691                 $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
692                         ->will( $this->returnValue( [
693                                 'Baz' => null,
694                                 'Bar' => [ 'X', 'Bar2' ],
695                                 'Quux' => [ 'Quux' ],
696                         ] ) );
697                 $provider2->expects( $this->any() )->method( '__toString' )
698                         ->will( $this->returnValue( 'MockProvider2' ) );
699
700                 $this->config->set( 'SessionProviders', [
701                         $this->objectCacheDef( $provider1 ),
702                         $this->objectCacheDef( $provider2 ),
703                 ] );
704
705                 $expect = [
706                         'Foo' => [],
707                         'Bar' => [ 'X', 'Bar1', 3 => 'Bar2' ],
708                         'Quux' => [ 'Quux' ],
709                         'Baz' => [],
710                 ];
711
712                 $this->assertEquals( $expect, $manager->getVaryHeaders() );
713
714                 // Again, to ensure it's cached
715                 $this->assertEquals( $expect, $manager->getVaryHeaders() );
716         }
717
718         public function testGetVaryCookies() {
719                 $manager = $this->getManager();
720
721                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
722                         ->setMethods( [ 'getVaryCookies', '__toString' ] );
723
724                 $provider1 = $providerBuilder->getMock();
725                 $provider1->expects( $this->once() )->method( 'getVaryCookies' )
726                         ->will( $this->returnValue( [ 'Foo', 'Bar' ] ) );
727                 $provider1->expects( $this->any() )->method( '__toString' )
728                         ->will( $this->returnValue( 'MockProvider1' ) );
729
730                 $provider2 = $providerBuilder->getMock();
731                 $provider2->expects( $this->once() )->method( 'getVaryCookies' )
732                         ->will( $this->returnValue( [ 'Foo', 'Baz' ] ) );
733                 $provider2->expects( $this->any() )->method( '__toString' )
734                         ->will( $this->returnValue( 'MockProvider2' ) );
735
736                 $this->config->set( 'SessionProviders', [
737                         $this->objectCacheDef( $provider1 ),
738                         $this->objectCacheDef( $provider2 ),
739                 ] );
740
741                 $expect = [ 'Foo', 'Bar', 'Baz' ];
742
743                 $this->assertEquals( $expect, $manager->getVaryCookies() );
744
745                 // Again, to ensure it's cached
746                 $this->assertEquals( $expect, $manager->getVaryCookies() );
747         }
748
749         public function testGetProviders() {
750                 $realManager = $this->getManager();
751                 $manager = TestingAccessWrapper::newFromObject( $realManager );
752
753                 $this->config->set( 'SessionProviders', [
754                         [ 'class' => 'DummySessionProvider' ],
755                 ] );
756                 $providers = $manager->getProviders();
757                 $this->assertArrayHasKey( 'DummySessionProvider', $providers );
758                 $provider = TestingAccessWrapper::newFromObject( $providers['DummySessionProvider'] );
759                 $this->assertSame( $manager->logger, $provider->logger );
760                 $this->assertSame( $manager->config, $provider->config );
761                 $this->assertSame( $realManager, $provider->getManager() );
762
763                 $this->config->set( 'SessionProviders', [
764                         [ 'class' => 'DummySessionProvider' ],
765                         [ 'class' => 'DummySessionProvider' ],
766                 ] );
767                 $manager->sessionProviders = null;
768                 try {
769                         $manager->getProviders();
770                         $this->fail( 'Expected exception not thrown' );
771                 } catch ( \UnexpectedValueException $ex ) {
772                         $this->assertSame(
773                                 'Duplicate provider name "DummySessionProvider"',
774                                 $ex->getMessage()
775                         );
776                 }
777         }
778
779         public function testShutdown() {
780                 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
781                 $manager->setLogger( new \Psr\Log\NullLogger() );
782
783                 $mock = $this->getMockBuilder( 'stdClass' )
784                         ->setMethods( [ 'shutdown' ] )->getMock();
785                 $mock->expects( $this->once() )->method( 'shutdown' );
786
787                 $manager->allSessionBackends = [ $mock ];
788                 $manager->shutdown();
789         }
790
791         public function testGetSessionFromInfo() {
792                 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
793                 $request = new \FauxRequest();
794
795                 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
796
797                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
798                         'provider' => $manager->getProvider( 'DummySessionProvider' ),
799                         'id' => $id,
800                         'persisted' => true,
801                         'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
802                         'idIsSafe' => true,
803                 ] );
804                 TestingAccessWrapper::newFromObject( $info )->idIsSafe = true;
805                 $session1 = TestingAccessWrapper::newFromObject(
806                         $manager->getSessionFromInfo( $info, $request )
807                 );
808                 $session2 = TestingAccessWrapper::newFromObject(
809                         $manager->getSessionFromInfo( $info, $request )
810                 );
811
812                 $this->assertSame( $session1->backend, $session2->backend );
813                 $this->assertNotEquals( $session1->index, $session2->index );
814                 $this->assertSame( $session1->getSessionId(), $session2->getSessionId() );
815                 $this->assertSame( $id, $session1->getId() );
816
817                 TestingAccessWrapper::newFromObject( $info )->idIsSafe = false;
818                 $session3 = $manager->getSessionFromInfo( $info, $request );
819                 $this->assertNotSame( $id, $session3->getId() );
820         }
821
822         public function testBackendRegistration() {
823                 $manager = $this->getManager();
824
825                 $session = $manager->getSessionForRequest( new \FauxRequest );
826                 $backend = TestingAccessWrapper::newFromObject( $session )->backend;
827                 $sessionId = $session->getSessionId();
828                 $id = (string)$sessionId;
829
830                 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
831
832                 $manager->changeBackendId( $backend );
833                 $this->assertSame( $sessionId, $session->getSessionId() );
834                 $this->assertNotEquals( $id, (string)$sessionId );
835                 $id = (string)$sessionId;
836
837                 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
838
839                 // Destruction of the session here causes the backend to be deregistered
840                 $session = null;
841
842                 try {
843                         $manager->changeBackendId( $backend );
844                         $this->fail( 'Expected exception not thrown' );
845                 } catch ( \InvalidArgumentException $ex ) {
846                         $this->assertSame(
847                                 'Backend was not registered with this SessionManager', $ex->getMessage()
848                         );
849                 }
850
851                 try {
852                         $manager->deregisterSessionBackend( $backend );
853                         $this->fail( 'Expected exception not thrown' );
854                 } catch ( \InvalidArgumentException $ex ) {
855                         $this->assertSame(
856                                 'Backend was not registered with this SessionManager', $ex->getMessage()
857                         );
858                 }
859
860                 $session = $manager->getSessionById( $id, true );
861                 $this->assertSame( $sessionId, $session->getSessionId() );
862         }
863
864         public function testGenerateSessionId() {
865                 $manager = $this->getManager();
866
867                 $id = $manager->generateSessionId();
868                 $this->assertTrue( SessionManager::validateSessionId( $id ), "Generated ID: $id" );
869         }
870
871         public function testPreventSessionsForUser() {
872                 $manager = $this->getManager();
873
874                 $providerBuilder = $this->getMockBuilder( 'DummySessionProvider' )
875                         ->setMethods( [ 'preventSessionsForUser', '__toString' ] );
876
877                 $provider1 = $providerBuilder->getMock();
878                 $provider1->expects( $this->once() )->method( 'preventSessionsForUser' )
879                         ->with( $this->equalTo( 'UTSysop' ) );
880                 $provider1->expects( $this->any() )->method( '__toString' )
881                         ->will( $this->returnValue( 'MockProvider1' ) );
882
883                 $this->config->set( 'SessionProviders', [
884                         $this->objectCacheDef( $provider1 ),
885                 ] );
886
887                 $this->assertFalse( $manager->isUserSessionPrevented( 'UTSysop' ) );
888                 $manager->preventSessionsForUser( 'UTSysop' );
889                 $this->assertTrue( $manager->isUserSessionPrevented( 'UTSysop' ) );
890         }
891
892         public function testLoadSessionInfoFromStore() {
893                 $manager = $this->getManager();
894                 $logger = new \TestLogger( true );
895                 $manager->setLogger( $logger );
896                 $request = new \FauxRequest();
897
898                 // TestingAccessWrapper can't handle methods with reference arguments, sigh.
899                 $rClass = new \ReflectionClass( $manager );
900                 $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
901                 $rMethod->setAccessible( true );
902                 $loadSessionInfoFromStore = function ( &$info ) use ( $rMethod, $manager, $request ) {
903                         return $rMethod->invokeArgs( $manager, [ &$info, $request ] );
904                 };
905
906                 $userInfo = UserInfo::newFromName( 'UTSysop', true );
907                 $unverifiedUserInfo = UserInfo::newFromName( 'UTSysop', false );
908
909                 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
910                 $metadata = [
911                         'userId' => $userInfo->getId(),
912                         'userName' => $userInfo->getName(),
913                         'userToken' => $userInfo->getToken( true ),
914                         'provider' => 'Mock',
915                 ];
916
917                 $builder = $this->getMockBuilder( SessionProvider::class )
918                         ->setMethods( [ '__toString', 'mergeMetadata', 'refreshSessionInfo' ] );
919
920                 $provider = $builder->getMockForAbstractClass();
921                 $provider->setManager( $manager );
922                 $provider->expects( $this->any() )->method( 'persistsSessionId' )
923                         ->will( $this->returnValue( true ) );
924                 $provider->expects( $this->any() )->method( 'canChangeUser' )
925                         ->will( $this->returnValue( true ) );
926                 $provider->expects( $this->any() )->method( 'refreshSessionInfo' )
927                         ->will( $this->returnValue( true ) );
928                 $provider->expects( $this->any() )->method( '__toString' )
929                         ->will( $this->returnValue( 'Mock' ) );
930                 $provider->expects( $this->any() )->method( 'mergeMetadata' )
931                         ->will( $this->returnCallback( function ( $a, $b ) {
932                                 if ( $b === [ 'Throw' ] ) {
933                                         throw new MetadataMergeException( 'no merge!' );
934                                 }
935                                 return [ 'Merged' ];
936                         } ) );
937
938                 $provider2 = $builder->getMockForAbstractClass();
939                 $provider2->setManager( $manager );
940                 $provider2->expects( $this->any() )->method( 'persistsSessionId' )
941                         ->will( $this->returnValue( false ) );
942                 $provider2->expects( $this->any() )->method( 'canChangeUser' )
943                         ->will( $this->returnValue( false ) );
944                 $provider2->expects( $this->any() )->method( '__toString' )
945                         ->will( $this->returnValue( 'Mock2' ) );
946                 $provider2->expects( $this->any() )->method( 'refreshSessionInfo' )
947                         ->will( $this->returnCallback( function ( $info, $request, &$metadata ) {
948                                 $metadata['changed'] = true;
949                                 return true;
950                         } ) );
951
952                 $provider3 = $builder->getMockForAbstractClass();
953                 $provider3->setManager( $manager );
954                 $provider3->expects( $this->any() )->method( 'persistsSessionId' )
955                         ->will( $this->returnValue( true ) );
956                 $provider3->expects( $this->any() )->method( 'canChangeUser' )
957                         ->will( $this->returnValue( true ) );
958                 $provider3->expects( $this->once() )->method( 'refreshSessionInfo' )
959                         ->will( $this->returnValue( false ) );
960                 $provider3->expects( $this->any() )->method( '__toString' )
961                         ->will( $this->returnValue( 'Mock3' ) );
962
963                 TestingAccessWrapper::newFromObject( $manager )->sessionProviders = [
964                         (string)$provider => $provider,
965                         (string)$provider2 => $provider2,
966                         (string)$provider3 => $provider3,
967                 ];
968
969                 // No metadata, basic usage
970                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
971                         'provider' => $provider,
972                         'id' => $id,
973                         'userInfo' => $userInfo
974                 ] );
975                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
976                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
977                 $this->assertFalse( $info->isIdSafe() );
978                 $this->assertSame( [], $logger->getBuffer() );
979
980                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
981                         'provider' => $provider,
982                         'userInfo' => $userInfo
983                 ] );
984                 $this->assertTrue( $info->isIdSafe(), 'sanity check' );
985                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
986                 $this->assertTrue( $info->isIdSafe() );
987                 $this->assertSame( [], $logger->getBuffer() );
988
989                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
990                         'provider' => $provider2,
991                         'id' => $id,
992                         'userInfo' => $userInfo
993                 ] );
994                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
995                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
996                 $this->assertTrue( $info->isIdSafe() );
997                 $this->assertSame( [], $logger->getBuffer() );
998
999                 // Unverified user, no metadata
1000                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1001                         'provider' => $provider,
1002                         'id' => $id,
1003                         'userInfo' => $unverifiedUserInfo
1004                 ] );
1005                 $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
1006                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1007                 $this->assertSame( [
1008                         [
1009                                 LogLevel::INFO,
1010                                 'Session "{session}": Unverified user provided and no metadata to auth it',
1011                         ]
1012                 ], $logger->getBuffer() );
1013                 $logger->clearBuffer();
1014
1015                 // No metadata, missing data
1016                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1017                         'id' => $id,
1018                         'userInfo' => $userInfo
1019                 ] );
1020                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1021                 $this->assertSame( [
1022                         [ LogLevel::WARNING, 'Session "{session}": Null provider and no metadata' ],
1023                 ], $logger->getBuffer() );
1024                 $logger->clearBuffer();
1025
1026                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1027                         'provider' => $provider,
1028                         'id' => $id,
1029                 ] );
1030                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1031                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1032                 $this->assertInstanceOf( UserInfo::class, $info->getUserInfo() );
1033                 $this->assertTrue( $info->getUserInfo()->isVerified() );
1034                 $this->assertTrue( $info->getUserInfo()->isAnon() );
1035                 $this->assertFalse( $info->isIdSafe() );
1036                 $this->assertSame( [], $logger->getBuffer() );
1037
1038                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1039                         'provider' => $provider2,
1040                         'id' => $id,
1041                 ] );
1042                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1043                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1044                 $this->assertSame( [
1045                         [ LogLevel::INFO, 'Session "{session}": No user provided and provider cannot set user' ]
1046                 ], $logger->getBuffer() );
1047                 $logger->clearBuffer();
1048
1049                 // Incomplete/bad metadata
1050                 $this->store->setRawSession( $id, true );
1051                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1052                 $this->assertSame( [
1053                         [ LogLevel::WARNING, 'Session "{session}": Bad data' ],
1054                 ], $logger->getBuffer() );
1055                 $logger->clearBuffer();
1056
1057                 $this->store->setRawSession( $id, [ 'data' => [] ] );
1058                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1059                 $this->assertSame( [
1060                         [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1061                 ], $logger->getBuffer() );
1062                 $logger->clearBuffer();
1063
1064                 $this->store->deleteSession( $id );
1065                 $this->store->setRawSession( $id, [ 'metadata' => $metadata ] );
1066                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1067                 $this->assertSame( [
1068                         [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1069                 ], $logger->getBuffer() );
1070                 $logger->clearBuffer();
1071
1072                 $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => true ] );
1073                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1074                 $this->assertSame( [
1075                         [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1076                 ], $logger->getBuffer() );
1077                 $logger->clearBuffer();
1078
1079                 $this->store->setRawSession( $id, [ 'metadata' => true, 'data' => [] ] );
1080                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1081                 $this->assertSame( [
1082                         [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1083                 ], $logger->getBuffer() );
1084                 $logger->clearBuffer();
1085
1086                 foreach ( $metadata as $key => $dummy ) {
1087                         $tmp = $metadata;
1088                         unset( $tmp[$key] );
1089                         $this->store->setRawSession( $id, [ 'metadata' => $tmp, 'data' => [] ] );
1090                         $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1091                         $this->assertSame( [
1092                                 [ LogLevel::WARNING, 'Session "{session}": Bad metadata' ],
1093                         ], $logger->getBuffer() );
1094                         $logger->clearBuffer();
1095                 }
1096
1097                 // Basic usage with metadata
1098                 $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => [] ] );
1099                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1100                         'provider' => $provider,
1101                         'id' => $id,
1102                         'userInfo' => $userInfo
1103                 ] );
1104                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1105                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1106                 $this->assertTrue( $info->isIdSafe() );
1107                 $this->assertSame( [], $logger->getBuffer() );
1108
1109                 // Mismatched provider
1110                 $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata );
1111                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1112                         'provider' => $provider,
1113                         'id' => $id,
1114                         'userInfo' => $userInfo
1115                 ] );
1116                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1117                 $this->assertSame( [
1118                         [ LogLevel::WARNING, 'Session "{session}": Wrong provider Bad !== Mock' ],
1119                 ], $logger->getBuffer() );
1120                 $logger->clearBuffer();
1121
1122                 // Unknown provider
1123                 $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata );
1124                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1125                         'id' => $id,
1126                         'userInfo' => $userInfo
1127                 ] );
1128                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1129                 $this->assertSame( [
1130                         [ LogLevel::WARNING, 'Session "{session}": Unknown provider Bad' ],
1131                 ], $logger->getBuffer() );
1132                 $logger->clearBuffer();
1133
1134                 // Fill in provider
1135                 $this->store->setSessionMeta( $id, $metadata );
1136                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1137                         'id' => $id,
1138                         'userInfo' => $userInfo
1139                 ] );
1140                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1141                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1142                 $this->assertTrue( $info->isIdSafe() );
1143                 $this->assertSame( [], $logger->getBuffer() );
1144
1145                 // Bad user metadata
1146                 $this->store->setSessionMeta( $id, [ 'userId' => -1, 'userToken' => null ] + $metadata );
1147                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1148                         'provider' => $provider,
1149                         'id' => $id,
1150                 ] );
1151                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1152                 $this->assertSame( [
1153                         [ LogLevel::ERROR, 'Session "{session}": {exception}' ],
1154                 ], $logger->getBuffer() );
1155                 $logger->clearBuffer();
1156
1157                 $this->store->setSessionMeta(
1158                         $id, [ 'userId' => 0, 'userName' => '<X>', 'userToken' => null ] + $metadata
1159                 );
1160                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1161                         'provider' => $provider,
1162                         'id' => $id,
1163                 ] );
1164                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1165                 $this->assertSame( [
1166                         [ LogLevel::ERROR, 'Session "{session}": {exception}', ],
1167                 ], $logger->getBuffer() );
1168                 $logger->clearBuffer();
1169
1170                 // Mismatched user by ID
1171                 $this->store->setSessionMeta(
1172                         $id, [ 'userId' => $userInfo->getId() + 1, 'userToken' => null ] + $metadata
1173                 );
1174                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1175                         'provider' => $provider,
1176                         'id' => $id,
1177                         'userInfo' => $userInfo
1178                 ] );
1179                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1180                 $this->assertSame( [
1181                         [ LogLevel::WARNING, 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}' ],
1182                 ], $logger->getBuffer() );
1183                 $logger->clearBuffer();
1184
1185                 // Mismatched user by name
1186                 $this->store->setSessionMeta(
1187                         $id, [ 'userId' => 0, 'userName' => 'X', 'userToken' => null ] + $metadata
1188                 );
1189                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1190                         'provider' => $provider,
1191                         'id' => $id,
1192                         'userInfo' => $userInfo
1193                 ] );
1194                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1195                 $this->assertSame( [
1196                         [ LogLevel::WARNING, 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}' ],
1197                 ], $logger->getBuffer() );
1198                 $logger->clearBuffer();
1199
1200                 // ID matches, name doesn't
1201                 $this->store->setSessionMeta(
1202                         $id, [ 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ] + $metadata
1203                 );
1204                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1205                         'provider' => $provider,
1206                         'id' => $id,
1207                         'userInfo' => $userInfo
1208                 ] );
1209                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1210                 $this->assertSame( [
1211                         [
1212                                 LogLevel::WARNING,
1213                                 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}'
1214                         ],
1215                 ], $logger->getBuffer() );
1216                 $logger->clearBuffer();
1217
1218                 // Mismatched anon user
1219                 $this->store->setSessionMeta(
1220                         $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata
1221                 );
1222                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1223                         'provider' => $provider,
1224                         'id' => $id,
1225                         'userInfo' => $userInfo
1226                 ] );
1227                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1228                 $this->assertSame( [
1229                         [
1230                                 LogLevel::WARNING,
1231                                 'Session "{session}": Metadata has an anonymous user, ' .
1232                                 'but a non-anon user was provided',
1233                         ],
1234                 ], $logger->getBuffer() );
1235                 $logger->clearBuffer();
1236
1237                 // Lookup user by ID
1238                 $this->store->setSessionMeta( $id, [ 'userToken' => null ] + $metadata );
1239                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1240                         'provider' => $provider,
1241                         'id' => $id,
1242                 ] );
1243                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1244                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1245                 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1246                 $this->assertTrue( $info->isIdSafe() );
1247                 $this->assertSame( [], $logger->getBuffer() );
1248
1249                 // Lookup user by name
1250                 $this->store->setSessionMeta(
1251                         $id, [ 'userId' => 0, 'userName' => 'UTSysop', 'userToken' => null ] + $metadata
1252                 );
1253                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1254                         'provider' => $provider,
1255                         'id' => $id,
1256                 ] );
1257                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1258                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1259                 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1260                 $this->assertTrue( $info->isIdSafe() );
1261                 $this->assertSame( [], $logger->getBuffer() );
1262
1263                 // Lookup anonymous user
1264                 $this->store->setSessionMeta(
1265                         $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata
1266                 );
1267                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1268                         'provider' => $provider,
1269                         'id' => $id,
1270                 ] );
1271                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1272                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1273                 $this->assertTrue( $info->getUserInfo()->isAnon() );
1274                 $this->assertTrue( $info->isIdSafe() );
1275                 $this->assertSame( [], $logger->getBuffer() );
1276
1277                 // Unverified user with metadata
1278                 $this->store->setSessionMeta( $id, $metadata );
1279                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1280                         'provider' => $provider,
1281                         'id' => $id,
1282                         'userInfo' => $unverifiedUserInfo
1283                 ] );
1284                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1285                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1286                 $this->assertTrue( $info->getUserInfo()->isVerified() );
1287                 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1288                 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1289                 $this->assertTrue( $info->isIdSafe() );
1290                 $this->assertSame( [], $logger->getBuffer() );
1291
1292                 // Unverified user with metadata
1293                 $this->store->setSessionMeta( $id, $metadata );
1294                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1295                         'provider' => $provider,
1296                         'id' => $id,
1297                         'userInfo' => $unverifiedUserInfo
1298                 ] );
1299                 $this->assertFalse( $info->isIdSafe(), 'sanity check' );
1300                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1301                 $this->assertTrue( $info->getUserInfo()->isVerified() );
1302                 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1303                 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1304                 $this->assertTrue( $info->isIdSafe() );
1305                 $this->assertSame( [], $logger->getBuffer() );
1306
1307                 // Wrong token
1308                 $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
1309                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1310                         'provider' => $provider,
1311                         'id' => $id,
1312                         'userInfo' => $userInfo
1313                 ] );
1314                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1315                 $this->assertSame( [
1316                         [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ],
1317                 ], $logger->getBuffer() );
1318                 $logger->clearBuffer();
1319
1320                 // Provider metadata
1321                 $this->store->setSessionMeta( $id, [ 'provider' => 'Mock2' ] + $metadata );
1322                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1323                         'provider' => $provider2,
1324                         'id' => $id,
1325                         'userInfo' => $userInfo,
1326                         'metadata' => [ 'Info' ],
1327                 ] );
1328                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1329                 $this->assertSame( [ 'Info', 'changed' => true ], $info->getProviderMetadata() );
1330                 $this->assertSame( [], $logger->getBuffer() );
1331
1332                 $this->store->setSessionMeta( $id, [ 'providerMetadata' => [ 'Saved' ] ] + $metadata );
1333                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1334                         'provider' => $provider,
1335                         'id' => $id,
1336                         'userInfo' => $userInfo,
1337                 ] );
1338                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1339                 $this->assertSame( [ 'Saved' ], $info->getProviderMetadata() );
1340                 $this->assertSame( [], $logger->getBuffer() );
1341
1342                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1343                         'provider' => $provider,
1344                         'id' => $id,
1345                         'userInfo' => $userInfo,
1346                         'metadata' => [ 'Info' ],
1347                 ] );
1348                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1349                 $this->assertSame( [ 'Merged' ], $info->getProviderMetadata() );
1350                 $this->assertSame( [], $logger->getBuffer() );
1351
1352                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1353                         'provider' => $provider,
1354                         'id' => $id,
1355                         'userInfo' => $userInfo,
1356                         'metadata' => [ 'Throw' ],
1357                 ] );
1358                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1359                 $this->assertSame( [
1360                         [
1361                                 LogLevel::WARNING,
1362                                 'Session "{session}": Metadata merge failed: {exception}',
1363                         ],
1364                 ], $logger->getBuffer() );
1365                 $logger->clearBuffer();
1366
1367                 // Remember from session
1368                 $this->store->setSessionMeta( $id, $metadata );
1369                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1370                         'provider' => $provider,
1371                         'id' => $id,
1372                 ] );
1373                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1374                 $this->assertFalse( $info->wasRemembered() );
1375                 $this->assertSame( [], $logger->getBuffer() );
1376
1377                 $this->store->setSessionMeta( $id, [ 'remember' => true ] + $metadata );
1378                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1379                         'provider' => $provider,
1380                         'id' => $id,
1381                 ] );
1382                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1383                 $this->assertTrue( $info->wasRemembered() );
1384                 $this->assertSame( [], $logger->getBuffer() );
1385
1386                 $this->store->setSessionMeta( $id, [ 'remember' => false ] + $metadata );
1387                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1388                         'provider' => $provider,
1389                         'id' => $id,
1390                         'userInfo' => $userInfo
1391                 ] );
1392                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1393                 $this->assertTrue( $info->wasRemembered() );
1394                 $this->assertSame( [], $logger->getBuffer() );
1395
1396                 // forceHTTPS from session
1397                 $this->store->setSessionMeta( $id, $metadata );
1398                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1399                         'provider' => $provider,
1400                         'id' => $id,
1401                         'userInfo' => $userInfo
1402                 ] );
1403                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1404                 $this->assertFalse( $info->forceHTTPS() );
1405                 $this->assertSame( [], $logger->getBuffer() );
1406
1407                 $this->store->setSessionMeta( $id, [ 'forceHTTPS' => true ] + $metadata );
1408                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1409                         'provider' => $provider,
1410                         'id' => $id,
1411                         'userInfo' => $userInfo
1412                 ] );
1413                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1414                 $this->assertTrue( $info->forceHTTPS() );
1415                 $this->assertSame( [], $logger->getBuffer() );
1416
1417                 $this->store->setSessionMeta( $id, [ 'forceHTTPS' => false ] + $metadata );
1418                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1419                         'provider' => $provider,
1420                         'id' => $id,
1421                         'userInfo' => $userInfo,
1422                         'forceHTTPS' => true
1423                 ] );
1424                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1425                 $this->assertTrue( $info->forceHTTPS() );
1426                 $this->assertSame( [], $logger->getBuffer() );
1427
1428                 // "Persist" flag from session
1429                 $this->store->setSessionMeta( $id, $metadata );
1430                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1431                         'provider' => $provider,
1432                         'id' => $id,
1433                         'userInfo' => $userInfo
1434                 ] );
1435                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1436                 $this->assertFalse( $info->wasPersisted() );
1437                 $this->assertSame( [], $logger->getBuffer() );
1438
1439                 $this->store->setSessionMeta( $id, [ 'persisted' => true ] + $metadata );
1440                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1441                         'provider' => $provider,
1442                         'id' => $id,
1443                         'userInfo' => $userInfo
1444                 ] );
1445                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1446                 $this->assertTrue( $info->wasPersisted() );
1447                 $this->assertSame( [], $logger->getBuffer() );
1448
1449                 $this->store->setSessionMeta( $id, [ 'persisted' => false ] + $metadata );
1450                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1451                         'provider' => $provider,
1452                         'id' => $id,
1453                         'userInfo' => $userInfo,
1454                         'persisted' => true
1455                 ] );
1456                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1457                 $this->assertTrue( $info->wasPersisted() );
1458                 $this->assertSame( [], $logger->getBuffer() );
1459
1460                 // Provider refreshSessionInfo() returning false
1461                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1462                         'provider' => $provider3,
1463                 ] );
1464                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1465                 $this->assertSame( [], $logger->getBuffer() );
1466
1467                 // Hook
1468                 $called = false;
1469                 $data = [ 'foo' => 1 ];
1470                 $this->store->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] );
1471                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1472                         'provider' => $provider,
1473                         'id' => $id,
1474                         'userInfo' => $userInfo
1475                 ] );
1476                 $this->mergeMwGlobalArrayValue( 'wgHooks', [
1477                         'SessionCheckInfo' => [ function ( &$reason, $i, $r, $m, $d ) use (
1478                                 $info, $metadata, $data, $request, &$called
1479                         ) {
1480                                 $this->assertSame( $info->getId(), $i->getId() );
1481                                 $this->assertSame( $info->getProvider(), $i->getProvider() );
1482                                 $this->assertSame( $info->getUserInfo(), $i->getUserInfo() );
1483                                 $this->assertSame( $request, $r );
1484                                 $this->assertEquals( $metadata, $m );
1485                                 $this->assertEquals( $data, $d );
1486                                 $called = true;
1487                                 return false;
1488                         } ]
1489                 ] );
1490                 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1491                 $this->assertTrue( $called );
1492                 $this->assertSame( [
1493                         [ LogLevel::WARNING, 'Session "{session}": Hook aborted' ],
1494                 ], $logger->getBuffer() );
1495                 $logger->clearBuffer();
1496                 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionCheckInfo' => [] ] );
1497
1498                 // forceUse deletes bad backend data
1499                 $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
1500                 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1501                         'provider' => $provider,
1502                         'id' => $id,
1503                         'userInfo' => $userInfo,
1504                         'forceUse' => true,
1505                 ] );
1506                 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1507                 $this->assertFalse( $this->store->getSession( $id ) );
1508                 $this->assertSame( [
1509                         [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ],
1510                 ], $logger->getBuffer() );
1511                 $logger->clearBuffer();
1512         }
1513 }