Unify crop storage folder and make IG/FB crop tests symmetric
- CropsImageForAspectRatio: drop the per-publisher path-prefix argument; crops from both platforms go to a single `social-crops/` directory (the folder has no functional meaning — the platform fetches by URL and nothing reads it back; UUID filenames prevent collisions). - Close the IG/FB test asymmetry surfaced in review: Instagram single-image crop now runs across 1:1/4:5/16:9 (dataset) and has a crop-download-failure test; Facebook gains an explicit 'original' bypass test. Both platforms now cover the same matrix (crop per ratio + uploaded-URL is the crop, original/no-meta bypass, multi-image, download-failure exception). Follow-up tracked in #83 (orphaned crops are never pruned from storage).
This commit is contained in:
parent
e25b320ad2
commit
1b096f9ede
5 changed files with 64 additions and 18 deletions
|
|
@ -12,12 +12,14 @@
|
|||
|
||||
trait CropsImageForAspectRatio
|
||||
{
|
||||
private const CROP_DIRECTORY = 'social-crops';
|
||||
|
||||
/**
|
||||
* Crop the image to the user-selected aspect ratio and return a public URL
|
||||
* the platform can fetch. Returns the original URL untouched when no ratio
|
||||
* is set or 'original' is selected.
|
||||
*/
|
||||
protected function cropImageForAspectRatio(string $imageUrl, ?string $aspectRatio, string $pathPrefix): string
|
||||
protected function cropImageForAspectRatio(string $imageUrl, ?string $aspectRatio): string
|
||||
{
|
||||
if (! $aspectRatio || $aspectRatio === 'original') {
|
||||
return $imageUrl;
|
||||
|
|
@ -36,7 +38,7 @@ protected function cropImageForAspectRatio(string $imageUrl, ?string $aspectRati
|
|||
|
||||
$cropped = app(MediaOptimizer::class)->cropToAspectRatio($tempInput, $ratio);
|
||||
|
||||
$path = "{$pathPrefix}/".Str::uuid()->toString().'.jpg';
|
||||
$path = self::CROP_DIRECTORY.'/'.Str::uuid()->toString().'.jpg';
|
||||
Storage::put($path, file_get_contents($cropped));
|
||||
|
||||
@unlink($cropped);
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ private function publishTextPost(string $pageId, string $accessToken, string $co
|
|||
private function publishSingleImagePost(string $pageId, string $accessToken, ?string $content, $media, ?string $aspectRatio): array
|
||||
{
|
||||
$payload = [
|
||||
'url' => $this->cropImageForAspectRatio($media->url, $aspectRatio, 'facebook-crops'),
|
||||
'url' => $this->cropImageForAspectRatio($media->url, $aspectRatio),
|
||||
'access_token' => $accessToken,
|
||||
];
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ private function publishMultiImagePost(string $pageId, string $accessToken, ?str
|
|||
}
|
||||
|
||||
$uploadResponse = $this->facebookHttp()->post("{$this->baseUrl}/{$pageId}/photos", [
|
||||
'url' => $this->cropImageForAspectRatio($media->url, $aspectRatio, 'facebook-crops'),
|
||||
'url' => $this->cropImageForAspectRatio($media->url, $aspectRatio),
|
||||
'published' => 'false',
|
||||
'access_token' => $accessToken,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ private function publishFeed(string $instagramId, string $accessToken, ?string $
|
|||
|
||||
private function publishSingleImage(string $instagramId, string $accessToken, ?string $content, $media, ?string $aspectRatio): array
|
||||
{
|
||||
$imageUrl = $this->cropImageForAspectRatio($media->url, $aspectRatio, 'instagram-crops');
|
||||
$imageUrl = $this->cropImageForAspectRatio($media->url, $aspectRatio);
|
||||
|
||||
// Step 1: Create container
|
||||
$containerResponse = $this->socialHttp()->post("{$this->baseUrl}/{$instagramId}/media", [
|
||||
|
|
@ -205,7 +205,7 @@ private function publishCarousel(string $instagramId, string $accessToken, ?stri
|
|||
$params['video_url'] = $media->url;
|
||||
$params['media_type'] = 'VIDEO';
|
||||
} else {
|
||||
$params['image_url'] = $this->cropImageForAspectRatio($media->url, $aspectRatio, 'instagram-crops');
|
||||
$params['image_url'] = $this->cropImageForAspectRatio($media->url, $aspectRatio);
|
||||
}
|
||||
|
||||
$containerResponse = $this->socialHttp()->post("{$this->baseUrl}/{$instagramId}/media", $params);
|
||||
|
|
|
|||
|
|
@ -665,7 +665,7 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
|
|||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'facebook-crops/'));
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'social-crops/'));
|
||||
expect($crops)->toHaveCount(1);
|
||||
|
||||
$manager = new ImageManager(Driver::class);
|
||||
|
|
@ -676,7 +676,7 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
|
|||
@unlink($tempFile);
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/page_123/photos')
|
||||
&& str_contains($request['url'], 'facebook-crops/')
|
||||
&& str_contains($request['url'], 'social-crops/')
|
||||
&& ! str_contains($request['url'], 'example.com'));
|
||||
})->with([
|
||||
'1:1' => ['1:1', 1.0],
|
||||
|
|
@ -706,7 +706,7 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
|
|||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'facebook-crops/'));
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'social-crops/'));
|
||||
expect($crops)->toHaveCount(2);
|
||||
|
||||
$manager = new ImageManager(Driver::class);
|
||||
|
|
@ -719,7 +719,7 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
|
|||
}
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/page_123/photos')
|
||||
&& str_contains($request['url'] ?? '', 'facebook-crops/'));
|
||||
&& str_contains($request['url'] ?? '', 'social-crops/'));
|
||||
});
|
||||
|
||||
test('facebook image post throws when the source image cannot be downloaded for cropping', function () {
|
||||
|
|
@ -755,7 +755,29 @@ function facebookJpegBytes(int $width = 1200, int $height = 800): string
|
|||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect(collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'facebook-crops/')))->toBeEmpty();
|
||||
expect(collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'social-crops/')))->toBeEmpty();
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/page_123/photos')
|
||||
&& $request['url'] === 'https://example.com/media/a.jpg');
|
||||
});
|
||||
|
||||
test('facebook single image post with original aspect ratio uploads the original image (no crop)', function () {
|
||||
Storage::fake();
|
||||
|
||||
$this->postPlatform->update(['meta' => ['aspect_ratio' => 'original']]);
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/a.jpg', 'url' => 'https://example.com/media/a.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'a.jpg'],
|
||||
],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'*/page_123/photos' => Http::response(['id' => 'photo_1', 'post_id' => 'post_1'], 200),
|
||||
]);
|
||||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
expect(collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'social-crops/')))->toBeEmpty();
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/page_123/photos')
|
||||
&& $request['url'] === 'https://example.com/media/a.jpg');
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Exceptions\Social\InstagramPublishException;
|
||||
use App\Exceptions\TokenExpiredException;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
|
|
@ -726,10 +727,10 @@ function fakeJpegBytes(int $width = 1200, int $height = 800): string
|
|||
->toThrow(Exception::class);
|
||||
});
|
||||
|
||||
test('feed image is cropped to chosen aspect ratio before publishing', function () {
|
||||
test('feed image is cropped to chosen aspect ratio before publishing', function (string $aspectRatio, float $expected) {
|
||||
Storage::fake();
|
||||
|
||||
$this->postPlatform->update(['meta' => ['aspect_ratio' => '4:5']]);
|
||||
$this->postPlatform->update(['meta' => ['aspect_ratio' => $aspectRatio]]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
|
|
@ -753,15 +754,14 @@ function fakeJpegBytes(int $width = 1200, int $height = 800): string
|
|||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
$cropped = collect(Storage::allFiles())->first(fn (string $path) => str_starts_with($path, 'instagram-crops/'));
|
||||
$cropped = collect(Storage::allFiles())->first(fn (string $path) => str_starts_with($path, 'social-crops/'));
|
||||
expect($cropped)->not->toBeNull();
|
||||
|
||||
$manager = new ImageManager(Driver::class);
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'verify_');
|
||||
file_put_contents($tempFile, Storage::get($cropped));
|
||||
$image = $manager->decodePath($tempFile);
|
||||
$ratio = $image->width() / $image->height();
|
||||
expect(abs($ratio - 0.8))->toBeLessThan(0.01);
|
||||
expect(abs($image->width() / $image->height() - $expected))->toBeLessThan(0.01);
|
||||
@unlink($tempFile);
|
||||
|
||||
Http::assertSent(function ($request) {
|
||||
|
|
@ -770,9 +770,31 @@ function fakeJpegBytes(int $width = 1200, int $height = 800): string
|
|||
}
|
||||
$imageUrl = $request['image_url'] ?? '';
|
||||
|
||||
return str_contains($imageUrl, 'instagram-crops/')
|
||||
return str_contains($imageUrl, 'social-crops/')
|
||||
&& ! str_contains($imageUrl, 'example.com/media/test.jpg');
|
||||
});
|
||||
})->with([
|
||||
'1:1' => ['1:1', 1.0],
|
||||
'4:5' => ['4:5', 4 / 5],
|
||||
'16:9' => ['16:9', 16 / 9],
|
||||
]);
|
||||
|
||||
test('feed image throws when the source image cannot be downloaded for cropping', function () {
|
||||
Storage::fake();
|
||||
|
||||
$this->postPlatform->update(['meta' => ['aspect_ratio' => '4:5']]);
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/a.jpg', 'url' => 'https://example.com/media/a.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'a.jpg'],
|
||||
],
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://example.com/media/a.jpg' => Http::response('', 404),
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(InstagramPublishException::class, 'Failed to download image for cropping');
|
||||
});
|
||||
|
||||
test('feed image with original aspect ratio bypasses crop', function () {
|
||||
|
|
@ -875,7 +897,7 @@ function fakeJpegBytes(int $width = 1200, int $height = 800): string
|
|||
|
||||
$this->publisher->publish($this->postPlatform);
|
||||
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'instagram-crops/'));
|
||||
$crops = collect(Storage::allFiles())->filter(fn (string $path) => str_starts_with($path, 'social-crops/'));
|
||||
expect($crops)->toHaveCount(2);
|
||||
|
||||
$manager = new ImageManager(Driver::class);
|
||||
|
|
|
|||
Loading…
Reference in a new issue