a04eb4da5f8e55a251869fb0f1e7b06722609773
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Unit / Authentication / FrontendUserAuthenticationTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Unit\Authentication;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Doctrine\DBAL\Statement;
19 use Prophecy\Argument;
20 use Psr\Log\NullLogger;
21 use TYPO3\CMS\Core\Authentication\AuthenticationService;
22 use TYPO3\CMS\Core\Crypto\Random;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
25 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
26 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
27 use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException;
28 use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
29 use TYPO3\CMS\Core\Session\SessionManager;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
32 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
33
34 /**
35 * Test cases for FrontendUserAuthentication
36 *
37 * @todo: Some of these tests would be better suited as functional tests
38 */
39 class FrontendUserAuthenticationTest extends UnitTestCase
40 {
41 /**
42 * @var bool Reset singletons created by subject
43 */
44 protected $resetSingletonInstances = true;
45
46 /**
47 * User properties should not be set for anonymous sessions
48 *
49 * @test
50 */
51 public function userFieldIsNotSetForAnonymousSessions()
52 {
53 $uniqueSessionId = $this->getUniqueId('test');
54 $_COOKIE['fe_typo_user'] = $uniqueSessionId;
55
56 // This setup fakes the "getAuthInfoArray() db call
57 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
58 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
59 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
60 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
61 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
62 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
63 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
64 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
65 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
66
67 // Main session backend setup
68 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
69 $sessionRecord = [
70 'ses_id' => $uniqueSessionId,
71 'ses_data' => serialize(['foo' => 'bar']),
72 'ses_anonymous' => true,
73 'ses_iplock' => '[DISABLED]',
74 ];
75 $sessionBackendProphecy->get($uniqueSessionId)->shouldBeCalled()->willReturn($sessionRecord);
76 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
77 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
78 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
79
80 $subject = new FrontendUserAuthentication();
81 $subject->setLogger(new NullLogger());
82 $subject->gc_probability = -1;
83 $subject->start();
84
85 $this->assertArrayNotHasKey('uid', $subject->user);
86 $this->assertEquals('bar', $subject->getSessionData('foo'));
87 $this->assertEquals($uniqueSessionId, $subject->id);
88 }
89
90 /**
91 * @test
92 */
93 public function storeSessionDataOnAnonymousUserWithNoData()
94 {
95 // This setup fakes the "getAuthInfoArray() db call
96 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
97 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
98 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
99 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
100 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
101 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
102 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
103 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
104 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
105
106 // Main session backend setup
107 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
108 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
109 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
110 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
111
112 // Verify new session id is generated
113 $randomProphecy = $this->prophesize(Random::class);
114 $randomProphecy->generateRandomHexString(32)->shouldBeCalled()->willReturn('newSessionId');
115 GeneralUtility::addInstance(Random::class, $randomProphecy->reveal());
116
117 // set() and update() shouldn't be called since no session cookie is set
118 $sessionBackendProphecy->set(Argument::cetera())->shouldNotBeCalled();
119 $sessionBackendProphecy->update(Argument::cetera())->shouldNotBeCalled();
120
121 $subject = new FrontendUserAuthentication();
122 $subject->setLogger(new NullLogger());
123 $subject->gc_probability = -1;
124 $subject->start();
125 $subject->storeSessionData();
126 }
127
128 /**
129 * Setting and immediately removing session data should be handled correctly.
130 * No write operations should be made
131 *
132 * @test
133 */
134 public function canSetAndUnsetSessionKey()
135 {
136 $uniqueSessionId = $this->getUniqueId('test');
137 $_COOKIE['fe_typo_user'] = $uniqueSessionId;
138
139 // This setup fakes the "getAuthInfoArray() db call
140 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
141 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
142 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
143 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
144 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
145 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
146 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
147 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
148 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
149
150 // Main session backend setup
151 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
152 $sessionRecord = [
153 'ses_id' => $uniqueSessionId,
154 'ses_data' => serialize(['foo' => 'bar']),
155 'ses_anonymous' => true,
156 'ses_iplock' => '[DISABLED]',
157 ];
158 $sessionBackendProphecy->get($uniqueSessionId)->shouldBeCalled()->willReturn($sessionRecord);
159 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
160 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
161 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
162
163 // set() and update() shouldn't be called since no session cookie is set
164 $sessionBackendProphecy->set(Argument::cetera())->shouldNotBeCalled();
165 $sessionBackendProphecy->update(Argument::cetera())->shouldNotBeCalled();
166 // remove() should be called with given session id
167 $sessionBackendProphecy->remove($uniqueSessionId)->shouldBeCalled();
168
169 $subject = new FrontendUserAuthentication();
170 $subject->setLogger(new NullLogger());
171 $subject->gc_probability = -1;
172 $subject->start();
173 $subject->setSessionData('foo', 'bar');
174 $subject->removeSessionData();
175 $this->assertAttributeEmpty('sessionData', $subject);
176 }
177
178 /**
179 * A user that is not signed in should be able to have associated session data
180 *
181 * @test
182 */
183 public function canSetSessionDataForAnonymousUser()
184 {
185 $uniqueSessionId = $this->getUniqueId('test');
186 $_COOKIE['fe_typo_user'] = $uniqueSessionId;
187 $currentTime = $GLOBALS['EXEC_TIME'];
188
189 // This setup fakes the "getAuthInfoArray() db call
190 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
191 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
192 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
193 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
194 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
195 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
196 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
197 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
198 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
199
200 // Main session backend setup
201 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
202 $sessionBackendProphecy->get($uniqueSessionId)->shouldBeCalled()->willThrow(new SessionNotFoundException('testing', 1486676313));
203 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
204 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
205 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
206
207 // Verify new session id is generated
208 $randomProphecy = $this->prophesize(Random::class);
209 $randomProphecy->generateRandomHexString(32)->shouldBeCalled()->willReturn('newSessionId');
210 GeneralUtility::addInstance(Random::class, $randomProphecy->reveal());
211
212 // set() and update() shouldn't be called since no session cookie is set
213 $sessionBackendProphecy->update(Argument::cetera())->shouldNotBeCalled();
214 $sessionBackendProphecy->get('newSessionId')->shouldBeCalled()->willThrow(new SessionNotFoundException('testing', 1486676314));
215
216 // new session should be written
217 $sessionBackendProphecy->set(
218 'newSessionId',
219 [
220 'ses_id' => 'newSessionId',
221 'ses_iplock' => '[DISABLED]',
222 'ses_userid' => 0,
223 'ses_tstamp' => $currentTime,
224 'ses_data' => serialize(['foo' => 'bar']),
225 'ses_permanent' => 0,
226 'ses_anonymous' => 1 // sic!
227 ]
228 )->shouldBeCalled();
229
230 $subject = new FrontendUserAuthentication();
231 $subject->setLogger(new NullLogger());
232 $subject->gc_probability = -1;
233 $subject->start();
234 $subject->lockIP = 0;
235 $this->assertEmpty($subject->getSessionData($uniqueSessionId));
236 $this->assertEmpty($subject->user);
237 $subject->setSessionData('foo', 'bar');
238 $this->assertAttributeNotEmpty('sessionData', $subject);
239
240 // Suppress "headers already sent" errors - phpunit does that internally already
241 $prev = error_reporting(0);
242 $subject->storeSessionData();
243 error_reporting($prev);
244 }
245
246 /**
247 * Session data should be loaded when a session cookie is available and user user is authenticated
248 *
249 * @test
250 */
251 public function canLoadExistingAuthenticatedSession()
252 {
253 $uniqueSessionId = $this->getUniqueId('test');
254 $_COOKIE['fe_typo_user'] = $uniqueSessionId;
255 $currentTime = $GLOBALS['EXEC_TIME'];
256
257 // This setup fakes the "getAuthInfoArray() db call
258 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
259 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
260 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
261 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
262 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
263 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
264 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
265 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
266 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
267
268 // Main session backend setup
269 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
270 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
271 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
272 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
273
274 // a valid session is returned
275 $sessionBackendProphecy->get($uniqueSessionId)->shouldBeCalled()->willReturn(
276 [
277 'ses_id' => $uniqueSessionId,
278 'ses_userid' => 1,
279 'ses_iplock' => '[DISABLED]',
280 'ses_tstamp' => $currentTime,
281 'ses_data' => serialize(['foo' => 'bar']),
282 'ses_permanent' => 0,
283 'ses_anonymous' => 0 // sic!
284 ]
285 );
286
287 // Mock call to fe_users table and let it return a valid user row
288 $connectionPoolFeUserProphecy = $this->prophesize(ConnectionPool::class);
289 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolFeUserProphecy->reveal());
290 $queryBuilderFeUserProphecy = $this->prophesize(QueryBuilder::class);
291 $queryBuilderFeUserProphecyRevelation = $queryBuilderFeUserProphecy->reveal();
292 $connectionPoolFeUserProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderFeUserProphecyRevelation);
293 $queryBuilderFeUserProphecy->select('*')->willReturn($queryBuilderFeUserProphecyRevelation);
294 $queryBuilderFeUserProphecy->setRestrictions(Argument::cetera())->shouldBeCalled();
295 $queryBuilderFeUserProphecy->from('fe_users')->shouldBeCalled()->willReturn($queryBuilderFeUserProphecyRevelation);
296 $expressionBuilderFeUserProphecy = $this->prophesize(ExpressionBuilder::class);
297 $queryBuilderFeUserProphecy->expr()->willReturn($expressionBuilderFeUserProphecy->reveal());
298 $queryBuilderFeUserProphecy->createNamedParameter(Argument::cetera())->willReturnArgument(0);
299 $expressionBuilderFeUserProphecy->eq(Argument::cetera())->willReturn('1=1');
300 $queryBuilderFeUserProphecy->where(Argument::cetera())->shouldBeCalled()->willReturn($queryBuilderFeUserProphecyRevelation);
301 $statementFeUserProphecy = $this->prophesize(Statement::class);
302 $queryBuilderFeUserProphecy->execute()->shouldBeCalled()->willReturn($statementFeUserProphecy->reveal());
303 $statementFeUserProphecy->fetch()->willReturn(
304 [
305 'uid' => 1,
306 'username' => 'existingUserName',
307 'password' => 'abc',
308 'deleted' => 0,
309 'disabled' => 0
310 ]
311 );
312
313 $subject = new FrontendUserAuthentication();
314 $subject->setLogger(new NullLogger());
315 $subject->gc_probability = -1;
316 $subject->start();
317
318 $this->assertAttributeNotEmpty('user', $subject);
319 $this->assertEquals('existingUserName', $subject->user['username']);
320 }
321
322 /**
323 * @test
324 */
325 public function canLogUserInWithoutAnonymousSession()
326 {
327 $GLOBALS['BE_USER'] = [];
328 // This setup fakes the "getAuthInfoArray() db call
329 $queryBuilderProphecy = $this->prophesize(QueryBuilder::class);
330 $connectionPoolProphecy = $this->prophesize(ConnectionPool::class);
331 $connectionPoolProphecy->getQueryBuilderForTable('fe_users')->willReturn($queryBuilderProphecy->reveal());
332 GeneralUtility::addInstance(ConnectionPool::class, $connectionPoolProphecy->reveal());
333 $expressionBuilderProphecy = $this->prophesize(ExpressionBuilder::class);
334 $queryBuilderProphecy->expr()->willReturn($expressionBuilderProphecy->reveal());
335 $compositeExpressionProphecy = $this->prophesize(CompositeExpression::class);
336 $expressionBuilderProphecy->andX(Argument::cetera())->willReturn($compositeExpressionProphecy->reveal());
337 $expressionBuilderProphecy->in(Argument::cetera())->willReturn('');
338
339 // Main session backend setup
340 $sessionBackendProphecy = $this->prophesize(SessionBackendInterface::class);
341 $sessionManagerProphecy = $this->prophesize(SessionManager::class);
342 GeneralUtility::setSingletonInstance(SessionManager::class, $sessionManagerProphecy->reveal());
343 $sessionManagerProphecy->getSessionBackend('FE')->willReturn($sessionBackendProphecy->reveal());
344
345 // no session exists, yet
346 $sessionBackendProphecy->get('newSessionId')->willThrow(new SessionNotFoundException('testing', 1486676358));
347 $sessionBackendProphecy->remove('newSessionId')->shouldBeCalled();
348
349 // Verify new session id is generated
350 $randomProphecy = $this->prophesize(Random::class);
351 $randomProphecy->generateRandomHexString(32)->shouldBeCalled()->willReturn('newSessionId');
352 GeneralUtility::addInstance(Random::class, $randomProphecy->reveal());
353
354 // Mock the login data and auth services here since fully prophesize this is a lot of hassle
355 $subject = $this->getMockBuilder($this->buildAccessibleProxy(FrontendUserAuthentication::class))
356 ->setMethods([
357 'getLoginFormData',
358 'getAuthServices',
359 'createUserSession',
360 'getCookie',
361 ])
362 ->getMock();
363 $subject->setLogger(new NullLogger());
364 $subject->gc_probability = -1;
365
366 // Mock a login attempt
367 $subject->method('getLoginFormData')->willReturn([
368 'status' => 'login',
369 'uname' => 'existingUserName',
370 'uident' => 'abc'
371 ]);
372
373 $authServiceMock = $this->getMockBuilder(AuthenticationService::class)->getMock();
374 $authServiceMock->method('getUser')->willReturn([
375 'uid' => 1,
376 'username' => 'existingUserName'
377 ]);
378 // Auth services can return true or 200
379 $authServiceMock->method('authUser')->willReturn(true);
380 // We need to wrap the array to something thats is \Traversable, in PHP 7.1 we can use traversable pseudo type instead
381 $subject->method('getAuthServices')->willReturn(new \ArrayIterator([$authServiceMock]));
382
383 $subject->method('createUserSession')->willReturn([
384 'ses_id' => 'newSessionId'
385 ]);
386
387 $subject->method('getCookie')->willReturn(null);
388
389 $subject->start();
390 $this->assertFalse($subject->_get('loginFailure'));
391 $this->assertEquals('existingUserName', $subject->user['username']);
392 }
393
394 /**
395 * Session data set before a user is signed in should be preserved when signing in
396 *
397 * @test
398 */
399 public function canPreserveSessionDataWhenAuthenticating()
400 {
401 $this->markTestSkipped('Test is flaky, convert to a functional test');
402 // Mock SessionBackend
403 $sessionBackend = $this->getMockBuilder(SessionBackendInterface::class)->getMock();
404
405 $oldSessionRecord = [
406 'ses_id' => 'oldSessionId',
407 'ses_data' => serialize(['foo' => 'bar']),
408 'ses_anonymous' => 1,
409 'ses_iplock' => 0,
410 ];
411
412 // Return old, non authenticated session
413 $sessionBackend->method('get')->willReturn($oldSessionRecord);
414
415 $expectedSessionRecord = array_merge(
416 $oldSessionRecord,
417 [
418 //ses_id is overwritten by the session backend
419 'ses_anonymous' => 0
420 ]
421 );
422
423 $expectedUserId = 1;
424
425 $sessionBackend->expects($this->once())->method('set')->with(
426 'newSessionId',
427 $this->equalTo($expectedSessionRecord)
428 )->willReturnArgument(1);
429
430 $this->subject->method('getSessionBackend')->willReturn($sessionBackend);
431 // Load old sessions
432 $this->subject->method('getCookie')->willReturn('oldSessionId');
433 $this->subject->method('createSessionId')->willReturn('newSessionId');
434
435 // Mock a login attempt
436 $this->subject->method('getLoginFormData')->willReturn([
437 'status' => 'login',
438 'uname' => 'existingUserName',
439 'uident' => 'abc'
440 ]);
441
442 $authServiceMock = $this->getMockBuilder(AuthenticationService::class)->getMock();
443 $authServiceMock->method('getUser')->willReturn([
444 'uid' => 1,
445 'username' => 'existingUserName'
446 ]);
447
448 $authServiceMock->method('authUser')->willReturn(true); // Auth services can return true or 200
449
450 // We need to wrap the array to something thats is \Traversable, in PHP 7.1 we can use traversable pseudo type instead
451 $this->subject->method('getAuthServices')->willReturn(new \ArrayIterator([$authServiceMock]));
452
453 // Should call regenerateSessionId
454 // New session should be stored with with old values
455 $this->subject->start();
456
457 $this->assertEquals('newSessionId', $this->subject->id);
458 $this->assertEquals($expectedUserId, $this->subject->user['uid']);
459 $this->subject->setSessionData('foobar', 'baz');
460 $this->assertArraySubset(['foo' => 'bar'], $this->subject->_get('sessionData'));
461 $this->assertTrue($this->subject->sesData_change);
462 }
463
464 /**
465 * removeSessionData should clear all session data
466 *
467 * @test
468 */
469 public function canRemoveSessionData()
470 {
471 $this->markTestSkipped('Test is flaky, convert to a functional test');
472 // Mock SessionBackend
473 $sessionBackend = $this->getMockBuilder(SessionBackendInterface::class)->getMock();
474 $sessionBackend->method('get')->willReturn(
475 [
476 'ses_id' => 'existingId',
477 'ses_userid' => 1, // fe_user with uid 0 assumed in database, see fixtures.xml
478 'ses_data' => serialize(['foo' => 'bar']),
479 'ses_iplock' => 0,
480 'ses_tstamp' => time() + 100 // Return a time in future to make avoid mocking $GLOBALS['EXEC_TIME']
481 ]
482 );
483 $this->subject->method('getSessionBackend')->willReturn($sessionBackend);
484 $this->subject->method('getCookie')->willReturn('existingId');
485
486 $this->subject->start();
487
488 $this->subject->removeSessionData();
489 $this->assertEmpty($this->subject->getSessionData('foo'));
490 $this->subject->storeSessionData();
491 $this->assertEmpty($this->subject->getSessionData('foo'));
492 }
493
494 /**
495 * @test
496 *
497 * If a user has an anonymous session, and its data is set to null, then the record is removed
498 */
499 public function destroysAnonymousSessionIfDataIsNull()
500 {
501 $this->markTestSkipped('Test is flaky, convert to a functional test');
502 $sessionBackend = $this->getMockBuilder(SessionBackendInterface::class)->getMock();
503 // Mock SessionBackend
504 $this->subject->method('getSessionBackend')->willReturn($sessionBackend);
505
506 $this->subject->method('createSessionId')->willReturn('newSessionId');
507
508 $expectedSessionRecord = [
509 'ses_anonymous' => 1,
510 'ses_data' => serialize(['foo' => 'bar'])
511 ];
512
513 $sessionBackend->expects($this->at(0))->method('get')->willThrowException(new SessionNotFoundException('testing', 1486045419));
514 $sessionBackend->expects($this->at(1))->method('get')->willThrowException(new SessionNotFoundException('testing', 1486045420));
515 $sessionBackend->expects($this->at(2))->method('get')->willReturn(
516 [
517 'ses_id' => 'newSessionId',
518 'ses_anonymous' => 1
519 ]
520 );
521
522 $sessionBackend->expects($this->once())
523 ->method('set')
524 ->with('newSessionId', new \PHPUnit_Framework_Constraint_ArraySubset($expectedSessionRecord))
525 ->willReturn([
526 'ses_id' => 'newSessionId',
527 'ses_anonymous' => 1,
528 'ses_data' => serialize(['foo' => 'bar']),
529 ]);
530
531 // Can set and store session data
532 $this->subject->start();
533 $this->assertEmpty($this->subject->_get('sessionData'));
534 $this->assertEmpty($this->subject->user);
535 $this->subject->setSessionData('foo', 'bar');
536 $this->assertAttributeNotEmpty('sessionData', $this->subject);
537 $this->subject->storeSessionData();
538
539 // Should delete session after setting to null
540 $this->subject->setSessionData('foo', null);
541 $this->assertAttributeEmpty('sessionData', $this->subject);
542 $sessionBackend->expects($this->once())->method('remove')->with('newSessionId');
543 $sessionBackend->expects($this->never())->method('update');
544
545 $this->subject->storeSessionData();
546 }
547
548 /**
549 * @test
550 * Any session data set when logged in should be preserved when logging out
551 */
552 public function sessionDataShouldBePreservedOnLogout()
553 {
554 $this->markTestSkipped('Test is flaky, convert to a functional test');
555 $sessionBackend = $this->getMockBuilder(SessionBackendInterface::class)->getMock();
556 $this->subject->method('getSessionBackend')->willReturn($sessionBackend);
557 $this->subject->method('createSessionId')->willReturn('newSessionId');
558
559 $sessionBackend->method('get')->willReturn(
560 [
561 'ses_id' => 'existingId',
562 'ses_userid' => 1,
563 'ses_data' => serialize(['foo' => 'bar']),
564 'ses_iplock' => 0,
565 'ses_tstamp' => time() + 100 // Return a time in future to make avoid mocking $GLOBALS['EXEC_TIME']
566 ]
567 );
568 $this->subject->method('getSessionBackend')->willReturn($sessionBackend);
569 $this->subject->method('getCookie')->willReturn('existingId');
570
571 $this->subject->method('getRawUserByUid')->willReturn([
572 'uid' => 1,
573 ]);
574
575 // fix logout data
576 // Mock a logout attempt
577 $this->subject->method('getLoginFormData')->willReturn([
578 'status' => 'logout',
579
580 ]);
581
582 $sessionBackend->expects($this->once())->method('set')->with('newSessionId', $this->anything())->willReturnArgument(1);
583 $sessionBackend->expects($this->once())->method('remove')->with('existingId');
584
585 // start
586 $this->subject->start();
587 // asset that session data is there
588 $this->assertNotEmpty($this->subject->user);
589 $this->assertEquals(1, (int)$this->subject->user['ses_anonymous']);
590 $this->assertEquals(['foo' => 'bar'], $this->subject->_get('sessionData'));
591
592 $this->assertEquals('newSessionId', $this->subject->id);
593 }
594 }