value)->toBe('abcdefghijklmnopqrstuvwxyz123456'); expect((string) $token)->toBe('abcdefghijklmnopqrstuvwxyz123456'); }); it('requires at least 16 characters', function () { expect(fn() => new MagicLinkToken('short')) ->toThrow(\InvalidArgumentException::class, 'Token must be at least 16 characters long'); }); it('accepts exactly 16 characters', function () { $token = new MagicLinkToken('1234567890123456'); expect($token->value)->toBe('1234567890123456'); }); it('accepts more than 16 characters', function () { $longToken = str_repeat('a', 64); $token = new MagicLinkToken($longToken); expect($token->value)->toBe($longToken); }); it('rejects empty string', function () { expect(fn() => new MagicLinkToken('')) ->toThrow(\InvalidArgumentException::class, 'Token value cannot be empty'); }); it('compares tokens correctly', function () { $token1 = new MagicLinkToken('abcdefghijklmnop'); $token2 = new MagicLinkToken('abcdefghijklmnop'); $token3 = new MagicLinkToken('different-token-'); expect($token1->equals($token2))->toBeTrue(); expect($token1->equals($token3))->toBeFalse(); }); it('uses constant-time comparison', function () { $token1 = new MagicLinkToken('1234567890123456'); $token2 = new MagicLinkToken('1234567890123456'); // hash_equals is used internally, which is timing-safe expect($token1->equals($token2))->toBeTrue(); }); it('converts to string', function () { $tokenValue = 'test-token-value-123'; $token = new MagicLinkToken($tokenValue); expect($token->__toString())->toBe($tokenValue); expect((string) $token)->toBe($tokenValue); }); });