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