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:
264
tests/Framework/Http/Session/FlashManagerTest.php
Normal file
264
tests/Framework/Http/Session/FlashManagerTest.php
Normal 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');
|
||||
});
|
||||
});
|
||||
193
tests/Framework/Http/Session/SessionComponentLazyInitTest.php
Normal file
193
tests/Framework/Http/Session/SessionComponentLazyInitTest.php
Normal 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([]);
|
||||
});
|
||||
});
|
||||
242
tests/Framework/Http/Session/SessionComponentPersistenceTest.php
Normal file
242
tests/Framework/Http/Session/SessionComponentPersistenceTest.php
Normal 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');
|
||||
});
|
||||
});
|
||||
411
tests/Framework/Http/Session/SessionLifecycleTest.php
Normal file
411
tests/Framework/Http/Session/SessionLifecycleTest.php
Normal 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
|
||||
});
|
||||
});
|
||||
387
tests/Framework/Http/Session/SessionManagerTest.php
Normal file
387
tests/Framework/Http/Session/SessionManagerTest.php
Normal 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');
|
||||
});
|
||||
});
|
||||
320
tests/Framework/Http/Session/SessionStorageTest.php
Normal file
320
tests/Framework/Http/Session/SessionStorageTest.php
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
229
tests/Framework/Http/Session/SessionTest.php
Normal file
229
tests/Framework/Http/Session/SessionTest.php
Normal 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user