toBeInstanceOf(Handle::class); expect($handle->errorNumber)->toBe(0); expect($handle->errorMessage)->toBe(''); }); it('can be initialized with URL', function () { $handle = new Handle('https://httpbin.org/get'); expect($handle)->toBeInstanceOf(Handle::class); }); it('can fetch from URL', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $response = $handle->fetch(); expect($response)->toBeString(); expect($response)->toContain('"url"'); expect($handle->getInfo(Info::ResponseCode))->toBe(200); }); it('supports fluent API with method chaining', function () { $handle = new Handle(); $result = $handle ->setOption(HandleOption::Url, 'https://httpbin.org/get') ->setOption(HandleOption::Timeout, 10); expect($result)->toBe($handle); // Same instance expect($handle->fetch())->toContain('"url"'); }); it('can set multiple options at once', function () { $handle = new Handle(); $handle->setOptions([ HandleOption::Url->value => 'https://httpbin.org/get', HandleOption::Timeout->value => 10, HandleOption::UserAgent->value => 'Test-Agent/1.0', ]); $response = $handle->fetch(); expect($response)->toContain('Test-Agent/1.0'); }); it('validates option types - integer expected', function () { $handle = new Handle(); expect(fn() => $handle->setOption(HandleOption::Timeout, 'invalid')) ->toThrow(HandleException::class, 'Invalid type for option Timeout: expected integer, got string'); }); it('validates option types - boolean expected', function () { $handle = new Handle(); expect(fn() => $handle->setOption(HandleOption::FollowLocation, 'yes')) ->toThrow(HandleException::class, 'Invalid type for option FollowLocation: expected boolean, got string'); }); it('validates option types - string expected', function () { $handle = new Handle(); expect(fn() => $handle->setOption(HandleOption::UserAgent, 123)) ->toThrow(HandleException::class, 'Invalid type for option UserAgent: expected string, got int'); }); it('validates option types - array expected', function () { $handle = new Handle(); expect(fn() => $handle->setOption(HandleOption::HttpHeader, 'not-an-array')) ->toThrow(HandleException::class, 'Invalid type for option HttpHeader: expected array, got string'); }); it('can get single info value', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $handle->fetch(); expect($handle->getInfo(Info::ResponseCode))->toBe(200); expect($handle->getInfo(Info::EffectiveUrl))->toBe('https://httpbin.org/get'); expect($handle->getInfo(Info::TotalTime))->toBeGreaterThan(0); }); it('can get all info at once', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $handle->fetch(); $allInfo = $handle->getInfo(); expect($allInfo)->toBeArray(); expect($allInfo)->toHaveKey('url'); expect($allInfo)->toHaveKey('http_code'); expect($allInfo)->toHaveKey('total_time'); }); it('throws on invalid URL', function () { $handle = new Handle('https://invalid-domain-that-does-not-exist-99999.com'); $handle->setOption(HandleOption::Timeout, 5); $handle->fetch(); })->throws(HandleException::class); it('updates error properties on failure', function () { $handle = new Handle('https://invalid-domain-that-does-not-exist-99999.com'); $handle->setOption(HandleOption::Timeout, 5); try { $handle->fetch(); } catch (HandleException $e) { expect($handle->errorNumber)->toBeGreaterThan(0); expect($handle->errorMessage)->not->toBeEmpty(); } }); it('can reset options', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::UserAgent, 'Test-Agent/1.0'); $handle->reset(); // After reset, default options apply $handle->setOption(HandleOption::Url, 'https://httpbin.org/user-agent'); $response = $handle->fetch(); // Should not contain custom user agent after reset expect($response)->not->toContain('Test-Agent/1.0'); }); it('can URL-escape strings', function () { $handle = new Handle(); $escaped = $handle->escapeUrl('hello world'); expect($escaped)->toBe('hello%20world'); }); it('can URL-unescape strings', function () { $handle = new Handle(); $unescaped = $handle->unescapeUrl('hello%20world'); expect($unescaped)->toBe('hello world'); }); it('can execute with resource output', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $tempFile = tmpfile(); $handle->execute($tempFile); fseek($tempFile, 0); $content = stream_get_contents($tempFile); fclose($tempFile); expect($content)->toContain('"url"'); }); it('can execute with callable output', function () { $handle = new Handle('https://httpbin.org/get'); $handle->setOption(HandleOption::Timeout, 10); $receivedData = ''; $callback = function ($ch, $data) use (&$receivedData) { $receivedData .= $data; return strlen($data); }; $handle->execute($callback); expect($receivedData)->toContain('"url"'); }); it('throws on invalid output target', function () { $handle = new Handle('https://httpbin.org/get'); expect(fn() => $handle->execute('invalid')) ->toThrow(HandleException::class, 'Invalid output target'); }); it('can perform POST request', function () { $handle = new Handle('https://httpbin.org/post'); $data = ['name' => 'Test', 'email' => 'test@example.com']; $handle->setOptions([ HandleOption::Post->value => true, HandleOption::PostFields->value => json_encode($data), HandleOption::HttpHeader->value => ['Content-Type: application/json'], HandleOption::Timeout->value => 10, ]); $response = $handle->fetch(); expect($response)->toContain('"name": "Test"'); expect($response)->toContain('"email": "test@example.com"'); }); it('can follow redirects', function () { $handle = new Handle('https://httpbin.org/redirect/2'); $handle->setOptions([ HandleOption::FollowLocation->value => true, HandleOption::MaxRedirs->value => 5, HandleOption::Timeout->value => 10, ]); $response = $handle->fetch(); expect($handle->getInfo(Info::RedirectCount))->toBe(2); expect($handle->getInfo(Info::ResponseCode))->toBe(200); }); it('respects timeout settings', function () { $handle = new Handle('https://httpbin.org/delay/10'); $handle->setOption(HandleOption::Timeout, 2); expect(fn() => $handle->fetch()) ->toThrow(HandleException::class); }); it('can set custom headers', function () { $handle = new Handle('https://httpbin.org/headers'); $handle->setOptions([ HandleOption::HttpHeader->value => [ 'X-Custom-Header: test-value', 'X-Another-Header: another-value', ], HandleOption::Timeout->value => 10, ]); $response = $handle->fetch(); expect($response)->toContain('X-Custom-Header'); expect($response)->toContain('test-value'); }); it('provides access to underlying resource', function () { $handle = new Handle(); $resource = $handle->getResource(); expect($resource)->toBeInstanceOf(CurlHandle::class); }); it('error properties use asymmetric visibility', function () { $handle = new Handle('https://invalid-domain.com'); // Can read expect($handle->errorNumber)->toBe(0); expect($handle->errorMessage)->toBe(''); // Cannot write (would be compile error, so we just verify they're public readable) expect(property_exists($handle, 'errorNumber'))->toBeTrue(); expect(property_exists($handle, 'errorMessage'))->toBeTrue(); }); it('can handle SSL verification', function () { $handle = new Handle('https://www.google.com'); $handle->setOptions([ HandleOption::SslVerifyPeer->value => true, HandleOption::SslVerifyHost->value => 2, HandleOption::Timeout->value => 10, ]); $response = $handle->fetch(); expect($handle->getInfo(Info::ResponseCode))->toBe(200); expect($handle->getInfo(Info::SslVerifyResult))->toBe(0); // 0 = success }); it('can disable SSL verification for testing', function () { $handle = new Handle('https://self-signed.badssl.com/'); $handle->setOptions([ HandleOption::SslVerifyPeer->value => false, HandleOption::SslVerifyHost->value => 0, HandleOption::Timeout->value => 10, ]); // Should not throw SSL error $response = $handle->fetch(); expect($response)->toBeString(); }); }); describe('Curl\HandleOption enum', function () { it('has getName() method', function () { expect(HandleOption::Timeout->getName())->toBe('Timeout'); expect(HandleOption::UserAgent->getName())->toBe('UserAgent'); }); it('correctly identifies boolean options', function () { expect(HandleOption::FollowLocation->expectsBoolean())->toBeTrue(); expect(HandleOption::SslVerifyPeer->expectsBoolean())->toBeTrue(); expect(HandleOption::Timeout->expectsBoolean())->toBeFalse(); }); it('correctly identifies integer options', function () { expect(HandleOption::Timeout->expectsInteger())->toBeTrue(); expect(HandleOption::MaxRedirs->expectsInteger())->toBeTrue(); expect(HandleOption::UserAgent->expectsInteger())->toBeFalse(); }); it('correctly identifies string options', function () { expect(HandleOption::UserAgent->expectsString())->toBeTrue(); expect(HandleOption::Url->expectsString())->toBeTrue(); expect(HandleOption::Timeout->expectsString())->toBeFalse(); }); it('correctly identifies array options', function () { expect(HandleOption::HttpHeader->expectsArray())->toBeTrue(); expect(HandleOption::Quote->expectsArray())->toBeTrue(); expect(HandleOption::UserAgent->expectsArray())->toBeFalse(); }); it('correctly identifies callable options', function () { expect(HandleOption::WriteFunction->expectsCallable())->toBeTrue(); expect(HandleOption::HeaderFunction->expectsCallable())->toBeTrue(); expect(HandleOption::Timeout->expectsCallable())->toBeFalse(); }); it('correctly identifies resource options', function () { expect(HandleOption::File->expectsResource())->toBeTrue(); expect(HandleOption::InFile->expectsResource())->toBeTrue(); expect(HandleOption::UserAgent->expectsResource())->toBeFalse(); }); }); describe('Curl\Info enum', function () { it('has all common info cases', function () { expect(Info::ResponseCode->value)->toBe(CURLINFO_RESPONSE_CODE); // Note: CURLINFO_HTTP_CODE is an alias for CURLINFO_RESPONSE_CODE expect(Info::TotalTime->value)->toBe(CURLINFO_TOTAL_TIME); expect(Info::EffectiveUrl->value)->toBe(CURLINFO_EFFECTIVE_URL); }); }); describe('Curl\Pause enum', function () { it('has all pause cases', function () { expect(Pause::Recv->value)->toBe(CURLPAUSE_RECV); expect(Pause::Send->value)->toBe(CURLPAUSE_SEND); expect(Pause::All->value)->toBe(CURLPAUSE_ALL); expect(Pause::Cont->value)->toBe(CURLPAUSE_CONT); }); });