feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,368 @@
<?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);
});
});