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('SessionManager Basic Operations', function () { test('creates new session when no cookie exists', function () { $request = new HttpRequest( path: '/', cookies: new Cookies() ); $session = $this->sessionManager->getOrCreateSession($request); expect($session)->toBeInstanceOf(Session::class); expect($session->isStarted())->toBeTrue(); }); test('creates new session explicitly', function () { $session = $this->sessionManager->createNewSession(); expect($session)->toBeInstanceOf(Session::class); expect($session->isStarted())->toBeTrue(); }); test('loads existing session from cookie', function () { // Erst eine Session erstellen und Daten speichern $sessionId = SessionId::fromString('existingsessionid1234567890abcdefg'); $testData = ['user_id' => 123, 'username' => 'testuser']; $this->storage->write($sessionId, $testData); // Request mit Session-Cookie erstellen $cookies = new Cookies( new Cookie('ms_context', $sessionId->toString()) ); $request = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); $session = $this->sessionManager->getOrCreateSession($request); expect($session->id->toString())->toBe($sessionId->toString()); expect($session->get('user_id'))->toBe(123); expect($session->get('username'))->toBe('testuser'); }); test('creates new session when existing session data is corrupted', function () { // Session-ID existiert, aber keine Daten im Storage $sessionId = SessionId::fromString('nonexistentsessionid1234567890abc'); $cookies = new Cookies( new Cookie('ms_context', $sessionId->toString()) ); $request = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); $session = $this->sessionManager->getOrCreateSession($request); // Sollte eine neue Session erstellen, nicht die alte laden expect($session->id->toString())->not->toBe($sessionId->toString()); expect($session->all())->toBe([]); }); }); describe('SessionManager Session Persistence', function () { test('saves session data correctly', function () { $session = $this->sessionManager->createNewSession(); $session->set('test_key', 'test_value'); $session->set('user_data', ['id' => 456, 'name' => 'Test User']); $response = new Response(200, [], ''); $responseWithCookie = $this->sessionManager->saveSession($session, $response); // Prüfe ob Daten im Storage gespeichert wurden $storedData = $this->storage->read($session->id); expect($storedData['test_key'])->toBe('test_value'); expect($storedData['user_data'])->toBe(['id' => 456, 'name' => 'Test User']); // Prüfe ob Cookie gesetzt wurde $headers = $responseWithCookie->headers; expect($headers)->toHaveKey('Set-Cookie'); expect($headers['Set-Cookie'])->toContain('ms_context=' . $session->id->toString()); }); test('session data persists across requests', function () { // Erste Request: Session erstellen und Daten speichern $session1 = $this->sessionManager->createNewSession(); $session1->set('persistent_data', 'should_persist'); $response = new Response(200, [], ''); $responseWithCookie = $this->sessionManager->saveSession($session1, $response); // Simulate Cookie aus Response extrahieren $sessionId = $session1->id->toString(); // Zweite Request: Session mit Cookie laden $cookies = new Cookies( new Cookie('ms_context', $sessionId) ); $request2 = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); $session2 = $this->sessionManager->getOrCreateSession($request2); expect($session2->get('persistent_data'))->toBe('should_persist'); expect($session2->id->toString())->toBe($sessionId); }); test('complex data structures persist correctly', function () { $session = $this->sessionManager->createNewSession(); $complexData = [ 'user' => [ 'id' => 789, 'profile' => [ 'name' => 'Complex User', 'preferences' => [ 'theme' => 'dark', 'notifications' => true, 'languages' => ['en', 'de', 'fr'], ], ], ], 'cart' => [ 'items' => [ ['id' => 1, 'quantity' => 2, 'price' => 19.99], ['id' => 2, 'quantity' => 1, 'price' => 39.99], ], 'total' => 79.97, ], ]; $session->set('complex_data', $complexData); $response = new Response(200, [], ''); $this->sessionManager->saveSession($session, $response); // Session erneut laden $cookies = new Cookies( new Cookie('ms_context', $session->id->toString()) ); $request = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); $reloadedSession = $this->sessionManager->getOrCreateSession($request); expect($reloadedSession->get('complex_data'))->toBe($complexData); }); }); describe('SessionManager Session Regeneration', function () { test('regenerates session correctly', function () { $originalSession = $this->sessionManager->createNewSession(); $originalSession->set('user_id', 123); $originalSession->set('data_to_preserve', 'important_data'); // Session speichern $response = new Response(200, [], ''); $this->sessionManager->saveSession($originalSession, $response); $originalId = $originalSession->id->toString(); // Session regenerieren $newSession = $this->sessionManager->regenerateSession($originalSession); // Neue Session sollte andere ID haben expect($newSession->id->toString())->not->toBe($originalId); // Aber die gleichen Daten expect($newSession->get('user_id'))->toBe(123); expect($newSession->get('data_to_preserve'))->toBe('important_data'); // Alte Session sollte nicht mehr im Storage existieren $oldData = $this->storage->read($originalSession->id); expect($oldData)->toBe([]); // Neue Session sollte im Storage existieren $newData = $this->storage->read($newSession->id); expect($newData['user_id'])->toBe(123); }); test('regeneration marks session as regenerated', function () { $originalSession = $this->sessionManager->createNewSession(); $originalSession->fromArray([]); // Initialize components $newSession = $this->sessionManager->regenerateSession($originalSession); // Security manager sollte die Regeneration registriert haben expect($newSession->security)->toBeInstanceOf(\App\Framework\Http\Session\SecurityManager::class); }); }); describe('SessionManager Session Destruction', function () { test('destroys session completely', function () { $session = $this->sessionManager->createNewSession(); $session->set('data_to_destroy', 'will_be_gone'); // Session speichern $response = new Response(200, [], ''); $this->sessionManager->saveSession($session, $response); // Prüfen dass Daten existieren $storedData = $this->storage->read($session->id); expect($storedData['data_to_destroy'])->toBe('will_be_gone'); // Session zerstören $destroyResponse = $this->sessionManager->destroySession($session, $response); // Daten sollten gelöscht sein $deletedData = $this->storage->read($session->id); expect($deletedData)->toBe([]); // Cookie sollte zum Löschen gesetzt werden (expires in der Vergangenheit) $headers = $destroyResponse->headers; expect($headers)->toHaveKey('Set-Cookie'); $cookieHeader = $headers['Set-Cookie']; expect($cookieHeader)->toContain('ms_context='); expect($cookieHeader)->toContain('expires='); // Should have expiry in past }); }); describe('SessionManager Configuration', function () { test('returns correct cookie name', function () { expect($this->sessionManager->getCookieName())->toBe('ms_context'); }); test('returns cookie configuration', function () { $config = $this->sessionManager->getCookieConfig(); expect($config)->toBeInstanceOf(SessionCookieConfig::class); }); test('respects custom cookie configuration', function () { $customConfig = new SessionCookieConfig( lifetime: 7200, path: '/custom', domain: 'example.com', secure: true, httpOnly: true ); $customManager = new SessionManager( $this->idGenerator, $this->responseManipulator, $this->clock, $this->csrfTokenGenerator, $this->storage, $customConfig ); $session = $customManager->createNewSession(); $response = new Response(200, [], ''); $responseWithCookie = $customManager->saveSession($session, $response); $cookieHeader = $responseWithCookie->headers['Set-Cookie']; expect($cookieHeader)->toContain('path=/custom'); expect($cookieHeader)->toContain('domain=example.com'); expect($cookieHeader)->toContain('secure'); expect($cookieHeader)->toContain('httponly'); }); }); describe('SessionManager Error Handling', function () { test('handles invalid session ID gracefully', function () { $cookies = new Cookies( new Cookie('ms_context', 'invalid-session-id-format') ); $request = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); // Sollte eine neue Session erstellen anstatt zu crashen $session = $this->sessionManager->getOrCreateSession($request); expect($session)->toBeInstanceOf(Session::class); expect($session->isStarted())->toBeTrue(); }); test('handles storage read errors gracefully', function () { // Mock eines Storage der beim Lesen fehlschlägt $failingStorage = new class () implements \App\Framework\Http\Session\SessionStorage { public function read(SessionId $id): array { throw new Exception('Storage read failed'); } public function write(SessionId $id, array $data): void { // No-op } public function remove(SessionId $id): void { // No-op } public function migrate(SessionId $fromId, SessionId $toId): void { // No-op } }; $failingManager = new SessionManager( $this->idGenerator, $this->responseManipulator, $this->clock, $this->csrfTokenGenerator, $failingStorage, $this->cookieConfig ); $sessionId = SessionId::fromString('existingsessionid1234567890abcdef'); $cookies = new Cookies( new Cookie('ms_context', $sessionId->toString()) ); $request = new Request( method: 'GET', uri: '/', cookies: $cookies, headers: [], body: '' ); // Sollte eine neue Session erstellen wenn das Laden fehlschlägt expect(fn () => $failingManager->getOrCreateSession($request)) ->toThrow(Exception::class, 'Storage read failed'); }); });