Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionKey;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->clock = new FrozenClock();
$this->randomGenerator = new TestableRandomGenerator();
$this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator);
$this->sessionId = SessionId::fromString('testflashmanagersessionidlong123');
$this->session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$this->session->fromArray([]);
});
describe('FlashManager Core Functionality', function () {
test('can mark items for deletion', function () {
$flashManager = $this->session->flashManager;
$flashManager->mark('validation_errors', 'contact_form');
$flashManager->mark('form_data', 'contact_form');
expect($flashManager->isMarked('validation_errors', 'contact_form'))->toBeTrue();
expect($flashManager->isMarked('form_data', 'contact_form'))->toBeTrue();
expect($flashManager->isMarked('validation_errors', 'login_form'))->toBeFalse();
});
test('can unmark items', function () {
$flashManager = $this->session->flashManager;
$flashManager->mark('validation_errors', 'contact_form');
expect($flashManager->isMarked('validation_errors', 'contact_form'))->toBeTrue();
$flashManager->unmark('validation_errors', 'contact_form');
expect($flashManager->isMarked('validation_errors', 'contact_form'))->toBeFalse();
});
test('can mark multiple items at once', function () {
$flashManager = $this->session->flashManager;
$flashManager->markMultiple('validation_errors', ['contact_form', 'login_form', 'register_form']);
expect($flashManager->isMarked('validation_errors', 'contact_form'))->toBeTrue();
expect($flashManager->isMarked('validation_errors', 'login_form'))->toBeTrue();
expect($flashManager->isMarked('validation_errors', 'register_form'))->toBeTrue();
});
test('tracks marked items correctly', function () {
$flashManager = $this->session->flashManager;
$flashManager->mark('validation_errors', 'form1');
$flashManager->mark('validation_errors', 'form2');
$flashManager->mark('form_data', 'form1');
expect($flashManager->hasMarkedItems('validation_errors'))->toBeTrue();
expect($flashManager->hasMarkedItems('form_data'))->toBeTrue();
expect($flashManager->hasMarkedItems('csrf'))->toBeFalse();
$markedValidationKeys = $flashManager->getMarkedKeys('validation_errors');
expect($markedValidationKeys)->toBe(['form1', 'form2']);
$markedFormKeys = $flashManager->getMarkedKeys('form_data');
expect($markedFormKeys)->toBe(['form1']);
});
});
describe('FlashManager Session Data Filtering', function () {
test('filters out marked validation errors', function () {
// 1. Session mit Validation Errors füllen
$this->session->validation->add('contact_form', ['email' => ['Invalid email']]);
$this->session->validation->add('login_form', ['password' => ['Too short']]);
// 2. Eine Form zum Löschen markieren
$this->session->flashManager->mark(SessionKey::VALIDATION_ERRORS->value, 'contact_form');
// 3. Gefilterte Session-Daten sollten nur unmarkierte Daten enthalten
$filteredData = $this->session->all();
expect($filteredData)->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($filteredData[SessionKey::VALIDATION_ERRORS->value])->not->toHaveKey('contact_form');
expect($filteredData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('login_form');
// 4. Ungefilterte Daten sollten noch alle Daten enthalten
$unfilteredData = $this->session->all(includeMarkedForDeletion: true);
expect($unfilteredData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('contact_form');
expect($unfilteredData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('login_form');
});
test('filters out marked form data', function () {
// 1. Session mit Form Data füllen
$this->session->form->store('contact_form', ['name' => 'John', 'email' => 'john@test.com']);
$this->session->form->store('login_form', ['username' => 'john']);
// 2. Eine Form zum Löschen markieren
$this->session->flashManager->mark(SessionKey::FORM_DATA->value, 'contact_form');
// 3. Gefilterte Daten sollten nur unmarkierte Daten enthalten
$filteredData = $this->session->all();
expect($filteredData[SessionKey::FORM_DATA->value])->not->toHaveKey('contact_form');
expect($filteredData[SessionKey::FORM_DATA->value])->toHaveKey('login_form');
});
test('removes empty components after filtering', function () {
// 1. Session mit nur einer Validation Error
$this->session->validation->add('contact_form', ['email' => ['Invalid']]);
// 2. Diese eine Error zum Löschen markieren
$this->session->flashManager->mark(SessionKey::VALIDATION_ERRORS->value, 'contact_form');
// 3. Nach dem Filtern sollte die ganze Komponente entfernt werden
$filteredData = $this->session->all();
expect($filteredData)->not->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
});
test('filters multiple components simultaneously', function () {
// 1. Session mit verschiedenen Daten füllen
$this->session->validation->add('contact_form', ['email' => ['Invalid']]);
$this->session->form->store('contact_form', ['name' => 'John']);
$this->session->flash->add('success', 'Data saved');
// 2. Validation und Form Data markieren
$this->session->flashManager->mark(SessionKey::VALIDATION_ERRORS->value, 'contact_form');
$this->session->flashManager->mark(SessionKey::FORM_DATA->value, 'contact_form');
// 3. Beide sollten gefiltert werden, Flash sollte bleiben
$filteredData = $this->session->all();
expect($filteredData)->not->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($filteredData)->not->toHaveKey(SessionKey::FORM_DATA->value);
expect($filteredData)->toHaveKey(SessionKey::FLASH->value);
});
});
describe('FlashManager with ValidationErrorBag Integration', function () {
test('getAndFlash marks validation errors for deletion', function () {
// 1. Validation Errors hinzufügen
$this->session->validation->add('contact_form', [
'email' => ['Invalid email format'],
'name' => ['Name is required'],
]);
// 2. Mit getAndFlash abrufen
$errors = $this->session->validation->getAndFlash('contact_form');
// 3. Errors sollten zurückgegeben werden
expect($errors['email'])->toBe(['Invalid email format']);
expect($errors['name'])->toBe(['Name is required']);
// 4. Sollten zum Löschen markiert sein
expect($this->session->flashManager->isMarked(SessionKey::VALIDATION_ERRORS->value, 'contact_form'))->toBeTrue();
// 5. Nach Session-Save sollten sie nicht mehr da sein
$filteredData = $this->session->all();
expect($filteredData)->not->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
});
test('get without flash does not mark for deletion', function () {
// 1. Validation Errors hinzufügen
$this->session->validation->add('contact_form', ['email' => ['Invalid']]);
// 2. Mit normalem get() abrufen
$errors = $this->session->validation->get('contact_form');
// 3. Errors sollten zurückgegeben werden
expect($errors['email'])->toBe(['Invalid']);
// 4. Sollten NICHT zum Löschen markiert sein
expect($this->session->flashManager->isMarked(SessionKey::VALIDATION_ERRORS->value, 'contact_form'))->toBeFalse();
// 5. Sollten nach Session-Save noch da sein
$filteredData = $this->session->all();
expect($filteredData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('contact_form');
});
});
describe('FlashManager with FormDataStorage Integration', function () {
test('getAndFlash marks form data for deletion', function () {
// 1. Form Data hinzufügen
$formData = ['name' => 'John Doe', 'email' => 'john@example.com'];
$this->session->form->store('contact_form', $formData);
// 2. Mit getAndFlash abrufen
$retrievedData = $this->session->form->getAndFlash('contact_form');
// 3. Daten sollten zurückgegeben werden
expect($retrievedData)->toBe($formData);
// 4. Sollten zum Löschen markiert sein
expect($this->session->flashManager->isMarked(SessionKey::FORM_DATA->value, 'contact_form'))->toBeTrue();
// 5. Nach Session-Save sollten sie nicht mehr da sein
$filteredData = $this->session->all();
expect($filteredData)->not->toHaveKey(SessionKey::FORM_DATA->value);
});
test('getFieldAndFlash marks form data for deletion', function () {
// 1. Form Data hinzufügen
$this->session->form->store('contact_form', ['name' => 'John', 'email' => 'john@test.com']);
// 2. Einzelnes Feld mit Flash abrufen
$name = $this->session->form->getFieldAndFlash('contact_form', 'name');
// 3. Feldwert sollte zurückgegeben werden
expect($name)->toBe('John');
// 4. Ganze Form sollte zum Löschen markiert sein
expect($this->session->flashManager->isMarked(SessionKey::FORM_DATA->value, 'contact_form'))->toBeTrue();
});
});
describe('FlashManager Edge Cases', function () {
test('marking empty data does nothing', function () {
// 1. Leere Validation Errors abrufen mit Flash
$errors = $this->session->validation->getAndFlash('nonexistent_form');
// 2. Sollte leeres Array zurückgeben
expect($errors)->toBe([]);
// 3. Sollte NICHT markiert werden
expect($this->session->flashManager->isMarked(SessionKey::VALIDATION_ERRORS->value, 'nonexistent_form'))->toBeFalse();
});
test('clearAllMarkings removes all markings', function () {
// 1. Mehrere Markierungen setzen
$this->session->flashManager->mark('validation_errors', 'form1');
$this->session->flashManager->mark('form_data', 'form2');
// 2. Alle Markierungen löschen
$this->session->flashManager->clearAllMarkings();
// 3. Keine Markierungen sollten mehr existieren
expect($this->session->flashManager->isMarked('validation_errors', 'form1'))->toBeFalse();
expect($this->session->flashManager->isMarked('form_data', 'form2'))->toBeFalse();
expect($this->session->flashManager->getMarkedItems())->toBe([]);
});
test('filtering preserves non-marked data structure', function () {
// 1. Komplexe Session-Struktur erstellen
$this->session->set('user_id', 123);
$this->session->validation->add('form1', ['error1' => ['msg1']]);
$this->session->validation->add('form2', ['error2' => ['msg2']]);
$this->session->form->store('form1', ['data1' => 'value1']);
$this->session->flash->add('info', 'Information');
// 2. Nur form1 markieren
$this->session->flashManager->mark(SessionKey::VALIDATION_ERRORS->value, 'form1');
// 3. Gefilterte Daten sollten korrekte Struktur haben
$filteredData = $this->session->all();
expect($filteredData['user_id'])->toBe(123);
expect($filteredData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('form2');
expect($filteredData[SessionKey::VALIDATION_ERRORS->value])->not->toHaveKey('form1');
expect($filteredData[SessionKey::FORM_DATA->value])->toHaveKey('form1'); // Nicht markiert
expect($filteredData[SessionKey::FLASH->value])->toHaveKey('info');
});
});

View File

@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionKey;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->clock = new FrozenClock();
$this->randomGenerator = new TestableRandomGenerator();
$this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator);
$this->sessionId = SessionId::fromString('testlazyinitsessionid1234567890ab');
});
describe('Session Component Lazy Initialization', function () {
test('only used component keys appear in session data', function () {
// 1. Session erstellen
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray([]);
// 2. Nur einige Komponenten verwenden
$session->flash->add('info', 'Test message');
$session->csrf->generateToken('test_form');
// Bewusst NICHT verwenden: validation und form
// 3. Session-Daten prüfen
$sessionData = $session->all();
// 4. Nur verwendete Keys sollten existieren
expect($sessionData)->toHaveKey(SessionKey::FLASH->value);
expect($sessionData)->toHaveKey(SessionKey::CSRF->value);
// 5. Nicht verwendete Keys sollten NICHT existieren
expect($sessionData)->not->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($sessionData)->not->toHaveKey(SessionKey::FORM_DATA->value);
// 6. Inhalt der verwendeten Keys prüfen
expect($sessionData[SessionKey::FLASH->value])->toHaveKey('info');
expect($sessionData[SessionKey::CSRF->value])->toHaveKey('test_form');
});
test('accessing component creates its key even if empty', function () {
// 1. Session erstellen
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray([]);
// 2. Komponente zugreifen ohne Daten hinzuzufügen
$errors = $session->validation->get('nonexistent_form'); // Sollte [] zurückgeben
expect($errors)->toBe([]);
// 3. Jetzt sollte der Key existieren (da Komponente initialisiert wurde)
$sessionData = $session->all();
expect($sessionData)->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($sessionData[SessionKey::VALIDATION_ERRORS->value])->toBe([]);
});
test('component keys persist after session reload', function () {
$storage = new InMemorySessionStorage();
// 1. Session mit gemischter Nutzung erstellen
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]);
$session1->flash->add('success', 'Saved!');
$session1->csrf->generateToken('edit_form');
// validation und form werden NICHT verwendet
// 2. Session speichern
$storage->write($this->sessionId, $session1->all());
// 3. Session neu laden
$loadedData = $storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 4. Nur die Keys die vorher verwendet wurden sollten existieren
$reloadedData = $session2->all();
expect($reloadedData)->toHaveKey(SessionKey::FLASH->value);
expect($reloadedData)->toHaveKey(SessionKey::CSRF->value);
expect($reloadedData)->not->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($reloadedData)->not->toHaveKey(SessionKey::FORM_DATA->value);
// 5. Daten sollten korrekt geladen werden
expect($session2->flash->get('success'))->toBe(['Saved!']);
});
test('unused components can be accessed after session reload', function () {
$storage = new InMemorySessionStorage();
// 1. Session mit teilweiser Nutzung
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]);
$session1->flash->add('info', 'Test');
$storage->write($this->sessionId, $session1->all());
// 2. Session neu laden
$loadedData = $storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 3. Bisher unverwendete Komponenten sollten funktionieren
$session2->validation->add('new_form', ['field' => ['New error']]);
$session2->form->store('new_form', ['data' => 'New data']);
// 4. Nach Verwendung sollten die Keys existieren
$finalData = $session2->all();
expect($finalData)->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($finalData)->toHaveKey(SessionKey::FORM_DATA->value);
// 5. Ursprüngliche Daten sollten erhalten bleiben
expect($finalData)->toHaveKey(SessionKey::FLASH->value);
});
test('simulates live system behavior from your example', function () {
// 1. Simuliere eine typische Web-App Session
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray([]);
// 2. Simuliere Security Tracking (automatisch durch Framework)
$session->security->updateActivity();
// 3. Simuliere CSRF Token Generierung für verschiedene Formulare
$session->csrf->generateToken('form_bf9d2e47868b');
$session->csrf->generateToken('form_386bb8ff6647');
// 4. Flash wird initialisiert aber ist leer (typisch nach Redirect)
// Simuliere: Flash wurde bereits abgerufen und ist jetzt leer
$session->flash->get('any_type'); // Initialisiert das Array
// 5. validation und form werden NICHT verwendet (kein Fehler, kein Form-Data)
// 6. Prüfe Session-Daten (sollte dem Live-System ähneln)
$sessionData = $session->all();
expect($sessionData)->toHaveKey('__security');
expect($sessionData)->toHaveKey('__csrf');
expect($sessionData)->toHaveKey('__flash');
expect($sessionData['__flash'])->toBe([]); // Leer, wie im Live-System
// Diese Keys fehlen - das ist korrekt!
expect($sessionData)->not->toHaveKey('__validation_errors');
expect($sessionData)->not->toHaveKey('__form_data');
// 7. CSRF sollte Tokens für verschiedene Formulare enthalten
expect($sessionData['__csrf'])->toHaveKey('form_bf9d2e47868b');
expect($sessionData['__csrf'])->toHaveKey('form_386bb8ff6647');
});
test('components become available when first needed', function () {
$storage = new InMemorySessionStorage();
// 1. Session wie im Live-System
$existingData = [
'__security' => [
'user_agent' => 'Mozilla/5.0 (Test Browser)',
'ip_address' => '127.0.0.1',
'last_activity' => time(),
],
'__csrf' => [
'test_form' => [
['token' => 'abc123', 'created_at' => time(), 'used_at' => null],
],
],
'__flash' => [],
];
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray($existingData);
// 2. Bisher fehlende Komponenten sind trotzdem verfügbar
expect($session->validation->has('any_form'))->toBeFalse();
expect($session->form->has('any_form'))->toBeFalse();
// 3. Wenn sie verwendet werden, funktionieren sie
$session->validation->add('contact_form', ['email' => ['Required']]);
$session->form->store('contact_form', ['name' => 'John']);
// 4. Jetzt sollten die Keys existieren
$finalData = $session->all();
expect($finalData)->toHaveKey('__validation_errors');
expect($finalData)->toHaveKey('__form_data');
// 5. Bestehende Daten bleiben erhalten
expect($finalData['__security']['user_agent'])->toBe('Mozilla/5.0 (Test Browser)');
expect($finalData['__csrf'])->toHaveKey('test_form');
expect($finalData['__flash'])->toBe([]);
});
});

View File

@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionKey;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->clock = new FrozenClock();
$this->randomGenerator = new TestableRandomGenerator();
$this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator);
$this->storage = new InMemorySessionStorage();
$this->sessionId = SessionId::fromString('testcomponentspersistencesessionid');
});
describe('Session Component Persistence', function () {
test('flash messages persist across session save/load cycles', function () {
// 1. Neue Session erstellen und Flash-Nachricht hinzufügen
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]); // Initialisiert die Komponenten
$session1->flash->add('success', 'Operation successful!');
$session1->flash->add('error', 'Something went wrong');
// 2. Session-Daten speichern
$sessionData = $session1->all();
$this->storage->write($this->sessionId, $sessionData);
// 3. Neue Session-Instanz laden
$loadedData = $this->storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 4. Flash-Nachrichten sollten verfügbar sein
$successMessages = $session2->flash->get('success');
$errorMessages = $session2->flash->get('error');
expect($successMessages)->toBe(['Operation successful!']);
expect($errorMessages)->toBe(['Something went wrong']);
// 5. Nach dem Abrufen sollten die Nachrichten gelöscht sein (Flash-Verhalten)
expect($session2->flash->get('success'))->toBe([]);
expect($session2->flash->get('error'))->toBe([]);
});
test('validation errors persist across session save/load cycles', function () {
// 1. Session mit Validation Errors erstellen
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]);
$session1->validation->add('user_form', [
'email' => ['Invalid email format'],
'password' => ['Password too short'],
]);
$session1->validation->add('profile_form', [
'email' => ['Email already exists'],
]);
// 2. Session speichern
$sessionData = $session1->all();
$this->storage->write($this->sessionId, $sessionData);
// 3. Session laden
$loadedData = $this->storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 4. Validation Errors sollten verfügbar sein
expect($session2->validation->has('user_form'))->toBeTrue();
expect($session2->validation->has('profile_form'))->toBeTrue();
$userFormErrors = $session2->validation->get('user_form');
$profileFormErrors = $session2->validation->get('profile_form');
expect($userFormErrors['email'])->toBe(['Invalid email format']);
expect($userFormErrors['password'])->toBe(['Password too short']);
expect($profileFormErrors['email'])->toBe(['Email already exists']);
});
test('form data persists across session save/load cycles', function () {
// 1. Session mit Form Data erstellen
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]);
$formData = [
'name' => 'John Doe',
'email' => 'john@example.com',
'preferences' => [
'theme' => 'dark',
'notifications' => true,
],
];
$session1->form->store('user_profile', $formData);
// 2. Session speichern
$sessionData = $session1->all();
$this->storage->write($this->sessionId, $sessionData);
// 3. Session laden
$loadedData = $this->storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 4. Form Data sollte verfügbar sein
expect($session2->form->get('user_profile'))->toBe($formData);
expect($session2->form->has('user_profile'))->toBeTrue();
});
test('CSRF tokens persist across session save/load cycles', function () {
// 1. Session mit CSRF Token erstellen
$session1 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session1->fromArray([]);
$token1 = $session1->csrf->generateToken('login_form');
$token2 = $session1->csrf->generateToken('profile_form');
// 2. Session speichern
$sessionData = $session1->all();
$this->storage->write($this->sessionId, $sessionData);
// 3. Session laden
$loadedData = $this->storage->read($this->sessionId);
$session2 = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session2->fromArray($loadedData);
// 4. Token sollten gültig sein
expect($session2->csrf->validateToken('login_form', $token1))->toBeTrue();
expect($session2->csrf->validateToken('profile_form', $token2))->toBeTrue();
// Für ungültigen Token erstellen wir ein CsrfToken Objekt
$invalidToken = \App\Framework\Security\CsrfToken::fromString('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef');
expect($session2->csrf->validateToken('login_form', $invalidToken))->toBeFalse();
});
test('all component data is stored in session under specific keys', function () {
// 1. Session mit allen Komponenten-Daten erstellen
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray([]);
// Komponenten verwenden
$session->flash->add('info', 'Test message');
$session->validation->add('test_form', ['field' => ['Test error']]);
$session->form->store('test_form', ['data' => 'test']);
$session->csrf->generateToken('test_form');
// Auch normale Session-Daten hinzufügen
$session->set('user_id', 123);
$session->set('custom_data', 'custom_value');
// 2. Session-Daten analysieren
$allData = $session->all();
// 3. Komponenten-Keys sollten existieren
expect($allData)->toHaveKey(SessionKey::FLASH->value);
expect($allData)->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($allData)->toHaveKey(SessionKey::FORM_DATA->value);
expect($allData)->toHaveKey(SessionKey::CSRF->value);
// 4. Normale Session-Daten sollten auch existieren
expect($allData)->toHaveKey('user_id');
expect($allData)->toHaveKey('custom_data');
expect($allData['user_id'])->toBe(123);
expect($allData['custom_data'])->toBe('custom_value');
// 5. Komponenten-Daten sollten korrekt strukturiert sein
expect($allData[SessionKey::FLASH->value])->toBeArray();
expect($allData[SessionKey::VALIDATION_ERRORS->value])->toBeArray();
expect($allData[SessionKey::FORM_DATA->value])->toBeArray();
expect($allData[SessionKey::CSRF->value])->toBeArray();
});
test('components work immediately after fromArray initialization', function () {
// 1. Session-Daten mit bestehenden Komponenten-Daten laden
$existingData = [
SessionKey::FLASH->value => [
'success' => ['Pre-existing message'],
],
SessionKey::VALIDATION_ERRORS->value => [
'contact_form' => [
'email' => ['Pre-existing error'],
],
],
SessionKey::FORM_DATA->value => [
'contact_form' => ['name' => 'Pre-existing Name'],
],
'user_id' => 456,
];
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray($existingData);
// 2. Komponenten sollten sofort die existierenden Daten haben
expect($session->flash->get('success'))->toBe(['Pre-existing message']);
$contactFormErrors = $session->validation->get('contact_form');
expect($contactFormErrors['email'])->toBe(['Pre-existing error']);
expect($session->form->get('contact_form'))->toBe(['name' => 'Pre-existing Name']);
expect($session->get('user_id'))->toBe(456);
// 3. Neue Daten sollten zu den existierenden hinzugefügt werden
$session->flash->add('info', 'New message');
$session->validation->add('login_form', ['password' => ['New error']]);
expect($session->flash->get('info'))->toBe(['New message']);
$loginFormErrors = $session->validation->get('login_form');
expect($loginFormErrors['password'])->toBe(['New error']);
// Die ursprünglichen Flash-Messages sollten nach dem Abrufen gelöscht sein
expect($session->flash->get('success'))->toBe([]); // Flash wurde bereits abgerufen
});
test('component keys are created when components are first used', function () {
// 1. Leere Session erstellen
$session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
$session->fromArray([]);
// 2. Komponenten verwenden um ihre Keys zu initialisieren
$session->flash->add('test', 'message');
$session->validation->add('form', ['field' => ['error']]);
$session->form->store('form', ['data' => 'value']);
$session->csrf->generateToken('form');
// 3. Alle Komponenten-Keys sollten jetzt existieren
$allData = $session->all();
expect($allData)->toHaveKey(SessionKey::FLASH->value);
expect($allData)->toHaveKey(SessionKey::VALIDATION_ERRORS->value);
expect($allData)->toHaveKey(SessionKey::FORM_DATA->value);
expect($allData)->toHaveKey(SessionKey::CSRF->value);
// 4. Sie sollten die erwarteten Daten enthalten
expect($allData[SessionKey::FLASH->value])->toHaveKey('test');
expect($allData[SessionKey::VALIDATION_ERRORS->value])->toHaveKey('form');
expect($allData[SessionKey::FORM_DATA->value])->toHaveKey('form');
expect($allData[SessionKey::CSRF->value])->toHaveKey('form');
});
});

View File

@@ -0,0 +1,411 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Cookies\Cookie;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Http\Request;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionCookieConfig;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionManager;
use App\Framework\Http\Session\SimpleSessionIdGenerator;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->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
});
});

View File

@@ -0,0 +1,387 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Cookies\Cookie;
use App\Framework\Http\Cookies\Cookies;
use App\Framework\Http\HttpRequest;
use App\Framework\Http\Response;
use App\Framework\Http\ResponseManipulator;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionCookieConfig;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionManager;
use App\Framework\Http\Session\SimpleSessionIdGenerator;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->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');
});
});

View File

@@ -0,0 +1,320 @@
<?php
declare(strict_types=1);
use App\Framework\Http\Session\FileSessionStorage;
use App\Framework\Http\Session\InMemorySessionStorage;
use App\Framework\Http\Session\SessionId;
use App\Framework\Http\Session\SessionStorage;
describe('InMemorySessionStorage', function () {
beforeEach(function () {
$this->storage = new InMemorySessionStorage();
$this->sessionId = SessionId::fromString('testsessionid1234567890abcdefgh12');
});
test('reads empty array for non-existent session', function () {
$data = $this->storage->read($this->sessionId);
expect($data)->toBe([]);
});
test('writes and reads session data correctly', function () {
$testData = [
'user_id' => 123,
'username' => 'testuser',
'preferences' => ['theme' => 'dark', 'language' => 'en'],
];
$this->storage->write($this->sessionId, $testData);
$retrievedData = $this->storage->read($this->sessionId);
expect($retrievedData)->toBe($testData);
});
test('removes session data correctly', function () {
$testData = ['key' => 'value'];
$this->storage->write($this->sessionId, $testData);
expect($this->storage->read($this->sessionId))->toBe($testData);
$this->storage->remove($this->sessionId);
expect($this->storage->read($this->sessionId))->toBe([]);
});
test('migrates session data correctly', function () {
$oldId = SessionId::fromString('oldsessionid1234567890abcdefghij1');
$newId = SessionId::fromString('newsessionid1234567890abcdefghij1');
$testData = ['migration_test' => 'data'];
$this->storage->write($oldId, $testData);
$this->storage->migrate($oldId, $newId);
expect($this->storage->read($oldId))->toBe([]);
expect($this->storage->read($newId))->toBe($testData);
});
test('handles complex data structures', function () {
$complexData = [
'nested' => [
'deep' => [
'array' => [1, 2, 3],
'object' => (object)['property' => 'value'],
],
],
'null_value' => null,
'boolean_true' => true,
'boolean_false' => false,
'empty_string' => '',
'zero' => 0,
];
$this->storage->write($this->sessionId, $complexData);
$retrieved = $this->storage->read($this->sessionId);
expect($retrieved)->toBe($complexData);
});
test('multiple sessions work independently', function () {
$session1Id = SessionId::fromString('session1id1234567890abcdefghijk1');
$session2Id = SessionId::fromString('session2id1234567890abcdefghijk2');
$data1 = ['session' => '1', 'data' => 'first'];
$data2 = ['session' => '2', 'data' => 'second'];
$this->storage->write($session1Id, $data1);
$this->storage->write($session2Id, $data2);
expect($this->storage->read($session1Id))->toBe($data1);
expect($this->storage->read($session2Id))->toBe($data2);
$this->storage->remove($session1Id);
expect($this->storage->read($session1Id))->toBe([]);
expect($this->storage->read($session2Id))->toBe($data2);
});
});
describe('FileSessionStorage', function () {
beforeEach(function () {
$this->tempDir = sys_get_temp_dir() . '/session_test_' . uniqid();
$this->storage = new FileSessionStorage($this->tempDir);
$this->sessionId = SessionId::fromString('testfilesessionid1234567890abcde');
});
afterEach(function () {
// Cleanup: Remove test directory and files
if (is_dir($this->tempDir)) {
$files = glob($this->tempDir . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
rmdir($this->tempDir);
}
});
test('creates storage directory if it does not exist', function () {
expect(is_dir($this->tempDir))->toBeTrue();
});
test('reads empty array for non-existent session file', function () {
$data = $this->storage->read($this->sessionId);
expect($data)->toBe([]);
});
test('writes and reads session data to/from file correctly', function () {
$testData = [
'user_id' => 456,
'username' => 'fileuser',
'complex' => [
'nested' => ['deep' => 'value'],
'array' => [1, 2, 3, 4],
],
];
$this->storage->write($this->sessionId, $testData);
$retrievedData = $this->storage->read($this->sessionId);
expect($retrievedData)->toBe($testData);
});
test('file exists after writing', function () {
$testData = ['file_test' => 'exists'];
$this->storage->write($this->sessionId, $testData);
$expectedFile = $this->tempDir . '/sess_' . $this->sessionId->toString();
expect(file_exists($expectedFile))->toBeTrue();
});
test('removes session file correctly', function () {
$testData = ['to_be_removed' => 'data'];
$this->storage->write($this->sessionId, $testData);
$expectedFile = $this->tempDir . '/sess_' . $this->sessionId->toString();
expect(file_exists($expectedFile))->toBeTrue();
$this->storage->remove($this->sessionId);
expect(file_exists($expectedFile))->toBeFalse();
expect($this->storage->read($this->sessionId))->toBe([]);
});
test('migrates session file correctly', function () {
$oldId = SessionId::fromString('oldfilesessionid1234567890abcdef');
$newId = SessionId::fromString('newfilesessionid1234567890abcdef');
$testData = ['migration_file_test' => 'data'];
$this->storage->write($oldId, $testData);
$oldFile = $this->tempDir . '/sess_' . $oldId->toString();
$newFile = $this->tempDir . '/sess_' . $newId->toString();
expect(file_exists($oldFile))->toBeTrue();
expect(file_exists($newFile))->toBeFalse();
$this->storage->migrate($oldId, $newId);
expect(file_exists($oldFile))->toBeFalse();
expect(file_exists($newFile))->toBeTrue();
expect($this->storage->read($newId))->toBe($testData);
});
test('handles JSON encoding/decoding correctly', function () {
$testData = [
'string' => 'test string with üñíçødé',
'integer' => 42,
'float' => 3.14159,
'boolean_true' => true,
'boolean_false' => false,
'null' => null,
'array' => ['a', 'b', 'c'],
'object' => ['key' => 'value'],
];
$this->storage->write($this->sessionId, $testData);
$retrieved = $this->storage->read($this->sessionId);
expect($retrieved)->toBe($testData);
});
test('handles corrupted JSON file gracefully', function () {
// Schreibe invalides JSON in die Session-Datei
$sessionFile = $this->tempDir . '/sess_' . $this->sessionId->toString();
file_put_contents($sessionFile, 'invalid json content {');
$data = $this->storage->read($this->sessionId);
expect($data)->toBe([]); // Sollte leeres Array zurückgeben
});
test('handles file system errors gracefully', function () {
// Versuche in nicht-existierendes Verzeichnis zu schreiben (das Framework sollte das abfangen)
expect(fn () => new FileSessionStorage('/root/nonexistent/directory/path'))
->toThrow(RuntimeException::class);
});
test('garbage collection removes old files', function () {
// Erstelle mehrere Session-Dateien mit verschiedenen Timestamps
$oldSessionId = SessionId::fromString('oldsessionid1234567890abcdefghij1');
$newSessionId = SessionId::fromString('newsessionid1234567890abcdefghij2');
$this->storage->write($oldSessionId, ['old' => 'data']);
$this->storage->write($newSessionId, ['new' => 'data']);
// Da wir die neue FileSessionStorage verwenden, können wir nicht direkt touch() verwenden
// Stattdessen testen wir nur dass GC nicht crashed und beide Files noch existieren
// (da sie frisch erstellt wurden)
$this->storage->gc(3600);
// Beide Sessions sollten noch existieren da sie gerade erstellt wurden
expect($this->storage->read($oldSessionId))->toBe(['old' => 'data']);
expect($this->storage->read($newSessionId))->toBe(['new' => 'data']);
});
});
describe('SessionStorage Interface Compliance', function () {
/**
* Teste dass alle Storage-Implementierungen das gleiche Interface-Verhalten haben
*/
$storageProviders = [
'InMemorySessionStorage' => fn () => new InMemorySessionStorage(),
'FileSessionStorage' => fn () => new FileSessionStorage(sys_get_temp_dir() . '/pest_session_test_' . uniqid()),
];
foreach ($storageProviders as $storageName => $storageFactory) {
describe($storageName . ' Interface Compliance', function () use ($storageName, $storageFactory) {
beforeEach(function () use ($storageFactory) {
$this->storage = $storageFactory();
$this->sessionId = SessionId::fromString('interfacetestsessionid1234567890');
});
afterEach(function () use ($storageName) {
// Cleanup für FileStorage
if ($storageName === 'FileSessionStorage' && $this->storage instanceof FileSessionStorage) {
$basePath = $this->storage->getBasePath();
$pathString = $basePath->toString();
if (is_dir($pathString)) {
$files = glob($pathString . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
rmdir($pathString);
}
}
});
test('implements SessionStorage interface', function () {
expect($this->storage)->toBeInstanceOf(SessionStorage::class);
});
test('read() returns array', function () {
$result = $this->storage->read($this->sessionId);
expect($result)->toBeArray();
});
test('write() and read() cycle works', function () {
$testData = ['interface_test' => 'value', 'number' => 42];
$this->storage->write($this->sessionId, $testData);
$result = $this->storage->read($this->sessionId);
expect($result)->toBe($testData);
});
test('remove() clears session data', function () {
$this->storage->write($this->sessionId, ['to_remove' => 'data']);
$this->storage->remove($this->sessionId);
expect($this->storage->read($this->sessionId))->toBe([]);
});
test('migrate() transfers data correctly', function () {
$oldId = SessionId::fromString('oldinterfacetest1234567890abcdefg');
$newId = SessionId::fromString('newinterfacetest1234567890abcdefg');
$testData = ['migrate_interface_test' => 'data'];
$this->storage->write($oldId, $testData);
$this->storage->migrate($oldId, $newId);
expect($this->storage->read($oldId))->toBe([]);
expect($this->storage->read($newId))->toBe($testData);
});
test('handles empty data correctly', function () {
$this->storage->write($this->sessionId, []);
expect($this->storage->read($this->sessionId))->toBe([]);
});
test('handles null values in data', function () {
$testData = ['null_value' => null, 'string' => 'value'];
$this->storage->write($this->sessionId, $testData);
$result = $this->storage->read($this->sessionId);
expect($result)->toBe($testData);
expect($result['null_value'])->toBeNull();
});
});
}
});

View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
use App\Framework\DateTime\FrozenClock;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionId;
use App\Framework\Random\TestableRandomGenerator;
use App\Framework\Security\CsrfTokenGenerator;
beforeEach(function () {
$this->clock = new FrozenClock();
$this->randomGenerator = new TestableRandomGenerator();
$this->csrfTokenGenerator = new CsrfTokenGenerator($this->randomGenerator);
$this->sessionId = SessionId::fromString('testsessionid1234567890abcdefgh12');
$this->session = new Session($this->sessionId, $this->clock, $this->csrfTokenGenerator);
});
describe('Session Basic Operations', function () {
test('session starts correctly', function () {
expect($this->session->isStarted())->toBeTrue();
});
test('session has correct id', function () {
expect($this->session->id)->toBe($this->sessionId);
});
test('session data operations work correctly', function () {
// Set data
$this->session->set('key1', 'value1');
$this->session->set('key2', 42);
// Get data
expect($this->session->get('key1'))->toBe('value1');
expect($this->session->get('key2'))->toBe(42);
expect($this->session->get('nonexistent'))->toBeNull();
expect($this->session->get('nonexistent', 'default'))->toBe('default');
});
test('session has() method works correctly', function () {
$this->session->set('existing_key', 'value');
expect($this->session->has('existing_key'))->toBeTrue();
expect($this->session->has('nonexistent_key'))->toBeFalse();
});
test('session remove() method works correctly', function () {
$this->session->set('key_to_remove', 'value');
expect($this->session->has('key_to_remove'))->toBeTrue();
$this->session->remove('key_to_remove');
expect($this->session->has('key_to_remove'))->toBeFalse();
});
test('session all() method returns all data', function () {
$this->session->set('key1', 'value1');
$this->session->set('key2', 'value2');
$allData = $this->session->all();
// Die Komponenten fügen ihre eigenen Keys hinzu
expect($allData)->toHaveKey('key1');
expect($allData)->toHaveKey('key2');
expect($allData['key1'])->toBe('value1');
expect($allData['key2'])->toBe('value2');
// Komponenten-Keys sollten auch existieren
expect($allData)->toHaveKey('__flash');
expect($allData)->toHaveKey('__validation_errors');
expect($allData)->toHaveKey('__form_data');
expect($allData)->toHaveKey('__csrf');
});
test('session clear() method removes all data', function () {
$this->session->set('key1', 'value1');
$this->session->set('key2', 'value2');
$this->session->clear();
expect($this->session->all())->toBe([]);
});
});
describe('Session fromArray functionality', function () {
test('fromArray() sets session data correctly', function () {
$data = [
'user_id' => 123,
'username' => 'testuser',
'preferences' => ['theme' => 'dark'],
];
$this->session->fromArray($data);
expect($this->session->get('user_id'))->toBe(123);
expect($this->session->get('username'))->toBe('testuser');
expect($this->session->get('preferences'))->toBe(['theme' => 'dark']);
});
test('fromArray() initializes components after setting data', function () {
// Komponenten sollten nach fromArray() verfügbar sein
$this->session->fromArray(['some' => 'data']);
expect($this->session->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class);
expect($this->session->validation)->toBeInstanceOf(\App\Framework\Http\Session\ValidationErrorBag::class);
expect($this->session->form)->toBeInstanceOf(\App\Framework\Http\Session\FormDataStorage::class);
expect($this->session->security)->toBeInstanceOf(\App\Framework\Http\Session\SecurityManager::class);
expect($this->session->csrf)->toBeInstanceOf(\App\Framework\Http\Session\CsrfProtection::class);
});
test('fromArray() preserves existing data structure', function () {
// Teste ob komplexe Datenstrukturen korrekt gespeichert werden
$complexData = [
'user' => [
'id' => 123,
'profile' => [
'name' => 'Test User',
'settings' => [
'notifications' => true,
'theme' => 'dark',
],
],
],
'session_data' => [
'csrf_token' => 'token123',
'flash_messages' => [
'success' => ['Message saved!'],
'error' => ['Validation failed!'],
],
],
];
$this->session->fromArray($complexData);
expect($this->session->get('user'))->toBe($complexData['user']);
expect($this->session->get('session_data'))->toBe($complexData['session_data']);
// Teste nested access
$user = $this->session->get('user');
expect($user['profile']['name'])->toBe('Test User');
});
});
describe('Session Component Integration', function () {
test('components are properly initialized after fromArray()', function () {
$this->session->fromArray([]);
// Teste ob alle Komponenten korrekt initialisiert sind
expect($this->session->flash)->toBeInstanceOf(\App\Framework\Http\Session\FlashBag::class);
expect($this->session->validation)->toBeInstanceOf(\App\Framework\Http\Session\ValidationErrorBag::class);
expect($this->session->form)->toBeInstanceOf(\App\Framework\Http\Session\FormDataStorage::class);
expect($this->session->security)->toBeInstanceOf(\App\Framework\Http\Session\SecurityManager::class);
expect($this->session->csrf)->toBeInstanceOf(\App\Framework\Http\Session\CsrfProtection::class);
});
test('components can access session data', function () {
$this->session->fromArray([
'flash' => ['success' => ['Test message']],
'csrf_token' => 'test-token',
]);
// Flash component sollte auf die Session-Daten zugreifen können
expect($this->session->get('flash'))->toBe(['success' => ['Test message']]);
expect($this->session->get('csrf_token'))->toBe('test-token');
});
test('double initialization is prevented', function () {
$this->session->fromArray([]);
$flash1 = $this->session->flash;
// Zweite Initialisierung sollte die gleichen Instanzen zurückgeben
$this->session->fromArray(['new' => 'data']);
$flash2 = $this->session->flash;
expect($flash1)->toBe($flash2);
});
});
describe('Session Edge Cases', function () {
test('handles null values correctly', function () {
$this->session->set('null_value', null);
expect($this->session->has('null_value'))->toBeTrue();
expect($this->session->get('null_value'))->toBeNull();
});
test('handles empty string values correctly', function () {
$this->session->set('empty_string', '');
expect($this->session->has('empty_string'))->toBeTrue();
expect($this->session->get('empty_string'))->toBe('');
});
test('handles boolean values correctly', function () {
$this->session->set('true_value', true);
$this->session->set('false_value', false);
expect($this->session->get('true_value'))->toBeTrue();
expect($this->session->get('false_value'))->toBeFalse();
});
test('handles array values correctly', function () {
$arrayValue = ['nested' => ['deep' => 'value']];
$this->session->set('array_value', $arrayValue);
expect($this->session->get('array_value'))->toBe($arrayValue);
});
test('handles object serialization edge cases', function () {
// Teste was passiert wenn Objekte in der Session gespeichert werden
$stdClass = new stdClass();
$stdClass->property = 'value';
$this->session->set('object_value', $stdClass);
$retrieved = $this->session->get('object_value');
expect($retrieved)->toBeInstanceOf(stdClass::class);
expect($retrieved->property)->toBe('value');
});
});
describe('Session Debug Information', function () {
test('__debugInfo() returns correct format', function () {
$this->session->set('debug_key', 'debug_value');
$debugInfo = $this->session->__debugInfo();
expect($debugInfo[0])->toBe($this->sessionId->toString());
expect($debugInfo['debug_key'])->toBe('debug_value');
});
});