Files
michaelschiemer/tests/Unit/Framework/HttpClient/Curl/MultiHandleTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

497 lines
15 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\MultiHandle;
use App\Framework\HttpClient\Curl\Exception\MultiHandleException;
describe('Curl\MultiHandle', function () {
it('can be initialized empty', function () {
$multi = new MultiHandle();
expect($multi)->toBeInstanceOf(MultiHandle::class);
expect($multi->count())->toBe(0);
});
it('can be initialized with handles', function () {
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$multi = new MultiHandle($handle1, $handle2);
expect($multi)->toBeInstanceOf(MultiHandle::class);
expect($multi->count())->toBe(2);
expect($multi->getHandles())->toHaveCount(2);
});
it('can add handles', function () {
$multi = new MultiHandle();
$handle = new Handle('https://httpbin.org/get');
$result = $multi->addHandle($handle);
expect($result)->toBe($multi); // Fluent API
expect($multi->count())->toBe(1);
});
it('can remove handles', function () {
$multi = new MultiHandle();
$handle = new Handle('https://httpbin.org/get');
$multi->addHandle($handle);
expect($multi->count())->toBe(1);
$multi->removeHandle($handle);
expect($multi->count())->toBe(0);
});
it('can get all attached handles', function () {
$multi = new MultiHandle();
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$multi->addHandle($handle1);
$multi->addHandle($handle2);
$handles = $multi->getHandles();
expect($handles)->toHaveCount(2);
expect($handles)->toContain($handle1);
expect($handles)->toContain($handle2);
});
it('can execute parallel requests with executeAll()', function () {
$multi = new MultiHandle();
$urls = [
'https://httpbin.org/delay/1',
'https://httpbin.org/uuid',
'https://httpbin.org/get',
];
$handles = [];
foreach ($urls as $url) {
$handle = new Handle($url);
$handle->setOption(HandleOption::Timeout, 10);
$handles[] = $handle;
$multi->addHandle($handle);
}
expect($multi->count())->toBe(3);
$startTime = microtime(true);
$completed = $multi->executeAll();
$duration = microtime(true) - $startTime;
// Parallel execution should be faster than sequential (3+ seconds)
expect($duration)->toBeLessThan(2.5);
expect($completed)->toHaveCount(3);
// Verify all requests completed successfully
foreach ($handles as $handle) {
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
}
});
it('executes requests in parallel not sequentially', function () {
$multi = new MultiHandle();
// Create 3 requests with 1 second delay each
$handles = [];
for ($i = 0; $i < 3; $i++) {
$handle = new Handle('https://httpbin.org/delay/1');
$handle->setOption(HandleOption::Timeout, 10);
$handles[] = $handle;
$multi->addHandle($handle);
}
$startTime = microtime(true);
$multi->executeAll();
$duration = microtime(true) - $startTime;
// If sequential: ~3 seconds
// If parallel: ~1 second
expect($duration)->toBeLessThan(2.0);
expect($duration)->toBeGreaterThan(0.8);
});
it('can handle mixed success and failure', function () {
$multi = new MultiHandle();
$handle1 = new Handle('https://httpbin.org/get');
$handle1->setOption(HandleOption::Timeout, 10);
$handle2 = new Handle('https://httpbin.org/status/404');
$handle2->setOption(HandleOption::Timeout, 10);
$handle3 = new Handle('https://httpbin.org/status/500');
$handle3->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle1);
$multi->addHandle($handle2);
$multi->addHandle($handle3);
$completed = $multi->executeAll();
expect($completed)->toHaveCount(3);
// Check individual status codes
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
expect($handle2->getInfo(Info::ResponseCode))->toBe(404);
expect($handle3->getInfo(Info::ResponseCode))->toBe(500);
});
it('can execute with manual control loop', function () {
$multi = new MultiHandle();
$handle = new Handle('https://httpbin.org/get');
$handle->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle);
$stillRunning = 0;
$iterations = 0;
do {
$multi->execute($stillRunning);
if ($stillRunning > 0) {
$multi->select();
}
$iterations++;
} while ($stillRunning > 0 && $iterations < 100);
expect($stillRunning)->toBe(0);
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
});
it('can handle many parallel requests', function () {
$multi = new MultiHandle();
$numRequests = 10;
$handles = [];
for ($i = 0; $i < $numRequests; $i++) {
$handle = new Handle('https://httpbin.org/get');
$handle->setOption(HandleOption::Timeout, 10);
$handles[] = $handle;
$multi->addHandle($handle);
}
expect($multi->count())->toBe($numRequests);
$startTime = microtime(true);
$completed = $multi->executeAll();
$duration = microtime(true) - $startTime;
expect($completed)->toHaveCount($numRequests);
// Should complete much faster than sequential
expect($duration)->toBeLessThan(5.0);
// Verify all completed
foreach ($handles as $handle) {
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
}
});
it('provides access to underlying resource', function () {
$multi = new MultiHandle();
$resource = $multi->getResource();
expect($resource)->toBeInstanceOf(CurlMultiHandle::class);
});
it('can set multi handle options', function () {
$multi = new MultiHandle();
// CURLMOPT_PIPELINING enable HTTP/1.1 pipelining
if (defined('CURLMOPT_PIPELINING')) {
$result = $multi->setOption(CURLMOPT_PIPELINING, 1);
expect($result)->toBe($multi); // Fluent API
}
expect($multi)->toBeInstanceOf(MultiHandle::class);
});
it('handles empty multi handle gracefully', function () {
$multi = new MultiHandle();
// Execute without any handles
$completed = $multi->executeAll();
expect($completed)->toHaveCount(0);
expect($multi->count())->toBe(0);
});
it('can add and remove handles multiple times', function () {
$multi = new MultiHandle();
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$multi->addHandle($handle1);
expect($multi->count())->toBe(1);
$multi->addHandle($handle2);
expect($multi->count())->toBe(2);
$multi->removeHandle($handle1);
expect($multi->count())->toBe(1);
$multi->removeHandle($handle2);
expect($multi->count())->toBe(0);
});
it('select returns number of handles with activity', function () {
$multi = new MultiHandle();
$handle = new Handle('https://httpbin.org/delay/1');
$handle->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle);
$stillRunning = 0;
$multi->execute($stillRunning);
if ($stillRunning > 0) {
$result = $multi->select(1.0);
// Result can be -1 (error), 0 (timeout), or >0 (activity)
expect($result)->toBeGreaterThanOrEqual(-1);
}
expect($stillRunning)->toBeGreaterThanOrEqual(0);
});
it('getInfo returns transfer information', function () {
$multi = new MultiHandle();
$handle = new Handle('https://httpbin.org/get');
$handle->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle);
$stillRunning = 0;
$messagesInQueue = 0;
do {
$multi->execute($stillRunning);
if ($stillRunning > 0) {
$multi->select();
}
// Check for completed transfers
while ($info = $multi->getInfo($messagesInQueue)) {
expect($info)->toBeArray();
expect($info)->toHaveKey('handle');
expect($info)->toHaveKey('result');
}
} while ($stillRunning > 0);
expect($stillRunning)->toBe(0);
});
it('can handle POST requests in parallel', function () {
$multi = new MultiHandle();
$handles = [];
$data = ['name' => 'Test', 'timestamp' => time()];
for ($i = 0; $i < 3; $i++) {
$handle = new Handle('https://httpbin.org/post');
$handle->setOptions([
HandleOption::Post->value => true,
HandleOption::PostFields->value => json_encode($data),
HandleOption::HttpHeader->value => ['Content-Type: application/json'],
HandleOption::Timeout->value => 10,
]);
$handles[] = $handle;
$multi->addHandle($handle);
}
$completed = $multi->executeAll();
expect($completed)->toHaveCount(3);
foreach ($handles as $handle) {
expect($handle->getInfo(Info::ResponseCode))->toBe(200);
}
});
it('automatically cleans up on destruction', function () {
$multi = new MultiHandle();
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$multi->addHandle($handle1);
$multi->addHandle($handle2);
expect($multi->count())->toBe(2);
unset($multi);
// If we reach here without errors, cleanup worked
expect(true)->toBeTrue();
});
it('can register onComplete callback', function () {
$multi = new MultiHandle();
$completedUrls = [];
$multi->onComplete(function(Handle $handle) use (&$completedUrls) {
$completedUrls[] = $handle->getInfo(Info::EffectiveUrl);
});
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$handle1->setOption(HandleOption::Timeout, 10);
$handle2->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle1);
$multi->addHandle($handle2);
$multi->executeAll();
expect($completedUrls)->toHaveCount(2);
expect($completedUrls[0])->toContain('httpbin.org');
expect($completedUrls[1])->toContain('httpbin.org');
});
it('can register onError callback', function () {
$multi = new MultiHandle();
$errors = [];
$multi->onError(function(Handle $handle, int $errorCode) use (&$errors) {
$errors[] = [
'url' => $handle->getInfo(Info::EffectiveUrl),
'code' => $errorCode
];
});
// Create a handle that will timeout
$handle = new Handle('https://httpbin.org/delay/10');
$handle->setOption(HandleOption::Timeout, 1); // 1 second timeout
$multi->addHandle($handle);
$multi->executeAll();
expect($errors)->toHaveCount(1);
expect($errors[0]['code'])->toBe(CURLE_OPERATION_TIMEDOUT);
});
it('can register onProgress callback', function () {
$multi = new MultiHandle();
$progressUpdates = [];
$multi->onProgress(function(int $completed, int $total) use (&$progressUpdates) {
$progressUpdates[] = ['completed' => $completed, 'total' => $total];
});
$handle1 = new Handle('https://httpbin.org/get');
$handle2 = new Handle('https://httpbin.org/uuid');
$handle3 = new Handle('https://httpbin.org/delay/1');
$handle1->setOption(HandleOption::Timeout, 10);
$handle2->setOption(HandleOption::Timeout, 10);
$handle3->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle1);
$multi->addHandle($handle2);
$multi->addHandle($handle3);
$multi->executeAll();
// Should have progress updates
expect($progressUpdates)->not->toBeEmpty();
expect($progressUpdates)->toHaveCount(3);
// Final update should show all completed
$finalUpdate = end($progressUpdates);
expect($finalUpdate['completed'])->toBe(3);
expect($finalUpdate['total'])->toBe(3);
});
it('can chain callback registrations', function () {
$multi = new MultiHandle();
$result = $multi
->onComplete(fn($handle) => null)
->onError(fn($handle, $code) => null)
->onProgress(fn($completed, $total) => null);
expect($result)->toBe($multi);
});
it('triggers callbacks in correct order', function () {
$multi = new MultiHandle();
$events = [];
$multi->onComplete(function(Handle $handle) use (&$events) {
$events[] = 'complete';
});
$multi->onProgress(function(int $completed, int $total) use (&$events) {
$events[] = 'progress';
});
$handle = new Handle('https://httpbin.org/get');
$handle->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle);
$multi->executeAll();
expect($events)->toContain('complete');
expect($events)->toContain('progress');
});
});
describe('Curl\MultiHandle edge cases', function () {
it('handles timeout in parallel requests', function () {
$multi = new MultiHandle();
$handle1 = new Handle('https://httpbin.org/delay/1');
$handle1->setOption(HandleOption::Timeout, 10);
$handle2 = new Handle('https://httpbin.org/delay/20');
$handle2->setOption(HandleOption::Timeout, 2);
$multi->addHandle($handle1);
$multi->addHandle($handle2);
$completed = $multi->executeAll();
// Handle1 should succeed
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
// Handle2 should timeout (response code 0 or error)
$handle2Code = $handle2->getInfo(Info::ResponseCode);
expect($handle2Code)->toBeIn([0, 28]); // 0 = timeout, 28 = operation timeout
});
it('can reuse multi handle for multiple batches', function () {
$multi = new MultiHandle();
// First batch
$handle1 = new Handle('https://httpbin.org/get');
$handle1->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle1);
$multi->executeAll();
expect($handle1->getInfo(Info::ResponseCode))->toBe(200);
$multi->removeHandle($handle1);
// Second batch
$handle2 = new Handle('https://httpbin.org/uuid');
$handle2->setOption(HandleOption::Timeout, 10);
$multi->addHandle($handle2);
$multi->executeAll();
expect($handle2->getInfo(Info::ResponseCode))->toBe(200);
});
});