trypost/tests/Feature/PostTemplateControllerTest.php

180 lines
6.1 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Enums\Post\CreatedVia;
use App\Enums\UserWorkspace\Role;
use App\Models\Account;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
beforeEach(function () {
Storage::fake();
$this->account = Account::factory()->create();
$this->user = User::factory()->create([
'account_id' => $this->account->id,
]);
$this->account->update(['owner_id' => $this->user->id]);
$this->workspace = Workspace::factory()->create([
'account_id' => $this->account->id,
'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->account->subscriptions()->create([
'type' => Account::SUBSCRIPTION_NAME,
'stripe_id' => 'sub_test_'.fake()->uuid(),
'stripe_status' => 'active',
'stripe_price' => 'price_123',
]);
});
test('index returns templates from the registry', function () {
$this->actingAs($this->user)
->get(route('app.post-templates.index'))
->assertOk()
->assertInertia(fn ($page) => $page
->component('posts/templates/Index')
->has('templates.data')
);
});
test('index requires authentication', function () {
$this->get(route('app.post-templates.index'))
->assertRedirect(route('login'));
});
test('index filters by platform', function () {
$this->actingAs($this->user)
->get(route('app.post-templates.index', ['platform' => 'instagram_carousel']))
->assertOk()
->assertInertia(fn ($page) => $page
->component('posts/templates/Index')
->where('templates.data', fn ($templates) => collect($templates)->every(
fn ($t) => $t['platform'] === 'instagram_carousel'
))
);
});
test('apply creates post and returns post_id and redirect_url', function () {
Http::fake(['api.unsplash.com/*' => Http::response(['results' => []])]);
$response = $this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'))
->assertOk();
expect($response->json('post_id'))->toBeString();
expect($response->json('redirect_url'))->toContain('edit');
$post = $this->workspace->posts()->find($response->json('post_id'));
expect($post->created_via)->toBe(CreatedVia::Web);
});
test('apply interpolates brand_name in content', function () {
Http::fake(['api.unsplash.com/*' => Http::response(['results' => []])]);
$this->workspace->update(['name' => 'Acme Corp']);
// success_story template content references {{brand_name}}
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'))
->assertOk();
$this->assertDatabaseHas('posts', [
'workspace_id' => $this->workspace->id,
]);
$post = $this->workspace->posts()->latest()->first();
expect($post->content)->toContain('Acme Corp');
});
test('apply rejects cross-workspace social_account_id', function () {
$otherAccount = Account::factory()->create();
$otherUser = User::factory()->create(['account_id' => $otherAccount->id]);
$otherAccount->update(['owner_id' => $otherUser->id]);
$otherWorkspace = Workspace::factory()->create([
'account_id' => $otherAccount->id,
'user_id' => $otherUser->id,
]);
$foreignSocialAccount = SocialAccount::factory()->create([
'workspace_id' => $otherWorkspace->id,
]);
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'), [
'social_account_id' => $foreignSocialAccount->id,
])
->assertForbidden();
});
test('apply requires authentication', function () {
$this->postJson(route('app.post-templates.apply', 'success_story'))
->assertUnauthorized();
});
test('apply returns 404 for unknown slug', function () {
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'this-slug-does-not-exist'))
->assertNotFound();
});
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
test('apply leaves scheduled_at null when no date is provided', function () {
Http::fake(['api.unsplash.com/*' => Http::response(['results' => []])]);
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'))
->assertOk();
$post = $this->workspace->posts()->latest()->first();
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();
});
test('apply schedules the post on the date param when provided', function () {
Http::fake(['api.unsplash.com/*' => Http::response(['results' => []])]);
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'), [
'date' => '2026-06-15',
])
->assertOk();
$post = $this->workspace->posts()->latest()->first();
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->utc()->format('Y-m-d H:i:s'))->toBe('2026-06-15 09:00:00');
});
test('apply rejects invalid date format', function () {
$this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'success_story'), [
'date' => 'not-a-date',
])
->assertUnprocessable()
->assertJsonValidationErrors(['date']);
});
test('apply with slides creates post even when image rendering fails', function () {
// Unsplash returns no results → generator returns null → no media attached, post still created.
Http::fake(['api.unsplash.com/*' => Http::response(['results' => []])]);
$socialAccount = SocialAccount::factory()->create([
'workspace_id' => $this->workspace->id,
]);
$response = $this->actingAs($this->user)
->postJson(route('app.post-templates.apply', 'feature_launch_carousel'), [
'social_account_id' => $socialAccount->id,
])
->assertOk();
expect($response->json('post_id'))->toBeString();
expect($response->json('redirect_url'))->toContain('edit');
});