tempDir = sys_get_temp_dir() . '/php_sessions_test_' . uniqid(); mkdir($this->tempDir, 0700, true); $this->clock = new SystemClock(); $this->storage = new FileSessionStorage($this->tempDir, $this->clock); $this->sessionIdGenerator = new SessionIdGenerator(new SecureRandomGenerator()); $this->csrfTokenGenerator = new CsrfTokenGenerator(new SecureRandomGenerator()); $this->cookieConfig = new SessionCookieConfig( name: 'test_session', lifetime: 3600, path: '/', domain: null, secure: false, httpOnly: true, sameSite: \App\Framework\Http\Cookies\SameSite::LAX ); $this->sessionManager = new SessionManager( generator: $this->sessionIdGenerator, responseManipulator: Mockery::mock(\App\Framework\Http\ResponseManipulator::class), clock: $this->clock, csrfTokenGenerator: $this->csrfTokenGenerator, storage: $this->storage, cookieConfig: $this->cookieConfig ); $this->sessionId = $this->sessionIdGenerator->generate(); $this->session = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, []); }); afterEach(function () { if (isset($this->tempDir) && is_dir($this->tempDir)) { array_map('unlink', glob($this->tempDir . '/*')); rmdir($this->tempDir); } }); it('handles concurrent token generation atomically', function () { $formId = 'test-form'; // Simulate concurrent token generation $tokens = []; $updates = []; // Generate tokens "concurrently" (sequentially in test, but with atomic updates) for ($i = 0; $i < 5; $i++) { $token = $this->session->csrf->generateToken($formId); $tokens[] = $token->toString(); // Save session after each token generation $this->sessionManager->saveSessionData($this->session); // Reload session to simulate new request $data = $this->storage->read($this->sessionId); $this->session = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $data); } // All tokens should be unique $uniqueTokens = array_unique($tokens); expect(count($uniqueTokens))->toBe(count($tokens)); // Should have max 3 tokens (cleanup) $count = $this->session->csrf->getActiveTokenCount($formId); expect($count)->toBeLessThanOrEqual(3); }); it('validates tokens correctly after atomic updates', function () { $formId = 'test-form'; // Generate token $token = $this->session->csrf->generateToken($formId); $this->sessionManager->saveSessionData($this->session); // Reload session $data = $this->storage->read($this->sessionId); $this->session = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $data); // Validate token $result = $this->session->csrf->validateTokenWithDebug($formId, $token); expect($result['valid'])->toBeTrue(); // Mark as used and save $this->sessionManager->saveSessionData($this->session); // Reload again $data = $this->storage->read($this->sessionId); $this->session = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $data); // Should still be valid within resubmit window $result2 = $this->session->csrf->validateTokenWithDebug($formId, $token); expect($result2['valid'])->toBeTrue(); }); it('handles version conflicts with optimistic locking', function () { $formId = 'test-form'; // Generate token and save $token1 = $this->session->csrf->generateToken($formId); $this->sessionManager->saveSessionData($this->session); // Read session data $data1 = $this->storage->read($this->sessionId); $session1 = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $data1); // Read again (simulating concurrent request) $data2 = $this->storage->read($this->sessionId); $session2 = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $data2); // Generate tokens in both "requests" $token2 = $session1->csrf->generateToken($formId); $token3 = $session2->csrf->generateToken($formId); // Save both (simulating concurrent writes) $this->sessionManager->saveSessionData($session1); $this->sessionManager->saveSessionData($session2); // Final read $finalData = $this->storage->read($this->sessionId); $finalSession = Session::fromArray($this->sessionId, $this->clock, $this->csrfTokenGenerator, $finalData); // Should have valid tokens $count = $finalSession->csrf->getActiveTokenCount($formId); expect($count)->toBeGreaterThan(0); });