fix(pinterest): restore board picker + require board_id in validation
The post editor lost the Pinterest board picker during a UI rewrite, causing scheduled posts to fail in production with 'Pinterest board_id is required'. This restores the picker and locks the contract with validation + tests so the regression cannot happen silently again. Backend: - PostController: pinterestBoards is now Record<account_id, Board[]> (mirrors the TikTok creator-info pattern); supports multi-account. - UpdatePostRequest: 'platforms.*.meta.board_id' rule + after-validator rejects Publishing/Scheduling Pinterest posts without board_id. Frontend: - PinterestSettings.vue: Combobox board picker with empty-state warning; emits update:meta with board_id. - ScheduleTab / PostEditorSidebar / Edit pass pinterestBoards down by social_account_id. Tests (6 new): - UpdatePostRequestTest: rejects publishing/scheduling without board_id across pin/carousel/video pin; allows draft without board_id; pinterest error doesn't block sibling platforms in multi-platform. - PinterestPublisherTest: publisher throws for carousel + video pin when no board_id (existing image-pin case kept). 1542 tests passing.
This commit is contained in:
parent
a9e8efffea
commit
44d891ef08
14 changed files with 427 additions and 13 deletions
|
|
@ -234,15 +234,15 @@ public function edit(Request $request, Post $post): Response|RedirectResponse
|
|||
$account->id => new PlatformConfigResource($account),
|
||||
]);
|
||||
|
||||
$pinterestBoards = [];
|
||||
$pinterestAccount = $socialAccounts->firstWhere('platform', Platform::Pinterest);
|
||||
if ($pinterestAccount) {
|
||||
try {
|
||||
$pinterestBoards = app(PinterestPublisher::class)->getBoards($pinterestAccount);
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail - boards will be empty
|
||||
}
|
||||
}
|
||||
$pinterestBoards = $socialAccounts
|
||||
->where('platform', Platform::Pinterest)
|
||||
->mapWithKeys(fn ($account) => [
|
||||
$account->id => rescue(
|
||||
fn () => app(PinterestPublisher::class)->getBoards($account),
|
||||
[],
|
||||
report: false,
|
||||
),
|
||||
]);
|
||||
|
||||
$tiktokCreatorInfos = $socialAccounts
|
||||
->where('platform', Platform::TikTok)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ public function rules(): array
|
|||
'platforms.*.meta.disclose' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.brand_content_toggle' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.brand_organic_toggle' => ['sometimes', 'boolean'],
|
||||
'platforms.*.meta.board_id' => ['sometimes', 'nullable', 'string'],
|
||||
'label_ids' => ['sometimes', 'array'],
|
||||
'label_ids.*' => ['uuid', Rule::exists('workspace_labels', 'id')->where('workspace_id', $this->user()->currentWorkspace->id)],
|
||||
];
|
||||
|
|
@ -106,6 +107,14 @@ public function withValidator(Validator $validator): void
|
|||
trans('posts.form.tiktok.privacy_required'),
|
||||
);
|
||||
}
|
||||
|
||||
if ($platformEnum === Platform::Pinterest
|
||||
&& blank(data_get($platform, 'meta.board_id'))) {
|
||||
$validator->errors()->add(
|
||||
"platforms.{$i}.meta.board_id",
|
||||
trans('posts.form.pinterest.board_required'),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carousel',
|
||||
],
|
||||
'board' => 'Board',
|
||||
'select_board' => 'Select a board',
|
||||
'no_boards' => 'No Pinterest boards found. Create one in your Pinterest account first.',
|
||||
'search_board' => 'Search boards...',
|
||||
'no_board_found' => 'No board matches your search.',
|
||||
'board_required' => 'Select a Pinterest board to publish this post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Pick a post type to continue.',
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carrusel',
|
||||
],
|
||||
'board' => 'Tablero',
|
||||
'select_board' => 'Selecciona un tablero',
|
||||
'no_boards' => 'No se encontraron tableros de Pinterest. Crea uno en tu cuenta de Pinterest primero.',
|
||||
'search_board' => 'Buscar tableros...',
|
||||
'no_board_found' => 'Ningún tablero coincide con tu búsqueda.',
|
||||
'board_required' => 'Selecciona un tablero de Pinterest para publicar este post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Elige un tipo de publicación para continuar.',
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -147,6 +147,12 @@
|
|||
'video_pin' => 'Video Pin',
|
||||
'carousel' => 'Carrossel',
|
||||
],
|
||||
'board' => 'Quadro',
|
||||
'select_board' => 'Selecione um quadro',
|
||||
'no_boards' => 'Nenhum quadro do Pinterest encontrado. Crie um na sua conta do Pinterest primeiro.',
|
||||
'search_board' => 'Pesquisar quadros...',
|
||||
'no_board_found' => 'Nenhum quadro encontrado.',
|
||||
'board_required' => 'Selecione um quadro do Pinterest para publicar este post.',
|
||||
],
|
||||
'warnings' => [
|
||||
'no_variant' => 'Escolha um tipo de publicação para continuar.',
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ import { IconAlertTriangle, IconChevronDown, IconChevronUp } from '@tabler/icons
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Avatar } from '@/components/ui/avatar';
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxAnchor,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
} from '@/components/ui/combobox';
|
||||
import { getMediaValidationWarning, type MediaItem } from '@/composables/useMedia';
|
||||
import { getPlatformLogo } from '@/composables/usePlatformLogo';
|
||||
import { ContentType } from '@/enums/content-type';
|
||||
|
|
@ -15,10 +25,17 @@ interface SocialAccount {
|
|||
avatar_url: string | null;
|
||||
}
|
||||
|
||||
interface BoardOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
socialAccount: SocialAccount | null;
|
||||
contentType: string;
|
||||
media: MediaItem[];
|
||||
boards: Array<{ id: string; name: string }>;
|
||||
meta: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const emit = defineEmits<{
|
||||
'update:contentType': [value: string];
|
||||
'update:meta': [value: Record<string, any>];
|
||||
}>();
|
||||
|
||||
const open = ref(false);
|
||||
|
|
@ -44,6 +62,15 @@ const pickVariant = (value: string) => {
|
|||
};
|
||||
|
||||
const warning = computed(() => getMediaValidationWarning(props.contentType, props.media));
|
||||
|
||||
const boardOptions = computed<BoardOption[]>(() =>
|
||||
props.boards.map((b) => ({ value: b.id, label: b.name })),
|
||||
);
|
||||
|
||||
const selectedBoard = computed<BoardOption | undefined>({
|
||||
get: () => boardOptions.value.find((b) => b.value === props.meta?.board_id),
|
||||
set: (board) => emit('update:meta', { ...props.meta, board_id: board?.value ?? null }),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -99,6 +126,51 @@ const warning = computed(() => getMediaValidationWarning(props.contentType, prop
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<p class="text-[11px] font-black uppercase tracking-widest text-foreground/60">{{ $t('posts.form.pinterest.board') }}</p>
|
||||
<p
|
||||
v-if="boards.length === 0"
|
||||
class="flex items-start gap-2 rounded-lg border-2 border-foreground/30 bg-foreground/5 p-2 text-xs font-semibold text-foreground/60"
|
||||
>
|
||||
<IconAlertTriangle class="mt-0.5 size-3.5 shrink-0" />
|
||||
{{ $t('posts.form.pinterest.no_boards') }}
|
||||
</p>
|
||||
<Combobox
|
||||
v-else
|
||||
v-model="selectedBoard"
|
||||
:display-value="(b: any) => b?.label ?? ''"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<ComboboxAnchor class="w-full">
|
||||
<ComboboxTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between rounded-lg border-2 border-foreground/30 bg-card px-3 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span :class="selectedBoard ? 'text-foreground' : 'text-foreground/50'">
|
||||
{{ selectedBoard ? selectedBoard.label : $t('posts.form.pinterest.select_board') }}
|
||||
</span>
|
||||
<IconChevronDown class="size-4 shrink-0 text-foreground/60" />
|
||||
</button>
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
<ComboboxList>
|
||||
<ComboboxInput :placeholder="$t('posts.form.pinterest.search_board')" />
|
||||
<ComboboxEmpty>{{ $t('posts.form.pinterest.no_board_found') }}</ComboboxEmpty>
|
||||
<ComboboxGroup>
|
||||
<ComboboxItem
|
||||
v-for="board in boardOptions"
|
||||
:key="board.value"
|
||||
:value="board"
|
||||
>
|
||||
{{ board.label }}
|
||||
</ComboboxItem>
|
||||
</ComboboxGroup>
|
||||
</ComboboxList>
|
||||
</Combobox>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="warning"
|
||||
class="flex items-start gap-2 rounded-lg border-2 border-foreground bg-rose-50 p-2 text-xs font-semibold text-rose-700"
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const props = defineProps<{
|
|||
labels: { id: string; name: string; color: string }[];
|
||||
selectedLabelIds: string[];
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, Array<{ id: string; name: string }>> | null;
|
||||
isReadOnly: boolean;
|
||||
authUserId: string;
|
||||
initialHighlightCommentId: string | null;
|
||||
|
|
@ -127,6 +128,7 @@ defineExpose({
|
|||
:platform-content-types="platformContentTypes"
|
||||
:platform-issues="platformIssues"
|
||||
:tiktok-creator-infos="tiktokCreatorInfos"
|
||||
:pinterest-boards="pinterestBoards"
|
||||
:media="media"
|
||||
@toggle-platform="(id) => emit('toggle-platform', id)"
|
||||
@toggle-label="(id) => emit('toggle-label', id)"
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ const props = defineProps<{
|
|||
platformContentTypes: Record<string, string>;
|
||||
platformIssues?: Record<string, string>;
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
pinterestBoards?: Record<string, Array<{ id: string; name: string }>> | null;
|
||||
media?: MediaItem[];
|
||||
}>();
|
||||
|
||||
|
|
@ -134,6 +135,9 @@ const getPublishConfig = (pp: PostPlatform): Record<string, any> | null =>
|
|||
const getCreatorInfo = (pp: PostPlatform): TikTokCreatorInfo | null =>
|
||||
pp.social_account_id ? props.tiktokCreatorInfos?.[pp.social_account_id] ?? null : null;
|
||||
|
||||
const getBoards = (pp: PostPlatform): Array<{ id: string; name: string }> =>
|
||||
pp.social_account_id ? props.pinterestBoards?.[pp.social_account_id] ?? [] : [];
|
||||
|
||||
const videoDurationSec = computed(() => {
|
||||
const video = props.media?.find((m) => m.type === 'video' || m.mime_type?.startsWith('video/'));
|
||||
const duration = video?.meta?.duration;
|
||||
|
|
@ -321,8 +325,11 @@ const getPlatformAvatar = (pp: PostPlatform): string | null =>
|
|||
:social-account="pp.social_account"
|
||||
:content-type="platformContentTypes[pp.id] ?? ''"
|
||||
:media="media ?? []"
|
||||
:boards="getBoards(pp)"
|
||||
:meta="platformMeta[pp.id] ?? {}"
|
||||
:disabled="isReadOnly"
|
||||
@update:content-type="emit('update:platformContentType', pp.id, $event)"
|
||||
@update:meta="emit('update:platformMeta', pp.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const props = defineProps<{
|
|||
post: Post;
|
||||
socialAccounts: SocialAccount[];
|
||||
platformConfigs: Record<string, any>;
|
||||
pinterestBoards: any[];
|
||||
pinterestBoards: Record<string, Array<{ id: string; name: string }>>;
|
||||
tiktokCreatorInfos?: Record<string, TikTokCreatorInfo> | null;
|
||||
labels: { id: string; name: string; color: string }[];
|
||||
signatures: { id: string; name: string; content: string }[];
|
||||
|
|
@ -576,6 +576,7 @@ usePostEcho(post.value.id, '.post.comment.created', (e: any) => {
|
|||
:labels="labels"
|
||||
:selected-label-ids="selectedLabelIds"
|
||||
:tiktok-creator-infos="tiktokCreatorInfos"
|
||||
:pinterest-boards="pinterestBoards"
|
||||
:is-read-only="isLocked"
|
||||
:auth-user-id="authUserId"
|
||||
:initial-highlight-comment-id="initialHighlightCommentId"
|
||||
|
|
|
|||
|
|
@ -113,6 +113,41 @@
|
|||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher throws exception for carousel when no board id', function () {
|
||||
$this->postPlatform->update([
|
||||
'content_type' => ContentType::PinterestCarousel,
|
||||
'meta' => [],
|
||||
]);
|
||||
$this->socialAccount->update(['meta' => []]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/img1.jpg', 'url' => 'https://example.com/img1.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'img1.jpg'],
|
||||
['id' => 'm2', 'path' => 'media/img2.jpg', 'url' => 'https://example.com/img2.jpg', 'mime_type' => 'image/jpeg', 'original_filename' => 'img2.jpg'],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher throws exception for video pin when no board id', function () {
|
||||
$this->postPlatform->update([
|
||||
'content_type' => ContentType::PinterestVideoPin,
|
||||
'meta' => [],
|
||||
]);
|
||||
$this->socialAccount->update(['meta' => []]);
|
||||
|
||||
$this->post->update([
|
||||
'media' => [
|
||||
['id' => 'm1', 'path' => 'media/video.mp4', 'url' => 'https://example.com/video.mp4', 'mime_type' => 'video/mp4', 'original_filename' => 'video.mp4'],
|
||||
],
|
||||
]);
|
||||
|
||||
expect(fn () => $this->publisher->publish($this->postPlatform))
|
||||
->toThrow(Exception::class, 'Pinterest board_id is required');
|
||||
});
|
||||
|
||||
test('pinterest publisher uses default board id from account', function () {
|
||||
$this->postPlatform->update(['meta' => []]); // No board_id in post meta
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,276 @@
|
|||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.privacy_level']);
|
||||
});
|
||||
|
||||
test('publishing a pinterest post without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest post with board_id passes board validation', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => ['board_id' => '123456789'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.board_id']);
|
||||
});
|
||||
|
||||
test('scheduling a pinterest post without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Scheduled->value,
|
||||
'scheduled_at' => now()->addDay()->toIso8601String(),
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest carousel without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterestCarousel()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'img-1',
|
||||
'path' => 'media/2026-01/img1.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/img1.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'img1.jpg',
|
||||
],
|
||||
[
|
||||
'id' => 'img-2',
|
||||
'path' => 'media/2026-01/img2.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/img2.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'img2.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestCarousel->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('publishing a pinterest video pin without board_id is rejected', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterestVideoPin()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $this->mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestVideoPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
});
|
||||
|
||||
test('pinterest board error does not block other platforms in multi-platform publish', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$linkedinAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
]);
|
||||
$linkedinPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $linkedinAccount->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
'content_type' => ContentType::LinkedInPost,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$mediaPayload = [
|
||||
[
|
||||
'id' => 'test-image',
|
||||
'path' => 'media/2026-01/pin.jpg',
|
||||
'url' => 'https://example.com/media/2026-01/pin.jpg',
|
||||
'type' => 'image',
|
||||
'mime_type' => 'image/jpeg',
|
||||
'original_filename' => 'pin.jpg',
|
||||
],
|
||||
];
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Publishing->value,
|
||||
'media' => $mediaPayload,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
[
|
||||
'id' => $linkedinPlatform->id,
|
||||
'content_type' => ContentType::LinkedInPost->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('platforms.0.meta.board_id');
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.1.meta.board_id']);
|
||||
});
|
||||
|
||||
test('saving a pinterest post as draft without board_id skips the board rule', function () {
|
||||
$pinterestAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::Pinterest,
|
||||
]);
|
||||
$pinterestPlatform = PostPlatform::factory()->pinterest()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $pinterestAccount->id,
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
'status' => Status::Draft->value,
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $pinterestPlatform->id,
|
||||
'content_type' => ContentType::PinterestPin->value,
|
||||
'meta' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionDoesntHaveErrors(['platforms.0.meta.board_id']);
|
||||
});
|
||||
|
||||
test('saving a tiktok post as draft without privacy_level skips the privacy_level rule', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->put(route('app.posts.update', $this->post), [
|
||||
|
|
|
|||
Loading…
Reference in a new issue