Some checks failed
Deploy Application / deploy (push) Has been cancelled
143 lines
5.1 KiB
PHP
143 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Http\Session\FileSessionStorage;
|
|
use App\Framework\Http\Session\Session;
|
|
use App\Framework\Http\Session\SessionId;
|
|
use App\Framework\Http\Session\SessionIdGenerator;
|
|
use App\Framework\Http\Session\SessionManager;
|
|
use App\Framework\Random\SecureRandomGenerator;
|
|
use App\Framework\Security\CsrfTokenGenerator;
|
|
use App\Framework\Http\Cookies\SessionCookieConfig;
|
|
|
|
beforeEach(function () {
|
|
$this->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);
|
|
});
|
|
|
|
|