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