feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-07-24 14:19:01 +00:00
|
|
|
use App\Enums\Post\CreatedVia;
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
use App\Enums\Post\Status as PostStatus;
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
use App\Enums\PostPlatform\ContentType;
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
use App\Enums\SocialAccount\Platform;
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
use App\Jobs\PublishPost;
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
use App\Models\Post;
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
use App\Models\PostPlatform;
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
use App\Models\SocialAccount;
|
|
|
|
|
use App\Models\Workspace;
|
fix: address PR review findings — publish, REST store, SSRF, race
Code-review surfaced two correctness bugs and a security gap that
needed to land before merging.
- UpdatePost::execute disabled every platform when called without
a `platforms` key. PublishPostTool relied on that path, so every
publish-via-MCP queued a job whose handler then found nothing
enabled to publish to. Wrap the platform toggle in
`Arr::has($data, 'platforms')` (matches the existing label_ids
guard a few lines up). Add a regression assertion to
`PostPublishToolTest::publish post immediate dispatches PublishPost
job` that the previously-enabled platform stays enabled.
- StorePostRequest declared rules for only `platforms`,
`scheduled_at`, and `status`. `validated()` then stripped
`content`, `media`, and `label_ids`, so REST `POST /api/posts`
silently created empty drafts. Added rules for content / media /
label_ids (with workspace-scoped `Rule::exists` for labels) and
dropped the unused `status` field — REST callers transition state
via `PUT /posts/{id}`. Removed the dead `platforms.*.content`
rule. Added a feature test that asserts content + media + labels
roundtrip on create, plus a regression that an `is_active=false`
social_account is rejected at validation.
- CreatePost::execute now syncs label_ids itself so REST and MCP
share the behavior. Removed the duplicate sync from CreatePostTool.
- MCP UpdatePostTool didn't scope `platforms.*.id` to the post being
updated, drifting from the REST UpdatePostRequest which adds
`Rule::exists('post_platforms','id')->where('post_id', ...)`. Now
it loads the post first (failing fast with `Post not found.` if
the workspace check rejects), then uses the same Rule::exists.
- MediaAttacher fetched any URL the caller passed, including
loopback / link-local / private targets — classic SSRF pivot.
Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP
ranges, and DNS hostnames whose A/AAAA records resolve into those
ranges (covers DNS rebinding). Bypassed under
`app()->runningUnitTests()` so `Http::fake()` keeps working.
Streaming the response body lets us abort early once we exceed
MAX_BYTES instead of buffering the full payload first; redirects
are disabled so a 200→302 trick can't bypass the host check.
- The `media[]` JSON column had a lost-update race in
`attachFromUrls`: read `$post->media`, mutate in PHP, write back.
Two concurrent calls clobbered each other. Now wrapped in a
transaction with `lockForUpdate()`.
- ESLint: `resources/js/actions/**` and `resources/js/routes/**`
are auto-generated by Wayfinder on every build. Their import
order matches PHP scan order, not alphabetical, so import/order
fought eslint-fix forever. Added them to ignores.
2026-05-04 15:16:39 +00:00
|
|
|
use App\Models\WorkspaceLabel;
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
use Illuminate\Support\Facades\Bus;
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
|
|
|
|
|
beforeEach(function () {
|
2026-05-03 21:38:17 +00:00
|
|
|
$result = createApiTestToken();
|
|
|
|
|
$this->user = $result['user'];
|
|
|
|
|
$this->workspace = $result['workspace'];
|
|
|
|
|
$this->plainToken = $result['plain_token'];
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
|
|
|
|
|
$this->socialAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::LinkedIn,
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('lists posts', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->getJson(route('api.posts.index'))
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonCount(1, 'data');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows a post', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->getJson(route('api.posts.show', $post))
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('id', $post->id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cannot show post from another workspace', function () {
|
|
|
|
|
$otherWorkspace = Workspace::factory()->create();
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $otherWorkspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->getJson(route('api.posts.show', $post))
|
|
|
|
|
->assertNotFound();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates a post', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
2026-03-30 17:58:25 +00:00
|
|
|
'platforms' => [
|
|
|
|
|
[
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'content_type' => 'linkedin_post',
|
|
|
|
|
],
|
|
|
|
|
],
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
])
|
|
|
|
|
->assertCreated()
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
->assertJsonPath('status', PostStatus::Draft->value)
|
|
|
|
|
->assertJsonPath('scheduled_at', null);
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
|
2026-07-24 14:19:01 +00:00
|
|
|
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
|
|
|
|
expect($post)->not->toBeNull();
|
|
|
|
|
expect($post->created_via)->toBe(CreatedVia::Api);
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
expect($post->scheduled_at)->toBeNull();
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
});
|
|
|
|
|
|
2026-07-24 14:30:15 +00:00
|
|
|
it('ignores a client-supplied created_via and always records api', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'created_via' => 'web',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
[
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'content_type' => 'linkedin_post',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertCreated();
|
|
|
|
|
|
|
|
|
|
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
|
|
|
|
expect($post->created_via)->toBe(CreatedVia::Api);
|
|
|
|
|
});
|
|
|
|
|
|
fix: address PR review findings — publish, REST store, SSRF, race
Code-review surfaced two correctness bugs and a security gap that
needed to land before merging.
- UpdatePost::execute disabled every platform when called without
a `platforms` key. PublishPostTool relied on that path, so every
publish-via-MCP queued a job whose handler then found nothing
enabled to publish to. Wrap the platform toggle in
`Arr::has($data, 'platforms')` (matches the existing label_ids
guard a few lines up). Add a regression assertion to
`PostPublishToolTest::publish post immediate dispatches PublishPost
job` that the previously-enabled platform stays enabled.
- StorePostRequest declared rules for only `platforms`,
`scheduled_at`, and `status`. `validated()` then stripped
`content`, `media`, and `label_ids`, so REST `POST /api/posts`
silently created empty drafts. Added rules for content / media /
label_ids (with workspace-scoped `Rule::exists` for labels) and
dropped the unused `status` field — REST callers transition state
via `PUT /posts/{id}`. Removed the dead `platforms.*.content`
rule. Added a feature test that asserts content + media + labels
roundtrip on create, plus a regression that an `is_active=false`
social_account is rejected at validation.
- CreatePost::execute now syncs label_ids itself so REST and MCP
share the behavior. Removed the duplicate sync from CreatePostTool.
- MCP UpdatePostTool didn't scope `platforms.*.id` to the post being
updated, drifting from the REST UpdatePostRequest which adds
`Rule::exists('post_platforms','id')->where('post_id', ...)`. Now
it loads the post first (failing fast with `Post not found.` if
the workspace check rejects), then uses the same Rule::exists.
- MediaAttacher fetched any URL the caller passed, including
loopback / link-local / private targets — classic SSRF pivot.
Now `isPublicHttpUrl` rejects non-http(s) schemes, restricted IP
ranges, and DNS hostnames whose A/AAAA records resolve into those
ranges (covers DNS rebinding). Bypassed under
`app()->runningUnitTests()` so `Http::fake()` keeps working.
Streaming the response body lets us abort early once we exceed
MAX_BYTES instead of buffering the full payload first; redirects
are disabled so a 200→302 trick can't bypass the host check.
- The `media[]` JSON column had a lost-update race in
`attachFromUrls`: read `$post->media`, mutate in PHP, write back.
Two concurrent calls clobbered each other. Now wrapped in a
transaction with `lockForUpdate()`.
- ESLint: `resources/js/actions/**` and `resources/js/routes/**`
are auto-generated by Wayfinder on every build. Their import
order matches PHP scan order, not alphabetical, so import/order
fought eslint-fix forever. Added them to ignores.
2026-05-04 15:16:39 +00:00
|
|
|
it('creates a post with content, media, and labels', function () {
|
|
|
|
|
$label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]);
|
|
|
|
|
|
|
|
|
|
$payload = [
|
|
|
|
|
'content' => 'Hello from the API',
|
|
|
|
|
'media' => [['id' => 'media-1', 'path' => 'media/foo.jpg', 'url' => 'https://example.com/foo.jpg', 'type' => 'image']],
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
|
|
|
|
],
|
|
|
|
|
'label_ids' => [$label->id],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$response = $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), $payload)
|
|
|
|
|
->assertCreated();
|
|
|
|
|
|
|
|
|
|
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
|
|
|
|
|
|
|
|
|
expect($post->content)->toBe('Hello from the API');
|
|
|
|
|
expect($post->media)->toHaveCount(1);
|
|
|
|
|
expect($post->labels()->pluck('workspace_labels.id')->all())->toContain($label->id);
|
|
|
|
|
|
|
|
|
|
$response->assertJsonPath('content', 'Hello from the API');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects creating a post with an inactive social account', function () {
|
|
|
|
|
$inactive = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::LinkedIn,
|
|
|
|
|
'is_active' => false,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $inactive->id, 'content_type' => 'linkedin_post'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.social_account_id']);
|
|
|
|
|
});
|
|
|
|
|
|
feat: implement MCP server with tools, Post API tests, auth middleware
- Create TryPostServer MCP server with 17 tools:
Post (List, Get, Create, Delete), Hashtag (List, Create, Update, Delete),
Label (List, Create, Update, Delete), Workspace (Get),
ApiKey (List, Create, Delete)
- Create AuthenticateMcpToken middleware (logs in workspace owner)
- Register mcp.auth middleware alias in bootstrap/app.php
- Create routes/ai.php with mcp.trypost.test subdomain
- Add PostApiTest with 6 tests (list, show, create, delete, isolation)
- Fix PostApiTest assertions for pagination/resource wrapping
- 704 tests passing, frontend build passing
2026-03-29 23:30:36 +00:00
|
|
|
it('deletes a post', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->deleteJson(route('api.posts.destroy', $post))
|
|
|
|
|
->assertNoContent();
|
|
|
|
|
|
|
|
|
|
expect(Post::find($post->id))->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cannot delete post from another workspace', function () {
|
|
|
|
|
$otherWorkspace = Workspace::factory()->create();
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $otherWorkspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->deleteJson(route('api.posts.destroy', $post))
|
|
|
|
|
->assertNotFound();
|
|
|
|
|
});
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
|
|
|
|
|
it('updates a post', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
'status' => 'draft',
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
'platforms' => [
|
|
|
|
|
[
|
|
|
|
|
'id' => $postPlatform->id,
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
'content_type' => ContentType::LinkedInPost->value,
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertOk();
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-04 20:08:57 +00:00
|
|
|
it('rejects creating a post with instagram_carousel — carousel is not a stored content_type', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'instagram_carousel'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects updating a post with instagram_carousel — carousel is not a stored content_type', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $postPlatform->id, 'content_type' => 'instagram_carousel'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
it('cannot update post from another workspace', function () {
|
|
|
|
|
$otherWorkspace = Workspace::factory()->create();
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
$otherSocialAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $otherWorkspace->id,
|
|
|
|
|
'platform' => Platform::LinkedIn,
|
|
|
|
|
]);
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $otherWorkspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $otherSocialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
[
|
|
|
|
|
'id' => $postPlatform->id,
|
|
|
|
|
'content_type' => ContentType::LinkedInPost->value,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
->assertNotFound();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 16:19:01 +00:00
|
|
|
it('cannot update post in any terminal state', function (PostStatus $status) {
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
2026-05-19 16:19:01 +00:00
|
|
|
'status' => $status,
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
]);
|
|
|
|
|
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
fix: overhaul social publishing — validation, uploads, token refresh
- Fix UpdatePostRequest missing content_type, synced, meta fields
(content_type was silently dropped, causing Instagram Reels to post as Feed)
- Create API FormRequests (StorePostRequest, UpdatePostRequest) replacing inline validation
- Fix syntax errors in all publishers ($media->isVideo() missing variable)
- Fix Instagram Feed with single video calling publishSingleImage instead of publishReel
- Fix TikTok hardcoded SELF_ONLY privacy — now queries creator_info API
- Refactor YouTubePublisher to use google/apiclient SDK with chunked resumable upload
- Fix all publishers using file_get_contents for large videos (memory overflow)
— X, LinkedIn, LinkedInPage, Pinterest, Bluesky, Mastodon now use temp file + stream
- Fix Media::isVideo/isImage to use mime_type instead of extension
- Fix Threads not saving refresh_token (was null, now saves access_token)
- Add Instagram token refresh to publisher and ConnectionVerifier
- Fix PublishToSocialPlatform job: tries 3→1 (prevents duplicate uploads),
timeout 60→600s, added failed() method for cleanup
- Increase Horizon worker timeout 60→630s, Redis retry_after 90→660s
- Increase upload limit 500MB→1GB
- Add mastodon to getDefaultContentType in Edit.vue
2026-03-31 22:25:19 +00:00
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
[
|
|
|
|
|
'id' => $postPlatform->id,
|
|
|
|
|
'content_type' => ContentType::LinkedInPost->value,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
])
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
->assertUnprocessable();
|
2026-05-19 16:19:01 +00:00
|
|
|
})->with([
|
|
|
|
|
PostStatus::Published,
|
|
|
|
|
PostStatus::PartiallyPublished,
|
|
|
|
|
PostStatus::Failed,
|
|
|
|
|
PostStatus::Publishing,
|
|
|
|
|
]);
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
|
|
|
|
|
it('validates post update fields', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['content' => 'missing id'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates post creation requires platforms', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['platforms']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates post creation platform fields', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['content' => 'missing social_account_id and content_type'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.social_account_id', 'platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates post update invalid status', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'invalid_status',
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['status']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates post update scheduled_at must be date', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'scheduled_at' => 'not-a-date',
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['scheduled_at']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates post update label_ids must be uuids', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'label_ids' => ['not-a-uuid'],
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['label_ids.0']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-04 16:31:44 +00:00
|
|
|
it('rejects creating a post with content_type not in the enum', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'made_up_type'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-11 22:39:41 +00:00
|
|
|
it('rejects scheduling an over-limit threads post via the api store', function () {
|
|
|
|
|
$threadsAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::Threads,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response = $this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'content' => str_repeat('a', 537),
|
|
|
|
|
'scheduled_at' => now()->addDay()->toIso8601String(),
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $threadsAccount->id, 'content_type' => ContentType::ThreadsPost->value],
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$response->assertUnprocessable()->assertJsonValidationErrors(['content']);
|
|
|
|
|
expect($response->json('errors.content.0'))
|
|
|
|
|
->toContain('Threads')
|
|
|
|
|
->toContain('500')
|
|
|
|
|
->toContain('37');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('accepts creating an over-limit draft (no scheduled_at) via the api store', function () {
|
|
|
|
|
$threadsAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::Threads,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'content' => str_repeat('a', 1000),
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $threadsAccount->id, 'content_type' => ContentType::ThreadsPost->value],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertCreated();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects scheduling an over-limit threads post via the api update', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
$threadsAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::Threads,
|
|
|
|
|
]);
|
|
|
|
|
$threadsPlatform = PostPlatform::factory()->threads()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $threadsAccount->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => PostStatus::Scheduled->value,
|
|
|
|
|
'content' => str_repeat('a', 600),
|
|
|
|
|
'scheduled_at' => now()->addDay()->toIso8601String(),
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $threadsPlatform->id, 'content_type' => ContentType::ThreadsPost->value],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertUnprocessable()
|
|
|
|
|
->assertJsonValidationErrors(['content']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('saving an over-limit threads post as draft via api skips the content-length check', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
$threadsAccount = SocialAccount::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'platform' => Platform::Threads,
|
|
|
|
|
]);
|
|
|
|
|
$threadsPlatform = PostPlatform::factory()->threads()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $threadsAccount->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => PostStatus::Draft->value,
|
|
|
|
|
'content' => str_repeat('a', 1000),
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $threadsPlatform->id, 'content_type' => ContentType::ThreadsPost->value],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertSuccessful();
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-04 16:31:44 +00:00
|
|
|
it('rejects creating a post when content_type does not match the social account platform', function () {
|
|
|
|
|
// x_post on a LinkedIn account — ContentTypeMatchesPlatform should reject.
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'x_post'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects creating a post with a label from another workspace', function () {
|
|
|
|
|
$otherWorkspace = Workspace::factory()->create();
|
|
|
|
|
$foreignLabel = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
|
|
|
|
],
|
|
|
|
|
'label_ids' => [$foreignLabel->id],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['label_ids.0']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects updating a post with a platforms[].id that belongs to another post', function () {
|
|
|
|
|
$myPost = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$otherPost = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
$foreignPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $otherPost->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $myPost), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $foreignPlatform->id, 'content_type' => ContentType::LinkedInPost->value],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.id']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects updating a post when content_type does not match the post_platform', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $postPlatform->id, 'content_type' => 'x_post'],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.content_type']);
|
|
|
|
|
});
|
|
|
|
|
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
it('rejects scheduled status without a future scheduled_at', function (?string $existingScheduledAt) {
|
2026-05-04 16:31:44 +00:00
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
'scheduled_at' => $existingScheduledAt,
|
2026-05-04 16:31:44 +00:00
|
|
|
]);
|
|
|
|
|
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'scheduled',
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['scheduled_at']);
|
|
|
|
|
|
2026-05-04 16:31:44 +00:00
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'scheduled',
|
|
|
|
|
'scheduled_at' => now()->subHour()->toIso8601String(),
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['scheduled_at']);
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
})->with([
|
|
|
|
|
'missing schedule' => [null],
|
|
|
|
|
'past schedule' => [now()->subDay()->toDateTimeString()],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
it('accepts scheduled status reusing an existing future scheduled_at', function () {
|
|
|
|
|
$scheduledAt = now()->addDay()->startOfSecond();
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
'scheduled_at' => $scheduledAt,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'scheduled',
|
|
|
|
|
])
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('status', PostStatus::Scheduled->value);
|
|
|
|
|
|
|
|
|
|
expect($post->fresh()->scheduled_at->toDateTimeString())->toBe($scheduledAt->toDateTimeString());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('schedules an unscheduled draft with an explicit future scheduled_at', function () {
|
|
|
|
|
$scheduledAt = now()->addDay()->startOfSecond();
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
'scheduled_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'scheduled',
|
|
|
|
|
'scheduled_at' => $scheduledAt->toIso8601String(),
|
|
|
|
|
])
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('status', PostStatus::Scheduled->value);
|
|
|
|
|
|
|
|
|
|
expect($post->fresh()->scheduled_at->toDateTimeString())->toBe($scheduledAt->toDateTimeString());
|
2026-05-04 16:31:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('accepts draft status with no scheduled_at', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
'scheduled_at' => null,
|
2026-05-04 16:31:44 +00:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
])
|
fix: keep post drafts unscheduled by default (#209)
* fix: keep post drafts unscheduled by default
* Align schedule validation and keep drafts unscheduled.
Require scheduled_at only when status is scheduled and the post has no
usable future schedule. Share that rule across web, API, and MCP, keep
create without a date as null, and preserve the legacy date → 09:00 UTC
fallback.
* Polish schedule validation typing and tests.
Type requiresExplicitSchedule status as ?string, reuse a local status
variable in request/tool validation, tighten the web reject assertion,
and collapse overlapping MCP unscheduled-create cases.
* Centralize status helper in post update validation.
Reuse the typed status() helper across FormRequests and the already-parsed
$status in UpdatePostTool so schedule checks stay consistent and less noisy.
* Share scheduled_at update rules across web, API, and MCP.
Centralize schedule validation in PostStatusRules, normalize status parsing
in one place, and align past-schedule coverage across entry points.
* Cover the full unscheduled-draft checklist in Pest.
Add feature coverage for null/past schedule rejection, explicit scheduling,
draft saves, publish-now without a schedule, calendar exclusion, and
09:00 UTC date defaults across web, API, and MCP.
* Remove normalizeStatus helper.
Keep the inline is_string check at the few call sites that read raw
request status before validation — no shared wrapper needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Drop is_string status guards from schedule validation.
Accept mixed status in PostStatusRules and rely on strict comparisons
with Rule::requiredIf / Rule::when — malformed input simply does not match.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
2026-08-01 20:39:18 +00:00
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('scheduled_at', null);
|
|
|
|
|
|
|
|
|
|
expect($post->fresh()->scheduled_at)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('publishes an unscheduled draft without requiring scheduled_at', function () {
|
|
|
|
|
Bus::fake();
|
|
|
|
|
$this->freezeTime();
|
|
|
|
|
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
'scheduled_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
PostPlatform::factory()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'publishing',
|
|
|
|
|
])
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('status', PostStatus::Publishing->value);
|
|
|
|
|
|
|
|
|
|
expect($post->fresh()->scheduled_at->toDateTimeString())->toBe(now()->toDateTimeString());
|
|
|
|
|
Bus::assertDispatched(PublishPost::class);
|
2026-05-04 16:31:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects creating a post with a past scheduled_at', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post'],
|
|
|
|
|
],
|
|
|
|
|
'scheduled_at' => now()->subDay()->toIso8601String(),
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['scheduled_at']);
|
|
|
|
|
});
|
|
|
|
|
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
it('list posts returns correct structure', function () {
|
|
|
|
|
Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->getJson(route('api.posts.index'))
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonStructure([
|
|
|
|
|
'data' => [
|
2026-04-15 23:11:36 +00:00
|
|
|
'*' => ['id', 'status', 'scheduled_at', 'published_at', 'created_at', 'updated_at'],
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('show post returns correct structure', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->getJson(route('api.posts.show', $post))
|
|
|
|
|
->assertOk()
|
2026-04-15 23:11:36 +00:00
|
|
|
->assertJsonStructure(['id', 'status', 'scheduled_at', 'published_at']);
|
feat: social account toggle action, API, MCP + full test coverage
- Extract ToggleSocialAccount action from SocialController
- Add API endpoints: GET /social-accounts, PUT /social-accounts/{id}/toggle
- Add MCP tools: ListSocialAccountsTool, ToggleSocialAccountTool
- Fix all MCP tools: findOrFail → find + Response::error for graceful errors
- Fix MCP tools using $request->validated() without validate() call
- Fix return types to Response|ResponseFactory for error paths
- Add SocialAccountResource is_active/status fields (no tokens exposed)
- Add 43 MCP tests covering all 18 tools (CRUD, validation, cross-workspace)
- Add API response structure tests for posts, hashtags, labels, workspace
- Add API validation tests for post create/update, api-key expiry, label color
- Add API cross-workspace delete tests for hashtags and labels
- Add app validation tests for hashtag/label update, invite fields, password
- Add auth required tests for notifications, profile delete, api-keys index
- Add media reorder validation tests
2026-03-31 04:42:39 +00:00
|
|
|
});
|
2026-06-10 18:36:47 +00:00
|
|
|
|
|
|
|
|
it('creates a post with platform meta (aspect_ratio) and returns it', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertCreated()
|
|
|
|
|
->assertJsonPath('platforms.0.meta.aspect_ratio', '4:5');
|
|
|
|
|
|
|
|
|
|
$platform = Post::where('workspace_id', $this->workspace->id)->first()
|
|
|
|
|
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
|
|
|
|
|
expect($platform->meta['aspect_ratio'])->toBe('4:5');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects creating a post with an invalid aspect_ratio', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects updating a post with an invalid aspect_ratio', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $postPlatform->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
|
|
|
|
|
});
|
2026-06-10 18:52:41 +00:00
|
|
|
|
|
|
|
|
it('accepts a valid aspect_ratio on update and persists it', function () {
|
|
|
|
|
$post = Post::factory()->create([
|
|
|
|
|
'workspace_id' => $this->workspace->id,
|
|
|
|
|
'user_id' => $this->user->id,
|
|
|
|
|
'status' => PostStatus::Draft,
|
|
|
|
|
]);
|
|
|
|
|
$postPlatform = PostPlatform::factory()->linkedin()->create([
|
|
|
|
|
'post_id' => $post->id,
|
|
|
|
|
'social_account_id' => $this->socialAccount->id,
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->putJson(route('api.posts.update', $post), [
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['id' => $postPlatform->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '16:9']],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonPath('platforms.0.meta.aspect_ratio', '16:9');
|
|
|
|
|
|
|
|
|
|
expect($postPlatform->fresh()->meta['aspect_ratio'])->toBe('16:9');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('accepts the original aspect_ratio (no crop) on create', function () {
|
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
|
|
|
|
->postJson(route('api.posts.store'), [
|
|
|
|
|
'platforms' => [
|
|
|
|
|
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => 'original']],
|
|
|
|
|
],
|
|
|
|
|
])
|
|
|
|
|
->assertCreated();
|
|
|
|
|
|
|
|
|
|
$platform = Post::where('workspace_id', $this->workspace->id)->first()
|
|
|
|
|
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
|
|
|
|
|
expect($platform->meta['aspect_ratio'])->toBe('original');
|
|
|
|
|
});
|