trypost/tests/Feature/Mcp/PostToolTest.php

279 lines
9.5 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Enums\SocialAccount\Platform;
use App\Enums\UserWorkspace\Role;
use App\Mcp\Servers\TryPostServer;
use App\Mcp\Tools\Post\CreatePostTool;
use App\Mcp\Tools\Post\DeletePostTool;
use App\Mcp\Tools\Post\GetPostTool;
use App\Mcp\Tools\Post\ListPostsTool;
use App\Mcp\Tools\Post\UpdatePostTool;
use App\Models\Post;
use App\Models\PostPlatform;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
test: add coverage for validation rules across REST + MCP + custom rules The previous suite asserted happy paths and a couple of basic field omissions but didn't probe the rules themselves. Adds 26 tests across 5 files: REST API (tests/Feature/Api/PostApiTest.php) — 9 new: - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - platforms[].id from another post on update (cross-post leak) - content_type mismatched with the post_platform on update - status=scheduled requires future scheduled_at - status=draft works with no scheduled_at - past scheduled_at on store MCP create-post-tool (tests/Feature/Mcp/PostToolTest.php) — 5 new: - inactive social account - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - already had: scheduled_at past MCP update-post-tool (tests/Feature/Mcp/PostPublishToolTest.php) — 2 new: - platforms[].id from another post (regression for the new Rule::exists scoping) - content_type mismatched with the post_platform MCP attach-media-from-url-tool (tests/Feature/Mcp/AttachMediaFromUrlToolTest.php) — 3 new: - non-http(s) scheme (ftp://...) - malformed url string - more than 10 URLs per call Custom rules unit tests — 2 new files: - ContentTypeMatchesPlatformTest covers happy path, cross-platform mismatch, the Instagram + InstagramFacebook compatibility bridge, and the no-op cases (missing account_id, unknown content_type — those are caught by Rule::in elsewhere). - ContentTypeMatchesPostPlatformTest covers the equivalent shape for the update flow that pivots through post_platform.id.
2026-05-04 16:31:44 +00:00
use App\Models\WorkspaceLabel;
use Illuminate\Testing\Fluent\AssertableJson;
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->socialAccount = SocialAccount::factory()->create([
'workspace_id' => $this->workspace->id,
'platform' => Platform::LinkedIn,
]);
});
test('list posts returns wrapped posts array with PostResource shape', function () {
Post::factory()->count(3)->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$response = TryPostServer::actingAs($this->user)
->tool(ListPostsTool::class, []);
$response->assertOk()
->assertStructuredContent(function (AssertableJson $json) {
$json->has('posts', 3, function (AssertableJson $post) {
$post->hasAll(['id', 'content', 'media', 'status', 'scheduled_at', 'published_at', 'platforms', 'labels', 'created_at', 'updated_at'])
->missing('user_id')
->missing('workspace_id');
});
});
});
test('list posts only returns own workspace posts', function () {
Post::factory()->create(['workspace_id' => $this->workspace->id, 'user_id' => $this->user->id]);
$otherWorkspace = Workspace::factory()->create();
Post::factory()->create(['workspace_id' => $otherWorkspace->id, 'user_id' => $this->user->id]);
$response = TryPostServer::actingAs($this->user)
->tool(ListPostsTool::class, []);
$response->assertOk()
->assertStructuredContent(function (AssertableJson $json) {
$json->has('posts', 1)->etc();
});
});
test('get post returns PostResource shape', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
'content' => 'Hello world',
]);
$response = TryPostServer::actingAs($this->user)
->tool(GetPostTool::class, ['post_id' => $post->id]);
$response->assertOk()
->assertStructuredContent(function (AssertableJson $json) use ($post) {
$json->where('id', $post->id)
->where('content', 'Hello world')
->missing('user_id')
->missing('workspace_id')
->etc();
});
});
test('get post 404 from another workspace', function () {
$otherWorkspace = Workspace::factory()->create();
$post = Post::factory()->create(['workspace_id' => $otherWorkspace->id, 'user_id' => $this->user->id]);
$response = TryPostServer::actingAs($this->user)
->tool(GetPostTool::class, ['post_id' => $post->id]);
$response->assertHasErrors(['Post not found.']);
});
test('create post with content and date', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'content' => 'My new post',
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
'scheduled_at' => '2099-12-31T15:30:00Z',
]);
$response->assertOk()
->assertStructuredContent(function (AssertableJson $json) {
$json->where('content', 'My new post')
->where('status', 'draft')
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
->where('scheduled_at', '2099-12-31 15:30:00')
->etc();
});
expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(1);
});
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
test('create post with platforms enables only those', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'content' => 'with platforms',
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
],
]);
$response->assertOk();
$post = Post::where('workspace_id', $this->workspace->id)->first();
$enabled = $post->postPlatforms()->where('enabled', true)->get();
expect($enabled)->toHaveCount(1);
expect($enabled->first()->social_account_id)->toBe($this->socialAccount->id);
expect($enabled->first()->content_type->value)->toBe('linkedin_post');
});
test('create post without args creates empty draft for today', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, []);
$response->assertOk()
->assertStructuredContent(function (AssertableJson $json) {
$json->where('content', '')
->where('status', 'draft')
->etc();
});
expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(1);
});
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
test('create post rejects scheduled_at in the past', function () {
$response = TryPostServer::actingAs($this->user)
feat: complete create + publish post flow via MCP and REST API Lets ChatGPT (MCP) and external clients (REST API) drive the full lifecycle of a post — create with platform selection, attach media from URLs, schedule or publish immediately, and fetch engagement metrics — without touching the web UI. MCP tools added: UpdatePostTool, PublishPostTool, AttachMediaFromUrlTool, ListContentTypesTool, GetPostMetricsTool, PreviewPostTool. CreatePostTool now accepts platforms[] + scheduled_at + label_ids; ListPostsTool gains status/search/limit filters. REST endpoints added: POST /api/posts/{post}/media, GET /api/posts/{post}/metrics, GET /api/posts/{post}/preview, GET /api/content-types. Also fixes a silent CreatePost::execute bug — the action validated platforms[] but ignored it, so REST callers never saw their selection persisted. Adds cross validation rules (ContentTypeMatchesPlatform / ContentTypeMatchesPostPlatform) so a LinkedIn account can't be saddled with x_post, and rejects inactive social accounts during validation instead of failing silently downstream. Shared services (PostMetricsFetcher, PostPreviewer, MediaAttacher) back both MCP tools and REST controllers so behaviour stays aligned. New Resources (PlatformContentTypesResource, PostMetricsResource, PostPreviewResource, PostMediaAttachResource) keep controllers free of inline model mapping. Suite: 1.332 passing, 0 failing — covers web (PostControllerTest), REST (PostApiTest, PlatformApiTest, PostMediaApiTest), MCP (66 tool tests), and the publish job (PublishToSocialPlatformTest). Removes /docs from git tracking and TIKTOK_REVIEW_VIDEO_SCRIPT.md.
2026-05-04 11:12:28 +00:00
->tool(CreatePostTool::class, ['scheduled_at' => '2020-01-01T00:00:00Z']);
$response->assertHasErrors();
});
test: add coverage for validation rules across REST + MCP + custom rules The previous suite asserted happy paths and a couple of basic field omissions but didn't probe the rules themselves. Adds 26 tests across 5 files: REST API (tests/Feature/Api/PostApiTest.php) — 9 new: - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - platforms[].id from another post on update (cross-post leak) - content_type mismatched with the post_platform on update - status=scheduled requires future scheduled_at - status=draft works with no scheduled_at - past scheduled_at on store MCP create-post-tool (tests/Feature/Mcp/PostToolTest.php) — 5 new: - inactive social account - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - already had: scheduled_at past MCP update-post-tool (tests/Feature/Mcp/PostPublishToolTest.php) — 2 new: - platforms[].id from another post (regression for the new Rule::exists scoping) - content_type mismatched with the post_platform MCP attach-media-from-url-tool (tests/Feature/Mcp/AttachMediaFromUrlToolTest.php) — 3 new: - non-http(s) scheme (ftp://...) - malformed url string - more than 10 URLs per call Custom rules unit tests — 2 new files: - ContentTypeMatchesPlatformTest covers happy path, cross-platform mismatch, the Instagram + InstagramFacebook compatibility bridge, and the no-op cases (missing account_id, unknown content_type — those are caught by Rule::in elsewhere). - ContentTypeMatchesPostPlatformTest covers the equivalent shape for the update flow that pivots through post_platform.id.
2026-05-04 16:31:44 +00:00
test('create post rejects an inactive social account', function () {
$inactive = SocialAccount::factory()->create([
'workspace_id' => $this->workspace->id,
'platform' => Platform::LinkedIn,
'is_active' => false,
]);
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $inactive->id, 'content_type' => 'linkedin_post'],
],
]);
$response->assertHasErrors();
});
test('create post rejects a content_type not in the enum', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'made_up_type'],
],
]);
$response->assertHasErrors();
});
test('create post rejects instagram_carousel — carousel is not a stored content_type', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'instagram_carousel'],
],
]);
$response->assertHasErrors();
});
test('update post rejects instagram_carousel — carousel is not a stored content_type', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$platform = PostPlatform::factory()->create([
'post_id' => $post->id,
'social_account_id' => $this->socialAccount->id,
]);
$response = TryPostServer::actingAs($this->user)
->tool(UpdatePostTool::class, [
'post_id' => $post->id,
'platforms' => [
['id' => $platform->id, 'content_type' => 'instagram_carousel'],
],
]);
$response->assertHasErrors();
});
test: add coverage for validation rules across REST + MCP + custom rules The previous suite asserted happy paths and a couple of basic field omissions but didn't probe the rules themselves. Adds 26 tests across 5 files: REST API (tests/Feature/Api/PostApiTest.php) — 9 new: - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - platforms[].id from another post on update (cross-post leak) - content_type mismatched with the post_platform on update - status=scheduled requires future scheduled_at - status=draft works with no scheduled_at - past scheduled_at on store MCP create-post-tool (tests/Feature/Mcp/PostToolTest.php) — 5 new: - inactive social account - content_type not in the enum - content_type mismatched with the social account's platform - label_id from another workspace - already had: scheduled_at past MCP update-post-tool (tests/Feature/Mcp/PostPublishToolTest.php) — 2 new: - platforms[].id from another post (regression for the new Rule::exists scoping) - content_type mismatched with the post_platform MCP attach-media-from-url-tool (tests/Feature/Mcp/AttachMediaFromUrlToolTest.php) — 3 new: - non-http(s) scheme (ftp://...) - malformed url string - more than 10 URLs per call Custom rules unit tests — 2 new files: - ContentTypeMatchesPlatformTest covers happy path, cross-platform mismatch, the Instagram + InstagramFacebook compatibility bridge, and the no-op cases (missing account_id, unknown content_type — those are caught by Rule::in elsewhere). - ContentTypeMatchesPostPlatformTest covers the equivalent shape for the update flow that pivots through post_platform.id.
2026-05-04 16:31:44 +00:00
test('create post rejects a content_type that does not match the social account platform', function () {
// x_post on a LinkedIn account — ContentTypeMatchesPlatform should reject.
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'x_post'],
],
]);
$response->assertHasErrors();
});
test('create post rejects a label_id from another workspace', function () {
$otherWorkspace = Workspace::factory()->create();
$foreignLabel = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]);
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
],
'label_ids' => [$foreignLabel->id],
]);
$response->assertHasErrors();
});
test('delete post removes from db', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$response = TryPostServer::actingAs($this->user)
->tool(DeletePostTool::class, ['post_id' => $post->id]);
$response->assertOk()
->assertStructuredContent(['deleted' => true]);
expect(Post::find($post->id))->toBeNull();
});
test('delete post 404 from another workspace', function () {
$otherWorkspace = Workspace::factory()->create();
$post = Post::factory()->create(['workspace_id' => $otherWorkspace->id, 'user_id' => $this->user->id]);
$response = TryPostServer::actingAs($this->user)
->tool(DeletePostTool::class, ['post_id' => $post->id]);
$response->assertHasErrors(['Post not found.']);
});
test('get post validates post_id required', function () {
$response = TryPostServer::actingAs($this->user)
->tool(GetPostTool::class, []);
$response->assertHasErrors();
});
test('delete post validates post_id required', function () {
$response = TryPostServer::actingAs($this->user)
->tool(DeletePostTool::class, []);
$response->assertHasErrors();
});