From 41cb9b02a3dfb8c4fed908e448c6bd4281e150d8 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 28 Jun 2026 21:15:01 -0300 Subject: [PATCH] test(api): cover unsupported-type and mixed-batch inline media scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - External URL that downloads (200) but isn't a supported media type → 422, nothing persisted (the type-rejection branch, distinct from a download 404). - Mixed batch: an already-hosted item + an external URL both succeed → both kept in order, only the external one creates a Media row. --- tests/Feature/Api/PostMediaApiTest.php | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/Feature/Api/PostMediaApiTest.php b/tests/Feature/Api/PostMediaApiTest.php index 067f2b71..c4008e32 100644 --- a/tests/Feature/Api/PostMediaApiTest.php +++ b/tests/Feature/Api/PostMediaApiTest.php @@ -299,6 +299,61 @@ 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]); + + // Downloads fine (200) but the bytes are not a supported media type. + Http::fake(['cdn.example.com/notes.txt' => Http::response('just some text', 200)]); + + $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken]) + ->postJson(route('api.posts.store'), [ + 'content' => 'Bad type post', + 'media' => [['url' => 'https://cdn.example.com/notes.txt']], + 'platforms' => [ + ['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'], + ], + ]) + ->assertUnprocessable() + ->assertJsonValidationErrors(['media']); + + expect(Post::where('content', 'Bad type post')->exists())->toBeFalse(); + expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(0); +}); + +it('keeps an already-hosted item and a freshly-hosted url in order', function () { + $this->socialAccount->update(['is_active' => true]); + + Http::fake([ + 'cdn.example.com/external.jpg' => Http::response( + file_get_contents(__DIR__.'/../../fixtures/1x1.png'), + 200, + ['Content-Type' => 'image/png'], + ), + ]); + + $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken]) + ->postJson(route('api.posts.store'), [ + 'content' => 'Mixed media post', + 'media' => [ + ['id' => 'hosted-1', 'path' => 'assets/already.jpg', 'url' => 'https://cdn.trypost.test/assets/already.jpg', 'type' => 'image'], + ['url' => 'https://cdn.example.com/external.jpg'], + ], + 'platforms' => [ + ['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'], + ], + ]) + ->assertCreated(); + + $media = Post::where('content', 'Mixed media post')->firstOrFail()->media; + + expect($media)->toHaveCount(2) + ->and(data_get($media, '0.path'))->toBe('assets/already.jpg') + ->and(data_get($media, '1.url'))->not->toContain('cdn.example.com') + ->and(data_get($media, '1.path'))->not->toBeNull(); + // Only the external URL is hosted; the passed-through item creates no new row. + expect(Media::where('mediable_id', $this->workspace->id)->count())->toBe(1); +}); + it('passes already-hosted media through on create without downloading', function () { $this->socialAccount->update(['is_active' => true]); Http::preventStrayRequests();