fix(linkedin): align PDF compatibility across editor, MCP schedule, and tests
Review follow-ups before QA: - Editor: getMediaIncompatibilityReason rejects a PDF on non-document content types, so the schedule gate and variant auto-snap match the backend rule (compliance i18n in en/es/pt-BR) - MCP UpdatePostTool: validate effective content_type vs stored media on schedule, closing the schedule-without-content_type gap; share entriesForUpdate/errorsFor with the API path - Tests: Platform allowedMediaTypes contains Document, URL-attach of a PDF (LinkedIn ok / TikTok rejected), multi-platform PDF rejection, document init-failure/missing-URN, Page publisher PROCESSING_FAILED
This commit is contained in:
parent
14e44538a7
commit
c339f8cf28
13 changed files with 304 additions and 39 deletions
|
|
@ -105,29 +105,13 @@ private function addMediaCompatibilityErrors(Validator $validator): void
|
|||
|
||||
$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,
|
||||
]);
|
||||
$entries = ContentTypeCompatibleWithMedia::entriesForUpdate(
|
||||
$post,
|
||||
$this->has('platforms') ? (array) $this->input('platforms', []) : null,
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
foreach (ContentTypeCompatibleWithMedia::errorsFor($entries, $media) as $key => $message) {
|
||||
$validator->errors()->add($key, $message);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
use App\Support\PostStatusRules;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\ResponseFactory;
|
||||
|
|
@ -36,9 +37,6 @@ 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'],
|
||||
|
|
@ -57,11 +55,25 @@ public function handle(Request $request): Response|ResponseFactory
|
|||
'string',
|
||||
Rule::in(array_column(ContentType::cases(), 'value')),
|
||||
new ContentTypeMatchesPostPlatform,
|
||||
Rule::when($enforcesMediaCompatibility, [new ContentTypeCompatibleWithMedia($storedMedia)]),
|
||||
],
|
||||
...PostPlatformMetaRules::rules(),
|
||||
]);
|
||||
|
||||
// On schedule, validate each platform's effective content_type (resubmitted
|
||||
// here, or stored) against the post's stored media — the tool can't change
|
||||
// media, so a misconfigured post can't be scheduled even without resubmitting
|
||||
// content_type. Mirrors the public API's withValidator check.
|
||||
if (data_get($validated, 'status') === Status::Scheduled->value) {
|
||||
$errors = ContentTypeCompatibleWithMedia::errorsFor(
|
||||
ContentTypeCompatibleWithMedia::entriesForUpdate($post, data_get($validated, 'platforms')),
|
||||
(array) ($post->media ?? []),
|
||||
);
|
||||
|
||||
if ($errors !== []) {
|
||||
throw ValidationException::withMessages($errors);
|
||||
}
|
||||
}
|
||||
|
||||
$payload = collect($validated)->except('post_id')->all();
|
||||
|
||||
$result = UpdatePost::execute($workspace, $post, $payload);
|
||||
|
|
|
|||
|
|
@ -48,24 +48,73 @@ public function setData(array $data): static
|
|||
*/
|
||||
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;
|
||||
},
|
||||
);
|
||||
}
|
||||
$errors = self::errorsFor(
|
||||
self::entriesForUpdate($post, null),
|
||||
(array) ($post->media ?? []),
|
||||
);
|
||||
|
||||
if ($errors !== []) {
|
||||
throw ValidationException::withMessages($errors);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The per-platform entries to validate for a post update: each platform's
|
||||
* effective content_type (resubmitted in this request, else its stored
|
||||
* value), keyed by the error path the caller surfaces. When $requestPlatforms
|
||||
* is null, the post's currently-enabled platforms are used.
|
||||
*
|
||||
* @param array<int, mixed>|null $requestPlatforms
|
||||
* @return array<int, array{key: string, content_type: string|null}>
|
||||
*/
|
||||
public static function entriesForUpdate(Post $post, ?array $requestPlatforms): array
|
||||
{
|
||||
if (is_array($requestPlatforms)) {
|
||||
return collect($requestPlatforms)->map(fn ($platform, $index): array => [
|
||||
'key' => "platforms.{$index}.content_type",
|
||||
'content_type' => data_get($platform, 'content_type')
|
||||
?? $post->postPlatforms()->where('id', data_get($platform, 'id'))->first()?->content_type?->value,
|
||||
])->all();
|
||||
}
|
||||
|
||||
return $post->postPlatforms()->where('enabled', true)->get()->values()
|
||||
->map(fn ($postPlatform, $index): array => [
|
||||
'key' => "platforms.{$index}.content_type",
|
||||
'content_type' => $postPlatform->content_type?->value,
|
||||
])->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a set of platform entries against the given media, returning
|
||||
* `[errorKey => message]` for each incompatible content_type.
|
||||
*
|
||||
* @param array<int, array{key: string, content_type: string|null}> $entries
|
||||
* @param array<int, mixed> $media
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function errorsFor(array $entries, array $media): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$contentType = data_get($entry, 'content_type');
|
||||
|
||||
if ($contentType === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(new self($media))->validate(
|
||||
$entry['key'],
|
||||
(string) $contentType,
|
||||
function (string $message) use (&$errors, $entry): void {
|
||||
$errors[$entry['key']] = $message;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -347,9 +347,11 @@
|
|||
'no_images' => 'Only videos are allowed for this format.',
|
||||
'no_mixed_media' => "Images and videos can't be combined in the same post.",
|
||||
'no_gifs' => 'GIFs are not supported here.',
|
||||
'no_documents' => 'PDF documents are not supported by this format.',
|
||||
'video_too_large' => 'Video exceeds the size limit for this platform.',
|
||||
'video_too_long' => 'Video must be under :seconds seconds for this format.',
|
||||
'image_too_large' => 'Image exceeds the size limit for this platform.',
|
||||
'document_too_large' => 'PDF exceeds the size limit for this platform.',
|
||||
'aspect_ratio_invalid' => 'Aspect ratio is not supported by this format.',
|
||||
'no_content_type' => 'Pick a content type for this platform.',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -347,9 +347,11 @@
|
|||
'no_images' => 'Solo se permiten videos en este formato.',
|
||||
'no_mixed_media' => 'No se pueden combinar imágenes y videos en la misma publicación.',
|
||||
'no_gifs' => 'Los GIFs no son compatibles aquí.',
|
||||
'no_documents' => 'Los documentos PDF no son compatibles con este formato.',
|
||||
'video_too_large' => 'El video supera el límite de tamaño de esta plataforma.',
|
||||
'video_too_long' => 'El video debe durar menos de :seconds segundos en este formato.',
|
||||
'image_too_large' => 'La imagen supera el límite de tamaño de esta plataforma.',
|
||||
'document_too_large' => 'El PDF supera el límite de tamaño de esta plataforma.',
|
||||
'aspect_ratio_invalid' => 'La proporción de aspecto no es compatible con este formato.',
|
||||
'no_content_type' => 'Elige un tipo de contenido para esta plataforma.',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -347,9 +347,11 @@
|
|||
'no_images' => 'Apenas vídeos são permitidos neste formato.',
|
||||
'no_mixed_media' => 'Não é possível combinar imagem e vídeo na mesma publicação.',
|
||||
'no_gifs' => 'GIFs não são suportados aqui.',
|
||||
'no_documents' => 'Documentos PDF não são suportados por este formato.',
|
||||
'video_too_large' => 'Vídeo excede o limite de tamanho desta plataforma.',
|
||||
'video_too_long' => 'Vídeo deve ter menos de :seconds segundos neste formato.',
|
||||
'image_too_large' => 'Imagem excede o limite de tamanho desta plataforma.',
|
||||
'document_too_large' => 'O PDF excede o limite de tamanho desta plataforma.',
|
||||
'aspect_ratio_invalid' => 'A proporção da imagem não é suportada por este formato.',
|
||||
'no_content_type' => 'Escolha um tipo de conteúdo para esta plataforma.',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export const getMediaIncompatibilityReason = (
|
|||
): string | null => {
|
||||
const rules = getMediaRulesForContentType(contentType);
|
||||
const videos = mediaItems.filter((m) => m.type === 'video' || m.mime_type?.startsWith('video/'));
|
||||
const documents = mediaItems.filter((m) => m.type === 'document' || m.mime_type === 'application/pdf');
|
||||
const images = mediaItems.filter((m) => m.type === 'image' || m.mime_type?.startsWith('image/'));
|
||||
const gifs = mediaItems.filter((m) => m.mime_type === 'image/gif');
|
||||
const total = mediaItems.length;
|
||||
|
|
@ -99,6 +100,7 @@ export const getMediaIncompatibilityReason = (
|
|||
if (rules.requiresMedia && total === 0) return trans('posts.edit.compliance.requires_media');
|
||||
if (!rules.acceptVideos && videos.length > 0) return trans('posts.edit.compliance.no_videos');
|
||||
if (!rules.acceptImages && images.length > 0) return trans('posts.edit.compliance.no_images');
|
||||
if (!rules.acceptDocuments && documents.length > 0) return trans('posts.edit.compliance.no_documents');
|
||||
if (rules.forbidsMixedMedia && videos.length > 0 && images.length > 0) return trans('posts.edit.compliance.no_mixed_media');
|
||||
if (!rules.acceptsGif && gifs.length > 0) return trans('posts.edit.compliance.no_gifs');
|
||||
if (total > rules.maxFiles) return trans('posts.edit.compliance.too_many_files', { max: String(rules.maxFiles) });
|
||||
|
|
@ -106,11 +108,17 @@ export const getMediaIncompatibilityReason = (
|
|||
|
||||
for (const m of mediaItems) {
|
||||
const isVideo = m.type === 'video' || m.mime_type?.startsWith('video/');
|
||||
const isDocument = m.type === 'document' || m.mime_type === 'application/pdf';
|
||||
const size = m.size ?? 0;
|
||||
const duration = m.meta?.duration ?? 0;
|
||||
const width = m.meta?.width ?? 0;
|
||||
const height = m.meta?.height ?? 0;
|
||||
|
||||
if (isDocument) {
|
||||
if (rules.maxDocumentBytes && size > 0 && size > rules.maxDocumentBytes) return trans('posts.edit.compliance.document_too_large');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
if (rules.maxVideoBytes && size > 0 && size > rules.maxVideoBytes) return trans('posts.edit.compliance.video_too_large');
|
||||
if (rules.maxVideoDurationSec && duration > 0 && duration > rules.maxVideoDurationSec) {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,41 @@
|
|||
->assertJsonValidationErrors(['platforms.0.content_type']);
|
||||
});
|
||||
|
||||
it('rejects only the platform that cannot take a PDF in a multi-platform post', function () {
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
$x = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::X]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'content' => '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',
|
||||
]],
|
||||
]);
|
||||
$linkedinPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
$xPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id, 'social_account_id' => $x->id,
|
||||
'platform' => Platform::X, 'content_type' => ContentType::XPost, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$this->withHeaders($this->headers)
|
||||
->putJson(route('api.posts.update', $post), [
|
||||
'status' => PostStatus::Publishing->value,
|
||||
'platforms' => [
|
||||
['id' => $linkedinPlatform->id, 'content_type' => ContentType::LinkedInDocument->value],
|
||||
['id' => $xPlatform->id, 'content_type' => ContentType::XPost->value],
|
||||
],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['platforms.1.content_type'])
|
||||
->assertJsonMissingValidationErrors(['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]);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,15 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Mcp\Servers\TryPostServer;
|
||||
use App\Mcp\Tools\Post\AttachMediaFromUrlTool;
|
||||
use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
|
@ -90,6 +94,60 @@
|
|||
expect($this->post->fresh()->media)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('attaches a pdf document from a url to a LinkedIn post', function () {
|
||||
Http::fake([
|
||||
'example.com/deck.pdf' => Http::response(
|
||||
"%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF\n",
|
||||
200,
|
||||
['Content-Type' => 'application/pdf'],
|
||||
),
|
||||
]);
|
||||
|
||||
$linkedin = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::LinkedIn]);
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id, 'social_account_id' => $linkedin->id,
|
||||
'platform' => Platform::LinkedIn, 'content_type' => ContentType::LinkedInDocument, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(AttachMediaFromUrlTool::class, [
|
||||
'post_id' => $this->post->id,
|
||||
'urls' => ['https://example.com/deck.pdf'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$media = $this->post->fresh()->media;
|
||||
expect($media)->toHaveCount(1)
|
||||
->and($media[0]['type'])->toBe('document');
|
||||
});
|
||||
|
||||
test('rejects a pdf url for a post with no PDF-capable platform', function () {
|
||||
Http::fake([
|
||||
'example.com/deck.pdf' => Http::response(
|
||||
"%PDF-1.4\n1 0 obj<</Type/Catalog>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF\n",
|
||||
200,
|
||||
['Content-Type' => 'application/pdf'],
|
||||
),
|
||||
]);
|
||||
|
||||
$tiktok = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::TikTok]);
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id, 'social_account_id' => $tiktok->id,
|
||||
'platform' => Platform::TikTok, 'content_type' => ContentType::TikTokVideo, 'enabled' => true,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(AttachMediaFromUrlTool::class, [
|
||||
'post_id' => $this->post->id,
|
||||
'urls' => ['https://example.com/deck.pdf'],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
expect($this->post->fresh()->media)->toHaveCount(0);
|
||||
});
|
||||
|
||||
test('post 404 from another workspace', function () {
|
||||
$other = Workspace::factory()->create();
|
||||
$post = Post::factory()->create(['workspace_id' => $other->id, 'user_id' => $this->user->id]);
|
||||
|
|
|
|||
|
|
@ -266,6 +266,34 @@
|
|||
$response->assertHasErrors(['Document does not support images.']);
|
||||
});
|
||||
|
||||
test('update post rejects scheduling a misconfigured LinkedIn post without resubmitting content_type', 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' => '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,
|
||||
]);
|
||||
|
||||
$response = TryPostServer::actingAs($this->user)
|
||||
->tool(UpdatePostTool::class, [
|
||||
'post_id' => $post->id,
|
||||
'status' => PostStatus::Scheduled->value,
|
||||
'scheduled_at' => now()->addDay()->toIso8601String(),
|
||||
'platforms' => [['id' => $platform->id]],
|
||||
]);
|
||||
|
||||
$response->assertHasErrors(['Post does not support PDF documents.']);
|
||||
});
|
||||
|
||||
test('publish post succeeds for a Discord platform with a channel', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
|
|
|||
|
|
@ -346,3 +346,39 @@
|
|||
&& data_get($request->data(), 'content.media.title') === 'Company Deck';
|
||||
});
|
||||
});
|
||||
|
||||
test('linkedin page publisher throws and does not post when document processing fails', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::LinkedInPageDocument]);
|
||||
$this->post->update([
|
||||
'media' => [[
|
||||
'id' => 'doc-media-1', 'path' => 'media/2026-01/company-deck.pdf',
|
||||
'url' => 'https://example.com/media/2026-01/company-deck.pdf',
|
||||
'mime_type' => 'application/pdf', 'original_filename' => 'company-deck.pdf',
|
||||
]],
|
||||
]);
|
||||
|
||||
$uploadUrl = 'https://www.linkedin.com/dms-uploads/document/org/fail';
|
||||
|
||||
Http::fake(function ($request) use ($uploadUrl) {
|
||||
$url = $request->url();
|
||||
|
||||
if (str_contains($url, '/rest/documents') && str_contains($url, 'initializeUpload')) {
|
||||
return Http::response(['value' => ['uploadUrl' => $uploadUrl, 'document' => 'urn:li:document:OrgFail']], 200);
|
||||
}
|
||||
|
||||
if ($url === $uploadUrl) {
|
||||
return Http::response(null, 201);
|
||||
}
|
||||
|
||||
if (str_contains($url, '/rest/documents/')) {
|
||||
return Http::response(['status' => 'PROCESSING_FAILED'], 200);
|
||||
}
|
||||
|
||||
return Http::response('fake-pdf-bytes', 200);
|
||||
});
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'LinkedIn Page document processing failed');
|
||||
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/rest/posts'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -578,3 +578,50 @@
|
|||
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/rest/posts'));
|
||||
});
|
||||
|
||||
test('linkedin publisher throws and does not post when document init fails', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::LinkedInDocument]);
|
||||
$this->post->update([
|
||||
'media' => [[
|
||||
'id' => 'doc-media-1', 'path' => 'media/2026-01/deck.pdf',
|
||||
'url' => 'https://example.com/media/2026-01/deck.pdf',
|
||||
'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
|
||||
Http::fake(function ($request) {
|
||||
if (str_contains($request->url(), '/rest/documents') && str_contains($request->url(), 'initializeUpload')) {
|
||||
return Http::response(['message' => 'Invalid request', 'status' => 400], 400);
|
||||
}
|
||||
|
||||
return Http::response('fake-pdf-bytes', 200);
|
||||
});
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))->toThrow(Exception::class);
|
||||
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/rest/posts'));
|
||||
});
|
||||
|
||||
test('linkedin publisher throws when document init response is missing the urn', function () {
|
||||
$this->postPlatform->update(['content_type' => ContentType::LinkedInDocument]);
|
||||
$this->post->update([
|
||||
'media' => [[
|
||||
'id' => 'doc-media-1', 'path' => 'media/2026-01/deck.pdf',
|
||||
'url' => 'https://example.com/media/2026-01/deck.pdf',
|
||||
'mime_type' => 'application/pdf', 'original_filename' => 'deck.pdf',
|
||||
]],
|
||||
]);
|
||||
|
||||
Http::fake(function ($request) {
|
||||
if (str_contains($request->url(), '/rest/documents') && str_contains($request->url(), 'initializeUpload')) {
|
||||
return Http::response(['value' => []], 200); // no uploadUrl / document
|
||||
}
|
||||
|
||||
return Http::response('fake-pdf-bytes', 200);
|
||||
});
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'missing uploadUrl or document URN');
|
||||
|
||||
Http::assertNotSent(fn ($request) => str_contains($request->url(), '/rest/posts'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,8 +35,10 @@
|
|||
});
|
||||
|
||||
test('platform has correct allowed media types', function () {
|
||||
expect(Platform::LinkedIn->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
||||
expect(Platform::LinkedIn->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
||||
expect(Platform::LinkedInPage->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video, MediaType::Document);
|
||||
expect(Platform::X->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
||||
expect(Platform::X->allowedMediaTypes())->not->toContain(MediaType::Document);
|
||||
expect(Platform::TikTok->allowedMediaTypes())->toBe([MediaType::Video]);
|
||||
expect(Platform::YouTube->allowedMediaTypes())->toBe([MediaType::Video]);
|
||||
expect(Platform::Instagram->allowedMediaTypes())->toContain(MediaType::Image, MediaType::Video);
|
||||
|
|
|
|||
Loading…
Reference in a new issue