Compare commits

..
Author SHA1 Message Date
f9f2744fad
Send Brrr push notifications for new webmentions
Dispatches a queued job to POST to a configured Brrr webhook whenever
a brand-new webmention is saved, so replies/likes/reposts show up as
push notifications instead of requiring a manual check of the site.
2026-08-02 18:07:39 +01:00
31c49ac3fc
Adopt Laravel's Image facade for media processing, upgrade Intervention to v4
Laravel 13's Image facade wraps Intervention Image v4 internally, so
switching our upload width probe and resize job/command to it required
bumping intervention/image ^3 -> ^4 (and its intervention/gif ^5
dependency). Removes our own ImageManager container binding and
config/image.php in favour of Laravel's built-in driver resolution.

Also fixes a latent filename mismatch in ProcessMediaJobTest that Pint's
stricter typing on the new Image API turned into a hard TypeError, and
tidies config/flare.php to use imported class names instead of FQCNs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Sorsgn85nw7uQyRMNvzyD
2026-08-01 17:07:38 +01:00
287520ad7b
Force HTTPS URL generation in production
Uses Laravel's built-in URL::forceHttps() so route(), url(), and asset()
always generate https:// links in production, without affecting local dev.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RP5TeZbk9KS754xuJMobjE
2026-07-28 19:20:18 +01:00
2eab5749ea
Update dependencies 2026-07-26 10:57:20 +01:00
e08763c526
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
2026-07-26 09:12:01 +01:00
40 changed files with 1469 additions and 1431 deletions

View file

@ -78,6 +78,8 @@ SESSION_SAME_SITE=strict
LOG_SLACK_WEBHOOK_URL= LOG_SLACK_WEBHOOK_URL=
BRRR_WEBHOOK_URL=
FLARE_KEY= FLARE_KEY=
IGNITION_OPEN_AI_KEY= IGNITION_OPEN_AI_KEY=

View file

@ -6,9 +6,9 @@ namespace App\Console\Commands;
use App\Models\Media; use App\Models\Media;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Image\ImageException;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ReprocessMediaImages extends Command class ReprocessMediaImages extends Command
{ {
@ -16,7 +16,7 @@ class ReprocessMediaImages extends Command
protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling'; protected $description = 'Regenerate medium and small image variants using correct aspect-ratio scaling';
public function handle(ImageManager $manager): void public function handle(): void
{ {
$media = Media::where('type', 'image') $media = Media::where('type', 'image')
->whereNotNull('image_widths') ->whereNotNull('image_widths')
@ -44,10 +44,10 @@ class ReprocessMediaImages extends Command
$this->info("Processing: {$path}"); $this->info("Processing: {$path}");
$image = Image::fromStorage($path, 'public');
try { try {
$file = Storage::disk('public')->get($path); $image->width();
$image = $manager->read($file); } catch (ImageException) {
} catch (DecoderException) {
$this->warn(' Could not decode image, skipping.'); $this->warn(' Could not decode image, skipping.');
continue; continue;
@ -57,11 +57,8 @@ class ReprocessMediaImages extends Command
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
$this->info(' Done.'); $this->info(' Done.');
} }

View file

@ -6,11 +6,11 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Contact; use App\Models\Contact;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\View\View; use Illuminate\View\View;
class ContactsController extends Controller class ContactsController extends Controller
@ -113,14 +113,13 @@ class ContactsController extends Controller
$avatar = null; $avatar = null;
$contact = Contact::findOrFail($contactId); $contact = Contact::findOrFail($contactId);
if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) { if ($contact->homepage !== null && mb_strlen($contact->homepage) !== 0) {
$client = resolve(Client::class);
try { try {
$response = $client->get($contact->homepage); $response = Http::throw()->get($contact->homepage);
} catch (BadResponseException $e) { } catch (RequestException $e) {
return redirect('/admin/contacts/'.$contactId.'/edit') return redirect('/admin/contacts/'.$contactId.'/edit')
->with('error', 'Bad resposne from contacts homepage'); ->with('error', 'Bad resposne from contacts homepage');
} }
$mf2 = \Mf2\parse((string) $response->getBody(), $contact->homepage); $mf2 = \Mf2\parse($response->body(), $contact->homepage);
foreach ($mf2['items'] as $microformat) { foreach ($mf2['items'] as $microformat) {
if (Arr::get($microformat, 'type.0') === 'h-card') { if (Arr::get($microformat, 'type.0') === 'h-card') {
$avatarURL = Arr::get($microformat, 'properties.photo.0.value'); $avatarURL = Arr::get($microformat, 'properties.photo.0.value');
@ -129,8 +128,8 @@ class ContactsController extends Controller
} }
if ($avatarURL !== null) { if ($avatarURL !== null) {
try { try {
$avatar = $client->get($avatarURL); $avatar = Http::throw()->get($avatarURL);
} catch (BadResponseException $e) { } catch (RequestException $e) {
return redirect('/admin/contacts/'.$contactId.'/edit') return redirect('/admin/contacts/'.$contactId.'/edit')
->with('error', 'Unable to download avatar'); ->with('error', 'Unable to download avatar');
} }
@ -141,7 +140,7 @@ class ContactsController extends Controller
if ($filesystem->isDirectory($directory) === false) { if ($filesystem->isDirectory($directory) === false) {
$filesystem->makeDirectory($directory); $filesystem->makeDirectory($directory);
} }
$filesystem->put($directory.'/image', $avatar->getBody()); $filesystem->put($directory.'/image', $avatar->body());
return view('admin.contacts.getavatarsuccess', [ return view('admin.contacts.getavatarsuccess', [
'homepage' => parse_url($contact->homepage, PHP_URL_HOST), 'homepage' => parse_url($contact->homepage, PHP_URL_HOST),

View file

@ -5,13 +5,12 @@ declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\TokenService; use App\Services\TokenService;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\View\View; use Illuminate\View\View;
use Random\RandomException; 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 // Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
$guzzle = resolve(Client::class);
try { try {
$clientInfo = $guzzle->get($clientId); $clientInfo = Http::throw()->get($clientId);
} catch (Exception) { } catch (\Throwable) {
return false; return false;
} }
$clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId); $clientInfoParsed = \Mf2\parse($clientInfo->body(), $clientId);
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? []; $redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];

View file

@ -13,9 +13,10 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Image\ImageException;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
class MicropubMediaController extends Controller class MicropubMediaController extends Controller
@ -111,12 +112,9 @@ class MicropubMediaController extends Controller
$filename = Storage::disk('local')->putFile('media', $file); $filename = Storage::disk('local')->putFile('media', $file);
/** @var ImageManager $manager */
$manager = resolve(ImageManager::class);
try { try {
$image = $manager->read($request->file('file')); $width = Image::fromUpload($request->file('file'))->width();
$width = $image->width(); } catch (ImageException) {
} catch (Exception) {
// not an image // not an image
$width = null; $width = null;
} }

View file

@ -4,14 +4,14 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\FileSystem\FileSystem; use Illuminate\FileSystem\FileSystem;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class DownloadWebMention implements ShouldQueue class DownloadWebMention implements ShouldQueue
{ {
@ -29,15 +29,15 @@ class DownloadWebMention implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
* *
* @throws GuzzleException * @throws RequestException
* @throws FileNotFoundException * @throws FileNotFoundException
*/ */
public function handle(Client $guzzle): void public function handle(): void
{ {
$response = $guzzle->request('GET', $this->source); // 4XX and 5XX responses should throw so Laravel can catch and
// 4XX and 5XX responses should get Guzzle to throw an exception, // retry these automatically.
// Laravel should catch and retry these automatically. $response = Http::throw()->get($this->source);
if ($response->getStatusCode() === 200) { if ($response->status() === 200) {
$filesystem = new FileSystem; $filesystem = new FileSystem;
$filename = storage_path('HTML').'/'.$this->createFilenameFromURL($this->source); $filename = storage_path('HTML').'/'.$this->createFilenameFromURL($this->source);
// backup file first // backup file first
@ -56,7 +56,7 @@ class DownloadWebMention implements ShouldQueue
// save new HTML // save new HTML
$filesystem->put( $filesystem->put(
$filename, $filename,
(string) $response->getBody() $response->body()
); );
// remove backup if the same // remove backup if the same
if ($filesystem->exists($filenameBackup)) { if ($filesystem->exists($filenameBackup)) {

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\WebMention;
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 NotifyBrrrOfWebMention implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
protected WebMention $webMention
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$webhookUrl = config('services.brrr.webhook_url');
if (blank($webhookUrl)) {
return;
}
Http::post($webhookUrl, [
'title' => $this->title(),
'message' => "From {$this->webMention->source}",
'open_url' => $this->webMention->target,
]);
}
/**
* Build a notification title based on the webmention type.
*/
private function title(): string
{
return match ($this->webMention->type) {
'in-reply-to' => 'New reply',
'like-of' => 'New like',
'repost-of' => 'New repost',
default => 'New webmention',
};
}
}

View file

@ -5,14 +5,13 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use App\Models\Like; use App\Models\Like;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Authorship;
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
@ -32,13 +31,11 @@ class ProcessLike implements ShouldQueue
/** /**
* Execute the job. * 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); $response = Http::throw()->get($this->like->url);
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url); $mf2 = \Mf2\parse($response->body(), $this->like->url);
if (Arr::has($mf2, 'items.0.properties.content')) { if (Arr::has($mf2, 'items.0.properties.content')) {
$this->like->content = $mf2['items'][0]['properties']['content'][0]['html']; $this->like->content = $mf2['items'][0]['properties']['content'][0]['html'];
} }

View file

@ -7,11 +7,11 @@ namespace App\Jobs;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Image\ImageException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Image;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\Exceptions\DecoderException;
use Intervention\Image\ImageManager;
class ProcessMedia implements ShouldQueue class ProcessMedia implements ShouldQueue
{ {
@ -30,15 +30,16 @@ class ProcessMedia implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*/ */
public function handle(ImageManager $manager): void public function handle(): void
{ {
// Load file // Load file
$file = Storage::disk('local')->get($this->filename); $file = Storage::disk('local')->get($this->filename);
// Open file // Open file
$image = Image::fromStorage($this->filename, 'local');
try { try {
$image = $manager->read($file); $width = $image->width();
} catch (DecoderException) { } catch (ImageException) {
// not an image; delete file and end job // not an image; delete file and end job
Storage::disk('local')->delete($this->filename); Storage::disk('local')->delete($this->filename);
@ -49,18 +50,15 @@ class ProcessMedia implements ShouldQueue
Storage::disk('public')->put($this->filename, $file); Storage::disk('public')->put($this->filename, $file);
// Create smaller versions if necessary // Create smaller versions if necessary
if ($image->width() > 1000) { if ($width > 1000) {
$filenameParts = explode('.', $this->filename); $filenameParts = explode('.', $this->filename);
$extension = array_pop($filenameParts); $extension = array_pop($filenameParts);
// the following achieves this data flow // the following achieves this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar // foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = trim(implode('.', $filenameParts), '.'); $basename = trim(implode('.', $filenameParts), '.');
$medium = $image->scale(width: 1000); Storage::disk('public')->put($basename.'-medium.'.$extension, $image->scale(width: 1000)->toBytes());
Storage::disk('public')->put($basename.'-medium.'.$extension, (string) $medium->encode()); Storage::disk('public')->put($basename.'-small.'.$extension, $image->scale(width: 500)->toBytes());
$small = $image->scale(width: 500);
Storage::disk('public')->put($basename.'-small.'.$extension, (string) $small->encode());
} }
// Now we can delete the locally saved image // Now we can delete the locally saved image

View file

@ -7,13 +7,12 @@ namespace App\Jobs;
use App\Exceptions\RemoteContentNotFoundException; use App\Exceptions\RemoteContentNotFoundException;
use App\Models\Note; use App\Models\Note;
use App\Models\WebMention; use App\Models\WebMention;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Jonnybarnes\WebmentionsParser\Exceptions\InvalidMentionException; use Jonnybarnes\WebmentionsParser\Exceptions\InvalidMentionException;
use Jonnybarnes\WebmentionsParser\Parser; use Jonnybarnes\WebmentionsParser\Parser;
use Mf2; use Mf2;
@ -36,18 +35,20 @@ class ProcessWebMention implements ShouldQueue
* Execute the job. * Execute the job.
* *
* @throws RemoteContentNotFoundException * @throws RemoteContentNotFoundException
* @throws GuzzleException
* @throws InvalidMentionException * @throws InvalidMentionException
*/ */
public function handle(Parser $parser, Client $guzzle): void public function handle(Parser $parser): void
{ {
try { try {
$response = $guzzle->request('GET', $this->source); $response = Http::get($this->source);
} catch (RequestException $e) { } catch (ConnectionException) {
throw new RemoteContentNotFoundException; throw new RemoteContentNotFoundException;
} }
$this->saveRemoteContent((string) $response->getBody(), $this->source); if ($response->failed()) {
$microformats = Mf2\parse((string) $response->getBody(), $this->source); throw new RemoteContentNotFoundException;
}
$this->saveRemoteContent($response->body(), $this->source);
$microformats = Mf2\parse($response->body(), $this->source);
$webmentions = WebMention::where('source', $this->source)->get(); $webmentions = WebMention::where('source', $this->source)->get();
foreach ($webmentions as $webmention) { foreach ($webmentions as $webmention) {
// check webmention still references target // check webmention still references target
@ -95,6 +96,7 @@ class ProcessWebMention implements ShouldQueue
$webmention->type = $type; $webmention->type = $type;
$webmention->mf2 = json_encode($microformats); $webmention->mf2 = json_encode($microformats);
$webmention->save(); $webmention->save();
dispatch(new NotifyBrrrOfWebMention($webmention));
} }
/** /**

View file

@ -4,13 +4,14 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Authorship;
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; 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) !== 'pbs.twimg.com'
&& parse_url($photo, PHP_URL_HOST) !== 'twitter.com' && parse_url($photo, PHP_URL_HOST) !== 'twitter.com'
) { ) {
$client = resolve(Client::class);
try { try {
$response = $client->get($photo); $response = Http::throw()->get($photo);
$image = $response->getBody(); $image = $response->body();
} catch (RequestException) { } catch (ConnectionException|RequestException) {
// we are opening and reading the default image so that // we are opening and reading the default image so that
$default = public_path().'/assets/profile-images/default-image'; $default = public_path().'/assets/profile-images/default-image';
$handle = fopen($default, 'rb'); $handle = fopen($default, 'rb');

View file

@ -5,14 +5,15 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use App\Models\Bookmark; use App\Models\Bookmark;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use JsonException;
class SaveScreenshot implements ShouldQueue class SaveScreenshot implements ShouldQueue
{ {
@ -27,77 +28,68 @@ class SaveScreenshot implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*
*
* @throws JsonException
*/ */
public function handle(): void public function handle(): void
{ {
// A normal Guzzle client $cloudConvert = Http::baseUrl('https://api.cloudconvert.com/v2')
$client = resolve(Client::class); ->withToken(config('services.cloudconvert.token'))
// A Guzzle client with a custom Middleware to retry the CloudConvert API requests ->throw();
$retryClient = resolve('RetryGuzzle');
// First request that CloudConvert takes a screenshot of the URL // First request that CloudConvert takes a screenshot of the URL
$takeScreenshotJobResponse = $client->request('POST', 'https://api.cloudconvert.com/v2/capture-website', [ $takeScreenshotJobResponse = $cloudConvert->post('/capture-website', [
'headers' => [
'Authorization' => 'Bearer '.config('services.cloudconvert.token'),
],
'json' => [
'url' => $this->bookmark->url, 'url' => $this->bookmark->url,
'output_format' => 'png', 'output_format' => 'png',
'screen_width' => 1440, 'screen_width' => 1440,
'screen_height' => 900, 'screen_height' => 900,
'wait_until' => 'networkidle0', 'wait_until' => 'networkidle0',
'wait_time' => 100, '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 // Now wait till the status job is finished
$screenshotJobStatusResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/'.$taskId, [ $screenshotJobStatusResponse = $this->pollUntilFinished($cloudConvert, $taskId);
'headers' => [
'Authorization' => 'Bearer '.config('services.cloudconvert.token'),
],
'query' => [
'include' => 'payload',
],
]);
$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 // 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', [ $exportImageJob = $cloudConvert->post('/export/url', [
'headers' => [
'Authorization' => 'Bearer '.config('services.cloudconvert.token'),
],
'json' => [
'input' => $finishedCaptureId, 'input' => $finishedCaptureId,
'archive_multiple_files' => false, '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 // Again, wait till the status of this export job is finished
$finalImageUrlResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/'.$exportImageJobId, [ $finalImageUrlResponse = $this->pollUntilFinished($cloudConvert, $exportImageJobId);
'headers' => [
'Authorization' => 'Bearer '.config('services.cloudconvert.token'),
],
'query' => [
'include' => 'payload',
],
]);
// Now we can download the screenshot and save it to the storage // 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->screenshot = $taskId;
$this->bookmark->save(); $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;
}
} }

View file

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use App\Models\Note; use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\Header;
use GuzzleHttp\Psr7\UriResolver; use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\Psr7\Utils; use GuzzleHttp\Psr7\Utils;
@ -14,6 +12,7 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Mf2\Parser; use Mf2\Parser;
@ -32,8 +31,6 @@ class SendWebMentions implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*
* @throws GuzzleException
*/ */
public function handle(): void public function handle(): void
{ {
@ -43,12 +40,9 @@ class SendWebMentions implements ShouldQueue
foreach ($urls as $url) { foreach ($urls as $url) {
$endpoint = $this->discoverWebmentionEndpoint($url); $endpoint = $this->discoverWebmentionEndpoint($url);
if ($endpoint !== null) { if ($endpoint !== null) {
$guzzle = resolve(Client::class); Http::asForm()->post($endpoint, [
$guzzle->post($endpoint, [
'form_params' => [
'source' => $this->note->uri, 'source' => $this->note->uri,
'target' => $url, 'target' => $url,
],
]); ]);
} }
} }
@ -56,8 +50,6 @@ class SendWebMentions implements ShouldQueue
/** /**
* Discover if a URL has a webmention endpoint. * Discover if a URL has a webmention endpoint.
*
* @throws GuzzleException
*/ */
public function discoverWebmentionEndpoint(string $url): ?string public function discoverWebmentionEndpoint(string $url): ?string
{ {
@ -71,10 +63,9 @@ class SendWebMentions implements ShouldQueue
$endpoint = null; $endpoint = null;
$guzzle = resolve(Client::class); $response = Http::get($url);
$response = $guzzle->get($url);
// check HTTP Headers for webmention endpoint // check HTTP Headers for webmention endpoint
$links = Header::parse($response->getHeader('Link')); $links = Header::parse($response->header('Link'));
foreach ($links as $link) { foreach ($links as $link) {
if (array_key_exists('rel', $link) && mb_stristr($link['rel'], 'webmention')) { if (array_key_exists('rel', $link) && mb_stristr($link['rel'], 'webmention')) {
return $this->resolveUri(trim($link[0], '<>'), $url); return $this->resolveUri(trim($link[0], '<>'), $url);
@ -82,7 +73,7 @@ class SendWebMentions implements ShouldQueue
} }
// failed to find a header so parse HTML // failed to find a header so parse HTML
$html = (string) $response->getBody(); $html = $response->body();
if ($html === '') { if ($html === '') {
return null; return null;

View file

@ -5,13 +5,12 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use App\Models\Note; use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SyndicateNoteToBluesky implements ShouldQueue class SyndicateNoteToBluesky implements ShouldQueue
{ {
@ -31,29 +30,18 @@ class SyndicateNoteToBluesky implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*
* @throws GuzzleException
*/ */
public function handle(Client $guzzle): void public function handle(): void
{ {
$response = $guzzle->request( // no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above
'POST', $response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [
'https://brid.gy/publish/webmention',
[
'headers' => [
'Accept' => 'application/json',
],
'form_params' => [
'source' => $this->note->uri, 'source' => $this->note->uri,
'target' => 'https://brid.gy/publish/bluesky', 'target' => 'https://brid.gy/publish/bluesky',
], ]);
'http_errors' => false,
]
);
$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->bluesky_url = $body['url'];
$this->note->save(); $this->note->save();
@ -61,7 +49,7 @@ class SyndicateNoteToBluesky implements ShouldQueue
} }
throw new \RuntimeException( throw new \RuntimeException(
'Bridgy publish to Bluesky failed: '.($body['error'] ?? (string) $response->getBody()) 'Bridgy publish to Bluesky failed: '.($body['error'] ?? $response->body())
); );
} }
} }

View file

@ -5,13 +5,12 @@ declare(strict_types=1);
namespace App\Jobs; namespace App\Jobs;
use App\Models\Note; use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class SyndicateNoteToMastodon implements ShouldQueue class SyndicateNoteToMastodon implements ShouldQueue
{ {
@ -31,29 +30,18 @@ class SyndicateNoteToMastodon implements ShouldQueue
/** /**
* Execute the job. * Execute the job.
*
* @throws GuzzleException
*/ */
public function handle(Client $guzzle): void public function handle(): void
{ {
$response = $guzzle->request( // no ->throw()/->retry() here — Bridgy would duplicate-publish on retry, see $tries above
'POST', $response = Http::acceptJson()->asForm()->post('https://brid.gy/publish/webmention', [
'https://brid.gy/publish/webmention',
[
'headers' => [
'Accept' => 'application/json',
],
'form_params' => [
'source' => $this->note->uri, 'source' => $this->note->uri,
'target' => 'https://brid.gy/publish/mastodon', 'target' => 'https://brid.gy/publish/mastodon',
], ]);
'http_errors' => false,
]
);
$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->mastodon_url = $body['url'];
$this->note->save(); $this->note->save();
@ -61,7 +49,7 @@ class SyndicateNoteToMastodon implements ShouldQueue
} }
throw new \RuntimeException( throw new \RuntimeException(
'Bridgy publish to Mastodon failed: '.($body['error'] ?? (string) $response->getBody()) 'Bridgy publish to Mastodon failed: '.($body['error'] ?? $response->body())
); );
} }
} }

View file

@ -7,7 +7,6 @@ namespace App\Models;
use App\CommonMark\Generators\MentionGenerator; use App\CommonMark\Generators\MentionGenerator;
use App\CommonMark\Renderers\MentionRenderer; use App\CommonMark\Renderers\MentionRenderer;
use App\Observers\NoteObserver; use App\Observers\NoteObserver;
use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\ObservedBy; 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\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Jonnybarnes\IndieWeb\Numbers; use Jonnybarnes\IndieWeb\Numbers;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
use League\CommonMark\Environment\Environment; use League\CommonMark\Environment\Environment;
@ -363,18 +363,15 @@ class Note extends Model
$latLng = $latitude.','.$longitude; $latLng = $latitude.','.$longitude;
return Cache::get($latLng, function () use ($latLng, $latitude, $longitude) { return Cache::get($latLng, function () use ($latLng, $latitude, $longitude) {
$guzzle = resolve(Client::class); $response = Http::withHeaders(['User-Agent' => 'jonnybarnes.uk, email jonny@jonnybarnes.uk'])
$response = $guzzle->request('GET', 'https://nominatim.openstreetmap.org/reverse', [ ->get('https://nominatim.openstreetmap.org/reverse', [
'query' => [
'format' => 'json', 'format' => 'json',
'lat' => $latitude, 'lat' => $latitude,
'lon' => $longitude, 'lon' => $longitude,
'zoom' => 18, 'zoom' => 18,
'addressdetails' => 1, '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)) { if (isset($json->address->suburb)) {
$locality = $json->address->suburb; $locality = $json->address->suburb;
if (isset($json->address->city)) { if (isset($json->address->city)) {

View file

@ -2,15 +2,11 @@
namespace App\Providers; namespace App\Providers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256; use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory; use Lcobucci\JWT\Signer\Key\InMemory;
@ -33,11 +29,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// configure Intervention/Image
$this->app->bind('Intervention\Image\ImageManager', function () {
return ImageManager::withDriver(config('image.driver'));
});
/** /**
* Paginate a standard Laravel Collection. * Paginate a standard Laravel Collection.
* *
@ -82,39 +73,10 @@ 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 // Turn on Eloquent strict mode when developing
Model::shouldBeStrict(! $this->app->isProduction()); Model::shouldBeStrict(! $this->app->isProduction());
// Force HTTPS URL generation in production
URL::forceHttps($this->app->isProduction());
} }
} }

View file

@ -8,10 +8,8 @@ use App\Exceptions\InternetArchiveException;
use App\Jobs\ProcessBookmark; use App\Jobs\ProcessBookmark;
use App\Models\Bookmark; use App\Models\Bookmark;
use App\Models\Tag; use App\Models\Tag;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class BookmarkService class BookmarkService
@ -55,21 +53,19 @@ class BookmarkService
* Given a URL, attempt to save it to the Internet Archive. * Given a URL, attempt to save it to the Internet Archive.
* *
* @throws InternetArchiveException * @throws InternetArchiveException
* @throws GuzzleException
*/ */
public function getArchiveLink(string $url): string public function getArchiveLink(string $url): string
{ {
$client = resolve(Client::class); $response = Http::get('https://web.archive.org/save/'.$url);
try {
$response = $client->request('GET', 'https://web.archive.org/save/'.$url); if ($response->clientError()) {
} catch (ClientException $e) {
// throw an exception to be caught // throw an exception to be caught
throw new InternetArchiveException; throw new InternetArchiveException;
} }
if ($response->hasHeader('Content-Location')) {
if (Str::startsWith(Arr::get($response->getHeader('Content-Location'), 0), '/web')) { $contentLocation = $response->header('Content-Location');
return $response->getHeader('Content-Location')[0]; if ($contentLocation !== '' && Str::startsWith($contentLocation, '/web')) {
} return $contentLocation;
} }
// throw an exception to be caught // throw an exception to be caught

View file

@ -14,9 +14,8 @@
"ext-pgsql": "*", "ext-pgsql": "*",
"ext-sodium": "*", "ext-sodium": "*",
"cviebrock/eloquent-sluggable": "^13.0", "cviebrock/eloquent-sluggable": "^13.0",
"guzzlehttp/guzzle": "^7.2",
"indieauth/client": "^1.1", "indieauth/client": "^1.1",
"intervention/image": "^3", "intervention/image": "^4.0",
"jonnybarnes/indieweb": "~0.2", "jonnybarnes/indieweb": "~0.2",
"jonnybarnes/webmentions-parser": "~0.5", "jonnybarnes/webmentions-parser": "~0.5",
"laravel/framework": "^13.0", "laravel/framework": "^13.0",
@ -27,7 +26,7 @@
"league/commonmark": "^2.0", "league/commonmark": "^2.0",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"mf2/mf2": "~0.3", "mf2/mf2": "~0.3",
"spatie/laravel-flare": "^2.2", "spatie/laravel-flare": "^3.0",
"symfony/html-sanitizer": "^8.0", "symfony/html-sanitizer": "^8.0",
"tempest/highlight": "^2.27", "tempest/highlight": "^2.27",
"web-auth/webauthn-lib": "^5.0" "web-auth/webauthn-lib": "^5.0"
@ -42,8 +41,8 @@
"laravel/sail": "^1.41", "laravel/sail": "^1.41",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6", "nunomaduro/collision": "^8.6",
"phpunit/php-code-coverage": "^12.0", "phpunit/php-code-coverage": "^14.0",
"phpunit/phpunit": "^12.5.12", "phpunit/phpunit": "^13.0",
"spatie/laravel-ray": "^1.12", "spatie/laravel-ray": "^1.12",
"spatie/x-ray": "^1.2" "spatie/x-ray": "^1.2"
}, },

1276
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,6 @@
<?php <?php
use Spatie\FlareClient\Api;
use Spatie\FlareClient\Sampling\RateSampler; use Spatie\FlareClient\Sampling\RateSampler;
use Spatie\LaravelFlare\AttributesProviders\LaravelUserAttributesProvider;
use Spatie\LaravelFlare\FlareConfig; use Spatie\LaravelFlare\FlareConfig;
use Spatie\LaravelFlare\Senders\LaravelHttpSender; use Spatie\LaravelFlare\Senders\LaravelHttpSender;
@ -21,17 +19,6 @@ return [
'key' => env('FLARE_KEY'), 'key' => env('FLARE_KEY'),
/*
|
|--------------------------------------------------------------------------
| Flare Base URL
|--------------------------------------------------------------------------
|
| Which server should be used to send the reports/traces to.
|
*/
'base_url' => env('FLARE_BASE_URL', Api::BASE_URL),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Collects | Collects
@ -47,22 +34,6 @@ return [
extra: [] extra: []
), ),
/*
|--------------------------------------------------------------------------
| Attribute providers
|--------------------------------------------------------------------------
|
| When sending an error report or trace to Flare attributes can be added to
| the report or trace for common entries. An example of such an entry is
| the currently authenticated user. In an attribute provider you can
| specify which attributes should be sent.
|
*/
'attribute_providers' => [
'user' => LaravelUserAttributesProvider::class,
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Censor data | Censor data
@ -88,19 +59,49 @@ return [
'X-XSRF-TOKEN', 'X-XSRF-TOKEN',
], ],
'client_ips' => false, 'client_ips' => false,
'cookies' => false,
'session' => false,
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Reporting log statements | Sender
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| If this setting is `false` log statements won't be sent as events to Flare, | The sender is responsible for sending the error reports and traces to
| no matter which error level you specified in the Flare log channel. | Flare. By default, Laravel Flare sends them over HTTP. To use the local
| Flare daemon, switch the sender class to
| `Spatie\FlareClient\Senders\DaemonSender::class` and set `daemon_url`.
| The daemon sender defaults to localhost on port 8787 and uses its own
| default timeouts and fallback sender config unless you override them.
| |
*/ */
'send_logs_as_events' => true, 'sender' => [
'class' => LaravelHttpSender::class,
'config' => [
'timeout' => 10,
],
],
// Daemon sender example
// 'sender' => [
// 'class' => \Spatie\FlareClient\Senders\DaemonSender::class,
// 'config' => [
// 'daemon_url' => env('FLARE_DAEMON_URL', 'http://127.0.0.1:8787'),
// ],
// ],
/*
|--------------------------------------------------------------------------
| Report
|--------------------------------------------------------------------------
|
| Flare reports errors and exceptions happening within your application.
|
*/
'report' => env('FLARE_REPORT', true),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -113,19 +114,6 @@ return [
'report_error_levels' => null, 'report_error_levels' => null,
/*
|--------------------------------------------------------------------------
| Share button
|--------------------------------------------------------------------------
|
| Flare automatically adds a Share button to the laravel error page. This
| button allows you to easily share errors with colleagues or friends. It
| is enabled by default, but you can disable it here.
|
*/
'enable_share_button' => true,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Override grouping | Override grouping
@ -145,20 +133,16 @@ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Sender | Share button
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| The sender is responsible for sending the error reports and traces to | Flare automatically adds a Share button to the laravel error page. This
| Flare it can be configured if needed. | button allows you to easily share errors with colleagues or friends. It
| is enabled by default, but you can disable it here.
| |
*/ */
'sender' => [ 'enable_share_button' => true,
'class' => LaravelHttpSender::class,
'config' => [
'timeout' => 10,
],
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -170,7 +154,7 @@ return [
| |
*/ */
'trace' => env('FLARE_TRACE', false), 'trace' => env('FLARE_TRACE', true),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -183,26 +167,35 @@ return [
| which means that 10% of the traces will be recorded. | which means that 10% of the traces will be recorded.
| |
*/ */
'sampler' => [ 'sampler' => [
'class' => RateSampler::class, 'class' => RateSampler::class,
'config' => [ 'config' => [
'rate' => 0.1, 'rate' => env('FLARE_SAMPLER_RATE', 0.1),
], ],
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Trace limits | Log
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Limits for the tracing data. These limits are used to prevent | Logging show you an overview of log entries within your application.
| the tracing data from growing too large.
| |
*/ */
'trace_limits' => [
'max_spans' => 512, 'log' => env('FLARE_LOG', false),
'max_attributes_per_span' => 128,
'max_span_events_per_span' => 128, /*
'max_attributes_per_span_event' => 128, |--------------------------------------------------------------------------
], | Minimal log level
|--------------------------------------------------------------------------
|
| You can specify the minimal (Monolog) log level that should be sent to Flare.
| Log levels lower than the specified level will be ignored.
| If null all log levels will be sent to Flare.
|
*/
'minimal_log_level' => null,
]; ];

View file

@ -1,22 +0,0 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
|
| Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => Driver::class,
];

View file

@ -39,4 +39,8 @@ return [
'token' => env('CLOUDCONVERT_API_TOKEN'), 'token' => env('CLOUDCONVERT_API_TOKEN'),
], ],
'brrr' => [
'webhook_url' => env('BRRR_WEBHOOK_URL'),
],
]; ];

314
package-lock.json generated
View file

@ -121,9 +121,9 @@
} }
}, },
"node_modules/@csstools/css-calc": { "node_modules/@csstools/css-calc": {
"version": "3.2.1", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -168,9 +168,9 @@
} }
}, },
"node_modules/@csstools/css-syntax-patches-for-csstree": { "node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.6", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
"integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -237,9 +237,9 @@
} }
}, },
"node_modules/@csstools/selector-resolve-nested": { "node_modules/@csstools/selector-resolve-nested": {
"version": "4.0.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.1.tgz",
"integrity": "sha512-9vAPxmp+Dx3wQBIUwc1v7Mdisw1kbbaGqXUM8QLTgWg7SoPGYtXBsMXvsFs/0Bn5yoFhcktzxNZGNaUt0VjgjA==", "integrity": "sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -725,9 +725,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1", "version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -782,9 +782,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.6.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
"integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -1019,9 +1019,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.62.0", "version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==", "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1135,16 +1135,16 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "5.0.7", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^4.0.2" "balanced-match": "^4.0.2"
}, },
"engines": { "engines": {
"node": "18 || 20 || >=22" "node": "20 || >=22"
} }
}, },
"node_modules/braces": { "node_modules/braces": {
@ -1418,9 +1418,9 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "10.6.0", "version": "10.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
@ -1430,7 +1430,7 @@
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.5", "@eslint/config-array": "^0.23.5",
"@eslint/config-helpers": "^0.6.0", "@eslint/config-helpers": "^0.7.0",
"@eslint/core": "^1.2.1", "@eslint/core": "^1.2.1",
"@eslint/plugin-kit": "^0.7.2", "@eslint/plugin-kit": "^0.7.2",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
@ -1454,7 +1454,7 @@
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"minimatch": "^10.2.4", "minimatch": "^10.2.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"optionator": "^0.9.3" "optionator": "^0.9.3"
}, },
@ -1655,9 +1655,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.3", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1749,9 +1749,9 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.4.2", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1836,9 +1836,9 @@
} }
}, },
"node_modules/globby": { "node_modules/globby": {
"version": "16.2.0", "version": "16.2.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz",
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==", "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1857,9 +1857,9 @@
} }
}, },
"node_modules/globby/node_modules/ignore": { "node_modules/globby/node_modules/ignore": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2137,9 +2137,9 @@
} }
}, },
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true, "dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"dependencies": { "dependencies": {
@ -2153,23 +2153,23 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-android-arm64": "1.32.0", "lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.32.0", "lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.32.0", "lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.32.0", "lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.32.0" "lightningcss-win32-x64-msvc": "1.33.0"
} }
}, },
"node_modules/lightningcss-android-arm64": { "node_modules/lightningcss-android-arm64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2188,9 +2188,9 @@
} }
}, },
"node_modules/lightningcss-cli": { "node_modules/lightningcss-cli": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli/-/lightningcss-cli-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli/-/lightningcss-cli-1.33.0.tgz",
"integrity": "sha512-IFb/ChmSEbeWU3xeRybR6WFlJXCvfDS84//PUzLrRACgvoWrwRJBmcPS9azSo7LMh5QqEuyKanPBByhKM5z01Q==", "integrity": "sha512-/tBcBZlBFFxy1iYKDC/HSH/NEjv7Frq6dmDVKRbhGJQI/gFJIJ1/4ZXclxJkd0eEjBjDc5MY0YugHcvJVVGJVg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MPL-2.0", "license": "MPL-2.0",
@ -2208,23 +2208,23 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
}, },
"optionalDependencies": { "optionalDependencies": {
"lightningcss-cli-android-arm64": "1.32.0", "lightningcss-cli-android-arm64": "1.33.0",
"lightningcss-cli-darwin-arm64": "1.32.0", "lightningcss-cli-darwin-arm64": "1.33.0",
"lightningcss-cli-darwin-x64": "1.32.0", "lightningcss-cli-darwin-x64": "1.33.0",
"lightningcss-cli-freebsd-x64": "1.32.0", "lightningcss-cli-freebsd-x64": "1.33.0",
"lightningcss-cli-linux-arm-gnueabihf": "1.32.0", "lightningcss-cli-linux-arm-gnueabihf": "1.33.0",
"lightningcss-cli-linux-arm64-gnu": "1.32.0", "lightningcss-cli-linux-arm64-gnu": "1.33.0",
"lightningcss-cli-linux-arm64-musl": "1.32.0", "lightningcss-cli-linux-arm64-musl": "1.33.0",
"lightningcss-cli-linux-x64-gnu": "1.32.0", "lightningcss-cli-linux-x64-gnu": "1.33.0",
"lightningcss-cli-linux-x64-musl": "1.32.0", "lightningcss-cli-linux-x64-musl": "1.33.0",
"lightningcss-cli-win32-arm64-msvc": "1.32.0", "lightningcss-cli-win32-arm64-msvc": "1.33.0",
"lightningcss-cli-win32-x64-msvc": "1.32.0" "lightningcss-cli-win32-x64-msvc": "1.33.0"
} }
}, },
"node_modules/lightningcss-cli-android-arm64": { "node_modules/lightningcss-cli-android-arm64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-android-arm64/-/lightningcss-cli-android-arm64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-android-arm64/-/lightningcss-cli-android-arm64-1.33.0.tgz",
"integrity": "sha512-4O3QY+VdgpBZLIq4crcKOEPVAXX0p7zDoykuTVstRtyolg9XU8CntdtbxcMPSC4SkzAuM/W4KsnPAzmv6Jb5LA==", "integrity": "sha512-c0Xd7Gxaw3mNOyrb9ET1JH3JjuB69GY/6pHO4vgwEbIoHCXAt6DBg6kxr5t/oZL9LRxdnZCXF+o0Evz6kuR/xA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2243,9 +2243,9 @@
} }
}, },
"node_modules/lightningcss-cli-darwin-arm64": { "node_modules/lightningcss-cli-darwin-arm64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-arm64/-/lightningcss-cli-darwin-arm64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-arm64/-/lightningcss-cli-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Xx+zeD7bDKJZwbd1N63TJfIUHEtYspf+tqObdnQEJEvZAwmGfA4iEGrkCRT8R57tDRBDSXg3XHMDDvo/cq7gBQ==", "integrity": "sha512-sso5hSFPis7ldw2FcBopsZviSVAXWRGX8ybUwMhQHssTlERenQJ88WihZs08tCdSnQULo2kunnrGhh4VjRw8Fg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2264,9 +2264,9 @@
} }
}, },
"node_modules/lightningcss-cli-darwin-x64": { "node_modules/lightningcss-cli-darwin-x64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-x64/-/lightningcss-cli-darwin-x64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-darwin-x64/-/lightningcss-cli-darwin-x64-1.33.0.tgz",
"integrity": "sha512-fYWANZ8RJDpI0tBcPQ7oBOYihfXmgDBHR4lZ6d4z7rcRLlZAOeI00mTO0IXAKfSm/UgnatjM4aBuNlLh8L9noA==", "integrity": "sha512-/aDYpMv2QKpJoSIwvwpIs4tIX9SCU894eospSm55FR5MxDZkJDy4fEX6elsXkHCBDqbAknb4qd2h/oDMLTC4Ew==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2285,9 +2285,9 @@
} }
}, },
"node_modules/lightningcss-cli-freebsd-x64": { "node_modules/lightningcss-cli-freebsd-x64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-freebsd-x64/-/lightningcss-cli-freebsd-x64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-freebsd-x64/-/lightningcss-cli-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-TJm7z1Ghvo9FKAza2KvfkFgH/9rcV1xAhCYQZlLrV0CiuTZ17uzLobBWb9oelGWUG+wTnEs6XEl4h/ve61YySg==", "integrity": "sha512-kK9u4IEAvt3+1m7lkloyfZNfXykXq2vCH7lgZ5aEqa8VuBZYqvcAIQ4Qt1593QJxbko9HEKUP0erdSxw7k7eFw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2306,9 +2306,9 @@
} }
}, },
"node_modules/lightningcss-cli-linux-arm-gnueabihf": { "node_modules/lightningcss-cli-linux-arm-gnueabihf": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm-gnueabihf/-/lightningcss-cli-linux-arm-gnueabihf-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm-gnueabihf/-/lightningcss-cli-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-/jS9L5p3eexs5QJvARiDGxibz1umKJmmtff86fWXl3r7RbEUFrMAD4q1WhAR7DurRFgc1YWld6r0mX1YHEYttA==", "integrity": "sha512-4bHmUlYCb8WKdPKx2yhk8y/kD034JVuUatDMWav0uU/hEIYJrxPimWBJJ4w2lgpcWchVnk/Kuihs4vaLjO/ozw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -2327,9 +2327,9 @@
} }
}, },
"node_modules/lightningcss-cli-linux-arm64-gnu": { "node_modules/lightningcss-cli-linux-arm64-gnu": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-gnu/-/lightningcss-cli-linux-arm64-gnu-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-gnu/-/lightningcss-cli-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-PSdjwcRtSrJpsaqnY3ebnCfDOJ5ePi8s/0ItL3CX1b1Eu0iy44xo7MjgES1ZOQ2ntOthPRqaGsbAiVBLbgFLYQ==", "integrity": "sha512-DkYmkBix3icAZKZsxGsjBAj59CIV0zflZtsT66lCPd2z3OqBLJlhKRrw4TlAW7mIkYeOEYWKVz/G00ZGXNse4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2351,9 +2351,9 @@
} }
}, },
"node_modules/lightningcss-cli-linux-arm64-musl": { "node_modules/lightningcss-cli-linux-arm64-musl": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-musl/-/lightningcss-cli-linux-arm64-musl-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-arm64-musl/-/lightningcss-cli-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-t6DdpXFtEdonZHzRgH8cmqhC2o4tl0KrD33cDBBniJz5TmWS20MJxb4YEr4WqBLksqW8GE399HJo+g8/YVuKcA==", "integrity": "sha512-euNOhzo1ysRl719HfFzRe9r9usWO2Z/Nb8RnmqXxb/kvzOgAv2BcflpoTu6tWhSrbHX6YS/WP6mMFrmII6T7Hw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2375,9 +2375,9 @@
} }
}, },
"node_modules/lightningcss-cli-linux-x64-gnu": { "node_modules/lightningcss-cli-linux-x64-gnu": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-gnu/-/lightningcss-cli-linux-x64-gnu-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-gnu/-/lightningcss-cli-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-QMQllbHYkbkQ4N+v8OGExQlGHBc3YZIcKlVkYucQQj66thkFQsRjmv8p5q3iCB0inNsCoSZ8lBspugkU1iPlPg==", "integrity": "sha512-9Q4DAglm17bLhA+HZPB+vxjrHXdhR/8U44zyc8MQNA5Rw+tisvcoR/lv4iuBUFn2qreYVRWQq0wP0rgdhTSxyQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2399,9 +2399,9 @@
} }
}, },
"node_modules/lightningcss-cli-linux-x64-musl": { "node_modules/lightningcss-cli-linux-x64-musl": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-musl/-/lightningcss-cli-linux-x64-musl-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-linux-x64-musl/-/lightningcss-cli-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-dMSWdk4kMAi5f+J1xetxRCDQOvPix2whT0UdjuwP8r/5Xcdl2SQU/c80MQzu1S82kVDEANs1vHVEHp/+26LUnA==", "integrity": "sha512-+EobHdqxQ21SUMF8OU0qcizNIrLpPDaPTKQCaGbra9gL3NrW3ELG+exZl5y+WzJsEjLZ0hhjbCFjHUL/03H2JA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2423,9 +2423,9 @@
} }
}, },
"node_modules/lightningcss-cli-win32-arm64-msvc": { "node_modules/lightningcss-cli-win32-arm64-msvc": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-win32-arm64-msvc/-/lightningcss-cli-win32-arm64-msvc-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-win32-arm64-msvc/-/lightningcss-cli-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-MJo22OqSp9FLV10nTRDJQhP6zkEBFqBFQe9mnjfCs+G1Ft+QimIPmC+gBqZHFXveOBOsjFLzU72q0FPvRWFmZQ==", "integrity": "sha512-iwQ+8rHQy3ewC4c43Ik3UZ8TCC/rjShP3r/8TOPw4fT8wQmW7dS3BNNHPK+LWz9qNrObATJ4MZEEZVRneckOBw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2444,9 +2444,9 @@
} }
}, },
"node_modules/lightningcss-cli-win32-x64-msvc": { "node_modules/lightningcss-cli-win32-x64-msvc": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-cli-win32-x64-msvc/-/lightningcss-cli-win32-x64-msvc-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-cli-win32-x64-msvc/-/lightningcss-cli-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-nOBHLPeePXZpGR4Ptp+gkOIkr9pxlq2dFmO954ol5zBi/iOKxuD1hUTIQ7aG+ldKIx4eH9jwoTfZ6owUkm1paA==", "integrity": "sha512-sgtNPw2gxnY8OzCCMTKCkHSIiBYb/wfXLpLMmDIJFzcX2OETY+VOnQd7OBHx+hVSlrMU7iWL6oJ5rc+nQwteVA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2465,9 +2465,9 @@
} }
}, },
"node_modules/lightningcss-darwin-arm64": { "node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2486,9 +2486,9 @@
} }
}, },
"node_modules/lightningcss-darwin-x64": { "node_modules/lightningcss-darwin-x64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2507,9 +2507,9 @@
} }
}, },
"node_modules/lightningcss-freebsd-x64": { "node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2528,9 +2528,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm-gnueabihf": { "node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -2549,9 +2549,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-gnu": { "node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2573,9 +2573,9 @@
} }
}, },
"node_modules/lightningcss-linux-arm64-musl": { "node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2597,9 +2597,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-gnu": { "node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2621,9 +2621,9 @@
} }
}, },
"node_modules/lightningcss-linux-x64-musl": { "node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2645,9 +2645,9 @@
} }
}, },
"node_modules/lightningcss-win32-arm64-msvc": { "node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2666,9 +2666,9 @@
} }
}, },
"node_modules/lightningcss-win32-x64-msvc": { "node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0", "version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2808,9 +2808,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.15", "version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2953,9 +2953,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "4.0.4", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2966,9 +2966,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.16", "version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2986,7 +2986,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.12", "nanoid": "^3.3.16",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@ -3236,9 +3236,9 @@
} }
}, },
"node_modules/string-width": { "node_modules/string-width": {
"version": "8.2.1", "version": "8.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -3269,9 +3269,9 @@
} }
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "17.14.0", "version": "17.14.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz",
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==", "integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -3287,7 +3287,7 @@
"dependencies": { "dependencies": {
"@csstools/css-calc": "^3.2.1", "@csstools/css-calc": "^3.2.1",
"@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-syntax-patches-for-csstree": "^1.1.5", "@csstools/css-syntax-patches-for-csstree": "^1.1.6",
"@csstools/css-tokenizer": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0",
"@csstools/media-query-list-parser": "^5.0.0", "@csstools/media-query-list-parser": "^5.0.0",
"@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-resolve-nested": "^4.0.0",
@ -3299,9 +3299,9 @@
"debug": "^4.4.3", "debug": "^4.4.3",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^11.1.3", "file-entry-cache": "^11.1.5",
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^16.2.0", "globby": "^16.2.1",
"globjoin": "^0.1.4", "globjoin": "^0.1.4",
"html-tags": "^5.1.0", "html-tags": "^5.1.0",
"ignore": "^7.0.5", "ignore": "^7.0.5",
@ -3311,12 +3311,12 @@
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"postcss": "^8.5.15", "postcss": "^8.5.16",
"postcss-safe-parser": "^7.0.1", "postcss-safe-parser": "^7.0.1",
"postcss-selector-parser": "^7.1.4", "postcss-selector-parser": "^7.1.4",
"postcss-value-parser": "^4.2.0", "postcss-value-parser": "^4.2.0",
"string-width": "^8.2.1", "string-width": "^8.2.1",
"supports-hyperlinks": "^4.4.0", "supports-hyperlinks": "^4.5.0",
"svg-tags": "^1.0.0", "svg-tags": "^1.0.0",
"table": "^6.9.0", "table": "^6.9.0",
"write-file-atomic": "^7.0.1" "write-file-atomic": "^7.0.1"
@ -3400,9 +3400,9 @@
} }
}, },
"node_modules/stylelint/node_modules/ignore": { "node_modules/stylelint/node_modules/ignore": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

File diff suppressed because one or more lines are too long

View file

@ -6,12 +6,9 @@ namespace Tests\Feature\Admin;
use App\Models\Contact; use App\Models\Contact;
use App\Models\User; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -141,14 +138,12 @@ class ContactsTest extends TestCase
<img class="u-photo" alt="" src="http://tantek.com/tantek.png"> <img class="u-photo" alt="" src="http://tantek.com/tantek.png">
</div> </div>
HTML; HTML;
$file = fopen(__DIR__.'/../../aaron.png', 'rb'); $file = file_get_contents(__DIR__.'/../../aaron.png');
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'text/html'], $html), '*' => Http::sequence()
new Response(200, ['Content-Type' => 'image/png'], $file), ->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(); $user = User::factory()->make();
$contact = Contact::factory()->create([ $contact = Contact::factory()->create([
'homepage' => 'https://tantek.com', 'homepage' => 'https://tantek.com',
@ -165,12 +160,9 @@ class ContactsTest extends TestCase
#[Test] #[Test]
public function getting_remote_avatar_fails_gracefully_with_remote_not_found(): void public function getting_remote_avatar_fails_gracefully_with_remote_not_found(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(404), '*' => Http::response('', 404),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->instance(Client::class, $client);
$user = User::factory()->make(); $user = User::factory()->make();
$contact = Contact::factory()->create(); $contact = Contact::factory()->create();
@ -187,13 +179,11 @@ class ContactsTest extends TestCase
<img class="u-photo" src="http://tantek.com/tantek.png"> <img class="u-photo" src="http://tantek.com/tantek.png">
</div> </div>
HTML; HTML;
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'text/html'], $html), '*' => Http::sequence()
new Response(404), ->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(); $user = User::factory()->make();
$contact = Contact::factory()->create(); $contact = Contact::factory()->create();

View file

@ -5,14 +5,11 @@ declare(strict_types=1);
namespace Tests\Feature; namespace Tests\Feature;
use App\Models\User; 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\Uri;
use GuzzleHttp\Psr7\UriResolver; use GuzzleHttp\Psr7\UriResolver;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -269,12 +266,9 @@ class IndieAuthTest extends TestCase
</html> </html>
HTML; HTML;
$mockHandler = new MockHandler([ Http::fake([
new Response(200, [], $appPageHtml), '*' => Http::response($appPageHtml, 200),
]); ]);
$handlerStack = HandlerStack::create($mockHandler);
$mockGuzzleClient = new Client(['handler' => $handlerStack]);
$this->app->instance(Client::class, $mockGuzzleClient);
$user = User::factory()->make(); $user = User::factory()->make();
$url = url()->query('/auth', [ $url = url()->query('/auth', [
@ -313,12 +307,9 @@ class IndieAuthTest extends TestCase
</html> </html>
HTML; HTML;
$mockHandler = new MockHandler([ Http::fake([
new Response(200, [], $appPageHtml), '*' => Http::response($appPageHtml, 200),
]); ]);
$handlerStack = HandlerStack::create($mockHandler);
$mockGuzzleClient = new Client(['handler' => $handlerStack]);
$this->app->instance(Client::class, $mockGuzzleClient);
$user = User::factory()->make(); $user = User::factory()->make();
$url = url()->query('/auth', [ $url = url()->query('/auth', [

View file

@ -6,11 +6,8 @@ namespace Tests\Feature;
use App\Jobs\ProcessLike; use App\Jobs\ProcessLike;
use App\Models\Like; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Authorship;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
@ -98,18 +95,12 @@ class LikesTest extends TestCase
</html> </html>
END; END;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $content), '*' => Http::response($content, 200),
new Response(200, [], $content),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->bind(Client::class, function () use ($client) {
return $client;
});
$authorship = new Authorship; $authorship = new Authorship;
$job->handle($client, $authorship); $job->handle($authorship);
$this->assertEquals('Fred Bloggs', Like::find($id)->author_name); $this->assertEquals('Fred Bloggs', Like::find($id)->author_name);
} }
@ -141,18 +132,12 @@ class LikesTest extends TestCase
</html> </html>
END; END;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $content), '*' => Http::response($content, 200),
new Response(200, [], $content),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->bind(Client::class, function () use ($client) {
return $client;
});
$authorship = new Authorship; $authorship = new Authorship;
$job->handle($client, $authorship); $job->handle($authorship);
$this->assertEquals('Fred Bloggs', Like::find($id)->author_name); $this->assertEquals('Fred Bloggs', Like::find($id)->author_name);
} }
@ -177,18 +162,12 @@ class LikesTest extends TestCase
</html> </html>
END; END;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $content), '*' => Http::response($content, 200),
new Response(200, [], $content),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->bind(Client::class, function () use ($client) {
return $client;
});
$authorship = new Authorship; $authorship = new Authorship;
$job->handle($client, $authorship); $job->handle($authorship);
$this->assertNull(Like::find($id)->author_name); $this->assertNull(Like::find($id)->author_name);
} }

View file

@ -3,11 +3,19 @@
namespace Tests; namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Http;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
use CreatesApplication; use CreatesApplication;
protected function setUp(): void
{
parent::setUp();
Http::preventStrayRequests();
}
public function removeDirIfEmpty(string $dir): void public function removeDirIfEmpty(string $dir): void
{ {
// scandir() will always return `.` and `..` so even an “empty” // scandir() will always return `.` and `..` so even an “empty”

View file

@ -6,10 +6,7 @@ namespace Tests\Unit;
use App\Exceptions\InternetArchiveException; use App\Exceptions\InternetArchiveException;
use App\Services\BookmarkService; use App\Services\BookmarkService;
use GuzzleHttp\Client; use Illuminate\Support\Facades\Http;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -29,12 +26,9 @@ class BookmarksTest extends TestCase
#[Test] #[Test]
public function archive_link_method_calls_archive_service(): void public function archive_link_method_calls_archive_service(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Location' => '/web/1234/example.org']), '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'); $url = (new BookmarkService)->getArchiveLink('https://example.org');
$this->assertEquals('/web/1234/example.org', $url); $this->assertEquals('/web/1234/example.org', $url);
} }
@ -44,12 +38,9 @@ class BookmarksTest extends TestCase
{ {
$this->expectException(InternetArchiveException::class); $this->expectException(InternetArchiveException::class);
$mock = new MockHandler([ Http::fake([
new Response(403), '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'); (new BookmarkService)->getArchiveLink('https://example.org');
} }
@ -58,12 +49,9 @@ class BookmarksTest extends TestCase
{ {
$this->expectException(InternetArchiveException::class); $this->expectException(InternetArchiveException::class);
$mock = new MockHandler([ Http::fake([
new Response(200), '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'); (new BookmarkService)->getArchiveLink('https://example.org');
} }
} }

View file

@ -5,11 +5,8 @@ declare(strict_types=1);
namespace Tests\Unit\Jobs; namespace Tests\Unit\Jobs;
use App\Jobs\DownloadWebMention; use App\Jobs\DownloadWebMention;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\FileSystem\FileSystem; use Illuminate\FileSystem\FileSystem;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -35,19 +32,16 @@ class DownloadWebMentionJobTest extends TestCase
</div> </div>
HTML; HTML;
$html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html);
$mock = new MockHandler([ Http::fake([
new Response(200, ['X-Foo' => 'Bar'], $html), 'example.org/*' => Http::response($html, 200, ['X-Foo' => 'Bar']),
new Response(200, ['X-Foo' => 'Bar'], $html),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$job = new DownloadWebMention($source); $job = new DownloadWebMention($source);
$job->handle($client); $job->handle();
$this->assertFileExists(storage_path('HTML/https')); $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'); $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;
$html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html);
$html2 = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html2); $html2 = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html2);
$mock = new MockHandler([ Http::fake([
new Response(200, ['X-Foo' => 'Bar'], $html), 'example.org/*' => Http::sequence()
new Response(200, ['X-Foo' => 'Bar'], $html2), ->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 = new DownloadWebMention($source);
$job->handle($client); $job->handle();
$this->assertFileExists(storage_path('HTML/https')); $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'); $this->assertFileExists(storage_path('HTML/https/example.org/reply').'/1.'.date('Y-m-d').'.backup');
} }
@ -98,14 +91,12 @@ class DownloadWebMentionJobTest extends TestCase
</div> </div>
HTML; HTML;
$html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html); $html = str_replace('href=""', 'href="'.config('app.url').'/notes/A"', $html);
$mock = new MockHandler([ Http::fake([
new Response(200, ['X-Foo' => 'Bar'], $html), 'example.org/*' => Http::response($html, 200, ['X-Foo' => 'Bar']),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$job = new DownloadWebMention($source); $job = new DownloadWebMention($source);
$job->handle($client); $job->handle();
$this->assertFileExists(storage_path('HTML/https/example.org/reply-one/index.html')); $this->assertFileExists(storage_path('HTML/https/example.org/reply-one/index.html'));
} }

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Jobs;
use App\Jobs\NotifyBrrrOfWebMention;
use App\Models\WebMention;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class NotifyBrrrOfWebMentionJobTest extends TestCase
{
#[Test]
public function it_posts_a_reply_notification_to_the_brrr_webhook(): void
{
config(['services.brrr.webhook_url' => 'https://api.brrr.now/v1/br_usr_test']);
Http::fake();
$webMention = WebMention::factory()->make([
'source' => 'https://example.org/reply/1',
'target' => 'https://jonnybarnes.uk/notes/1',
'type' => 'in-reply-to',
]);
$job = new NotifyBrrrOfWebMention($webMention);
$job->handle();
Http::assertSent(function (Request $request) {
return $request->url() === 'https://api.brrr.now/v1/br_usr_test'
&& $request['title'] === 'New reply'
&& $request['message'] === 'From https://example.org/reply/1'
&& $request['open_url'] === 'https://jonnybarnes.uk/notes/1';
});
}
#[Test]
public function it_titles_notifications_by_webmention_type(): void
{
config(['services.brrr.webhook_url' => 'https://api.brrr.now/v1/br_usr_test']);
Http::fake();
foreach ([
'in-reply-to' => 'New reply',
'like-of' => 'New like',
'repost-of' => 'New repost',
'something-else' => 'New webmention',
] as $type => $expectedTitle) {
$webMention = WebMention::factory()->make(['type' => $type]);
(new NotifyBrrrOfWebMention($webMention))->handle();
Http::assertSent(fn (Request $request) => $request['title'] === $expectedTitle);
}
}
#[Test]
public function it_does_nothing_when_no_webhook_url_is_configured(): void
{
config(['services.brrr.webhook_url' => null]);
Http::fake();
$webMention = WebMention::factory()->make();
(new NotifyBrrrOfWebMention($webMention))->handle();
Http::assertNothingSent();
}
}

View file

@ -6,7 +6,6 @@ namespace Tests\Unit\Jobs;
use App\Jobs\ProcessMedia; use App\Jobs\ProcessMedia;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -15,10 +14,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function non_media_files_are_not_saved(): void public function non_media_files_are_not_saved(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/file.txt', 'This is not an image'); Storage::disk('local')->put('media/file.txt', 'This is not an image');
$job = new ProcessMedia('file.txt'); $job = new ProcessMedia('media/file.txt');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'file.txt'); $this->assertFileDoesNotExist(storage_path('app/media/').'file.txt');
} }
@ -26,10 +24,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function small_images_are_not_resized(): void public function small_images_are_not_resized(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png')); Storage::disk('local')->put('media/aaron.png', file_get_contents(__DIR__.'/../../aaron.png'));
$job = new ProcessMedia('aaron.png'); $job = new ProcessMedia('media/aaron.png');
$job->handle($manager); $job->handle();
$this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png'); $this->assertFileDoesNotExist(storage_path('app/media/').'aaron.png');
@ -41,10 +38,9 @@ class ProcessMediaJobTest extends TestCase
#[Test] #[Test]
public function large_images_have_smaller_images_created(): void public function large_images_have_smaller_images_created(): void
{ {
$manager = app()->make(ImageManager::class);
Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg')); Storage::disk('local')->put('media/test-image.jpg', file_get_contents(__DIR__.'/../../test-image.jpg'));
$job = new ProcessMedia('media/test-image.jpg'); $job = new ProcessMedia('media/test-image.jpg');
$job->handle($manager); $job->handle();
// These need to look in public disk // These need to look in public disk
Storage::disk('public')->assertExists('media/test-image.jpg'); Storage::disk('public')->assertExists('media/test-image.jpg');

View file

@ -5,16 +5,14 @@ declare(strict_types=1);
namespace Tests\Unit\Jobs; namespace Tests\Unit\Jobs;
use App\Exceptions\RemoteContentNotFoundException; use App\Exceptions\RemoteContentNotFoundException;
use App\Jobs\NotifyBrrrOfWebMention;
use App\Jobs\ProcessWebMention; use App\Jobs\ProcessWebMention;
use App\Jobs\SaveProfileImage; use App\Jobs\SaveProfileImage;
use App\Models\Note; use App\Models\Note;
use App\Models\WebMention; use App\Models\WebMention;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\FileSystem\FileSystem; use Illuminate\FileSystem\FileSystem;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Jonnybarnes\WebmentionsParser\Parser; use Jonnybarnes\WebmentionsParser\Parser;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
@ -39,17 +37,15 @@ class ProcessWebMentionJobTest extends TestCase
$this->expectException(RemoteContentNotFoundException::class); $this->expectException(RemoteContentNotFoundException::class);
$parser = new Parser; $parser = new Parser;
$mock = new MockHandler([ Http::fake([
new Response(404), '*' => Http::response('', 404),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$source = 'https://example.org/mention/1/'; $source = 'https://example.org/mention/1/';
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
} }
#[Test] #[Test]
@ -65,19 +61,18 @@ class ProcessWebMentionJobTest extends TestCase
</div> </div>
HTML; HTML;
$html = str_replace('href="', 'href="'.config('app.url'), $html); $html = str_replace('href="', 'href="'.config('app.url'), $html);
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$source = 'https://example.org/mention/1/'; $source = 'https://example.org/mention/1/';
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
Queue::assertPushed(SaveProfileImage::class); Queue::assertPushed(SaveProfileImage::class);
Queue::assertPushed(NotifyBrrrOfWebMention::class);
$this->assertDatabaseHas('webmentions', [ $this->assertDatabaseHas('webmentions', [
'source' => $source, 'source' => $source,
'type' => 'like-of', 'type' => 'like-of',
@ -103,16 +98,15 @@ class ProcessWebMentionJobTest extends TestCase
<div class="e-content">Updated reply</div> <div class="e-content">Updated reply</div>
</div> </div>
HTML; HTML;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
Queue::assertPushed(SaveProfileImage::class); Queue::assertPushed(SaveProfileImage::class);
Queue::assertNotPushed(NotifyBrrrOfWebMention::class);
$this->assertDatabaseHas('webmentions', [ $this->assertDatabaseHas('webmentions', [
'source' => $source, 'source' => $source,
'type' => 'in-reply-to', 'type' => 'in-reply-to',
@ -132,11 +126,9 @@ class ProcessWebMentionJobTest extends TestCase
<div class="e-content">Replying to someone else</div> <div class="e-content">Replying to someone else</div>
</div> </div>
HTML; HTML;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$source = 'https://example.org/reply/1'; $source = 'https://example.org/reply/1';
@ -151,7 +143,7 @@ class ProcessWebMentionJobTest extends TestCase
]); ]);
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
$this->assertDatabaseMissing('webmentions', [ $this->assertDatabaseMissing('webmentions', [
'source' => $source, 'source' => $source,
@ -169,11 +161,9 @@ class ProcessWebMentionJobTest extends TestCase
<div class="e-content">I like someone else now</div> <div class="e-content">I like someone else now</div>
</div> </div>
HTML; HTML;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$source = 'https://example.org/reply/1'; $source = 'https://example.org/reply/1';
@ -188,7 +178,7 @@ class ProcessWebMentionJobTest extends TestCase
]); ]);
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
$this->assertDatabaseMissing('webmentions', [ $this->assertDatabaseMissing('webmentions', [
'source' => $source, 'source' => $source,
@ -208,19 +198,18 @@ class ProcessWebMentionJobTest extends TestCase
</div> </div>
HTML; HTML;
$html = str_replace('href="', 'href="'.config('app.url'), $html); $html = str_replace('href="', 'href="'.config('app.url'), $html);
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
// Simulate a long brid.gy Bluesky source URL (well over 255 characters) // 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'; $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 = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
Queue::assertPushed(NotifyBrrrOfWebMention::class);
$this->assertGreaterThan(255, strlen($source)); $this->assertGreaterThan(255, strlen($source));
$this->assertDatabaseHas('webmentions', [ $this->assertDatabaseHas('webmentions', [
'source' => $source, 'source' => $source,
@ -238,11 +227,9 @@ class ProcessWebMentionJobTest extends TestCase
<div class="e-content">Reposting someone else</div> <div class="e-content">Reposting someone else</div>
</div> </div>
HTML; HTML;
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$source = 'https://example.org/reply/1'; $source = 'https://example.org/reply/1';
@ -257,7 +244,7 @@ class ProcessWebMentionJobTest extends TestCase
]); ]);
$job = new ProcessWebMention($note, $source); $job = new ProcessWebMention($note, $source);
$job->handle($parser, $client); $job->handle($parser);
$this->assertDatabaseMissing('webmentions', [ $this->assertDatabaseMissing('webmentions', [
'source' => $source, 'source' => $source,

View file

@ -5,10 +5,7 @@ declare(strict_types=1);
namespace Tests\Unit\Jobs; namespace Tests\Unit\Jobs;
use App\Jobs\SaveProfileImage; use App\Jobs\SaveProfileImage;
use GuzzleHttp\Client; use Illuminate\Support\Facades\Http;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Jonnybarnes\WebmentionsParser\Authorship; use Jonnybarnes\WebmentionsParser\Authorship;
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException; use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
@ -58,12 +55,9 @@ class SaveProfileImageJobTest extends TestCase
#[Test] #[Test]
public function remote_author_images_are_saved_locally(): void public function remote_author_images_are_saved_locally(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), '*' => 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' => []]; $mf = ['items' => []];
$author = [ $author = [
'properties' => [ 'properties' => [
@ -83,12 +77,9 @@ class SaveProfileImageJobTest extends TestCase
#[Test] #[Test]
public function local_default_author_image_is_used_as_fallback(): void public function local_default_author_image_is_used_as_fallback(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(404), '*' => Http::response('', 404),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->instance(Client::class, $client);
$mf = ['items' => []]; $mf = ['items' => []];
$author = [ $author = [
'properties' => [ 'properties' => [
@ -111,12 +102,9 @@ class SaveProfileImageJobTest extends TestCase
#[Test] #[Test]
public function we_get_url_from_photo_object_if_alt_text_is_provided(): void public function we_get_url_from_photo_object_if_alt_text_is_provided(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), '*' => 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' => []]; $mf = ['items' => []];
$author = [ $author = [
'properties' => [ 'properties' => [
@ -139,12 +127,9 @@ class SaveProfileImageJobTest extends TestCase
#[Test] #[Test]
public function use_first_url_if_multiple_homepages_are_provided(): void public function use_first_url_if_multiple_homepages_are_provided(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'image/jpeg'], 'fake jpeg image'), '*' => 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' => []]; $mf = ['items' => []];
$author = [ $author = [
'properties' => [ 'properties' => [

View file

@ -6,13 +6,8 @@ namespace Tests\Unit\Jobs;
use App\Jobs\SaveScreenshot; use App\Jobs\SaveScreenshot;
use App\Models\Bookmark; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -25,57 +20,34 @@ class SaveScreenshotJobTest extends TestCase
public function screenshot_is_saved_by_job(): void public function screenshot_is_saved_by_job(): void
{ {
Storage::fake('public'); 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"}}}'), Http::fake([
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"}}}'), 'api.cloudconvert.com/v2/capture-website' => Http::response([
new Response(200, ['Content-Type' => 'image/png'], fopen(__DIR__.'/../../theverge.com.png', 'rb')), '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(); $bookmark = Bookmark::factory()->create();
$job = new SaveScreenshot($bookmark); $job = new SaveScreenshot($bookmark);
@ -84,68 +56,45 @@ class SaveScreenshotJobTest extends TestCase
$this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot); $this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot);
Storage::disk('public')->assertExists('/assets/img/bookmarks/'.$bookmark->screenshot.'.png'); 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] #[Test]
public function screenshot_job_handles_unfinished_tasks(): void public function screenshot_job_handles_unfinished_tasks(): void
{ {
Storage::fake('public'); 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"}}}'), Http::fake([
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"}}}'), 'api.cloudconvert.com/v2/capture-website' => Http::response([
new Response(200, ['Content-Type' => 'image/png'], fopen(__DIR__.'/../../theverge.com.png', 'rb')), '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(); $bookmark = Bookmark::factory()->create();
$job = new SaveScreenshot($bookmark); $job = new SaveScreenshot($bookmark);
@ -154,9 +103,10 @@ class SaveScreenshotJobTest extends TestCase
$this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot); $this->assertEquals('68d52633-e170-465e-b13e-746c97d01ffb', $bookmark->screenshot);
Storage::disk('public')->assertExists('/assets/img/bookmarks/'.$bookmark->screenshot.'.png'); Storage::disk('public')->assertExists('/assets/img/bookmarks/'.$bookmark->screenshot.'.png');
// Also assert we made the correct number of requests
$this->assertCount(2, $container); // capture-website, 2x poll (waiting then finished), export/url, 2x poll (waiting then finished), download
// However with retries there should be more than 4 responses for the 2 requests Http::assertSentCount(7);
$this->assertEquals(0, $retryMock->count()); // Also assert every queued response in each sequence was consumed, no more no less
Http::assertSequencesAreEmpty();
} }
} }

View file

@ -6,10 +6,7 @@ namespace Tests\Unit\Jobs;
use App\Jobs\SendWebMentions; use App\Jobs\SendWebMentions;
use App\Models\Note; use App\Models\Note;
use GuzzleHttp\Client; use Illuminate\Support\Facades\Http;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -28,12 +25,9 @@ class SendWebMentionJobTest extends TestCase
public function discover_webmention_endpoint_from_header_links(): void public function discover_webmention_endpoint_from_header_links(): void
{ {
$url = 'https://example.org/webmention'; $url = 'https://example.org/webmention';
$mock = new MockHandler([ Http::fake([
new Response(200, ['Link' => '<'.$url.'>; rel="webmention"']), '*' => 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); $job = new SendWebMentions(new Note);
$this->assertEquals($url, $job->discoverWebmentionEndpoint('https://example.org')); $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 public function discover_webmention_endpoint_from_html_link_tags(): void
{ {
$html = '<link rel="webmention" href="https://example.org/webmention">'; $html = '<link rel="webmention" href="https://example.org/webmention">';
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->instance(Client::class, $client);
$job = new SendWebMentions(new Note); $job = new SendWebMentions(new Note);
$this->assertEquals( $this->assertEquals(
@ -61,12 +52,9 @@ class SendWebMentionJobTest extends TestCase
public function discover_webmention_endpoint_from_legacy_html_markup(): void public function discover_webmention_endpoint_from_legacy_html_markup(): void
{ {
$html = '<link rel="http://webmention.org/" href="https://example.org/webmention">'; $html = '<link rel="http://webmention.org/" href="https://example.org/webmention">';
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), '*' => Http::response($html, 200),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->instance(Client::class, $client);
$job = new SendWebMentions(new Note); $job = new SendWebMentions(new Note);
$this->assertEquals( $this->assertEquals(
@ -95,13 +83,10 @@ class SendWebMentionJobTest extends TestCase
public function we_send_a_webmention_for_a_note(): void public function we_send_a_webmention_for_a_note(): void
{ {
$html = '<link rel="http://webmention.org/" href="https://example.org/webmention">'; $html = '<link rel="http://webmention.org/" href="https://example.org/webmention">';
$mock = new MockHandler([ Http::fake([
new Response(200, [], $html), 'example.org/webmention' => Http::response('', 202),
new 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 = new Note;
$note->note = 'Hi [Aaron](https://aaronparecki.com)'; $note->note = 'Hi [Aaron](https://aaronparecki.com)';
@ -114,13 +99,10 @@ class SendWebMentionJobTest extends TestCase
#[Test] #[Test]
public function links_in_notes_can_not_support_webmentions(): void public function links_in_notes_can_not_support_webmentions(): void
{ {
$mock = new MockHandler([ Http::fake([
// URLs with commas currently break the parse function Im using // URLs with commas currently break the parse function Im using
new Response(200, ['Link' => '<https://example.org/foo,bar>; rel="preconnect"']), '*' => Http::response('', 200, ['Link' => '<https://example.org/foo,bar>; rel="preconnect"']),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$this->app->instance(Client::class, $client);
$job = new SendWebMentions(new Note); $job = new SendWebMentions(new Note);
$this->assertNull($job->discoverWebmentionEndpoint('https://example.org')); $this->assertNull($job->discoverWebmentionEndpoint('https://example.org'));

View file

@ -5,12 +5,9 @@ namespace Tests\Unit\Jobs;
use App\Jobs\SyndicateNoteToBluesky; use App\Jobs\SyndicateNoteToBluesky;
use App\Models\Note; use App\Models\Note;
use Faker\Factory; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -24,19 +21,17 @@ class SyndicateNoteToBlueskyJobTest extends TestCase
$faker = Factory::create(); $faker = Factory::create();
$randomNumber = $faker->randomNumber(); $randomNumber = $faker->randomNumber();
$blueskyUrl = 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber; $blueskyUrl = 'https://bsky.app/profile/jonnybarnes.uk/'.$randomNumber;
$mock = new MockHandler([ Http::fake([
new Response(201, ['Content-Type' => 'application/json'], json_encode([ 'brid.gy/*' => Http::response([
'url' => $blueskyUrl, 'url' => $blueskyUrl,
'id' => (string) $randomNumber, 'id' => (string) $randomNumber,
'type' => ['h-entry'], 'type' => ['h-entry'],
])), ], 201),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$job = new SyndicateNoteToBluesky($note); $job = new SyndicateNoteToBluesky($note);
$job->handle($client); $job->handle();
$this->assertDatabaseHas('notes', [ $this->assertDatabaseHas('notes', [
'bluesky_url' => $blueskyUrl, 'bluesky_url' => $blueskyUrl,
@ -46,39 +41,31 @@ class SyndicateNoteToBlueskyJobTest extends TestCase
#[Test] #[Test]
public function we_post_the_correct_source_and_target(): void public function we_post_the_correct_source_and_target(): void
{ {
$container = []; Http::fake([
$history = Middleware::history($container); 'brid.gy/*' => Http::response([
$mock = new MockHandler([
new Response(201, ['Content-Type' => 'application/json'], json_encode([
'url' => 'https://bsky.app/profile/jonnybarnes.uk/1', '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**']); $note = Note::factory()->create(['note' => 'This is a **test**']);
$job = new SyndicateNoteToBluesky($note); $job = new SyndicateNoteToBluesky($note);
$job->handle($client); $job->handle();
$request = $container[0]['request']; Http::assertSent(function (Request $request) use ($note) {
$body = []; return $request->url() === 'https://brid.gy/publish/webmention'
parse_str((string) $request->getBody(), $body); && $request['source'] === $note->uri
&& $request['target'] === 'https://brid.gy/publish/bluesky';
$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']);
} }
#[Test] #[Test]
public function a_bridgy_failure_throws_and_does_not_set_bluesky_url(): void public function a_bridgy_failure_throws_and_does_not_set_bluesky_url(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(400, ['Content-Type' => 'application/json'], json_encode([ 'brid.gy/*' => Http::response([
'error' => 'Could not find target link', 'error' => 'Could not find target link',
])), ], 400),
]); ]);
$client = new Client(['handler' => HandlerStack::create($mock)]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$job = new SyndicateNoteToBluesky($note); $job = new SyndicateNoteToBluesky($note);
@ -86,7 +73,7 @@ class SyndicateNoteToBlueskyJobTest extends TestCase
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
try { try {
$job->handle($client); $job->handle();
} finally { } finally {
$this->assertDatabaseHas('notes', [ $this->assertDatabaseHas('notes', [
'id' => $note->id, 'id' => $note->id,

View file

@ -5,12 +5,9 @@ namespace Tests\Unit\Jobs;
use App\Jobs\SyndicateNoteToMastodon; use App\Jobs\SyndicateNoteToMastodon;
use App\Models\Note; use App\Models\Note;
use Faker\Factory; 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\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; use Tests\TestCase;
@ -24,19 +21,17 @@ class SyndicateNoteToMastodonJobTest extends TestCase
$faker = Factory::create(); $faker = Factory::create();
$randomNumber = $faker->randomNumber(); $randomNumber = $faker->randomNumber();
$mastodonUrl = 'https://mastodon.example/@jonny/'.$randomNumber; $mastodonUrl = 'https://mastodon.example/@jonny/'.$randomNumber;
$mock = new MockHandler([ Http::fake([
new Response(201, ['Content-Type' => 'application/json'], json_encode([ 'brid.gy/*' => Http::response([
'url' => $mastodonUrl, 'url' => $mastodonUrl,
'id' => (string) $randomNumber, 'id' => (string) $randomNumber,
'type' => ['h-entry'], 'type' => ['h-entry'],
])), ], 201),
]); ]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$job = new SyndicateNoteToMastodon($note); $job = new SyndicateNoteToMastodon($note);
$job->handle($client); $job->handle();
$this->assertDatabaseHas('notes', [ $this->assertDatabaseHas('notes', [
'mastodon_url' => $mastodonUrl, 'mastodon_url' => $mastodonUrl,
@ -46,39 +41,31 @@ class SyndicateNoteToMastodonJobTest extends TestCase
#[Test] #[Test]
public function we_post_the_correct_source_and_target(): void public function we_post_the_correct_source_and_target(): void
{ {
$container = []; Http::fake([
$history = Middleware::history($container); 'brid.gy/*' => Http::response([
$mock = new MockHandler([
new Response(201, ['Content-Type' => 'application/json'], json_encode([
'url' => 'https://mastodon.example/@jonny/1', '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**']); $note = Note::factory()->create(['note' => 'This is a **test**']);
$job = new SyndicateNoteToMastodon($note); $job = new SyndicateNoteToMastodon($note);
$job->handle($client); $job->handle();
$request = $container[0]['request']; Http::assertSent(function (Request $request) use ($note) {
$body = []; return $request->url() === 'https://brid.gy/publish/webmention'
parse_str((string) $request->getBody(), $body); && $request['source'] === $note->uri
&& $request['target'] === 'https://brid.gy/publish/mastodon';
$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']);
} }
#[Test] #[Test]
public function a_bridgy_failure_throws_and_does_not_set_mastodon_url(): void public function a_bridgy_failure_throws_and_does_not_set_mastodon_url(): void
{ {
$mock = new MockHandler([ Http::fake([
new Response(400, ['Content-Type' => 'application/json'], json_encode([ 'brid.gy/*' => Http::response([
'error' => 'Could not find target link', 'error' => 'Could not find target link',
])), ], 400),
]); ]);
$client = new Client(['handler' => HandlerStack::create($mock)]);
$note = Note::factory()->create(); $note = Note::factory()->create();
$job = new SyndicateNoteToMastodon($note); $job = new SyndicateNoteToMastodon($note);
@ -86,7 +73,7 @@ class SyndicateNoteToMastodonJobTest extends TestCase
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
try { try {
$job->handle($client); $job->handle();
} finally { } finally {
$this->assertDatabaseHas('notes', [ $this->assertDatabaseHas('notes', [
'id' => $note->id, 'id' => $note->id,

View file

@ -9,13 +9,10 @@ use App\Models\Media;
use App\Models\Note; use App\Models\Note;
use App\Models\Place; use App\Models\Place;
use App\Models\Tag; use App\Models\Tag;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase; 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"]} {"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; JSON;
// phpcs:enable Generic.Files.LineLength.TooLong // phpcs:enable Generic.Files.LineLength.TooLong
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'application/json'], $json), '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; $note = new Note;
$address = $note->reverseGeoCode(51.50084, -0.14264); $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"]} {"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; JSON;
// phpcs:enable Generic.Files.LineLength.TooLong // phpcs:enable Generic.Files.LineLength.TooLong
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'application/json'], $json), '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; $note = new Note;
$address = $note->reverseGeoCode(51.02, 0.91); $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"]} {"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; JSON;
// phpcs:enable Generic.Files.LineLength.TooLong // phpcs:enable Generic.Files.LineLength.TooLong
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'application/json'], $json), '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; $note = new Note;
$address = $note->reverseGeoCode(53.466277988406, -2.2304474827445); $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"]} {"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; JSON;
// phpcs:enable Generic.Files.LineLength.TooLong // phpcs:enable Generic.Files.LineLength.TooLong
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'application/json'], $json), '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; $note = new Note;
$address = $note->reverseGeoCode(51.1, 0.61); $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"]} {"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; JSON;
// phpcs:enable Generic.Files.LineLength.TooLong // phpcs:enable Generic.Files.LineLength.TooLong
$mock = new MockHandler([ Http::fake([
new Response(200, ['Content-Type' => 'application/json'], $json), '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; $note = new Note;
$address = $note->reverseGeoCode(54.3, 9.4); $address = $note->reverseGeoCode(54.3, 9.4);