feat: pass and persist post dates through AI creation wizard and template application flows
This commit is contained in:
parent
3e642ef520
commit
1d116cfbb2
15 changed files with 262 additions and 29 deletions
11
CLAUDE.md
11
CLAUDE.md
|
|
@ -259,6 +259,17 @@ ## Form Validation
|
|||
|
||||
- NEVER use HTML5 validation attributes (`required`, `minlength`, `pattern`, etc.) on form inputs. Always rely solely on backend validation.
|
||||
|
||||
## Backend Validation
|
||||
|
||||
- Validation rules always live in a dedicated `Illuminate\Foundation\Http\FormRequest` subclass under `app/Http/Requests/App/<Group>/`. Controller actions must type-hint the FormRequest as the parameter — NEVER call `$request->validate([...])` inline in the controller.
|
||||
- Naming: `<Verb><Resource>Request.php` (e.g. `StorePostRequest`, `ApplyPostTemplateRequest`, `IndexPostTemplateRequest`).
|
||||
|
||||
## Pest / Feature Tests
|
||||
|
||||
- ALWAYS use named routes via the `route()` helper in feature tests. NEVER hardcode URL strings like `'/posts/ai/create'`.
|
||||
- Example: `$this->postJson(route('app.posts.store'))` instead of `$this->postJson('/posts')`.
|
||||
- With params: `route('app.posts.ai.create.finalize', $creationId)`.
|
||||
|
||||
## Dusk (Browser Tests)
|
||||
|
||||
- In Dusk tests, ALWAYS use named routes via `route()` helper. NEVER hardcode URLs like `'https://trypost.test/login'`.
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ public function start(StartPostCreationRequest $request): JsonResponse
|
|||
socialAccountId: $socialAccountId,
|
||||
imageCount: (int) $request->input('image_count', 0),
|
||||
prompt: $request->string('prompt')->toString(),
|
||||
date: $request->input('date'),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
@ -100,6 +101,7 @@ public function finalize(Request $request, string $creationId): JsonResponse
|
|||
$post = CreatePost::execute($workspace, $request->user(), [
|
||||
'content' => $caption,
|
||||
'media' => $media,
|
||||
'date' => data_get($state, 'date'),
|
||||
]);
|
||||
|
||||
// Sync the post_platform with the wizard choice: set content_type to
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
use App\Enums\Post\Action as PostAction;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Http\Requests\App\Post\StorePostRequest;
|
||||
use App\Http\Requests\App\Post\UpdatePostRequest;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Http\Resources\App\PlatformConfigResource;
|
||||
|
|
@ -136,7 +137,7 @@ public function create(Request $request): Response
|
|||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
public function store(StorePostRequest $request): RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$workspace = $request->user()->currentWorkspace;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,14 @@
|
|||
|
||||
use App\Actions\Post\CreatePost;
|
||||
use App\Enums\Media\Type as MediaType;
|
||||
use App\Http\Requests\App\PostTemplate\ApplyPostTemplateRequest;
|
||||
use App\Http\Requests\App\PostTemplate\IndexPostTemplateRequest;
|
||||
use App\Http\Resources\App\PostTemplateResource;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Image\TemplateImageGenerator;
|
||||
use App\Services\PostTemplate\Registry;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response as InertiaResponse;
|
||||
|
|
@ -22,13 +23,8 @@ class PostTemplateController extends Controller
|
|||
{
|
||||
public function __construct(private readonly Registry $registry) {}
|
||||
|
||||
public function index(Request $request): InertiaResponse
|
||||
public function index(IndexPostTemplateRequest $request): InertiaResponse
|
||||
{
|
||||
$request->validate([
|
||||
'platform' => ['nullable', 'string'],
|
||||
'search' => ['nullable', 'string', 'max:120'],
|
||||
]);
|
||||
|
||||
$paginator = $this->registry->paginate(
|
||||
locale: app()->getLocale(),
|
||||
platform: $request->input('platform'),
|
||||
|
|
@ -45,19 +41,16 @@ public function index(Request $request): InertiaResponse
|
|||
'search' => $request->input('search', ''),
|
||||
'platform' => $request->input('platform', ''),
|
||||
],
|
||||
'date' => $request->input('date'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function apply(Request $request, string $slug, TemplateImageGenerator $generator): JsonResponse
|
||||
public function apply(ApplyPostTemplateRequest $request, string $slug, TemplateImageGenerator $generator): JsonResponse
|
||||
{
|
||||
$workspace = $request->user()->currentWorkspace;
|
||||
|
||||
$this->authorize('createPost', $workspace);
|
||||
|
||||
$request->validate([
|
||||
'social_account_id' => ['nullable', 'uuid'],
|
||||
]);
|
||||
|
||||
$template = $this->registry->find($slug, app()->getLocale());
|
||||
|
||||
$socialAccountId = $request->input('social_account_id');
|
||||
|
|
@ -98,6 +91,7 @@ public function apply(Request $request, string $slug, TemplateImageGenerator $ge
|
|||
$post = CreatePost::execute($workspace, $request->user(), [
|
||||
'content' => $content,
|
||||
'media' => $media,
|
||||
'date' => $request->input('date'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public function rules(): array
|
|||
'image_count' => ['nullable', 'integer', 'min:0', 'max:10'],
|
||||
// Stories accept 1 image, no carousel — the wizard handles this client-side too.
|
||||
'prompt' => ['required', 'string', 'max:2000'],
|
||||
'date' => ['nullable', 'date_format:Y-m-d'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
app/Http/Requests/App/Post/StorePostRequest.php
Normal file
26
app/Http/Requests/App/Post/StorePostRequest.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\App\Post;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePostRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'media' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\App\PostTemplate;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ApplyPostTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'social_account_id' => ['nullable', 'uuid'],
|
||||
'date' => ['nullable', 'date_format:Y-m-d'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\App\PostTemplate;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class IndexPostTemplateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'platform' => ['nullable', 'string'],
|
||||
'search' => ['nullable', 'string', 'max:120'],
|
||||
'date' => ['nullable', 'date_format:Y-m-d'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ public function __construct(
|
|||
public ?string $socialAccountId,
|
||||
public int $imageCount,
|
||||
public string $prompt,
|
||||
public ?string $date = null,
|
||||
) {
|
||||
$this->onQueue('ai');
|
||||
}
|
||||
|
|
@ -251,6 +252,7 @@ private function handleCarousel(Workspace $workspace, ?SocialAccount $socialAcco
|
|||
'social_account_id' => $this->socialAccountId,
|
||||
'content' => $caption,
|
||||
'slides' => $renderedSlides,
|
||||
'date' => $this->date,
|
||||
'created_at' => now()->toIso8601String(),
|
||||
], now()->addMinutes(30));
|
||||
|
||||
|
|
@ -296,6 +298,7 @@ private function handleSingle(Workspace $workspace, ?SocialAccount $socialAccoun
|
|||
'image_body' => $imageBody,
|
||||
'image_keywords' => $keywords,
|
||||
'image_path' => $imagePath,
|
||||
'date' => $this->date,
|
||||
'created_at' => now()->toIso8601String(),
|
||||
], now()->addMinutes(30));
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,13 @@ interface SocialAccount {
|
|||
|
||||
interface Props {
|
||||
socialAccounts: SocialAccount[];
|
||||
/** ISO date (YYYY-MM-DD) carried over from the calendar's per-day "+" button. */
|
||||
date?: string | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
date: null,
|
||||
});
|
||||
|
||||
type WizardStep = 'configure' | 'preview';
|
||||
|
||||
|
|
@ -67,7 +71,8 @@ const httpStart = useHttp<{
|
|||
social_account_id: string | null;
|
||||
image_count: number;
|
||||
prompt: string;
|
||||
}>({ format: null, social_account_id: null, image_count: 0, prompt: '' });
|
||||
date: string | null;
|
||||
}>({ format: null, social_account_id: null, image_count: 0, prompt: '', date: null });
|
||||
|
||||
const httpFinalize = useHttp<{ content: string; image_title: string; image_body: string }>({
|
||||
content: '',
|
||||
|
|
@ -253,6 +258,7 @@ const startGeneration = async () => {
|
|||
httpStart.social_account_id = selectedAccountId.value;
|
||||
httpStart.image_count = submittedImageCount.value;
|
||||
httpStart.prompt = promptText.value.trim();
|
||||
httpStart.date = props.date;
|
||||
|
||||
try {
|
||||
const data = await httpStart.post(startRoute.url()) as { creation_id: string; channel: string };
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const stepHeader = computed(() => {
|
|||
</button>
|
||||
|
||||
<Link
|
||||
:href="templatesIndex.url()"
|
||||
:href="templatesIndex.url({ query: { date: props.date } })"
|
||||
class="group flex flex-col items-start gap-4 rounded-2xl border-2 border-foreground bg-card p-5 text-left shadow-2xs transition-all hover:-translate-y-0.5 hover:shadow-md"
|
||||
>
|
||||
<div class="inline-flex size-12 -rotate-1 items-center justify-center rounded-2xl border-2 border-foreground bg-emerald-200 shadow-2xs transition-transform group-hover:rotate-0">
|
||||
|
|
@ -132,6 +132,7 @@ const stepHeader = computed(() => {
|
|||
<AiPostWizard
|
||||
v-else-if="view === 'ai'"
|
||||
:social-accounts="socialAccounts"
|
||||
:date="props.date"
|
||||
@update:step-header="aiHeader = $event"
|
||||
@cancel="view = 'choice'; aiHeader = null"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -53,9 +53,13 @@ interface Props {
|
|||
search: string;
|
||||
platform: string;
|
||||
};
|
||||
/** ISO date carried over from the calendar's per-day "+" button. */
|
||||
date?: string | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
date: null,
|
||||
});
|
||||
|
||||
const searchQuery = ref(props.filters.search);
|
||||
const selectedPlatform = ref<string>(props.filters.platform || '');
|
||||
|
|
@ -130,6 +134,7 @@ const reload = (params: { search?: string; platform?: string }) => {
|
|||
{
|
||||
search: params.search || undefined,
|
||||
platform: params.platform || undefined,
|
||||
date: props.date || undefined,
|
||||
},
|
||||
{ preserveState: true, preserveScroll: true, replace: true },
|
||||
);
|
||||
|
|
@ -158,7 +163,8 @@ const applyTemplate = async (template: PostTemplate) => {
|
|||
applyingSlug.value = template.slug;
|
||||
|
||||
try {
|
||||
const data = (await useHttp().post(applyRoute.url(template.slug))) as {
|
||||
const http = useHttp<{ date: string | null }>({ date: props.date ?? null });
|
||||
const data = (await http.post(applyRoute.url(template.slug))) as {
|
||||
post_id: string;
|
||||
redirect_url: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
// --- POST /posts/ai/create (start) ---
|
||||
|
||||
test('start requires authentication', function () {
|
||||
$this->postJson('/posts/ai/create', ['prompt' => 'hello', 'format' => 'x_post'])
|
||||
$this->postJson(route('app.posts.ai.create'), ['prompt' => 'hello', 'format' => 'x_post'])
|
||||
->assertStatus(Response::HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', ['format' => 'x_post'])
|
||||
->postJson(route('app.posts.ai.create'), ['format' => 'x_post'])
|
||||
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
|
||||
->assertJsonValidationErrors(['prompt']);
|
||||
});
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', ['prompt' => 'hello'])
|
||||
->postJson(route('app.posts.ai.create'), ['prompt' => 'hello'])
|
||||
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
|
||||
->assertJsonValidationErrors(['format']);
|
||||
});
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', ['prompt' => 'hello', 'format' => 'tiktok_video'])
|
||||
->postJson(route('app.posts.ai.create'), ['prompt' => 'hello', 'format' => 'tiktok_video'])
|
||||
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
|
||||
->assertJsonValidationErrors(['format']);
|
||||
});
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', [
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'hello',
|
||||
'format' => 'x_post',
|
||||
'social_account_id' => $foreignAccount->id,
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', [
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'Write a post about productivity',
|
||||
'format' => 'x_post',
|
||||
'social_account_id' => $account->id,
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create', [
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'Write a LinkedIn post',
|
||||
'format' => 'linkedin_post',
|
||||
])
|
||||
|
|
@ -121,13 +121,13 @@
|
|||
// --- POST /posts/ai/create/{creationId}/finalize ---
|
||||
|
||||
test('finalize requires authentication', function () {
|
||||
$this->postJson('/posts/ai/create/fake-id/finalize')
|
||||
$this->postJson(route('app.posts.ai.create.finalize', 'fake-id'))
|
||||
->assertStatus(Response::HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('finalize returns 404 if creation not found in cache', function () {
|
||||
$this->actingAs($this->user)
|
||||
->postJson('/posts/ai/create/nonexistent-id/finalize')
|
||||
->postJson(route('app.posts.ai.create.finalize', 'nonexistent-id'))
|
||||
->assertStatus(Response::HTTP_NOT_FOUND);
|
||||
});
|
||||
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
], now()->addMinutes(30));
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson("/posts/ai/create/{$creationId}/finalize")
|
||||
->postJson(route('app.posts.ai.create.finalize', $creationId))
|
||||
->assertStatus(Response::HTTP_NOT_FOUND);
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
], now()->addMinutes(30));
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson("/posts/ai/create/{$creationId}/finalize")
|
||||
->postJson(route('app.posts.ai.create.finalize', $creationId))
|
||||
->assertStatus(Response::HTTP_OK)
|
||||
->assertJsonStructure(['post_id', 'redirect_url']);
|
||||
|
||||
|
|
@ -183,3 +183,75 @@
|
|||
// Redirect URL should point to the edit page
|
||||
expect($response->json('redirect_url'))->toContain("/posts/{$postId}/edit");
|
||||
});
|
||||
|
||||
test('finalize defaults scheduled_at to today when no date is in cache state', function () {
|
||||
$creationId = 'no-date-creation';
|
||||
|
||||
Cache::put("ai-creation:{$creationId}", [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'format' => 'x_post',
|
||||
'social_account_id' => null,
|
||||
'image_count' => 0,
|
||||
'content' => 'hello',
|
||||
'created_at' => now()->toIso8601String(),
|
||||
], now()->addMinutes(30));
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson(route('app.posts.ai.create.finalize', $creationId))
|
||||
->assertOk();
|
||||
|
||||
$post = Post::find($response->json('post_id'));
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe(now('UTC')->format('Y-m-d'));
|
||||
});
|
||||
|
||||
test('finalize schedules the post on the date stored in cache state', function () {
|
||||
$creationId = 'with-date-creation';
|
||||
|
||||
Cache::put("ai-creation:{$creationId}", [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'format' => 'x_post',
|
||||
'social_account_id' => null,
|
||||
'image_count' => 0,
|
||||
'content' => 'hello',
|
||||
'date' => '2026-06-15',
|
||||
'created_at' => now()->toIso8601String(),
|
||||
], now()->addMinutes(30));
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson(route('app.posts.ai.create.finalize', $creationId))
|
||||
->assertOk();
|
||||
|
||||
$post = Post::find($response->json('post_id'));
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe('2026-06-15');
|
||||
});
|
||||
|
||||
test('start dispatches the job carrying the date param when provided', function () {
|
||||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'hello',
|
||||
'format' => 'x_post',
|
||||
'date' => '2026-06-15',
|
||||
])
|
||||
->assertAccepted();
|
||||
|
||||
Bus::assertDispatched(StreamPostCreation::class, fn ($job) => $job->date === '2026-06-15');
|
||||
});
|
||||
|
||||
test('start rejects invalid date format', function () {
|
||||
Bus::fake();
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->postJson(route('app.posts.ai.create'), [
|
||||
'prompt' => 'hello',
|
||||
'format' => 'x_post',
|
||||
'date' => 'not-a-date',
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['date']);
|
||||
|
||||
Bus::assertNotDispatched(StreamPostCreation::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -150,6 +150,30 @@
|
|||
expect($post->postPlatforms)->toHaveCount(1);
|
||||
});
|
||||
|
||||
test('store post defaults scheduled_at to today when no date is provided', function () {
|
||||
$this->actingAs($this->user)->post(route('app.posts.store'))->assertRedirect();
|
||||
|
||||
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe(now('UTC')->format('Y-m-d'));
|
||||
});
|
||||
|
||||
test('store post schedules draft on the date param when provided', function () {
|
||||
$this->actingAs($this->user)->post(route('app.posts.store'), [
|
||||
'date' => '2026-06-15',
|
||||
])->assertRedirect();
|
||||
|
||||
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe('2026-06-15');
|
||||
});
|
||||
|
||||
test('store post rejects invalid date format', function () {
|
||||
$this->actingAs($this->user)
|
||||
->post(route('app.posts.store'), ['date' => 'not-a-date'])
|
||||
->assertSessionHasErrors(['date']);
|
||||
|
||||
expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
// Edit tests
|
||||
test('edit post requires authentication', function () {
|
||||
$post = Post::factory()->create([
|
||||
|
|
|
|||
|
|
@ -123,6 +123,39 @@
|
|||
->assertNotFound();
|
||||
});
|
||||
|
||||
test('apply defaults scheduled_at to today 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();
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe(now('UTC')->format('Y-m-d'));
|
||||
});
|
||||
|
||||
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();
|
||||
expect($post->scheduled_at->format('Y-m-d'))->toBe('2026-06-15');
|
||||
});
|
||||
|
||||
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' => []])]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue