diff --git a/app/Http/Requests/Api/Post/StorePostRequest.php b/app/Http/Requests/Api/Post/StorePostRequest.php index 979a448c..303ae8ba 100644 --- a/app/Http/Requests/Api/Post/StorePostRequest.php +++ b/app/Http/Requests/Api/Post/StorePostRequest.php @@ -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.*' => [ diff --git a/app/Http/Requests/Api/Post/UpdatePostRequest.php b/app/Http/Requests/Api/Post/UpdatePostRequest.php index 939e4a5d..22487d0b 100644 --- a/app/Http/Requests/Api/Post/UpdatePostRequest.php +++ b/app/Http/Requests/Api/Post/UpdatePostRequest.php @@ -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 */ diff --git a/app/Http/Requests/App/Post/UpdatePostRequest.php b/app/Http/Requests/App/Post/UpdatePostRequest.php index 9e6d9c74..29c7343c 100644 --- a/app/Http/Requests/App/Post/UpdatePostRequest.php +++ b/app/Http/Requests/App/Post/UpdatePostRequest.php @@ -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, + ); }); } diff --git a/app/Mcp/Tools/Post/CreatePostTool.php b/app/Mcp/Tools/Post/CreatePostTool.php index 35437834..2fb51f25 100644 --- a/app/Mcp/Tools/Post/CreatePostTool.php +++ b/app/Mcp/Tools/Post/CreatePostTool.php @@ -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.'), ]; diff --git a/app/Mcp/Tools/Post/PublishPostTool.php b/app/Mcp/Tools/Post/PublishPostTool.php index ee95f439..da2feac9 100644 --- a/app/Mcp/Tools/Post/PublishPostTool.php +++ b/app/Mcp/Tools/Post/PublishPostTool.php @@ -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, [ diff --git a/app/Mcp/Tools/Post/UpdatePostTool.php b/app/Mcp/Tools/Post/UpdatePostTool.php index c3e26aae..aa0903ca 100644 --- a/app/Mcp/Tools/Post/UpdatePostTool.php +++ b/app/Mcp/Tools/Post/UpdatePostTool.php @@ -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.'), ]; diff --git a/app/Support/PostPlatformMetaRules.php b/app/Support/PostPlatformMetaRules.php new file mode 100644 index 00000000..65529b47 --- /dev/null +++ b/app/Support/PostPlatformMetaRules.php @@ -0,0 +1,138 @@ + + */ + 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 + */ + 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 $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, + }; + } +} diff --git a/database/factories/PostPlatformFactory.php b/database/factories/PostPlatformFactory.php index 508abec8..cb39e970 100644 --- a/database/factories/PostPlatformFactory.php +++ b/database/factories/PostPlatformFactory.php @@ -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) => [ diff --git a/tests/Feature/Api/PostApiPlatformMetaTest.php b/tests/Feature/Api/PostApiPlatformMetaTest.php new file mode 100644 index 00000000..aa06fbd5 --- /dev/null +++ b/tests/Feature/Api/PostApiPlatformMetaTest.php @@ -0,0 +1,107 @@ +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); +}); diff --git a/tests/Feature/Mcp/PostPlatformMetaToolTest.php b/tests/Feature/Mcp/PostPlatformMetaToolTest.php new file mode 100644 index 00000000..2cff7b8d --- /dev/null +++ b/tests/Feature/Mcp/PostPlatformMetaToolTest.php @@ -0,0 +1,92 @@ +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); +});