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:
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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user