clock = new SystemClock(); $this->tokenGenerator = new CsrfTokenGenerator(new SecureRandomGenerator()); $this->sessionId = SessionId::fromString('test-session-' . uniqid()); $this->session = Session::fromArray( $this->sessionId, $this->clock, $this->tokenGenerator, [] ); $this->csrfProtection = $this->session->csrf; }); it('generates a new token every time', function () { $formId = 'test-form'; $token1 = $this->csrfProtection->generateToken($formId); $token2 = $this->csrfProtection->generateToken($formId); // Tokens should be different (no reuse) expect($token1->toString())->not->toBe($token2->toString()); }); it('generates valid 64-character hex tokens', function () { $token = $this->csrfProtection->generateToken('test-form'); expect($token->toString())->toHaveLength(64); expect(ctype_xdigit($token->toString()))->toBeTrue(); }); it('stores multiple tokens per form', function () { $formId = 'test-form'; $token1 = $this->csrfProtection->generateToken($formId); $token2 = $this->csrfProtection->generateToken($formId); $token3 = $this->csrfProtection->generateToken($formId); $count = $this->csrfProtection->getActiveTokenCount($formId); // Should have 3 tokens expect($count)->toBe(3); }); it('validates correct token', function () { $formId = 'test-form'; $token = $this->csrfProtection->generateToken($formId); $result = $this->csrfProtection->validateTokenWithDebug($formId, $token); expect($result['valid'])->toBeTrue(); }); it('rejects invalid token', function () { $formId = 'test-form'; $this->csrfProtection->generateToken($formId); $invalidToken = CsrfToken::fromString(str_repeat('0', 64)); $result = $this->csrfProtection->validateTokenWithDebug($formId, $invalidToken); expect($result['valid'])->toBeFalse(); expect($result['debug']['reason'])->toBe('No matching token found in session'); }); it('allows token reuse within resubmit window', function () { $formId = 'test-form'; $token = $this->csrfProtection->generateToken($formId); // First validation $result1 = $this->csrfProtection->validateTokenWithDebug($formId, $token); expect($result1['valid'])->toBeTrue(); // Second validation within resubmit window (should still work) $result2 = $this->csrfProtection->validateTokenWithDebug($formId, $token); expect($result2['valid'])->toBeTrue(); }); it('limits tokens per form to maximum', function () { $formId = 'test-form'; // Generate more than MAX_TOKENS_PER_FORM (3) for ($i = 0; $i < 5; $i++) { $this->csrfProtection->generateToken($formId); } $count = $this->csrfProtection->getActiveTokenCount($formId); // Should be limited to 3 expect($count)->toBeLessThanOrEqual(3); }); it('handles multiple forms independently', function () { $formId1 = 'form-1'; $formId2 = 'form-2'; $token1 = $this->csrfProtection->generateToken($formId1); $token2 = $this->csrfProtection->generateToken($formId2); // Tokens should be different expect($token1->toString())->not->toBe($token2->toString()); // Each form should have its own token expect($this->csrfProtection->getActiveTokenCount($formId1))->toBe(1); expect($this->csrfProtection->getActiveTokenCount($formId2))->toBe(1); // Validation should work independently expect($this->csrfProtection->validateToken($formId1, $token1))->toBeTrue(); expect($this->csrfProtection->validateToken($formId2, $token2))->toBeTrue(); // Cross-validation should fail expect($this->csrfProtection->validateToken($formId1, $token2))->toBeFalse(); expect($this->csrfProtection->validateToken($formId2, $token1))->toBeFalse(); });