diff --git a/app/Services/Post/MediaAttacher.php b/app/Services/Post/MediaAttacher.php index d84ec4ca..3319eb63 100644 --- a/app/Services/Post/MediaAttacher.php +++ b/app/Services/Post/MediaAttacher.php @@ -10,6 +10,7 @@ use App\Models\Workspace; use Illuminate\Support\Facades\Http; use RuntimeException; +use Throwable; /** * Downloads public URLs and attaches them as media to a post — used by @@ -153,7 +154,8 @@ private function download(string $url): ?array }, ]) ->get($url); - } catch (RuntimeException) { + } catch (Throwable) { + // Any fetch failure (timeout, DNS, refused, oversize) is a failed download. @unlink($temp); return null; diff --git a/tests/Feature/Api/PostMediaApiTest.php b/tests/Feature/Api/PostMediaApiTest.php index c4008e32..daa7c3eb 100644 --- a/tests/Feature/Api/PostMediaApiTest.php +++ b/tests/Feature/Api/PostMediaApiTest.php @@ -8,6 +8,7 @@ use App\Models\PostPlatform; use App\Models\SocialAccount; use App\Models\Workspace; +use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; @@ -299,6 +300,36 @@ expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(0); }); +it('rejects and rolls back when a media url connection fails (timeout/dns)', function () { + $this->socialAccount->update(['is_active' => true]); + + Http::fake([ + 'cdn.example.com/good.jpg' => Http::response( + file_get_contents(__DIR__.'/../../fixtures/1x1.png'), + 200, + ['Content-Type' => 'image/png'], + ), + 'cdn.example.com/timeout.jpg' => fn () => throw new ConnectionException('Connection timed out'), + ]); + + $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken]) + ->postJson(route('api.posts.store'), [ + 'content' => 'Timeout media post', + 'media' => [ + ['url' => 'https://cdn.example.com/good.jpg'], + ['url' => 'https://cdn.example.com/timeout.jpg'], + ], + 'platforms' => [ + ['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'], + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['media']); + + expect(Post::where('content', 'Timeout media post')->exists())->toBeFalse(); + expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(0); +}); + it('rejects creating a post when an external media url is not a supported type', function () { $this->socialAccount->update(['is_active' => true]);