feat(api,mcp): full per-platform meta parity for posts

Centralize per-platform PostPlatform.meta validation in PostPlatformMetaRules
(shared by web, REST API and MCP). The API and MCP previously only accepted
aspect_ratio, silently stripping channel_id/board_id/privacy_level and the rest
via validated(), so Discord/Pinterest/TikTok couldn't be configured or published
through those entry points. Now all per-platform meta is accepted and persisted,
required-on-publish is enforced on API update (TikTok privacy, Pinterest board,
Discord channel), and the MCP publish tool guards a post's stored meta before
dispatching. Adds API + MCP tests and a PostPlatform discord() factory state.
This commit is contained in:
Paulo Castellano 2026-06-16 17:39:07 -03:00
parent 206d01f77e
commit bdea56d5e1
10 changed files with 383 additions and 59 deletions

View file

@ -4,12 +4,12 @@
namespace App\Http\Requests\Api\Post;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Models\SocialAccount;
use App\Rules\ContentFitsPlatformLimits;
use App\Rules\ContentTypeMatchesPlatform;
use App\Support\PostPlatformMetaRules;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
@ -51,7 +51,7 @@ public function rules(): array
new ContentTypeMatchesPlatform,
],
'platforms.*.meta' => ['sometimes', 'nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
...PostPlatformMetaRules::rules(),
'scheduled_at' => ['nullable', 'date', 'after:now'],
'label_ids' => ['sometimes', 'array'],
'label_ids.*' => [

View file

@ -5,16 +5,17 @@
namespace App\Http\Requests\Api\Post;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Rules\ContentFitsPlatformLimits;
use App\Rules\ContentTypeMatchesPostPlatform;
use App\Support\PostPlatformMetaRules;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class UpdatePostRequest extends FormRequest
{
@ -52,7 +53,7 @@ public function rules(): array
new ContentTypeMatchesPostPlatform,
],
'platforms.*.meta' => ['nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
...PostPlatformMetaRules::rules(),
'scheduled_at' => [
'nullable',
'date',
@ -66,6 +67,23 @@ public function rules(): array
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator): void {
if (! in_array($this->input('status'), [Status::Scheduled->value, Status::Publishing->value], true)) {
return;
}
$platformsById = $this->resolveSelectedPlatforms();
PostPlatformMetaRules::addRequiredOnPublishErrors(
$validator,
$this->input('platforms', []),
fn ($platform) => $platformsById[data_get($platform, 'id')] ?? null,
);
});
}
/**
* @return Collection<int|string, Platform>
*/

View file

@ -6,11 +6,11 @@
use App\Enums\Media\Source;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Rules\ContentFitsPlatformLimits;
use App\Rules\ContentTypeCompatibleWithMedia;
use App\Support\PostPlatformMetaRules;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
@ -71,28 +71,7 @@ public function rules(): array
Rule::when($enforcesMediaCompatibility, [new ContentTypeCompatibleWithMedia]),
],
'platforms.*.meta' => ['nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
'platforms.*.meta.privacy_level' => ['sometimes', 'nullable', 'string', Rule::in(['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY'])],
'platforms.*.meta.auto_add_music' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_comments' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_duet' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_stitch' => ['sometimes', 'boolean'],
'platforms.*.meta.is_aigc' => ['sometimes', 'boolean'],
'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'],
'platforms.*.meta.channel_id' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.channel_name' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.mentions' => ['sometimes', 'nullable', 'array'],
'platforms.*.meta.mentions.*.token' => ['required', 'string'],
'platforms.*.meta.mentions.*.label' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.embeds' => ['sometimes', 'nullable', 'array', 'max:10'],
'platforms.*.meta.embeds.*.title' => ['sometimes', 'nullable', 'string', 'max:256'],
'platforms.*.meta.embeds.*.description' => ['sometimes', 'nullable', 'string', 'max:4096'],
'platforms.*.meta.embeds.*.url' => ['sometimes', 'nullable', 'url'],
'platforms.*.meta.embeds.*.image' => ['sometimes', 'nullable', 'url'],
'platforms.*.meta.embeds.*.color' => ['sometimes', 'nullable', 'string', 'regex:/^#?[0-9A-Fa-f]{6}$/'],
...PostPlatformMetaRules::rules(),
'label_ids' => ['sometimes', 'array'],
'label_ids.*' => ['uuid', Rule::exists('workspace_labels', 'id')->where('workspace_id', $this->user()->currentWorkspace->id)],
];
@ -113,32 +92,11 @@ public function withValidator(Validator $validator): void
->whereIn('id', $ids)
->pluck('platform', 'id');
foreach ($platforms as $i => $platform) {
$platformEnum = $platformsById[data_get($platform, 'id')] ?? null;
if ($platformEnum === Platform::TikTok
&& blank(data_get($platform, 'meta.privacy_level'))) {
$validator->errors()->add(
"platforms.{$i}.meta.privacy_level",
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'),
);
}
if ($platformEnum === Platform::Discord
&& blank(data_get($platform, 'meta.channel_id'))) {
$validator->errors()->add(
"platforms.{$i}.meta.channel_id",
trans('posts.form.discord.channel_required'),
);
}
}
PostPlatformMetaRules::addRequiredOnPublishErrors(
$validator,
$platforms,
fn ($platform) => $platformsById[data_get($platform, 'id')] ?? null,
);
});
}

View file

@ -5,10 +5,10 @@
namespace App\Mcp\Tools\Post;
use App\Actions\Post\CreatePost;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Http\Resources\Api\PostResource;
use App\Rules\ContentTypeMatchesPlatform;
use App\Support\PostPlatformMetaRules;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Validation\Rule;
use Laravel\Mcp\Request;
@ -39,7 +39,7 @@ public function handle(Request $request): ResponseFactory
],
'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPlatform],
'platforms.*.meta' => ['sometimes', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
...PostPlatformMetaRules::rules(),
]);
$post = CreatePost::execute($workspace, $request->user(), $validated);
@ -61,7 +61,7 @@ public function schema(JsonSchema $schema): array
->items($schema->object(fn ($p) => [
'social_account_id' => $p->string()->required()->description('UUID of the connected social account.'),
'content_type' => $p->string()->required()->description('Format for this platform (e.g. linkedin_post, x_post, instagram_feed).'),
'meta' => $p->object()->description('Per-platform metadata (e.g. aspect_ratio: 1:1|4:5|16:9|original for Instagram/Facebook feed images).'),
'meta' => $p->object()->description('Per-platform metadata. Instagram/Facebook: aspect_ratio (1:1|4:5|16:9|original). TikTok: privacy_level (required to publish) + flags (allow_comments, allow_duet, allow_stitch, disclose, brand_content_toggle, brand_organic_toggle, is_aigc, auto_add_music). Pinterest: board_id (required to publish). Discord: channel_id (required to publish), mentions ([{token,label}]), embeds ([{title,description,url,image,color}]).'),
]))
->description('Platforms to publish on. Accounts not listed remain available but disabled.'),
];

View file

@ -9,6 +9,7 @@
use App\Enums\Post\Status;
use App\Http\Resources\Api\PostResource;
use App\Models\Post;
use App\Support\PostPlatformMetaRules;
use App\Support\PostStatusRules;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
@ -41,6 +42,8 @@ public function handle(Request $request): Response|ResponseFactory
return Response::error('Post has no enabled platforms. Use update-post-tool to enable at least one platform first.');
}
PostPlatformMetaRules::assertStoredPostPublishable($post);
$scheduledAt = data_get($validated, 'scheduled_at');
$result = UpdatePost::execute($workspace, $post, [

View file

@ -7,11 +7,11 @@
use App\Actions\Post\UpdatePost;
use App\Enums\Post\Action as PostAction;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Http\Resources\Api\PostResource;
use App\Models\Post;
use App\Rules\ContentTypeMatchesPostPlatform;
use App\Support\PostPlatformMetaRules;
use App\Support\PostStatusRules;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Validation\Rule;
@ -50,7 +50,7 @@ public function handle(Request $request): Response|ResponseFactory
],
'platforms.*.content_type' => ['sometimes', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPostPlatform],
'platforms.*.meta' => ['sometimes', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
...PostPlatformMetaRules::rules(),
]);
$payload = collect($validated)->except('post_id')->all();
@ -84,7 +84,7 @@ public function schema(JsonSchema $schema): array
->items($schema->object(fn ($p) => [
'id' => $p->string()->required()->description('UUID of the post_platform row (from get-post-tool / list-posts-tool).'),
'content_type' => $p->string()->description('New content_type for this platform.'),
'meta' => $p->object()->description('Per-platform metadata override (e.g. aspect_ratio, board_id for Pinterest).'),
'meta' => $p->object()->description('Per-platform metadata override. Instagram/Facebook: aspect_ratio. TikTok: privacy_level (required to publish) + flags. Pinterest: board_id (required to publish). Discord: channel_id (required to publish), mentions, embeds. Merged with existing meta.'),
]))
->description('Platforms to enable for publishing. Any platform NOT listed will be disabled. Pass an empty array to disable all.'),
];

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace App\Support;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\SocialAccount\Platform;
use App\Models\Post;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Validator;
/**
* Single source of truth for per-platform `PostPlatform.meta` validation, shared
* by the web, public REST API, and MCP post create/update flows so every entry
* point accepts the same per-platform settings and enforces the same
* required-on-publish rules.
*/
class PostPlatformMetaRules
{
/**
* TikTok content visibility options.
*
* @var array<int, string>
*/
public const TIKTOK_PRIVACY_LEVELS = [
'PUBLIC_TO_EVERYONE',
'MUTUAL_FOLLOW_FRIENDS',
'FOLLOWER_OF_CREATOR',
'SELF_ONLY',
];
/**
* Validation rules for the `platforms.*.meta.*` keys. The caller keeps its own
* `platforms.*.meta` (array) parent rule; these are the per-platform sub-keys.
*
* @return array<string, mixed>
*/
public static function rules(): array
{
return [
// Instagram / Facebook
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
// TikTok
'platforms.*.meta.privacy_level' => ['sometimes', 'nullable', 'string', Rule::in(self::TIKTOK_PRIVACY_LEVELS)],
'platforms.*.meta.auto_add_music' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_comments' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_duet' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_stitch' => ['sometimes', 'boolean'],
'platforms.*.meta.is_aigc' => ['sometimes', 'boolean'],
'platforms.*.meta.disclose' => ['sometimes', 'boolean'],
'platforms.*.meta.brand_content_toggle' => ['sometimes', 'boolean'],
'platforms.*.meta.brand_organic_toggle' => ['sometimes', 'boolean'],
// Pinterest
'platforms.*.meta.board_id' => ['sometimes', 'nullable', 'string'],
// Discord
'platforms.*.meta.channel_id' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.channel_name' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.mentions' => ['sometimes', 'nullable', 'array'],
'platforms.*.meta.mentions.*.token' => ['required', 'string'],
'platforms.*.meta.mentions.*.label' => ['sometimes', 'nullable', 'string'],
'platforms.*.meta.embeds' => ['sometimes', 'nullable', 'array', 'max:10'],
'platforms.*.meta.embeds.*.title' => ['sometimes', 'nullable', 'string', 'max:256'],
'platforms.*.meta.embeds.*.description' => ['sometimes', 'nullable', 'string', 'max:4096'],
'platforms.*.meta.embeds.*.url' => ['sometimes', 'nullable', 'url'],
'platforms.*.meta.embeds.*.image' => ['sometimes', 'nullable', 'url'],
'platforms.*.meta.embeds.*.color' => ['sometimes', 'nullable', 'string', 'regex:/^#?[0-9A-Fa-f]{6}$/'],
];
}
/**
* Adds validation errors for per-platform meta that becomes mandatory once a
* post is published or scheduled (TikTok privacy, Pinterest board, Discord
* channel), based on the submitted request platforms. The caller resolves each
* platform row to its Platform enum, since that lookup differs between create
* (by social account) and update (by post platform).
*
* @param array<int, mixed> $platforms
* @param callable(mixed, int): ?Platform $resolvePlatform
*/
public static function addRequiredOnPublishErrors(Validator $validator, array $platforms, callable $resolvePlatform): void
{
foreach ($platforms as $index => $platform) {
$violation = self::requiredMetaViolation($resolvePlatform($platform, $index), data_get($platform, 'meta'));
if ($violation !== null) {
[$field, $message] = $violation;
$validator->errors()->add("platforms.{$index}.meta.{$field}", $message);
}
}
}
/**
* Asserts that every ENABLED platform already stored on a post has the meta it
* needs to publish. Used by entry points that publish a post's stored state
* without resubmitting platforms (e.g. the MCP publish tool), so a misconfigured
* post fails fast with a clear message instead of only at publish time.
*
* @throws ValidationException
*/
public static function assertStoredPostPublishable(Post $post): void
{
$errors = [];
foreach ($post->postPlatforms()->where('enabled', true)->get()->values() as $index => $postPlatform) {
$violation = self::requiredMetaViolation($postPlatform->platform, $postPlatform->meta);
if ($violation !== null) {
[$field, $message] = $violation;
$errors["platforms.{$index}.meta.{$field}"] = $message;
}
}
if ($errors !== []) {
throw ValidationException::withMessages($errors);
}
}
/**
* The missing required meta field for a platform about to publish, or null when
* nothing is missing. Single source of "what each platform requires to publish".
*
* @return array{0: string, 1: string}|null [field, message]
*/
private static function requiredMetaViolation(?Platform $platform, mixed $meta): ?array
{
return match (true) {
$platform === Platform::TikTok && blank(data_get($meta, 'privacy_level')) => ['privacy_level', trans('posts.form.tiktok.privacy_required')],
$platform === Platform::Pinterest && blank(data_get($meta, 'board_id')) => ['board_id', trans('posts.form.pinterest.board_required')],
$platform === Platform::Discord && blank(data_get($meta, 'channel_id')) => ['channel_id', trans('posts.form.discord.channel_required')],
default => null,
};
}
}

View file

@ -155,6 +155,14 @@ public function facebook(): static
]);
}
public function discord(): static
{
return $this->state(fn (array $attributes) => [
'platform' => Platform::Discord,
'content_type' => ContentType::DiscordMessage,
]);
}
public function facebookReel(): static
{
return $this->state(fn (array $attributes) => [

View file

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
use App\Enums\Post\Status as PostStatus;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Jobs\PublishPost;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
$result = createApiTestToken();
$this->user = $result['user'];
$this->workspace = $result['workspace'];
$this->plainToken = $result['plain_token'];
$this->headers = ['Authorization' => 'Bearer '.$this->plainToken];
$this->discordAccount = SocialAccount::factory()->discord()->create([
'workspace_id' => $this->workspace->id,
'platform_user_id' => '111222333',
]);
});
it('persists Discord channel, mentions and embeds meta on store', function () {
$this->withHeaders($this->headers)
->postJson(route('api.posts.store'), [
'content' => 'Hello Discord',
'platforms' => [[
'social_account_id' => $this->discordAccount->id,
'content_type' => ContentType::DiscordMessage->value,
'meta' => [
'channel_id' => '444555666',
'mentions' => [['token' => '@everyone', 'label' => '@everyone']],
'embeds' => [['title' => 'Release', 'color' => '#5865F2']],
],
]],
])
->assertCreated();
$meta = PostPlatform::where('social_account_id', $this->discordAccount->id)->sole()->meta;
expect($meta['channel_id'])->toBe('444555666')
->and($meta['mentions'])->toHaveCount(1)
->and($meta['embeds.0.title'] ?? data_get($meta, 'embeds.0.title'))->toBe('Release');
});
it('persists Pinterest board and TikTok privacy meta on store', function () {
$pinterest = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::Pinterest]);
$tiktok = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id, 'platform' => Platform::TikTok]);
$this->withHeaders($this->headers)
->postJson(route('api.posts.store'), [
'content' => 'Cross-platform',
'platforms' => [
['social_account_id' => $pinterest->id, 'content_type' => ContentType::PinterestPin->value, 'meta' => ['board_id' => 'board-99']],
['social_account_id' => $tiktok->id, 'content_type' => ContentType::TikTokVideo->value, 'meta' => ['privacy_level' => 'SELF_ONLY']],
],
])
->assertCreated();
expect(PostPlatform::where('social_account_id', $pinterest->id)->sole()->meta['board_id'])->toBe('board-99')
->and(PostPlatform::where('social_account_id', $tiktok->id)->sole()->meta['privacy_level'])->toBe('SELF_ONLY');
});
it('rejects publishing a Discord post without a channel', function () {
$post = Post::factory()->create(['workspace_id' => $this->workspace->id, 'user_id' => $this->user->id]);
$platform = PostPlatform::factory()->discord()->create([
'post_id' => $post->id,
'social_account_id' => $this->discordAccount->id,
'enabled' => true,
'meta' => [],
]);
$this->withHeaders($this->headers)
->putJson(route('api.posts.update', $post), [
'status' => PostStatus::Publishing->value,
'platforms' => [['id' => $platform->id]],
])
->assertStatus(422)
->assertJsonValidationErrors(['platforms.0.meta.channel_id']);
});
it('publishes a Discord post when the channel is set', function () {
Queue::fake();
$post = Post::factory()->create(['workspace_id' => $this->workspace->id, 'user_id' => $this->user->id]);
$platform = PostPlatform::factory()->discord()->create([
'post_id' => $post->id,
'social_account_id' => $this->discordAccount->id,
'enabled' => true,
'meta' => [],
]);
$this->withHeaders($this->headers)
->putJson(route('api.posts.update', $post), [
'status' => PostStatus::Publishing->value,
'platforms' => [['id' => $platform->id, 'meta' => ['channel_id' => '444555666']]],
])
->assertOk();
expect($platform->fresh()->meta['channel_id'])->toBe('444555666');
Queue::assertPushed(PublishPost::class);
});

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
use App\Enums\Post\Status as PostStatus;
use App\Enums\PostPlatform\ContentType;
use App\Enums\UserWorkspace\Role;
use App\Jobs\PublishPost;
use App\Mcp\Servers\TryPostServer;
use App\Mcp\Tools\Post\CreatePostTool;
use App\Mcp\Tools\Post\PublishPostTool;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
$this->user = User::factory()->create();
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]);
$this->user->update(['current_workspace_id' => $this->workspace->id]);
$this->discordAccount = SocialAccount::factory()->discord()->create([
'workspace_id' => $this->workspace->id,
'platform_user_id' => '111222333',
]);
});
test('create post persists Discord channel + embeds meta', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'content' => 'Hello Discord',
'platforms' => [[
'social_account_id' => $this->discordAccount->id,
'content_type' => ContentType::DiscordMessage->value,
'meta' => [
'channel_id' => '444555666',
'embeds' => [['title' => 'Release']],
],
]],
]);
$response->assertOk();
$meta = PostPlatform::where('social_account_id', $this->discordAccount->id)->sole()->meta;
expect($meta['channel_id'])->toBe('444555666')
->and(data_get($meta, 'embeds.0.title'))->toBe('Release');
});
test('publish post rejects a Discord platform without a channel', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
'status' => PostStatus::Draft,
]);
PostPlatform::factory()->discord()->create([
'post_id' => $post->id,
'social_account_id' => $this->discordAccount->id,
'enabled' => true,
'meta' => [],
]);
$response = TryPostServer::actingAs($this->user)
->tool(PublishPostTool::class, ['post_id' => $post->id]);
$response->assertHasErrors([__('posts.form.discord.channel_required')]);
});
test('publish post succeeds for a Discord platform with a channel', function () {
Queue::fake();
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
'status' => PostStatus::Draft,
]);
PostPlatform::factory()->discord()->create([
'post_id' => $post->id,
'social_account_id' => $this->discordAccount->id,
'enabled' => true,
'meta' => ['channel_id' => '444555666'],
]);
$response = TryPostServer::actingAs($this->user)
->tool(PublishPostTool::class, ['post_id' => $post->id]);
$response->assertOk();
Queue::assertPushed(PublishPost::class);
});