feat(linkedin): enforce PDF/media compatibility on API and MCP
Make the document (PDF) exclusivity validation — previously web-only — also apply when scheduling/publishing via the public API and MCP, so a misconfigured post can't slip through these entry points. - ContentTypeCompatibleWithMedia: stored-media fallback for partial updates + a stored-state assertStoredPostCompatible(Post) - MCP PublishPostTool: assert stored-state compatibility before publish (the media-side mirror of assertStoredPostPublishable) - API UpdatePostRequest: validate each platform's effective content_type against effective media on schedule/publish (covers publishing without resubmitting content_type) - MCP UpdatePostTool: apply the rule on schedule with stored-media fallback - Tests: API + MCP happy + rejection paths, plus rule fallback/precedence units
This commit is contained in:
parent
7388313f5c
commit
14e44538a7
8 changed files with 358 additions and 5 deletions
|
|
@ -52,9 +52,6 @@ public function isDocument(): bool
|
|||
return strtolower(pathinfo($this->path, PATHINFO_EXTENSION)) === 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Rules\ContentFitsPlatformLimits;
|
||||
use App\Rules\ContentTypeCompatibleWithMedia;
|
||||
use App\Rules\ContentTypeMatchesPostPlatform;
|
||||
use App\Support\PostPlatformMetaRules;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
|
@ -73,6 +74,8 @@ public function withValidator(Validator $validator): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->addMediaCompatibilityErrors($validator);
|
||||
|
||||
$platformsById = $this->resolveSelectedPlatforms();
|
||||
|
||||
PostPlatformMetaRules::addRequiredOnPublishErrors(
|
||||
|
|
@ -83,6 +86,51 @@ public function withValidator(Validator $validator): void
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* On publish/schedule, validate every platform's *effective* content_type
|
||||
* (resubmitted in this request, or its stored value) against the *effective*
|
||||
* media (the request's media when sent, otherwise the post's stored media).
|
||||
* This closes the gap where a client publishes a misconfigured post — e.g. a
|
||||
* PDF on a regular LinkedIn post — without resubmitting content_type, which a
|
||||
* field-level rule on `platforms.*.content_type` would skip.
|
||||
*/
|
||||
private function addMediaCompatibilityErrors(Validator $validator): void
|
||||
{
|
||||
$routePost = $this->route('post');
|
||||
$post = $routePost instanceof Post ? $routePost : Post::find($routePost);
|
||||
|
||||
if (! $post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$media = $this->has('media') ? (array) $this->input('media', []) : (array) ($post->media ?? []);
|
||||
|
||||
$entries = $this->has('platforms')
|
||||
? collect($this->input('platforms', []))->map(fn ($platform, $index) => [
|
||||
'key' => "platforms.{$index}.content_type",
|
||||
'content_type' => data_get($platform, 'content_type')
|
||||
?? $post->postPlatforms()->where('id', data_get($platform, 'id'))->first()?->content_type?->value,
|
||||
])
|
||||
: $post->postPlatforms()->where('enabled', true)->get()->values()->map(fn ($postPlatform, $index) => [
|
||||
'key' => "platforms.{$index}.content_type",
|
||||
'content_type' => $postPlatform->content_type?->value,
|
||||
]);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (data_get($entry, 'content_type') === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(new ContentTypeCompatibleWithMedia($media))->validate(
|
||||
$entry['key'],
|
||||
(string) $entry['content_type'],
|
||||
function (string $message) use ($validator, $entry): void {
|
||||
$validator->errors()->add($entry['key'], $message);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int|string, Platform>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use App\Enums\Post\Status;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Models\Post;
|
||||
use App\Rules\ContentTypeCompatibleWithMedia;
|
||||
use App\Support\PostPlatformMetaRules;
|
||||
use App\Support\PostStatusRules;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
|
|
@ -43,6 +44,7 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
}
|
||||
|
||||
PostPlatformMetaRules::assertStoredPostPublishable($post);
|
||||
ContentTypeCompatibleWithMedia::assertStoredPostCompatible($post);
|
||||
|
||||
$scheduledAt = data_get($validated, 'scheduled_at');
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Models\Post;
|
||||
use App\Rules\ContentTypeCompatibleWithMedia;
|
||||
use App\Rules\ContentTypeMatchesPostPlatform;
|
||||
use App\Support\PostPlatformMetaRules;
|
||||
use App\Support\PostStatusRules;
|
||||
|
|
@ -35,6 +36,9 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
return Response::error('Post not found.');
|
||||
}
|
||||
|
||||
$enforcesMediaCompatibility = data_get($request->all(), 'status') === Status::Scheduled->value;
|
||||
$storedMedia = (array) ($post->media ?? []);
|
||||
|
||||
$validated = $request->validate([
|
||||
'post_id' => ['required', 'uuid'],
|
||||
'content' => ['nullable', 'string', 'max:10000'],
|
||||
|
|
@ -48,7 +52,13 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
'uuid',
|
||||
Rule::exists('post_platforms', 'id')->where('post_id', $post->id),
|
||||
],
|
||||
'platforms.*.content_type' => ['sometimes', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPostPlatform],
|
||||
'platforms.*.content_type' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
Rule::in(array_column(ContentType::cases(), 'value')),
|
||||
new ContentTypeMatchesPostPlatform,
|
||||
Rule::when($enforcesMediaCompatibility, [new ContentTypeCompatibleWithMedia($storedMedia)]),
|
||||
],
|
||||
...PostPlatformMetaRules::rules(),
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@
|
|||
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Models\Post;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\DataAwareRule;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Translation\PotentiallyTranslatedString;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ContentTypeCompatibleWithMedia implements DataAwareRule, ValidationRule
|
||||
{
|
||||
|
|
@ -18,6 +20,14 @@ class ContentTypeCompatibleWithMedia implements DataAwareRule, ValidationRule
|
|||
*/
|
||||
private array $data = [];
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>>|null $fallbackMedia Stored media used
|
||||
* when the request omits the `media` key entirely — lets API/MCP partial
|
||||
* updates (which don't resubmit media) validate a content_type against the
|
||||
* post's already-stored media.
|
||||
*/
|
||||
public function __construct(private ?array $fallbackMedia = null) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
|
@ -28,6 +38,34 @@ public function setData(array $data): static
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate every enabled platform's stored content_type against the post's
|
||||
* stored media. Used by publish flows that don't resubmit media (e.g. the
|
||||
* MCP publish tool) — the media-side mirror of
|
||||
* PostPlatformMetaRules::assertStoredPostPublishable().
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function assertStoredPostCompatible(Post $post): void
|
||||
{
|
||||
$media = (array) ($post->media ?? []);
|
||||
$errors = [];
|
||||
|
||||
foreach ($post->postPlatforms()->where('enabled', true)->get()->values() as $index => $postPlatform) {
|
||||
(new self($media))->validate(
|
||||
"platforms.{$index}.content_type",
|
||||
(string) $postPlatform->content_type?->value,
|
||||
function (string $message) use (&$errors, $index): void {
|
||||
$errors["platforms.{$index}.content_type"] = $message;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if ($errors !== []) {
|
||||
throw ValidationException::withMessages($errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
||||
*/
|
||||
|
|
@ -38,7 +76,11 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
|||
return;
|
||||
}
|
||||
|
||||
$media = (array) data_get($this->data, 'media', []);
|
||||
// Use the request's media when present; otherwise fall back to the
|
||||
// post's stored media so partial publish/schedule updates still validate.
|
||||
$media = array_key_exists('media', $this->data)
|
||||
? (array) data_get($this->data, 'media', [])
|
||||
: (array) ($this->fallbackMedia ?? []);
|
||||
$count = count($media);
|
||||
|
||||
if ($contentType->requiresMedia() && $count === 0) {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,135 @@
|
|||
expect(PostPlatform::where('social_account_id', $linkedin->id)->sole()->meta['document_title'])->toBe('Q2 Report');
|
||||
});
|
||||
|
||||
it('publishes a LinkedIn document post that has a PDF', function () {
|
||||
Queue::fake();
|
||||
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Our latest deck',
|
||||
'media' => [[
|
||||
'id' => 'doc-1', 'path' => 'medias/deck.pdf', 'url' => 'https://example.com/deck.pdf',
|
||||
'type' => 'document', 'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [['id' => $platform->id, 'content_type' => ContentType::LinkedInDocument->value]],
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
Queue::assertPushed(PublishPost::class);
|
||||
});
|
||||
|
||||
it('rejects publishing a LinkedIn document post whose media is an image, not a PDF', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'media' => [[
|
||||
'id' => 'img-1', 'path' => 'medias/slide.jpg', 'url' => 'https://example.com/slide.jpg',
|
||||
'type' => 'image', 'mime_type' => 'image/jpeg', 'original_filename' => 'slide.jpg',
|
||||
]],
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [['id' => $platform->id, 'content_type' => ContentType::LinkedInDocument->value]],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('rejects publishing a regular LinkedIn post that has a PDF attached', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'media' => [[
|
||||
'id' => 'doc-1', 'path' => 'medias/deck.pdf', 'url' => 'https://example.com/deck.pdf',
|
||||
'type' => 'document', 'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInPost, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [['id' => $platform->id, 'content_type' => ContentType::LinkedInPost->value]],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('publishes a valid LinkedIn document without resubmitting content_type', function () {
|
||||
Queue::fake();
|
||||
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => 'Our deck',
|
||||
'media' => [[
|
||||
'id' => 'doc-1', 'path' => 'medias/deck.pdf', 'url' => 'https://example.com/deck.pdf',
|
||||
'type' => 'document', 'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [['id' => $platform->id]],
|
||||
])
|
||||
->assertOk();
|
||||
|
||||
Queue::assertPushed(PublishPost::class);
|
||||
});
|
||||
|
||||
it('rejects publishing a misconfigured post even when content_type is not resubmitted', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'media' => [[
|
||||
'id' => 'doc-1', 'path' => 'medias/deck.pdf', 'url' => 'https://example.com/deck.pdf',
|
||||
'type' => 'document', 'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
// Stored as a regular post but carrying a PDF — the client publishes without resubmitting content_type.
|
||||
$platform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInPost, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [['id' => $platform->id]],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('persists per-platform meta across networks on store', function () {
|
||||
$instagram = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::Instagram]);
|
||||
$pinterest = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::Pinterest]);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Jobs\PublishPost;
|
||||
use App\Mcp\Servers\TryPostServer;
|
||||
use App\Mcp\Tools\Post\AttachMediaFromUploadTool;
|
||||
use App\Mcp\Tools\Post\CreatePostTool;
|
||||
use App\Mcp\Tools\Post\PublishPostTool;
|
||||
use App\Mcp\Tools\Post\UpdatePostTool;
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create();
|
||||
|
|
@ -175,6 +177,95 @@
|
|||
'pinterest' => ['pinterest', 'board_id', 'posts.form.pinterest.board_required'],
|
||||
]);
|
||||
|
||||
test('attach media from upload accepts a PDF for a LinkedIn post', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
]);
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$uploadToken = (string) Str::uuid();
|
||||
$this->workspace->media()->create([
|
||||
'group_id' => (string) Str::uuid(),
|
||||
'collection' => 'assets',
|
||||
'type' => 'document',
|
||||
'path' => 'medias/deck.pdf',
|
||||
'original_filename' => 'deck.pdf',
|
||||
'mime_type' => 'application/pdf',
|
||||
'size' => 1000,
|
||||
'order' => 0,
|
||||
'upload_token' => $uploadToken,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(AttachMediaFromUploadTool::class, [
|
||||
'post_id' => $post->id,
|
||||
'upload_token' => $uploadToken,
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$media = $post->fresh()->media;
|
||||
expect($media)->toHaveCount(1)
|
||||
->and($media[0]['type'])->toBe('document')
|
||||
->and($media[0]['mime_type'])->toBe('application/pdf');
|
||||
});
|
||||
|
||||
test('publish post succeeds for a LinkedIn document that has a PDF', function () {
|
||||
Queue::fake();
|
||||
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
'media' => [[
|
||||
'id' => 'doc-1', 'path' => 'medias/deck.pdf', 'url' => 'https://example.com/deck.pdf',
|
||||
'type' => 'document', 'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(PublishPostTool::class, ['post_id' => $post->id]);
|
||||
|
||||
$response->assertOk();
|
||||
Queue::assertPushed(PublishPost::class);
|
||||
});
|
||||
|
||||
test('publish post rejects a LinkedIn document whose media is an image, not a PDF', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
'media' => [[
|
||||
'id' => 'img-1', 'path' => 'medias/slide.jpg', 'url' => 'https://example.com/slide.jpg',
|
||||
'type' => 'image', 'mime_type' => 'image/jpeg', 'original_filename' => 'slide.jpg',
|
||||
]],
|
||||
]);
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(PublishPostTool::class, ['post_id' => $post->id]);
|
||||
|
||||
$response->assertHasErrors(['Document does not support images.']);
|
||||
});
|
||||
|
||||
test('publish post succeeds for a Discord platform with a channel', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,40 @@ function runMediaRule(string $contentType, array $media): array
|
|||
expect(runMediaRule(ContentType::LinkedInCarousel->value, $media)[0])->toContain('does not support PDF documents');
|
||||
});
|
||||
|
||||
test('falls back to stored media when the request omits the media key', function () {
|
||||
$errors = [];
|
||||
(new ContentTypeCompatibleWithMedia([['type' => 'document', 'mime_type' => 'application/pdf']]))
|
||||
->setData([]) // no 'media' key in the request -> use the fallback
|
||||
->validate('platforms.0.content_type', ContentType::LinkedInDocument->value, function (string $message) use (&$errors): void {
|
||||
$errors[] = $message;
|
||||
});
|
||||
|
||||
expect($errors)->toBe([]);
|
||||
|
||||
$imageErrors = [];
|
||||
(new ContentTypeCompatibleWithMedia([['type' => 'image', 'mime_type' => 'image/jpeg']]))
|
||||
->setData([])
|
||||
->validate('platforms.0.content_type', ContentType::LinkedInDocument->value, function (string $message) use (&$imageErrors): void {
|
||||
$imageErrors[] = $message;
|
||||
});
|
||||
|
||||
expect($imageErrors)->toHaveCount(1);
|
||||
expect($imageErrors[0])->toContain('does not support images');
|
||||
});
|
||||
|
||||
test('request media takes precedence over the stored fallback', function () {
|
||||
// Fallback is a PDF, but the request explicitly carries an image instead.
|
||||
$errors = [];
|
||||
(new ContentTypeCompatibleWithMedia([['type' => 'document', 'mime_type' => 'application/pdf']]))
|
||||
->setData(['media' => [['type' => 'image', 'mime_type' => 'image/jpeg']]])
|
||||
->validate('platforms.0.content_type', ContentType::LinkedInDocument->value, function (string $message) use (&$errors): void {
|
||||
$errors[] = $message;
|
||||
});
|
||||
|
||||
expect($errors)->toHaveCount(1);
|
||||
expect($errors[0])->toContain('does not support images');
|
||||
});
|
||||
|
||||
test('does nothing for invalid content type values', function () {
|
||||
expect(runMediaRule('not_a_real_content_type', []))->toBe([]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue