clock = new FrozenClock(); $this->randomGenerator = new TestableRandomGenerator(); $this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator); $this->storage = new InMemorySessionStorage(); $this->idGenerator = new SimpleSessionIdGenerator($this->randomGenerator); $this->responseManipulator = new ResponseManipulator(); $this->cookieConfig = new SessionCookieConfig(); $this->sessionManager = new SessionManager( $this->idGenerator, $this->responseManipulator, $this->clock, $this->csrfTokenGenerator, $this->storage, $this->cookieConfig ); }); describe('Complete Session Lifecycle', function () { test('full session lifecycle: create -> use -> persist -> reload -> destroy', function () { // Phase 1: Session Creation $request1 = new Request( method: 'GET', uri: '/login', cookies: new Cookies([]), headers: [], body: '' ); $session = $this->sessionManager->getOrCreateSession($request1); expect($session)->toBeInstanceOf(Session::class); expect($session->isStarted())->toBeTrue(); $originalSessionId = $session->id->toString(); // Phase 2: Session Usage (simulate login process) $session->set('user_id', 123); $session->set('username', 'testuser'); $session->set('login_time', time()); $session->set('user_permissions', ['read', 'write', 'admin']); // Initialize components $session->fromArray($session->all()); // Use session components $session->flash->add('success', 'Login successful!'); $session->csrf->generateToken('login_form'); expect($session->get('user_id'))->toBe(123); expect($session->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class); // Phase 3: Session Persistence $response1 = new Response(200, [], 'Login successful'); $responseWithCookie = $this->sessionManager->saveSession($session, $response1); // Check cookie is set expect($responseWithCookie->headers)->toHaveKey('Set-Cookie'); expect($responseWithCookie->headers['Set-Cookie'])->toContain($originalSessionId); // Verify data is stored $storedData = $this->storage->read($session->id); expect($storedData['user_id'])->toBe(123); expect($storedData['username'])->toBe('testuser'); // Phase 4: Session Reload (simulate new request) $cookies = new Cookies([ new Cookie('ms_context', $originalSessionId), ]); $request2 = new Request( method: 'GET', uri: '/dashboard', cookies: $cookies, headers: [], body: '' ); $reloadedSession = $this->sessionManager->getOrCreateSession($request2); // Verify session data persisted expect($reloadedSession->id->toString())->toBe($originalSessionId); expect($reloadedSession->get('user_id'))->toBe(123); expect($reloadedSession->get('username'))->toBe('testuser'); expect($reloadedSession->get('user_permissions'))->toBe(['read', 'write', 'admin']); // Verify components are working expect($reloadedSession->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class); expect($reloadedSession->csrf)->toBeInstanceOf(\App\Framework\Http\Session\CsrfProtection::class); // Phase 5: Session Modification $reloadedSession->set('last_activity', time()); $reloadedSession->remove('login_time'); $reloadedSession->flash->add('info', 'Dashboard loaded'); $response2 = new Response(200, [], 'Dashboard'); $this->sessionManager->saveSession($reloadedSession, $response2); // Phase 6: Session Destruction (logout) $request3 = new Request( method: 'POST', uri: '/logout', cookies: $cookies, headers: [], body: '' ); $sessionToDestroy = $this->sessionManager->getOrCreateSession($request3); expect($sessionToDestroy->get('user_id'))->toBe(123); // Data still there expect($sessionToDestroy->has('login_time'))->toBeFalse(); // But modifications persisted expect($sessionToDestroy->has('last_activity'))->toBeTrue(); $response3 = new Response(200, [], 'Logged out'); $logoutResponse = $this->sessionManager->destroySession($sessionToDestroy, $response3); // Verify session is destroyed $destroyedData = $this->storage->read($sessionToDestroy->id); expect($destroyedData)->toBe([]); // Verify cookie is set to expire expect($logoutResponse->headers['Set-Cookie'])->toContain('ms_context='); // Phase 7: Verify new request creates new session $request4 = new Request( method: 'GET', uri: '/', cookies: new Cookies([]), // No session cookie headers: [], body: '' ); $newSession = $this->sessionManager->getOrCreateSession($request4); expect($newSession->id->toString())->not->toBe($originalSessionId); expect($newSession->get('user_id'))->toBeNull(); expect($newSession->all())->toBe([]); }); test('session regeneration during lifecycle', function () { // Create initial session $session = $this->sessionManager->createNewSession(); $session->set('sensitive_data', 'important'); $session->set('user_role', 'user'); $response = new Response(200, [], ''); $this->sessionManager->saveSession($session, $response); $originalId = $session->id->toString(); // Simulate privilege escalation requiring session regeneration $session->set('user_role', 'admin'); // Regenerate session for security $newSession = $this->sessionManager->regenerateSession($session); expect($newSession->id->toString())->not->toBe($originalId); expect($newSession->get('sensitive_data'))->toBe('important'); expect($newSession->get('user_role'))->toBe('admin'); // Old session should not exist $oldData = $this->storage->read($session->id); expect($oldData)->toBe([]); // New session should exist $newData = $this->storage->read($newSession->id); expect($newData['sensitive_data'])->toBe('important'); expect($newData['user_role'])->toBe('admin'); }); test('concurrent session handling', function () { // Simulate multiple concurrent requests with same session $sessionId = SessionId::fromString('concurrentsessionid1234567890abcd'); $initialData = ['user_id' => 456, 'concurrent_test' => true]; $this->storage->write($sessionId, $initialData); $cookies = new Cookies([ new Cookie('ms_context', $sessionId->toString()), ]); // Request 1: Load session and modify $request1 = new Request('GET', '/api/data', $cookies, [], ''); $session1 = $this->sessionManager->getOrCreateSession($request1); $session1->set('request_1_data', 'modified_by_request_1'); // Request 2: Load same session and modify differently $request2 = new Request('GET', '/api/other', $cookies, [], ''); $session2 = $this->sessionManager->getOrCreateSession($request2); $session2->set('request_2_data', 'modified_by_request_2'); // Both sessions should have the same ID but different local state expect($session1->id->toString())->toBe($session2->id->toString()); expect($session1->get('request_1_data'))->toBe('modified_by_request_1'); expect($session1->get('request_2_data'))->toBeNull(); expect($session2->get('request_1_data'))->toBeNull(); expect($session2->get('request_2_data'))->toBe('modified_by_request_2'); // Save both sessions (last one wins) $response1 = new Response(200, [], ''); $response2 = new Response(200, [], ''); $this->sessionManager->saveSession($session1, $response1); $this->sessionManager->saveSession($session2, $response2); // Reload session to see final state $request3 = new Request('GET', '/verify', $cookies, [], ''); $finalSession = $this->sessionManager->getOrCreateSession($request3); // Should have data from the last saved session (session2) expect($finalSession->get('user_id'))->toBe(456); expect($finalSession->get('request_1_data'))->toBeNull(); expect($finalSession->get('request_2_data'))->toBe('modified_by_request_2'); }); test('session data integrity throughout lifecycle', function () { // Test with complex data that could be corrupted $complexData = [ 'user' => [ 'id' => 789, 'profile' => [ 'name' => 'Test User with üñíçødé', 'email' => 'test@example.com', 'preferences' => [ 'language' => 'de-DE', 'timezone' => 'Europe/Berlin', 'notifications' => [ 'email' => true, 'push' => false, 'sms' => null, ], ], ], ], 'shopping_cart' => [ 'items' => [ [ 'id' => 1, 'name' => 'Product with "quotes" and \'apostrophes\'', 'price' => 19.99, 'quantity' => 2, 'metadata' => [ 'color' => 'red', 'size' => 'large', 'custom_data' => '{"json": "inside", "json": true}', ], ], ], 'total' => 39.98, 'currency' => 'EUR', 'discount_codes' => ['SAVE10', 'WELCOME'], ], 'session_metadata' => [ 'created_at' => time(), 'ip_address' => '192.168.1.1', 'user_agent' => 'Mozilla/5.0 Test Browser', 'csrf_tokens' => [ 'form_1' => 'token_abc123', 'form_2' => 'token_def456', ], ], ]; $session = $this->sessionManager->createNewSession(); // Set complex data foreach ($complexData as $key => $value) { $session->set($key, $value); } // Save session $response = new Response(200, [], ''); $responseWithCookie = $this->sessionManager->saveSession($session, $response); // Extract session ID from cookie for next request $sessionId = $session->id->toString(); $cookies = new Cookies([ new Cookie('ms_context', $sessionId), ]); // Reload session multiple times to test persistence for ($i = 0; $i < 3; $i++) { $request = new Request('GET', "/request-{$i}", $cookies, [], ''); $reloadedSession = $this->sessionManager->getOrCreateSession($request); // Verify all complex data is intact expect($reloadedSession->get('user'))->toBe($complexData['user']); expect($reloadedSession->get('shopping_cart'))->toBe($complexData['shopping_cart']); expect($reloadedSession->get('session_metadata'))->toBe($complexData['session_metadata']); // Test deep nested access $user = $reloadedSession->get('user'); expect($user['profile']['preferences']['notifications']['email'])->toBeTrue(); expect($user['profile']['preferences']['notifications']['push'])->toBeFalse(); expect($user['profile']['preferences']['notifications']['sms'])->toBeNull(); // Modify and save again $cart = $reloadedSession->get('shopping_cart'); $cart['items'][0]['quantity'] = $i + 3; $reloadedSession->set('shopping_cart', $cart); $reloadedSession->set("request_{$i}_timestamp", time()); $this->sessionManager->saveSession($reloadedSession, $response); } // Final verification $finalRequest = new Request('GET', '/final', $cookies, [], ''); $finalSession = $this->sessionManager->getOrCreateSession($finalRequest); $finalCart = $finalSession->get('shopping_cart'); expect($finalCart['items'][0]['quantity'])->toBe(5); // 2 + 3 from last iteration expect($finalSession->has('request_0_timestamp'))->toBeTrue(); expect($finalSession->has('request_1_timestamp'))->toBeTrue(); expect($finalSession->has('request_2_timestamp'))->toBeTrue(); }); test('session lifecycle error recovery', function () { // Test recovery from various error conditions // 1. Corrupted session data $sessionId = SessionId::fromString('corruptedsessionid1234567890abcde'); $session = $this->sessionManager->createNewSession(); $session->set('valid_data', 'should_persist'); $response = new Response(200, [], ''); $this->sessionManager->saveSession($session, $response); // Simulate corrupted storage (storage returns invalid data) $corruptedStorage = new class ($this->storage) implements \App\Framework\Http\Session\SessionStorage { private $originalStorage; private $corruptOnRead = false; public function __construct($storage) { $this->originalStorage = $storage; } public function enableCorruption() { $this->corruptOnRead = true; } public function read(SessionId $id): array { if ($this->corruptOnRead) { return ['corrupted' => 'data', 'invalid' => null]; } return $this->originalStorage->read($id); } public function write(SessionId $id, array $data): void { $this->originalStorage->write($id, $data); } public function remove(SessionId $id): void { $this->originalStorage->remove($id); } public function migrate(SessionId $fromId, SessionId $toId): void { $this->originalStorage->migrate($fromId, $toId); } }; $corruptedManager = new SessionManager( $this->idGenerator, $this->responseManipulator, $this->clock, $this->csrfTokenGenerator, $corruptedStorage, $this->cookieConfig ); // Normal read should work $cookies = new Cookies([ new Cookie('ms_context', $session->id->toString()), ]); $request = new Request('GET', '/test', $cookies, [], ''); $loadedSession = $corruptedManager->getOrCreateSession($request); expect($loadedSession->get('valid_data'))->toBe('should_persist'); // Enable corruption and try to read $corruptedStorage->enableCorruption(); $corruptedSession = $corruptedManager->getOrCreateSession($request); // Should handle corrupted data gracefully expect($corruptedSession->get('corrupted'))->toBe('data'); expect($corruptedSession->get('valid_data'))->toBeNull(); // Original data lost due to corruption }); });