- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
369 lines
12 KiB
PHP
369 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\HttpClient\Curl\Handle;
|
|
use App\Framework\HttpClient\Curl\HandleOption;
|
|
use App\Framework\HttpClient\Curl\Info;
|
|
use App\Framework\HttpClient\Curl\Pause;
|
|
use App\Framework\HttpClient\Curl\Exception\HandleException;
|
|
|
|
describe('Curl\Handle', function () {
|
|
it('can be initialized without URL', function () {
|
|
$handle = new Handle();
|
|
|
|
expect($handle)->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);
|
|
});
|
|
});
|