Migrate outbound HTTP calls from Guzzle to the Http facade
Replaces every raw GuzzleHttp\Client usage (Nominatim, web.archive.org, webmention fetch/discover/send, Bridgy syndication, IndieAuth client_id lookup, contact avatar/h-card fetch, profile image download, and the CloudConvert screenshot pipeline) with Illuminate\Support\Facades\Http, and enables Http::preventStrayRequests() globally in tests so any un-faked outbound call now fails loudly instead of silently hitting the network. The CloudConvert retry-until-finished middleware in AppServiceProvider is replaced by a plain polling loop in SaveScreenshot, which also fixes a latent bug where the old middleware decoded a response object instead of its body. Bridgy syndication jobs keep their tries=1/no-retry semantics unchanged to avoid duplicate publishes. Guzzle's PSR-7 helpers (Header, UriResolver, Utils) stay in SendWebMentions since Http has no equivalent for them. Every affected test file's Guzzle MockHandler/HandlerStack boilerplate is replaced with Http::fake()/Http::sequence(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8j7cJhCiYDsGUgB7obKNQ
This commit is contained in:
parent
c420e19f08
commit
e08763c526
26 changed files with 337 additions and 630 deletions
|
|
@ -6,13 +6,8 @@ namespace Tests\Unit\Jobs;
|
|||
|
||||
use App\Jobs\SaveScreenshot;
|
||||
use App\Models\Bookmark;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Middleware;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -25,57 +20,34 @@ class SaveScreenshotJobTest extends TestCase
|
|||
public function screenshot_is_saved_by_job(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$guzzleMock = new MockHandler([
|
||||
new Response(201, ['Content-Type' => 'application/json'], '{"data":{"id":"68d52633-e170-465e-b13e-746c97d01ffb","job_id":null,"status":"finished","credits":null,"code":null,"message":null,"percent":100,"operation":"capture-website","engine":"chrome","engine_version":"107","result":null,"created_at":"2023-01-07T21:05:48+00:00","started_at":null,"ended_at":null,"retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":null,"storage":"ceph-fra","depends_on_task_ids":[],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/68d52633-e170-465e-b13e-746c97d01ffb"}}}'),
|
||||
new Response(201, ['Content-Type' => 'application/json'], '{"data":{"id":"27f33137-cc03-4468-aba4-1e1aa8c096fb","job_id":null,"status":"finished","credits":null,"code":null,"message":null,"percent":100,"operation":"export\/url","result":null,"created_at":"2023-01-07T21:10:02+00:00","started_at":null,"ended_at":null,"retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":null,"storage":"ceph-fra","depends_on_task_ids":["68d52633-e170-465e-b13e-746c97d01ffb"],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'image/png'], fopen(__DIR__.'/../../theverge.com.png', 'rb')),
|
||||
|
||||
Http::fake([
|
||||
'api.cloudconvert.com/v2/capture-website' => Http::response([
|
||||
'data' => ['id' => '68d52633-e170-465e-b13e-746c97d01ffb', 'status' => 'finished'],
|
||||
], 201),
|
||||
'api.cloudconvert.com/v2/tasks/68d52633-e170-465e-b13e-746c97d01ffb*' => Http::response([
|
||||
'data' => ['id' => '68d52633-e170-465e-b13e-746c97d01ffb', 'status' => 'finished'],
|
||||
], 200),
|
||||
'api.cloudconvert.com/v2/export/url' => Http::response([
|
||||
'data' => ['id' => '27f33137-cc03-4468-aba4-1e1aa8c096fb', 'status' => 'finished'],
|
||||
], 201),
|
||||
'api.cloudconvert.com/v2/tasks/27f33137-cc03-4468-aba4-1e1aa8c096fb*' => Http::response([
|
||||
'data' => [
|
||||
'id' => '27f33137-cc03-4468-aba4-1e1aa8c096fb',
|
||||
'status' => 'finished',
|
||||
'result' => [
|
||||
'files' => [[
|
||||
'url' => 'https://storage.cloudconvert.com/tasks/27f33137-cc03-4468-aba4-1e1aa8c096fb/theverge.com.png',
|
||||
]],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
'storage.cloudconvert.com/*' => Http::response(
|
||||
file_get_contents(__DIR__.'/../../theverge.com.png'),
|
||||
200,
|
||||
['Content-Type' => 'image/png']
|
||||
),
|
||||
]);
|
||||
$guzzleHandler = HandlerStack::create($guzzleMock);
|
||||
$guzzleClient = new Client(['handler' => $guzzleHandler]);
|
||||
$this->app->instance(Client::class, $guzzleClient);
|
||||
$retryMock = new MockHandler([
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"data":{"id":"68d52633-e170-465e-b13e-746c97d01ffb","job_id":null,"status":"finished","credits":1,"code":null,"message":null,"percent":100,"operation":"capture-website","engine":"chrome","engine_version":"107","payload":{"url":"https:\/\/theverge.com","output_format":"png","screen_width":1440,"screen_height":900,"wait_until":"networkidle0","wait_time":"100"},"result":{"files":[{"filename":"theverge.com.png","size":811819}]},"created_at":"2023-01-07T21:05:48+00:00","started_at":"2023-01-07T21:05:48+00:00","ended_at":"2023-01-07T21:05:55+00:00","retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":[],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/68d52633-e170-465e-b13e-746c97d01ffb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"data":{"id":"27f33137-cc03-4468-aba4-1e1aa8c096fb","job_id":null,"status":"finished","credits":0,"code":null,"message":null,"percent":100,"operation":"export\/url","payload":{"input":"68d52633-e170-465e-b13e-746c97d01ffb","archive_multiple_files":false},"result":{"files":[{"filename":"theverge.com.png","size":811819,"url":"https:\/\/storage.cloudconvert.com\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb\/theverge.com.png?AWSAccessKeyId=cloudconvert-production&Expires=1673212203&Signature=xyz&response-content-disposition=attachment%3B%20filename%3D%22theverge.com.png%22&response-content-type=image%2Fpng"}]},"created_at":"2023-01-07T21:10:02+00:00","started_at":"2023-01-07T21:10:03+00:00","ended_at":"2023-01-07T21:10:03+00:00","retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":["68d52633-e170-465e-b13e-746c97d01ffb"],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb"}}}'),
|
||||
]);
|
||||
$retryHandler = HandlerStack::create($retryMock);
|
||||
$retryHandler->push(Middleware::retry(
|
||||
function ($retries, $request, $response, $exception) {
|
||||
// Limit the number of retries to 5
|
||||
if ($retries >= 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry connection exceptions
|
||||
if ($exception instanceof ConnectException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retry on server errors
|
||||
if ($response && $response->getStatusCode() >= 500) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$responseBody = '';
|
||||
|
||||
if (is_string($response)) {
|
||||
$responseBody = $response;
|
||||
}
|
||||
|
||||
if ($response instanceof Response) {
|
||||
$responseBody = $response->getBody()->getContents();
|
||||
$response->getBody()->rewind();
|
||||
}
|
||||
|
||||
// Finally for CloudConvert, retry if status is not final
|
||||
return json_decode($responseBody, false, 512, JSON_THROW_ON_ERROR)?->data?->status !== 'finished';
|
||||
},
|
||||
function () {
|
||||
// Retry after 1 second
|
||||
return 1000;
|
||||
}
|
||||
));
|
||||
$retryClient = new Client(['handler' => $retryHandler]);
|
||||
$this->app->instance('RetryGuzzle', $retryClient);
|
||||
|
||||
$bookmark = Bookmark::factory()->create();
|
||||
$job = new SaveScreenshot($bookmark);
|
||||
|
|
@ -84,68 +56,45 @@ class SaveScreenshotJobTest extends TestCase
|
|||
|
||||
$this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot);
|
||||
Storage::disk('public')->assertExists('/assets/img/bookmarks/'.$bookmark->screenshot.'.png');
|
||||
|
||||
// capture-website, 1x poll (finished immediately), export/url, 1x poll (finished immediately), download
|
||||
Http::assertSentCount(5);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function screenshot_job_handles_unfinished_tasks(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$guzzleMock = new MockHandler([
|
||||
new Response(201, ['Content-Type' => 'application/json'], '{"id":1,"data":{"id":"68d52633-e170-465e-b13e-746c97d01ffb","job_id":null,"status":"waiting","credits":null,"code":null,"message":null,"percent":100,"operation":"capture-website","engine":"chrome","engine_version":"107","result":null,"created_at":"2023-01-07T21:05:48+00:00","started_at":null,"ended_at":null,"retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":null,"storage":"ceph-fra","depends_on_task_ids":[],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/68d52633-e170-465e-b13e-746c97d01ffb"}}}'),
|
||||
new Response(201, ['Content-Type' => 'application/json'], '{"id":2,"data":{"id":"27f33137-cc03-4468-aba4-1e1aa8c096fb","job_id":null,"status":"waiting","credits":null,"code":null,"message":null,"percent":100,"operation":"export\/url","result":null,"created_at":"2023-01-07T21:10:02+00:00","started_at":null,"ended_at":null,"retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":null,"storage":"ceph-fra","depends_on_task_ids":["68d52633-e170-465e-b13e-746c97d01ffb"],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'image/png'], fopen(__DIR__.'/../../theverge.com.png', 'rb')),
|
||||
|
||||
Http::fake([
|
||||
'api.cloudconvert.com/v2/capture-website' => Http::response([
|
||||
'data' => ['id' => '68d52633-e170-465e-b13e-746c97d01ffb', 'status' => 'waiting'],
|
||||
], 201),
|
||||
'api.cloudconvert.com/v2/tasks/68d52633-e170-465e-b13e-746c97d01ffb*' => Http::sequence()
|
||||
->push(['data' => ['id' => '68d52633-e170-465e-b13e-746c97d01ffb', 'status' => 'waiting']], 200)
|
||||
->push(['data' => ['id' => '68d52633-e170-465e-b13e-746c97d01ffb', 'status' => 'finished']], 200),
|
||||
'api.cloudconvert.com/v2/export/url' => Http::response([
|
||||
'data' => ['id' => '27f33137-cc03-4468-aba4-1e1aa8c096fb', 'status' => 'waiting'],
|
||||
], 201),
|
||||
'api.cloudconvert.com/v2/tasks/27f33137-cc03-4468-aba4-1e1aa8c096fb*' => Http::sequence()
|
||||
->push(['data' => ['id' => '27f33137-cc03-4468-aba4-1e1aa8c096fb', 'status' => 'waiting']], 200)
|
||||
->push([
|
||||
'data' => [
|
||||
'id' => '27f33137-cc03-4468-aba4-1e1aa8c096fb',
|
||||
'status' => 'finished',
|
||||
'result' => [
|
||||
'files' => [[
|
||||
'url' => 'https://storage.cloudconvert.com/tasks/27f33137-cc03-4468-aba4-1e1aa8c096fb/theverge.com.png',
|
||||
]],
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
'storage.cloudconvert.com/*' => Http::response(
|
||||
file_get_contents(__DIR__.'/../../theverge.com.png'),
|
||||
200,
|
||||
['Content-Type' => 'image/png']
|
||||
),
|
||||
]);
|
||||
$guzzleHandler = HandlerStack::create($guzzleMock);
|
||||
$guzzleClient = new Client(['handler' => $guzzleHandler]);
|
||||
$this->app->instance(Client::class, $guzzleClient);
|
||||
$container = [];
|
||||
$history = Middleware::history($container);
|
||||
$retryMock = new MockHandler([
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"id":3,"data":{"id":"68d52633-e170-465e-b13e-746c97d01ffb","job_id":null,"status":"waiting","credits":1,"code":null,"message":null,"percent":50,"operation":"capture-website","engine":"chrome","engine_version":"107","payload":{"url":"https:\/\/theverge.com","output_format":"png","screen_width":1440,"screen_height":900,"wait_until":"networkidle0","wait_time":"100"},"result":{"files":[{"filename":"theverge.com.png","size":811819}]},"created_at":"2023-01-07T21:05:48+00:00","started_at":"2023-01-07T21:05:48+00:00","ended_at":"2023-01-07T21:05:55+00:00","retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":[],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/68d52633-e170-465e-b13e-746c97d01ffb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"id":4,"data":{"id":"68d52633-e170-465e-b13e-746c97d01ffb","job_id":null,"status":"finished","credits":1,"code":null,"message":null,"percent":100,"operation":"capture-website","engine":"chrome","engine_version":"107","payload":{"url":"https:\/\/theverge.com","output_format":"png","screen_width":1440,"screen_height":900,"wait_until":"networkidle0","wait_time":"100"},"result":{"files":[{"filename":"theverge.com.png","size":811819}]},"created_at":"2023-01-07T21:05:48+00:00","started_at":"2023-01-07T21:05:48+00:00","ended_at":"2023-01-07T21:05:55+00:00","retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":[],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/68d52633-e170-465e-b13e-746c97d01ffb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"id":5,"data":{"id":"27f33137-cc03-4468-aba4-1e1aa8c096fb","job_id":null,"status":"waiting","credits":0,"code":null,"message":null,"percent":50,"operation":"export\/url","payload":{"input":"68d52633-e170-465e-b13e-746c97d01ffb","archive_multiple_files":false},"created_at":"2023-01-07T21:10:02+00:00","started_at":"2023-01-07T21:10:03+00:00","ended_at":null,"retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":["68d52633-e170-465e-b13e-746c97d01ffb"],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb"}}}'),
|
||||
new Response(200, ['Content-Type' => 'application/json'], '{"id":6,"data":{"id":"27f33137-cc03-4468-aba4-1e1aa8c096fb","job_id":null,"status":"finished","credits":0,"code":null,"message":null,"percent":100,"operation":"export\/url","payload":{"input":"68d52633-e170-465e-b13e-746c97d01ffb","archive_multiple_files":false},"result":{"files":[{"filename":"theverge.com.png","size":811819,"url":"https:\/\/storage.cloudconvert.com\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb\/theverge.com.png?AWSAccessKeyId=cloudconvert-production&Expires=1673212203&Signature=xyz&response-content-disposition=attachment%3B%20filename%3D%22theverge.com.png%22&response-content-type=image%2Fpng"}]},"created_at":"2023-01-07T21:10:02+00:00","started_at":"2023-01-07T21:10:03+00:00","ended_at":"2023-01-07T21:10:03+00:00","retry_of_task_id":null,"copy_of_task_id":null,"user_id":61485254,"priority":-10,"host_name":"virgie","storage":"ceph-fra","depends_on_task_ids":["68d52633-e170-465e-b13e-746c97d01ffb"],"links":{"self":"https:\/\/api.cloudconvert.com\/v2\/tasks\/27f33137-cc03-4468-aba4-1e1aa8c096fb"}}}'),
|
||||
]);
|
||||
$retryHandler = HandlerStack::create($retryMock);
|
||||
$retryHandler->push($history);
|
||||
$retryHandler->push(Middleware::retry(
|
||||
function ($retries, $request, $response, $exception) {
|
||||
// Limit the number of retries to 5
|
||||
if ($retries >= 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry connection exceptions
|
||||
if ($exception instanceof ConnectException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retry on server errors
|
||||
if ($response && $response->getStatusCode() >= 500) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$responseBody = '';
|
||||
|
||||
if (is_string($response)) {
|
||||
$responseBody = $response;
|
||||
}
|
||||
|
||||
if ($response instanceof Response) {
|
||||
$responseBody = $response->getBody()->getContents();
|
||||
$response->getBody()->rewind();
|
||||
}
|
||||
|
||||
// Finally for CloudConvert, retry if status is not final
|
||||
return json_decode($responseBody, false, 512, JSON_THROW_ON_ERROR)?->data?->status !== 'finished';
|
||||
},
|
||||
function () {
|
||||
// Retry after 1 second
|
||||
return 1000;
|
||||
}
|
||||
));
|
||||
$retryClient = new Client(['handler' => $retryHandler]);
|
||||
$this->app->instance('RetryGuzzle', $retryClient);
|
||||
|
||||
$bookmark = Bookmark::factory()->create();
|
||||
$job = new SaveScreenshot($bookmark);
|
||||
|
|
@ -154,9 +103,10 @@ class SaveScreenshotJobTest extends TestCase
|
|||
|
||||
$this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot);
|
||||
Storage::disk('public')->assertExists('/assets/img/bookmarks/'.$bookmark->screenshot.'.png');
|
||||
// Also assert we made the correct number of requests
|
||||
$this->assertCount(2, $container);
|
||||
// However with retries there should be more than 4 responses for the 2 requests
|
||||
$this->assertEquals(0, $retryMock->count());
|
||||
|
||||
// capture-website, 2x poll (waiting then finished), export/url, 2x poll (waiting then finished), download
|
||||
Http::assertSentCount(7);
|
||||
// Also assert every queued response in each sequence was consumed, no more no less
|
||||
Http::assertSequencesAreEmpty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue