value)->toBe($value); expect($sessionId->toString())->toBe($value); }); it('rejects session id shorter than 32 characters', function () { $value = str_repeat('a', 31); // 31 characters - too short expect(fn() => UploadSessionId::fromString($value)) ->toThrow(InvalidArgumentException::class); }); it('accepts session id longer than 32 characters', function () { $value = str_repeat('a', 64); // 64 characters - valid $sessionId = UploadSessionId::fromString($value); expect($sessionId->value)->toBe($value); }); it('rejects non-alphanumeric characters', function () { $value = str_repeat('a', 31) . '@'; // 32 chars with special char expect(fn() => UploadSessionId::fromString($value)) ->toThrow(InvalidArgumentException::class); }); it('accepts uppercase and lowercase alphanumeric', function () { $value = 'abcdefghijklmnopqrstuvwxyz012345'; // 32 chars $sessionId = UploadSessionId::fromString($value); expect($sessionId->value)->toBe($value); }); it('accepts mixed case alphanumeric', function () { $value = 'AbCdEfGh12345678IjKlMnOp90123456'; // 32 chars $sessionId = UploadSessionId::fromString($value); expect($sessionId->value)->toBe($value); }); it('compares session ids for equality', function () { $value = str_repeat('a', 32); $id1 = UploadSessionId::fromString($value); $id2 = UploadSessionId::fromString($value); $id3 = UploadSessionId::fromString(str_repeat('b', 32)); expect($id1->equals($id2))->toBeTrue(); expect($id1->equals($id3))->toBeFalse(); }); it('uses timing-safe comparison', function () { // Test that equals() uses hash_equals for timing safety $id1 = UploadSessionId::fromString(str_repeat('a', 32)); $id2 = UploadSessionId::fromString(str_repeat('a', 32)); // This tests the implementation detail that hash_equals is used expect($id1->equals($id2))->toBeTrue(); }); it('converts to string via toString', function () { $value = str_repeat('a', 32); $sessionId = UploadSessionId::fromString($value); expect($sessionId->toString())->toBe($value); }); it('converts to string via __toString', function () { $value = str_repeat('a', 32); $sessionId = UploadSessionId::fromString($value); expect((string) $sessionId)->toBe($value); }); it('handles hex-encoded session ids', function () { // Typical format from bin2hex(random_bytes(16)) $hexValue = bin2hex(random_bytes(16)); // 32 hex characters $sessionId = UploadSessionId::fromString($hexValue); expect(strlen($sessionId->value))->toBe(32); expect(ctype_xdigit($sessionId->value))->toBeTrue(); }); it('rejects empty string', function () { expect(fn() => UploadSessionId::fromString('')) ->toThrow(InvalidArgumentException::class); }); it('rejects whitespace characters', function () { $value = str_repeat('a', 31) . ' '; // 32 chars with space expect(fn() => UploadSessionId::fromString($value)) ->toThrow(InvalidArgumentException::class); }); it('rejects special characters', function () { $specialChars = ['!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=']; foreach ($specialChars as $char) { expect(fn() => UploadSessionId::fromString(str_repeat('a', 31) . $char)) ->toThrow(InvalidArgumentException::class); } }); });