diff --git a/app/Http/Controllers/Admin/ContactsController.php b/app/Http/Controllers/Admin/ContactsController.php index 17e4a8a7..211f9fa8 100644 --- a/app/Http/Controllers/Admin/ContactsController.php +++ b/app/Http/Controllers/Admin/ContactsController.php @@ -6,11 +6,11 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Contact; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\BadResponseException; use Illuminate\Filesystem\Filesystem; +use Illuminate\Http\Client\RequestException; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Http; use Illuminate\View\View; class ContactsController extends Controller @@ -113,14 +113,13 @@ class ContactsController extends Controller $avatar = null; $contact = Contact::findOrFail($contactId); if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) { - $client = resolve(Client::class); try { - $response = $client->get($contact->homepage); - } catch (BadResponseException $e) { + $response = Http::throw()->get($contact->homepage); + } catch (RequestException $e) { return redirect('/admin/contacts/'.$contactId.'/edit') ->with('error', 'Bad resposne from contact’s homepage'); } - $mf2 = \Mf2\parse((string) $response->getBody(), $contact->homepage); + $mf2 = \Mf2\parse($response->body(), $contact->homepage); foreach ($mf2['items'] as $microformat) { if (Arr::get($microformat, 'type.0') === 'h-card') { $avatarURL = Arr::get($microformat, 'properties.photo.0.value'); @@ -129,8 +128,8 @@ class ContactsController extends Controller } if ($avatarURL !== null) { try { - $avatar = $client->get($avatarURL); - } catch (BadResponseException $e) { + $avatar = Http::throw()->get($avatarURL); + } catch (RequestException $e) { return redirect('/admin/contacts/'.$contactId.'/edit') ->with('error', 'Unable to download avatar'); } @@ -141,7 +140,7 @@ class ContactsController extends Controller if ($filesystem->isDirectory($directory) === false) { $filesystem->makeDirectory($directory); } - $filesystem->put($directory.'/image', $avatar->getBody()); + $filesystem->put($directory.'/image', $avatar->body()); return view('admin.contacts.getavatarsuccess', [ 'homepage' => parse_url($contact->homepage, PHP_URL_HOST), diff --git a/app/Http/Controllers/IndieAuthController.php b/app/Http/Controllers/IndieAuthController.php index 45b488da..eeb59770 100644 --- a/app/Http/Controllers/IndieAuthController.php +++ b/app/Http/Controllers/IndieAuthController.php @@ -5,13 +5,12 @@ declare(strict_types=1); namespace App\Http\Controllers; use App\Services\TokenService; -use Exception; -use GuzzleHttp\Client; use GuzzleHttp\Psr7\Uri; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Validator; use Illuminate\View\View; use Random\RandomException; @@ -199,15 +198,13 @@ class IndieAuthController extends Controller } // Otherwise we need to check the redirect_uri is in the client_id's redirect_uris - $guzzle = resolve(Client::class); - try { - $clientInfo = $guzzle->get($clientId); - } catch (Exception) { + $clientInfo = Http::throw()->get($clientId); + } catch (\Throwable) { return false; } - $clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId); + $clientInfoParsed = \Mf2\parse($clientInfo->body(), $clientId); $redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? []; diff --git a/app/Jobs/DownloadWebMention.php b/app/Jobs/DownloadWebMention.php index 341c35c8..0cb073d5 100644 --- a/app/Jobs/DownloadWebMention.php +++ b/app/Jobs/DownloadWebMention.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace App\Jobs; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\FileSystem\FileSystem; +use Illuminate\Http\Client\RequestException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; class DownloadWebMention implements ShouldQueue { @@ -29,15 +29,15 @@ class DownloadWebMention implements ShouldQueue /** * Execute the job. * - * @throws GuzzleException + * @throws RequestException * @throws FileNotFoundException */ - public function handle(Client $guzzle): void + public function handle(): void { - $response = $guzzle->request('GET', $this->source); - // 4XX and 5XX responses should get Guzzle to throw an exception, - // Laravel should catch and retry these automatically. - if ($response->getStatusCode() === 200) { + // 4XX and 5XX responses should throw so Laravel can catch and + // retry these automatically. + $response = Http::throw()->get($this->source); + if ($response->status() === 200) { $filesystem = new FileSystem; $filename = storage_path('HTML').'/'.$this->createFilenameFromURL($this->source); // backup file first @@ -56,7 +56,7 @@ class DownloadWebMention implements ShouldQueue // save new HTML $filesystem->put( $filename, - (string) $response->getBody() + $response->body() ); // remove backup if the same if ($filesystem->exists($filenameBackup)) { diff --git a/app/Jobs/ProcessLike.php b/app/Jobs/ProcessLike.php index 49302885..3ed065c1 100644 --- a/app/Jobs/ProcessLike.php +++ b/app/Jobs/ProcessLike.php @@ -5,14 +5,13 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\Like; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Http; use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; @@ -32,13 +31,11 @@ class ProcessLike implements ShouldQueue /** * Execute the job. - * - * @throws GuzzleException */ - public function handle(Client $client, Authorship $authorship): int + public function handle(Authorship $authorship): int { - $response = $client->request('GET', $this->like->url); - $mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url); + $response = Http::throw()->get($this->like->url); + $mf2 = \Mf2\parse($response->body(), $this->like->url); if (Arr::has($mf2, 'items.0.properties.content')) { $this->like->content = $mf2['items'][0]['properties']['content'][0]['html']; } diff --git a/app/Jobs/ProcessWebMention.php b/app/Jobs/ProcessWebMention.php index 6677b285..6cb276f8 100644 --- a/app/Jobs/ProcessWebMention.php +++ b/app/Jobs/ProcessWebMention.php @@ -7,13 +7,12 @@ namespace App\Jobs; use App\Exceptions\RemoteContentNotFoundException; use App\Models\Note; use App\Models\WebMention; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Exception\RequestException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Http\Client\ConnectionException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; use Jonnybarnes\WebmentionsParser\Exceptions\InvalidMentionException; use Jonnybarnes\WebmentionsParser\Parser; use Mf2; @@ -36,18 +35,20 @@ class ProcessWebMention implements ShouldQueue * Execute the job. * * @throws RemoteContentNotFoundException - * @throws GuzzleException * @throws InvalidMentionException */ - public function handle(Parser $parser, Client $guzzle): void + public function handle(Parser $parser): void { try { - $response = $guzzle->request('GET', $this->source); - } catch (RequestException $e) { + $response = Http::get($this->source); + } catch (ConnectionException) { throw new RemoteContentNotFoundException; } - $this->saveRemoteContent((string) $response->getBody(), $this->source); - $microformats = Mf2\parse((string) $response->getBody(), $this->source); + if ($response->failed()) { + throw new RemoteContentNotFoundException; + } + $this->saveRemoteContent($response->body(), $this->source); + $microformats = Mf2\parse($response->body(), $this->source); $webmentions = WebMention::where('source', $this->source)->get(); foreach ($webmentions as $webmention) { // check webmention still references target diff --git a/app/Jobs/SaveProfileImage.php b/app/Jobs/SaveProfileImage.php index 0bcbd4e7..aa7d8af7 100644 --- a/app/Jobs/SaveProfileImage.php +++ b/app/Jobs/SaveProfileImage.php @@ -4,13 +4,14 @@ declare(strict_types=1); namespace App\Jobs; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Http\Client\ConnectionException; +use Illuminate\Http\Client\RequestException; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Http; use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; @@ -55,12 +56,10 @@ class SaveProfileImage implements ShouldQueue && parse_url($photo, PHP_URL_HOST) !== 'pbs.twimg.com' && parse_url($photo, PHP_URL_HOST) !== 'twitter.com' ) { - $client = resolve(Client::class); - try { - $response = $client->get($photo); - $image = $response->getBody(); - } catch (RequestException) { + $response = Http::throw()->get($photo); + $image = $response->body(); + } catch (ConnectionException|RequestException) { // we are opening and reading the default image so that $default = public_path().'/assets/profile-images/default-image'; $handle = fopen($default, 'rb'); diff --git a/app/Jobs/SaveScreenshot.php b/app/Jobs/SaveScreenshot.php index 4661ccfe..b72da7b0 100755 --- a/app/Jobs/SaveScreenshot.php +++ b/app/Jobs/SaveScreenshot.php @@ -5,14 +5,15 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\Bookmark; -use GuzzleHttp\Client; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\Response; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; -use JsonException; class SaveScreenshot implements ShouldQueue { @@ -27,77 +28,68 @@ class SaveScreenshot implements ShouldQueue /** * Execute the job. - * - * - * @throws JsonException */ public function handle(): void { - // A normal Guzzle client - $client = resolve(Client::class); - // A Guzzle client with a custom Middleware to retry the CloudConvert API requests - $retryClient = resolve('RetryGuzzle'); + $cloudConvert = Http::baseUrl('https://api.cloudconvert.com/v2') + ->withToken(config('services.cloudconvert.token')) + ->throw(); // First request that CloudConvert takes a screenshot of the URL - $takeScreenshotJobResponse = $client->request('POST', 'https://api.cloudconvert.com/v2/capture-website', [ - 'headers' => [ - 'Authorization' => 'Bearer '.config('services.cloudconvert.token'), - ], - 'json' => [ - 'url' => $this->bookmark->url, - 'output_format' => 'png', - 'screen_width' => 1440, - 'screen_height' => 900, - 'wait_until' => 'networkidle0', - 'wait_time' => 100, - ], + $takeScreenshotJobResponse = $cloudConvert->post('/capture-website', [ + 'url' => $this->bookmark->url, + 'output_format' => 'png', + 'screen_width' => 1440, + 'screen_height' => 900, + 'wait_until' => 'networkidle0', + 'wait_time' => 100, ]); - $taskId = json_decode($takeScreenshotJobResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id; + $taskId = $takeScreenshotJobResponse->json('data.id'); // Now wait till the status job is finished - $screenshotJobStatusResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/'.$taskId, [ - 'headers' => [ - 'Authorization' => 'Bearer '.config('services.cloudconvert.token'), - ], - 'query' => [ - 'include' => 'payload', - ], - ]); + $screenshotJobStatusResponse = $this->pollUntilFinished($cloudConvert, $taskId); - $finishedCaptureId = json_decode($screenshotJobStatusResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id; + $finishedCaptureId = $screenshotJobStatusResponse->json('data.id'); // Now we can create a new job to request thst the screenshot is exported to a temporary URL we can download the screenshot from - $exportImageJob = $client->request('POST', 'https://api.cloudconvert.com/v2/export/url', [ - 'headers' => [ - 'Authorization' => 'Bearer '.config('services.cloudconvert.token'), - ], - 'json' => [ - 'input' => $finishedCaptureId, - 'archive_multiple_files' => false, - ], + $exportImageJob = $cloudConvert->post('/export/url', [ + 'input' => $finishedCaptureId, + 'archive_multiple_files' => false, ]); - $exportImageJobId = json_decode($exportImageJob->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id; + $exportImageJobId = $exportImageJob->json('data.id'); // Again, wait till the status of this export job is finished - $finalImageUrlResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/'.$exportImageJobId, [ - 'headers' => [ - 'Authorization' => 'Bearer '.config('services.cloudconvert.token'), - ], - 'query' => [ - 'include' => 'payload', - ], - ]); + $finalImageUrlResponse = $this->pollUntilFinished($cloudConvert, $exportImageJobId); // Now we can download the screenshot and save it to the storage - $finalImageUrl = json_decode($finalImageUrlResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->result->files[0]->url; + $finalImageUrl = $finalImageUrlResponse->json('data.result.files.0.url'); - $finalImageUrlContent = $client->request('GET', $finalImageUrl); + $finalImageUrlContent = Http::throw()->get($finalImageUrl); - Storage::disk('public')->put('/assets/img/bookmarks/'.$taskId.'.png', $finalImageUrlContent->getBody()->getContents()); + Storage::disk('public')->put('/assets/img/bookmarks/'.$taskId.'.png', $finalImageUrlContent->body()); $this->bookmark->screenshot = $taskId; $this->bookmark->save(); } + + /** + * Poll a CloudConvert task until it reports a "finished" status. + */ + private function pollUntilFinished(PendingRequest $client, string $taskId): Response + { + $attempts = 0; + + do { + $response = $client->get('/tasks/'.$taskId, ['include' => 'payload']); + $finished = $response->json('data.status') === 'finished'; + if (! $finished) { + $attempts++; + usleep(1_000_000); // 1 second, matches CloudConvert's own polling guidance + } + } while (! $finished && $attempts < 5); + + return $response; + } } diff --git a/app/Jobs/SendWebMentions.php b/app/Jobs/SendWebMentions.php index 827aaf0a..d8e962e3 100644 --- a/app/Jobs/SendWebMentions.php +++ b/app/Jobs/SendWebMentions.php @@ -5,8 +5,6 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\Note; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\UriResolver; use GuzzleHttp\Psr7\Utils; @@ -14,6 +12,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Mf2\Parser; @@ -32,8 +31,6 @@ class SendWebMentions implements ShouldQueue /** * Execute the job. - * - * @throws GuzzleException */ public function handle(): void { @@ -43,12 +40,9 @@ class SendWebMentions implements ShouldQueue foreach ($urls as $url) { $endpoint = $this->discoverWebmentionEndpoint($url); if ($endpoint !== null) { - $guzzle = resolve(Client::class); - $guzzle->post($endpoint, [ - 'form_params' => [ - 'source' => $this->note->uri, - 'target' => $url, - ], + Http::asForm()->post($endpoint, [ + 'source' => $this->note->uri, + 'target' => $url, ]); } } @@ -56,8 +50,6 @@ class SendWebMentions implements ShouldQueue /** * Discover if a URL has a webmention endpoint. - * - * @throws GuzzleException */ public function discoverWebmentionEndpoint(string $url): ?string { @@ -71,10 +63,9 @@ class SendWebMentions implements ShouldQueue $endpoint = null; - $guzzle = resolve(Client::class); - $response = $guzzle->get($url); + $response = Http::get($url); // check HTTP Headers for webmention endpoint - $links = Header::parse($response->getHeader('Link')); + $links = Header::parse($response->header('Link')); foreach ($links as $link) { if (array_key_exists('rel', $link) && mb_stristr($link['rel'], 'webmention')) { return $this->resolveUri(trim($link[0], '<>'), $url); @@ -82,7 +73,7 @@ class SendWebMentions implements ShouldQueue } // failed to find a header so parse HTML - $html = (string) $response->getBody(); + $html = $response->body(); if ($html === '') { return null; diff --git a/app/Jobs/SyndicateNoteToBluesky.php b/app/Jobs/SyndicateNoteToBluesky.php index a306801b..582ef760 100644 --- a/app/Jobs/SyndicateNoteToBluesky.php +++ b/app/Jobs/SyndicateNoteToBluesky.php @@ -5,13 +5,12 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\Note; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; class SyndicateNoteToBluesky implements ShouldQueue { @@ -31,29 +30,18 @@ class SyndicateNoteToBluesky implements ShouldQueue /** * Execute the job. - * - * @throws GuzzleException */ - public function handle(Client $guzzle): void + public function handle(): void { - $response = $guzzle->request( - 'POST', - 'https://brid.gy/publish/webmention', - [ - 'headers' => [ - 'Accept' => 'application/json', - ], - 'form_params' => [ - 'source' => $this->note->uri, - 'target' => 'https://brid.gy/publish/bluesky', - ], - 'http_errors' => false, - ] - ); + // no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above + $response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [ + 'source' => $this->note->uri, + 'target' => 'https://brid.gy/publish/bluesky', + ]); - $body = json_decode((string) $response->getBody(), true); + $body = $response->json(); - if ($response->getStatusCode() === 201) { + if ($response->status() === 201) { $this->note->bluesky_url = $body['url']; $this->note->save(); @@ -61,7 +49,7 @@ class SyndicateNoteToBluesky implements ShouldQueue } throw new \RuntimeException( - 'Bridgy publish to Bluesky failed: '.($body['error'] ?? (string) $response->getBody()) + 'Bridgy publish to Bluesky failed: '.($body['error'] ?? $response->body()) ); } } diff --git a/app/Jobs/SyndicateNoteToMastodon.php b/app/Jobs/SyndicateNoteToMastodon.php index 456680e2..3f5cfcd4 100644 --- a/app/Jobs/SyndicateNoteToMastodon.php +++ b/app/Jobs/SyndicateNoteToMastodon.php @@ -5,13 +5,12 @@ declare(strict_types=1); namespace App\Jobs; use App\Models\Note; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; class SyndicateNoteToMastodon implements ShouldQueue { @@ -31,29 +30,18 @@ class SyndicateNoteToMastodon implements ShouldQueue /** * Execute the job. - * - * @throws GuzzleException */ - public function handle(Client $guzzle): void + public function handle(): void { - $response = $guzzle->request( - 'POST', - 'https://brid.gy/publish/webmention', - [ - 'headers' => [ - 'Accept' => 'application/json', - ], - 'form_params' => [ - 'source' => $this->note->uri, - 'target' => 'https://brid.gy/publish/mastodon', - ], - 'http_errors' => false, - ] - ); + // no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above + $response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [ + 'source' => $this->note->uri, + 'target' => 'https://brid.gy/publish/mastodon', + ]); - $body = json_decode((string) $response->getBody(), true); + $body = $response->json(); - if ($response->getStatusCode() === 201) { + if ($response->status() === 201) { $this->note->mastodon_url = $body['url']; $this->note->save(); @@ -61,7 +49,7 @@ class SyndicateNoteToMastodon implements ShouldQueue } throw new \RuntimeException( - 'Bridgy publish to Mastodon failed: '.($body['error'] ?? (string) $response->getBody()) + 'Bridgy publish to Mastodon failed: '.($body['error'] ?? $response->body()) ); } } diff --git a/app/Models/Note.php b/app/Models/Note.php index af7d2c3d..89ce6b63 100644 --- a/app/Models/Note.php +++ b/app/Models/Note.php @@ -7,7 +7,6 @@ namespace App\Models; use App\CommonMark\Generators\MentionGenerator; use App\CommonMark\Renderers\MentionRenderer; use App\Observers\NoteObserver; -use GuzzleHttp\Client; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\ObservedBy; @@ -20,6 +19,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; use Jonnybarnes\IndieWeb\Numbers; use Laravel\Scout\Searchable; use League\CommonMark\Environment\Environment; @@ -363,18 +363,15 @@ class Note extends Model $latLng = $latitude.','.$longitude; return Cache::get($latLng, function () use ($latLng, $latitude, $longitude) { - $guzzle = resolve(Client::class); - $response = $guzzle->request('GET', 'https://nominatim.openstreetmap.org/reverse', [ - 'query' => [ + $response = Http::withHeaders(['User-Agent' => 'jonnybarnes.uk, email jonny@jonnybarnes.uk']) + ->get('https://nominatim.openstreetmap.org/reverse', [ 'format' => 'json', 'lat' => $latitude, 'lon' => $longitude, 'zoom' => 18, 'addressdetails' => 1, - ], - 'headers' => ['User-Agent' => 'jonnybarnes.uk via Guzzle, email jonny@jonnybarnes.uk'], - ]); - $json = json_decode((string) $response->getBody()); + ]); + $json = $response->object(); if (isset($json->address->suburb)) { $locality = $json->address->suburb; if (isset($json->address->city)) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ba42853e..d1e28bcf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,10 +2,6 @@ namespace App\Providers; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ConnectException; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; use Illuminate\Database\Eloquent\Model; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; @@ -82,38 +78,6 @@ class AppServiceProvider extends ServiceProvider ); }); - // Configure Guzzle - $this->app->bind('RetryGuzzle', function () { - $handlerStack = HandlerStack::create(); - $handlerStack->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; - } - - // Finally for CloudConvert, retry if status is not final - return json_decode($response, false, 512, JSON_THROW_ON_ERROR)->data->status !== 'finished'; - }, - function () { - // Retry after 1 second - return 1000; - } - )); - - return new Client(['handler' => $handlerStack]); - }); - // Turn on Eloquent strict mode when developing Model::shouldBeStrict(! $this->app->isProduction()); } diff --git a/app/Services/BookmarkService.php b/app/Services/BookmarkService.php index 25017e16..b873610f 100644 --- a/app/Services/BookmarkService.php +++ b/app/Services/BookmarkService.php @@ -8,10 +8,8 @@ use App\Exceptions\InternetArchiveException; use App\Jobs\ProcessBookmark; use App\Models\Bookmark; use App\Models\Tag; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; class BookmarkService @@ -55,21 +53,19 @@ class BookmarkService * Given a URL, attempt to save it to the Internet Archive. * * @throws InternetArchiveException - * @throws GuzzleException */ public function getArchiveLink(string $url): string { - $client = resolve(Client::class); - try { - $response = $client->request('GET', 'https://web.archive.org/save/'.$url); - } catch (ClientException $e) { + $response = Http::get('https://web.archive.org/save/'.$url); + + if ($response->clientError()) { // throw an exception to be caught throw new InternetArchiveException; } - if ($response->hasHeader('Content-Location')) { - if (Str::startsWith(Arr::get($response->getHeader('Content-Location'), 0), '/web')) { - return $response->getHeader('Content-Location')[0]; - } + + $contentLocation = $response->header('Content-Location'); + if ($contentLocation !== '' && Str::startsWith($contentLocation, '/web')) { + return $contentLocation; } // throw an exception to be caught diff --git a/tests/Feature/Admin/ContactsTest.php b/tests/Feature/Admin/ContactsTest.php index 9fc338eb..44320b9d 100644 --- a/tests/Feature/Admin/ContactsTest.php +++ b/tests/Feature/Admin/ContactsTest.php @@ -6,12 +6,9 @@ namespace Tests\Feature\Admin; use App\Models\Contact; use App\Models\User; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -141,14 +138,12 @@ class ContactsTest extends TestCase HTML; - $file = fopen(__DIR__.'/../../aaron.png', 'rb'); - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'text/html'], $html), - new Response(200, ['Content-Type' => 'image/png'], $file), + $file = file_get_contents(__DIR__.'/../../aaron.png'); + Http::fake([ + '*' => Http::sequence() + ->push($html, 200, ['Content-Type' => 'text/html']) + ->push($file, 200, ['Content-Type' => 'image/png']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $user = User::factory()->make(); $contact = Contact::factory()->create([ 'homepage' => 'https://tantek.com', @@ -165,12 +160,9 @@ class ContactsTest extends TestCase #[Test] public function getting_remote_avatar_fails_gracefully_with_remote_not_found(): void { - $mock = new MockHandler([ - new Response(404), + Http::fake([ + '*' => Http::response('', 404), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $user = User::factory()->make(); $contact = Contact::factory()->create(); @@ -187,13 +179,11 @@ class ContactsTest extends TestCase HTML; - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'text/html'], $html), - new Response(404), + Http::fake([ + '*' => Http::sequence() + ->push($html, 200, ['Content-Type' => 'text/html']) + ->push('', 404), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $user = User::factory()->make(); $contact = Contact::factory()->create(); diff --git a/tests/Feature/IndieAuthTest.php b/tests/Feature/IndieAuthTest.php index 534fe452..b32f4420 100644 --- a/tests/Feature/IndieAuthTest.php +++ b/tests/Feature/IndieAuthTest.php @@ -5,14 +5,11 @@ declare(strict_types=1); namespace Tests\Feature; use App\Models\User; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\UriResolver; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -269,12 +266,9 @@ class IndieAuthTest extends TestCase HTML; - $mockHandler = new MockHandler([ - new Response(200, [], $appPageHtml), + Http::fake([ + '*' => Http::response($appPageHtml, 200), ]); - $handlerStack = HandlerStack::create($mockHandler); - $mockGuzzleClient = new Client(['handler' => $handlerStack]); - $this->app->instance(Client::class, $mockGuzzleClient); $user = User::factory()->make(); $url = url()->query('/auth', [ @@ -313,12 +307,9 @@ class IndieAuthTest extends TestCase HTML; - $mockHandler = new MockHandler([ - new Response(200, [], $appPageHtml), + Http::fake([ + '*' => Http::response($appPageHtml, 200), ]); - $handlerStack = HandlerStack::create($mockHandler); - $mockGuzzleClient = new Client(['handler' => $handlerStack]); - $this->app->instance(Client::class, $mockGuzzleClient); $user = User::factory()->make(); $url = url()->query('/auth', [ diff --git a/tests/Feature/LikesTest.php b/tests/Feature/LikesTest.php index 6101536c..2bda91f3 100644 --- a/tests/Feature/LikesTest.php +++ b/tests/Feature/LikesTest.php @@ -6,11 +6,8 @@ namespace Tests\Feature; use App\Jobs\ProcessLike; use App\Models\Like; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; use Jonnybarnes\WebmentionsParser\Authorship; use PHPUnit\Framework\Attributes\Test; @@ -98,18 +95,12 @@ class LikesTest extends TestCase END; - $mock = new MockHandler([ - new Response(200, [], $content), - new Response(200, [], $content), + Http::fake([ + '*' => Http::response($content, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->bind(Client::class, function () use ($client) { - return $client; - }); $authorship = new Authorship; - $job->handle($client, $authorship); + $job->handle($authorship); $this->assertEquals('Fred Bloggs', Like::find($id)->author_name); } @@ -141,18 +132,12 @@ class LikesTest extends TestCase END; - $mock = new MockHandler([ - new Response(200, [], $content), - new Response(200, [], $content), + Http::fake([ + '*' => Http::response($content, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->bind(Client::class, function () use ($client) { - return $client; - }); $authorship = new Authorship; - $job->handle($client, $authorship); + $job->handle($authorship); $this->assertEquals('Fred Bloggs', Like::find($id)->author_name); } @@ -177,18 +162,12 @@ class LikesTest extends TestCase END; - $mock = new MockHandler([ - new Response(200, [], $content), - new Response(200, [], $content), + Http::fake([ + '*' => Http::response($content, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->bind(Client::class, function () use ($client) { - return $client; - }); $authorship = new Authorship; - $job->handle($client, $authorship); + $job->handle($authorship); $this->assertNull(Like::find($id)->author_name); } diff --git a/tests/TestCase.php b/tests/TestCase.php index cade29d9..c3307f1f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,11 +3,19 @@ namespace Tests; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Illuminate\Support\Facades\Http; abstract class TestCase extends BaseTestCase { use CreatesApplication; + protected function setUp(): void + { + parent::setUp(); + + Http::preventStrayRequests(); + } + public function removeDirIfEmpty(string $dir): void { // scandir() will always return `.` and `..` so even an “empty” diff --git a/tests/Unit/BookmarksTest.php b/tests/Unit/BookmarksTest.php index ce64848c..159e9c0e 100644 --- a/tests/Unit/BookmarksTest.php +++ b/tests/Unit/BookmarksTest.php @@ -6,10 +6,7 @@ namespace Tests\Unit; use App\Exceptions\InternetArchiveException; use App\Services\BookmarkService; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -29,12 +26,9 @@ class BookmarksTest extends TestCase #[Test] public function archive_link_method_calls_archive_service(): void { - $mock = new MockHandler([ - new Response(200, ['Content-Location' => '/web/1234/example.org']), + Http::fake([ + 'web.archive.org/*' => Http::response('', 200, ['Content-Location' => '/web/1234/example.org']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $url = (new BookmarkService)->getArchiveLink('https://example.org'); $this->assertEquals('/web/1234/example.org', $url); } @@ -44,12 +38,9 @@ class BookmarksTest extends TestCase { $this->expectException(InternetArchiveException::class); - $mock = new MockHandler([ - new Response(403), + Http::fake([ + 'web.archive.org/*' => Http::response('', 403), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); (new BookmarkService)->getArchiveLink('https://example.org'); } @@ -58,12 +49,9 @@ class BookmarksTest extends TestCase { $this->expectException(InternetArchiveException::class); - $mock = new MockHandler([ - new Response(200), + Http::fake([ + 'web.archive.org/*' => Http::response('', 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); (new BookmarkService)->getArchiveLink('https://example.org'); } } diff --git a/tests/Unit/Jobs/DownloadWebMentionJobTest.php b/tests/Unit/Jobs/DownloadWebMentionJobTest.php index 6ea6e068..530f1590 100644 --- a/tests/Unit/Jobs/DownloadWebMentionJobTest.php +++ b/tests/Unit/Jobs/DownloadWebMentionJobTest.php @@ -5,11 +5,8 @@ declare(strict_types=1); namespace Tests\Unit\Jobs; use App\Jobs\DownloadWebMention; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use Illuminate\FileSystem\FileSystem; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -35,19 +32,16 @@ class DownloadWebMentionJobTest extends TestCase HTML; $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); - $mock = new MockHandler([ - new Response(200, ['X-Foo' => 'Bar'], $html), - new Response(200, ['X-Foo' => 'Bar'], $html), + Http::fake([ + 'example.org/*' => Http::response($html, 200, ['X-Foo' => 'Bar']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $job = new DownloadWebMention($source); - $job->handle($client); + $job->handle(); $this->assertFileExists(storage_path('HTML/https')); - $job->handle($client); + $job->handle(); $this->assertFileDoesNotExist(storage_path('HTML/https/example.org/reply').'/1.'.date('Y-m-d').'.backup'); } @@ -70,19 +64,18 @@ class DownloadWebMentionJobTest extends TestCase HTML; $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); $html2 = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html2); - $mock = new MockHandler([ - new Response(200, ['X-Foo' => 'Bar'], $html), - new Response(200, ['X-Foo' => 'Bar'], $html2), + Http::fake([ + 'example.org/*' => Http::sequence() + ->push($html, 200, ['X-Foo' => 'Bar']) + ->push($html2, 200, ['X-Foo' => 'Bar']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $job = new DownloadWebMention($source); - $job->handle($client); + $job->handle(); $this->assertFileExists(storage_path('HTML/https')); - $job->handle($client); + $job->handle(); $this->assertFileExists(storage_path('HTML/https/example.org/reply').'/1.'.date('Y-m-d').'.backup'); } @@ -98,14 +91,12 @@ class DownloadWebMentionJobTest extends TestCase HTML; $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); - $mock = new MockHandler([ - new Response(200, ['X-Foo' => 'Bar'], $html), + Http::fake([ + 'example.org/*' => Http::response($html, 200, ['X-Foo' => 'Bar']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $job = new DownloadWebMention($source); - $job->handle($client); + $job->handle(); $this->assertFileExists(storage_path('HTML/https/example.org/reply-one/index.html')); } diff --git a/tests/Unit/Jobs/ProcessWebMentionJobTest.php b/tests/Unit/Jobs/ProcessWebMentionJobTest.php index 5eb98d9e..001d8f3d 100644 --- a/tests/Unit/Jobs/ProcessWebMentionJobTest.php +++ b/tests/Unit/Jobs/ProcessWebMentionJobTest.php @@ -9,12 +9,9 @@ use App\Jobs\ProcessWebMention; use App\Jobs\SaveProfileImage; use App\Models\Note; use App\Models\WebMention; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use Illuminate\FileSystem\FileSystem; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Queue; use Jonnybarnes\WebmentionsParser\Parser; use PHPUnit\Framework\Attributes\Test; @@ -39,17 +36,15 @@ class ProcessWebMentionJobTest extends TestCase $this->expectException(RemoteContentNotFoundException::class); $parser = new Parser; - $mock = new MockHandler([ - new Response(404), + Http::fake([ + '*' => Http::response('', 404), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $source = 'https://example.org/mention/1/'; $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); } #[Test] @@ -65,17 +60,15 @@ class ProcessWebMentionJobTest extends TestCase HTML; $html = str_replace('href="', 'href="'.config('app.url'), $html); - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $source = 'https://example.org/mention/1/'; $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); Queue::assertPushed(SaveProfileImage::class); $this->assertDatabaseHas('webmentions', [ @@ -103,14 +96,12 @@ class ProcessWebMentionJobTest extends TestCase
Updated reply
HTML; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); Queue::assertPushed(SaveProfileImage::class); $this->assertDatabaseHas('webmentions', [ @@ -132,11 +123,9 @@ class ProcessWebMentionJobTest extends TestCase
Replying to someone else
HTML; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $source = 'https://example.org/reply/1'; @@ -151,7 +140,7 @@ class ProcessWebMentionJobTest extends TestCase ]); $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); $this->assertDatabaseMissing('webmentions', [ 'source' => $source, @@ -169,11 +158,9 @@ class ProcessWebMentionJobTest extends TestCase
I like someone else now
HTML; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $source = 'https://example.org/reply/1'; @@ -188,7 +175,7 @@ class ProcessWebMentionJobTest extends TestCase ]); $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); $this->assertDatabaseMissing('webmentions', [ 'source' => $source, @@ -208,18 +195,16 @@ class ProcessWebMentionJobTest extends TestCase HTML; $html = str_replace('href="', 'href="'.config('app.url'), $html); - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); // Simulate a long brid.gy Bluesky source URL (well over 255 characters) $source = 'https://brid.gy/comment/bluesky/did:plc:n3jhgiq2ykctnpgzlm6p6b25/at%253A%252F%252Fdid%253Aplc%253An3jhgiq2ykctnpgzlm6p6b25%252Fapp.bsky.feed.post%252F3mfekjuhykb2k/at%253A%252F%252Fdid%253Aplc%253Afsfuwigo5juzdwp23hyianzg%252Fapp.bsky.feed.post%252F3mfemw4rrvs2t'; $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); $this->assertGreaterThan(255, strlen($source)); $this->assertDatabaseHas('webmentions', [ @@ -238,11 +223,9 @@ class ProcessWebMentionJobTest extends TestCase
Reposting someone else
HTML; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $source = 'https://example.org/reply/1'; @@ -257,7 +240,7 @@ class ProcessWebMentionJobTest extends TestCase ]); $job = new ProcessWebMention($note, $source); - $job->handle($parser, $client); + $job->handle($parser); $this->assertDatabaseMissing('webmentions', [ 'source' => $source, diff --git a/tests/Unit/Jobs/SaveProfileImageJobTest.php b/tests/Unit/Jobs/SaveProfileImageJobTest.php index f9b92bb3..8e0edbb4 100644 --- a/tests/Unit/Jobs/SaveProfileImageJobTest.php +++ b/tests/Unit/Jobs/SaveProfileImageJobTest.php @@ -5,10 +5,7 @@ declare(strict_types=1); namespace Tests\Unit\Jobs; use App\Jobs\SaveProfileImage; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Facades\Http; use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; use PHPUnit\Framework\Attributes\Test; @@ -58,12 +55,9 @@ class SaveProfileImageJobTest extends TestCase #[Test] public function remote_author_images_are_saved_locally(): void { - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), + Http::fake([ + '*' => Http::response('fake jpeg image', 200, ['Content-Type' => 'image/jpeg']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $mf = ['items' => []]; $author = [ 'properties' => [ @@ -83,12 +77,9 @@ class SaveProfileImageJobTest extends TestCase #[Test] public function local_default_author_image_is_used_as_fallback(): void { - $mock = new MockHandler([ - new Response(404), + Http::fake([ + '*' => Http::response('', 404), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $mf = ['items' => []]; $author = [ 'properties' => [ @@ -111,12 +102,9 @@ class SaveProfileImageJobTest extends TestCase #[Test] public function we_get_url_from_photo_object_if_alt_text_is_provided(): void { - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), + Http::fake([ + '*' => Http::response('fake jpeg image', 200, ['Content-Type' => 'image/jpeg']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $mf = ['items' => []]; $author = [ 'properties' => [ @@ -139,12 +127,9 @@ class SaveProfileImageJobTest extends TestCase #[Test] public function use_first_url_if_multiple_homepages_are_provided(): void { - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), + Http::fake([ + '*' => Http::response('fake jpeg image', 200, ['Content-Type' => 'image/jpeg']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $mf = ['items' => []]; $author = [ 'properties' => [ diff --git a/tests/Unit/Jobs/SaveScreenshotJobTest.php b/tests/Unit/Jobs/SaveScreenshotJobTest.php index a407e3d3..f7f68f89 100644 --- a/tests/Unit/Jobs/SaveScreenshotJobTest.php +++ b/tests/Unit/Jobs/SaveScreenshotJobTest.php @@ -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(); } } diff --git a/tests/Unit/Jobs/SendWebMentionJobTest.php b/tests/Unit/Jobs/SendWebMentionJobTest.php index 29973328..1c26460a 100644 --- a/tests/Unit/Jobs/SendWebMentionJobTest.php +++ b/tests/Unit/Jobs/SendWebMentionJobTest.php @@ -6,10 +6,7 @@ namespace Tests\Unit\Jobs; use App\Jobs\SendWebMentions; use App\Models\Note; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -28,12 +25,9 @@ class SendWebMentionJobTest extends TestCase public function discover_webmention_endpoint_from_header_links(): void { $url = 'https://example.org/webmention'; - $mock = new MockHandler([ - new Response(200, ['Link' => '<'.$url.'>; rel="webmention"']), + Http::fake([ + '*' => Http::response('', 200, ['Link' => '<'.$url.'>; rel="webmention"']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $job = new SendWebMentions(new Note); $this->assertEquals($url, $job->discoverWebmentionEndpoint('https://example.org')); @@ -43,12 +37,9 @@ class SendWebMentionJobTest extends TestCase public function discover_webmention_endpoint_from_html_link_tags(): void { $html = ''; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $job = new SendWebMentions(new Note); $this->assertEquals( @@ -61,12 +52,9 @@ class SendWebMentionJobTest extends TestCase public function discover_webmention_endpoint_from_legacy_html_markup(): void { $html = ''; - $mock = new MockHandler([ - new Response(200, [], $html), + Http::fake([ + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $job = new SendWebMentions(new Note); $this->assertEquals( @@ -95,13 +83,10 @@ class SendWebMentionJobTest extends TestCase public function we_send_a_webmention_for_a_note(): void { $html = ''; - $mock = new MockHandler([ - new Response(200, [], $html), - new Response(202), + Http::fake([ + 'example.org/webmention' => Http::response('', 202), + '*' => Http::response($html, 200), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $note = new Note; $note->note = 'Hi [Aaron](https://aaronparecki.com)'; @@ -114,13 +99,10 @@ class SendWebMentionJobTest extends TestCase #[Test] public function links_in_notes_can_not_support_webmentions(): void { - $mock = new MockHandler([ + Http::fake([ // URLs with commas currently break the parse function I’m using - new Response(200, ['Link' => '; rel="preconnect"']), + '*' => Http::response('', 200, ['Link' => '; rel="preconnect"']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - $this->app->instance(Client::class, $client); $job = new SendWebMentions(new Note); $this->assertNull($job->discoverWebmentionEndpoint('https://example.org')); diff --git a/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php index 23ab2dcd..29935011 100644 --- a/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php +++ b/tests/Unit/Jobs/SyndicateNoteToBlueskyJobTest.php @@ -5,12 +5,9 @@ namespace Tests\Unit\Jobs; use App\Jobs\SyndicateNoteToBluesky; use App\Models\Note; use Faker\Factory; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; -use GuzzleHttp\Psr7\Response; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\Client\Request; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -24,19 +21,17 @@ class SyndicateNoteToBlueskyJobTest extends TestCase $faker = Factory::create(); $randomNumber = $faker->randomNumber(); $blueskyUrl = 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber; - $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'url' => $blueskyUrl, 'id' => (string) $randomNumber, 'type' => ['h-entry'], - ])), + ], 201), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $job = new SyndicateNoteToBluesky($note); - $job->handle($client); + $job->handle(); $this->assertDatabaseHas('notes', [ 'bluesky_url' => $blueskyUrl, @@ -46,39 +41,31 @@ class SyndicateNoteToBlueskyJobTest extends TestCase #[Test] public function we_post_the_correct_source_and_target(): void { - $container = []; - $history = Middleware::history($container); - $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'url' => 'https://bsky.app/profile/jonnybarnes.uk/1', - ])), + ], 201), ]); - $handler = HandlerStack::create($mock); - $handler->push($history); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(['note' => 'This is a **test**']); $job = new SyndicateNoteToBluesky($note); - $job->handle($client); + $job->handle(); - $request = $container[0]['request']; - $body = []; - parse_str((string) $request->getBody(), $body); - - $this->assertSame('https://brid.gy/publish/webmention', (string) $request->getUri()); - $this->assertSame($note->uri, $body['source']); - $this->assertSame('https://brid.gy/publish/bluesky', $body['target']); + Http::assertSent(function (Request $request) use ($note) { + return $request->url() === 'https://brid.gy/publish/webmention' + && $request['source'] === $note->uri + && $request['target'] === 'https://brid.gy/publish/bluesky'; + }); } #[Test] public function a_bridgy_failure_throws_and_does_not_set_bluesky_url(): void { - $mock = new MockHandler([ - new Response(400, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'error' => 'Could not find target link', - ])), + ], 400), ]); - $client = new Client(['handler' => HandlerStack::create($mock)]); $note = Note::factory()->create(); $job = new SyndicateNoteToBluesky($note); @@ -86,7 +73,7 @@ class SyndicateNoteToBlueskyJobTest extends TestCase $this->expectException(\RuntimeException::class); try { - $job->handle($client); + $job->handle(); } finally { $this->assertDatabaseHas('notes', [ 'id' => $note->id, diff --git a/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php b/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php index bdc780e6..be0a04c2 100644 --- a/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php +++ b/tests/Unit/Jobs/SyndicateNoteToMastodonJobTest.php @@ -5,12 +5,9 @@ namespace Tests\Unit\Jobs; use App\Jobs\SyndicateNoteToMastodon; use App\Models\Note; use Faker\Factory; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Middleware; -use GuzzleHttp\Psr7\Response; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\Client\Request; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -24,19 +21,17 @@ class SyndicateNoteToMastodonJobTest extends TestCase $faker = Factory::create(); $randomNumber = $faker->randomNumber(); $mastodonUrl = 'https://mastodon.example/@jonny/'.$randomNumber; - $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'url' => $mastodonUrl, 'id' => (string) $randomNumber, 'type' => ['h-entry'], - ])), + ], 201), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(); $job = new SyndicateNoteToMastodon($note); - $job->handle($client); + $job->handle(); $this->assertDatabaseHas('notes', [ 'mastodon_url' => $mastodonUrl, @@ -46,39 +41,31 @@ class SyndicateNoteToMastodonJobTest extends TestCase #[Test] public function we_post_the_correct_source_and_target(): void { - $container = []; - $history = Middleware::history($container); - $mock = new MockHandler([ - new Response(201, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'url' => 'https://mastodon.example/@jonny/1', - ])), + ], 201), ]); - $handler = HandlerStack::create($mock); - $handler->push($history); - $client = new Client(['handler' => $handler]); $note = Note::factory()->create(['note' => 'This is a **test**']); $job = new SyndicateNoteToMastodon($note); - $job->handle($client); + $job->handle(); - $request = $container[0]['request']; - $body = []; - parse_str((string) $request->getBody(), $body); - - $this->assertSame('https://brid.gy/publish/webmention', (string) $request->getUri()); - $this->assertSame($note->uri, $body['source']); - $this->assertSame('https://brid.gy/publish/mastodon', $body['target']); + Http::assertSent(function (Request $request) use ($note) { + return $request->url() === 'https://brid.gy/publish/webmention' + && $request['source'] === $note->uri + && $request['target'] === 'https://brid.gy/publish/mastodon'; + }); } #[Test] public function a_bridgy_failure_throws_and_does_not_set_mastodon_url(): void { - $mock = new MockHandler([ - new Response(400, ['Content-Type' => 'application/json'], json_encode([ + Http::fake([ + 'brid.gy/*' => Http::response([ 'error' => 'Could not find target link', - ])), + ], 400), ]); - $client = new Client(['handler' => HandlerStack::create($mock)]); $note = Note::factory()->create(); $job = new SyndicateNoteToMastodon($note); @@ -86,7 +73,7 @@ class SyndicateNoteToMastodonJobTest extends TestCase $this->expectException(\RuntimeException::class); try { - $job->handle($client); + $job->handle(); } finally { $this->assertDatabaseHas('notes', [ 'id' => $note->id, diff --git a/tests/Unit/NotesTest.php b/tests/Unit/NotesTest.php index 0778ce2a..afd669ab 100644 --- a/tests/Unit/NotesTest.php +++ b/tests/Unit/NotesTest.php @@ -9,13 +9,10 @@ use App\Models\Media; use App\Models\Note; use App\Models\Place; use App\Models\Tag; -use GuzzleHttp\Client; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\HandlerStack; -use GuzzleHttp\Psr7\Response; use Illuminate\Filesystem\Filesystem; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; use Tests\TestCase; @@ -182,13 +179,9 @@ class NotesTest extends TestCase {"place_id":"198791063","licence":"Data © OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright","osm_type":"relation","osm_id":"5208404","lat":"51.50084125","lon":"-0.142990166340849","display_name":"Buckingham Palace, Ambassador's Court, St. James's, Victoria, Westminster, London, Greater London, England, SW1E 6LA, United Kingdom","address":{"attraction":"Buckingham Palace","road":"Ambassador's Court","neighbourhood":"St. James's","suburb":"Victoria","city":"London","state_district":"Greater London","state":"England","postcode":"SW1E 6LA","country":"UK","country_code":"gb"},"boundingbox":["51.4997342","51.5019473","-0.143984","-0.1413002"]} JSON; // phpcs:enable Generic.Files.LineLength.TooLong - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $json), + Http::fake([ + 'nominatim.openstreetmap.org/*' => Http::response($json, 200, ['Content-Type' => 'application/json']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $this->app->instance(Client::class, $client); $note = new Note; $address = $note->reverseGeoCode(51.50084, -0.14264); @@ -207,13 +200,9 @@ class NotesTest extends TestCase {"place_id":"96518506","licence":"Data © OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright","osm_type":"way","osm_id":"94107885","lat":"51.0225764535969","lon":"0.906664040464189","display_name":"Melon Lane, Newchurch, Shepway, Kent, South East, England, TN29 0AS, United Kingdom","address":{"road":"Melon Lane","suburb":"Newchurch","city":"Shepway","county":"Kent","state_district":"South East","state":"England","postcode":"TN29 0AS","country":"UK","country_code":"gb"},"boundingbox":["51.0140377","51.0371494","0.8873312","0.9109506"]} JSON; // phpcs:enable Generic.Files.LineLength.TooLong - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $json), + Http::fake([ + 'nominatim.openstreetmap.org/*' => Http::response($json, 200, ['Content-Type' => 'application/json']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $this->app->instance(Client::class, $client); $note = new Note; $address = $note->reverseGeoCode(51.02, 0.91); @@ -234,13 +223,9 @@ class NotesTest extends TestCase {"place_id":"198561071","licence":"Data © OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright","osm_type":"relation","osm_id":"1839026","lat":"53.46600455","lon":"-2.23300880782987","display_name":"University of Manchester - Main Campus, Brunswick Street, Curry Mile, Ardwick, Manchester, Greater Manchester, North West England, England, M13 9NR, United Kingdom","address":{"university":"University of Manchester - Main Campus","city":"Manchester","county":"Greater Manchester","state_district":"North West England","state":"England","postcode":"M13 9NR","country":"UK","country_code":"gb"},"boundingbox":["53.4598667","53.4716848","-2.2390346","-2.2262754"]} JSON; // phpcs:enable Generic.Files.LineLength.TooLong - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $json), + Http::fake([ + 'nominatim.openstreetmap.org/*' => Http::response($json, 200, ['Content-Type' => 'application/json']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $this->app->instance(Client::class, $client); $note = new Note; $address = $note->reverseGeoCode(53.466277988406, -2.2304474827445); @@ -261,13 +246,9 @@ class NotesTest extends TestCase {"place_id":"98085404","licence":"Data © OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright","osm_type":"way","osm_id":"103703318","lat":"51.0997470194065","lon":"0.609897771085209","display_name":"Biddenden, Ashford, Kent, South East, England, TN27 8ET, United Kingdom","address":{"county":"Kent","state_district":"South East","state":"England","postcode":"TN27 8ET","country":"UK","country_code":"gb"},"boundingbox":["51.0986632","51.104459","0.5954434","0.6167775"]} JSON; // phpcs:enable Generic.Files.LineLength.TooLong - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $json), + Http::fake([ + 'nominatim.openstreetmap.org/*' => Http::response($json, 200, ['Content-Type' => 'application/json']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $this->app->instance(Client::class, $client); $note = new Note; $address = $note->reverseGeoCode(51.1, 0.61); @@ -285,13 +266,9 @@ class NotesTest extends TestCase {"place_id":"120553244","licence":"Data © OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright","osm_type":"way","osm_id":"191508282","lat":"54.3004150140189","lon":"-9.39993720828084","display_name":"R314, Doonfeeny Lower, Ballycastle ED, Ballina, County Mayo, Connacht, Ireland","address":{"country":"Ireland","country_code":"ie"},"boundingbox":["54.2964027","54.3045856","-9.4337961","-9.3960403"]} JSON; // phpcs:enable Generic.Files.LineLength.TooLong - $mock = new MockHandler([ - new Response(200, ['Content-Type' => 'application/json'], $json), + Http::fake([ + 'nominatim.openstreetmap.org/*' => Http::response($json, 200, ['Content-Type' => 'application/json']), ]); - $handler = HandlerStack::create($mock); - $client = new Client(['handler' => $handler]); - - $this->app->instance(Client::class, $client); $note = new Note; $address = $note->reverseGeoCode(54.3, 9.4);