Some checks failed
Deploy Application / deploy (push) Has been cancelled
199 lines
6.4 KiB
PHP
199 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Http\Session\FileSessionStorage;
|
|
use App\Framework\Http\Session\FormIdGenerator;
|
|
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\View\Response\FormDataResponseProcessor;
|
|
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->formIdGenerator = new FormIdGenerator();
|
|
|
|
$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, []);
|
|
|
|
$this->processor = new FormDataResponseProcessor(
|
|
$this->formIdGenerator,
|
|
$this->sessionManager
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
if (isset($this->tempDir) && is_dir($this->tempDir)) {
|
|
array_map('unlink', glob($this->tempDir . '/*'));
|
|
rmdir($this->tempDir);
|
|
}
|
|
});
|
|
|
|
it('processes form HTML and replaces token placeholder', function () {
|
|
$formId = $this->formIdGenerator->generateFormId('/test', 'post');
|
|
|
|
$html = <<<HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Test</title></head>
|
|
<body>
|
|
<form method="post" action="/test">
|
|
<input type="hidden" name="_form_id" value="{$formId}">
|
|
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
|
|
<input type="text" name="email" value="">
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
|
|
$processed = $this->processor->process($html, $this->session);
|
|
|
|
// Token should be replaced
|
|
expect($processed)->not->toContain("___TOKEN_{$formId}___");
|
|
|
|
// Should contain a valid token
|
|
preg_match('/name="_token"[^>]*value="([^"]+)"/', $processed, $matches);
|
|
expect($matches)->toHaveCount(2);
|
|
|
|
$token = $matches[1];
|
|
expect(strlen($token))->toBe(64);
|
|
expect(ctype_xdigit($token))->toBeTrue();
|
|
|
|
// Token should be valid in session
|
|
$tokenObj = \App\Framework\Security\CsrfToken::fromString($token);
|
|
$result = $this->session->csrf->validateTokenWithDebug($formId, $tokenObj);
|
|
expect($result['valid'])->toBeTrue();
|
|
});
|
|
|
|
it('processes multiple forms with different form IDs', function () {
|
|
$formId1 = $this->formIdGenerator->generateFormId('/form1', 'post');
|
|
$formId2 = $this->formIdGenerator->generateFormId('/form2', 'post');
|
|
|
|
$html = <<<HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<form method="post" action="/form1">
|
|
<input type="hidden" name="_form_id" value="{$formId1}">
|
|
<input type="hidden" name="_token" value="___TOKEN_{$formId1}___">
|
|
</form>
|
|
<form method="post" action="/form2">
|
|
<input type="hidden" name="_form_id" value="{$formId2}">
|
|
<input type="hidden" name="_token" value="___TOKEN_{$formId2}___">
|
|
</form>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
|
|
$processed = $this->processor->process($html, $this->session);
|
|
|
|
// Both tokens should be replaced
|
|
expect($processed)->not->toContain("___TOKEN_{$formId1}___");
|
|
expect($processed)->not->toContain("___TOKEN_{$formId2}___");
|
|
|
|
// Extract tokens
|
|
preg_match_all('/name="_token"[^>]*value="([^"]+)"/', $processed, $matches);
|
|
expect($matches[1])->toHaveCount(2);
|
|
|
|
$token1 = $matches[1][0];
|
|
$token2 = $matches[1][1];
|
|
|
|
// Tokens should be different
|
|
expect($token1)->not->toBe($token2);
|
|
|
|
// Both should be valid
|
|
expect(strlen($token1))->toBe(64);
|
|
expect(strlen($token2))->toBe(64);
|
|
});
|
|
|
|
it('handles malformed HTML gracefully', function () {
|
|
$formId = $this->formIdGenerator->generateFormId('/test', 'post');
|
|
|
|
// HTML with unclosed tags
|
|
$html = <<<HTML
|
|
<form method="post" action="/test">
|
|
<input type="hidden" name="_form_id" value="{$formId}">
|
|
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
|
|
<div>
|
|
<unclosed-tag>
|
|
</form>
|
|
HTML;
|
|
|
|
// Should not throw exception
|
|
$processed = $this->processor->process($html, $this->session);
|
|
|
|
// Should still replace token (via regex fallback)
|
|
expect($processed)->not->toContain("___TOKEN_{$formId}___");
|
|
});
|
|
|
|
it('preserves HTML structure after processing', function () {
|
|
$formId = $this->formIdGenerator->generateFormId('/test', 'post');
|
|
|
|
$html = <<<HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Test Page</title>
|
|
</head>
|
|
<body>
|
|
<h1>Test Form</h1>
|
|
<form method="post" action="/test">
|
|
<input type="hidden" name="_form_id" value="{$formId}">
|
|
<input type="hidden" name="_token" value="___TOKEN_{$formId}___">
|
|
<label>Email:</label>
|
|
<input type="email" name="email">
|
|
<button type="submit">Submit</button>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
|
|
$processed = $this->processor->process($html, $this->session);
|
|
|
|
// Should preserve structure
|
|
expect($processed)->toContain('<!DOCTYPE html>');
|
|
expect($processed)->toContain('<html>');
|
|
expect($processed)->toContain('<head>');
|
|
expect($processed)->toContain('<title>Test Page</title>');
|
|
expect($processed)->toContain('<h1>Test Form</h1>');
|
|
expect($processed)->toContain('<label>Email:</label>');
|
|
expect($processed)->toContain('<button type="submit">Submit</button>');
|
|
|
|
// Token should be replaced
|
|
expect($processed)->not->toContain("___TOKEN_{$formId}___");
|
|
});
|
|
|
|
|