withoutMiddleware(ConditionalValidateCsrfToken::class); } public function test_academy_homepage_loads_when_enabled(): void { $this->get('/academy') ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Academy/Index') ->where('featureFlags.paymentsEnabled', false)); } public function test_academy_routes_are_hidden_when_feature_is_disabled(): void { config(['academy.enabled' => false]); $this->get('/academy')->assertNotFound(); $this->get('/academy/lessons')->assertNotFound(); } public function test_free_lesson_is_visible_to_guest(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Free Lesson', 'slug' => 'free-lesson', 'excerpt' => 'Visible to guests.', 'content' => 'Free lesson content', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $this->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Academy/Show') ->where('item.locked', false) ->where('item.content', 'Free lesson content')); } public function test_creator_lesson_is_locked_for_regular_user(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Creator Lesson', 'slug' => 'creator-lesson', 'excerpt' => 'Preview only', 'content' => 'Creator only lesson content', 'difficulty' => 'intermediate', 'access_level' => 'creator', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $user = User::factory()->create(); $this->actingAs($user) ->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertDontSee('Creator only lesson content') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.content', null)); } public function test_pro_lesson_is_locked_for_creator_user(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Pro Lesson', 'slug' => 'pro-lesson', 'excerpt' => 'Pro preview', 'content' => 'Pro only lesson content', 'difficulty' => 'pro', 'access_level' => 'pro', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $creator = User::factory()->create(['role' => 'academy_creator']); $this->actingAs($creator) ->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertDontSee('Pro only lesson content') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.content', null)); } public function test_premium_prompt_full_text_is_not_exposed_to_unauthorized_users(): void { $prompt = AcademyPromptTemplate::query()->create([ 'title' => 'Creator Prompt', 'slug' => 'creator-prompt', 'excerpt' => 'Locked preview.', 'prompt' => 'SECRET PREMIUM PROMPT STRING', 'negative_prompt' => 'SECRET NEGATIVE STRING', 'difficulty' => 'beginner', 'access_level' => 'creator', 'active' => true, 'published_at' => now()->subMinute(), ]); $this->get(route('academy.prompts.show', ['slug' => $prompt->slug])) ->assertOk() ->assertDontSee('SECRET PREMIUM PROMPT STRING') ->assertDontSee('SECRET NEGATIVE STRING') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.prompt', null) ->where('item.negative_prompt', null)); $version = app(HandleInertiaRequests::class) ->version(Request::create(route('academy.prompts.show', ['slug' => $prompt->slug]), 'GET')); $this->withHeaders([ 'X-Inertia' => 'true', 'X-Requested-With' => 'XMLHttpRequest', 'X-Inertia-Version' => $version, ])->get(route('academy.prompts.show', ['slug' => $prompt->slug])) ->assertOk() ->assertJsonPath('props.item.locked', true) ->assertJsonPath('props.item.prompt', null) ->assertJsonPath('props.item.negative_prompt', null) ->assertDontSee('SECRET PREMIUM PROMPT STRING') ->assertDontSee('SECRET NEGATIVE STRING'); } public function test_authorized_user_can_view_premium_prompt(): void { $prompt = AcademyPromptTemplate::query()->create([ 'title' => 'Creator Prompt', 'slug' => 'creator-prompt-allowed', 'excerpt' => 'Full prompt visible.', 'prompt' => 'VISIBLE PREMIUM PROMPT', 'negative_prompt' => 'VISIBLE NEGATIVE PROMPT', 'difficulty' => 'beginner', 'access_level' => 'creator', 'active' => true, 'published_at' => now()->subMinute(), ]); $creator = User::factory()->create(['role' => 'academy_creator']); $this->actingAs($creator) ->get(route('academy.prompts.show', ['slug' => $prompt->slug])) ->assertOk() ->assertSee('VISIBLE PREMIUM PROMPT') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', false) ->where('item.prompt', 'VISIBLE PREMIUM PROMPT')); } public function test_public_lesson_payload_includes_active_ai_comparison_block_and_hides_inactive_results(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Free Lesson With Comparison', 'slug' => 'free-lesson-with-comparison', 'excerpt' => 'Visible to guests.', 'content' => 'Free lesson content', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $block = AcademyLessonBlock::query()->create([ 'lesson_id' => $lesson->id, 'type' => 'ai_comparison', 'title' => 'Same Prompt, Different AI Models', 'payload' => [ 'title' => 'Same Prompt, Different AI Models', 'intro' => 'We used the same prompt in multiple tools.', 'prompt' => 'A peaceful fantasy forest wallpaper.', 'negative_prompt' => 'text, watermark', 'aspect_ratio' => '16:9', 'criteria' => ['Composition', 'Lighting'], ], 'sort_order' => 0, 'active' => true, ]); AcademyAiComparisonResult::query()->create([ 'lesson_block_id' => $block->id, 'provider' => 'OpenAI', 'model_name' => 'ChatGPT Images', 'image_path' => 'academy/lessons/body/aa/bb/example.webp', 'strengths' => 'Strong composition', 'score' => 9, 'sort_order' => 0, 'active' => true, ]); AcademyAiComparisonResult::query()->create([ 'lesson_block_id' => $block->id, 'provider' => 'Google', 'model_name' => 'Gemini', 'image_path' => 'academy/lessons/body/aa/bb/example-2.webp', 'score' => 7, 'sort_order' => 1, 'active' => false, ]); $this->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Academy/Show') ->where('item.blocks.0.payload.prompt', 'A peaceful fantasy forest wallpaper.') ->has('item.blocks.0.comparison_results', 1) ->where('item.blocks.0.comparison_results.0.model_name', 'ChatGPT Images')); } public function test_public_lesson_with_sparse_ai_comparison_block_still_renders_payload(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Sparse Comparison Lesson', 'slug' => 'sparse-comparison-lesson', 'excerpt' => 'Sparse block test.', 'content' => 'Free lesson content', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); AcademyLessonBlock::query()->create([ 'lesson_id' => $lesson->id, 'type' => 'ai_comparison', 'title' => 'Prompt only block', 'payload' => [ 'title' => 'Prompt only block', 'prompt' => 'A fantasy forest at sunrise.', ], 'sort_order' => 0, 'active' => true, ]); $this->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->has('item.blocks', 1) ->where('item.blocks.0.payload.prompt', 'A fantasy forest at sunrise.') ->has('item.blocks.0.comparison_results', 0)); } public function test_logged_in_user_can_mark_lesson_completed(): void { $lesson = AcademyLesson::query()->create([ 'title' => 'Trackable Lesson', 'slug' => 'trackable-lesson', 'content' => 'Track this lesson', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $user = User::factory()->create(); $this->actingAs($user) ->postJson(route('academy.lessons.complete', ['lesson' => $lesson->id])) ->assertOk() ->assertJsonPath('completed', true); $this->assertDatabaseHas('academy_lesson_progress', [ 'lesson_id' => $lesson->id, 'user_id' => $user->id, ]); } public function test_logged_in_user_can_save_and_unsave_prompt(): void { $prompt = AcademyPromptTemplate::query()->create([ 'title' => 'Free Prompt', 'slug' => 'free-prompt', 'prompt' => 'Save me', 'difficulty' => 'beginner', 'access_level' => 'free', 'active' => true, 'published_at' => now()->subMinute(), ]); $user = User::factory()->create(); $this->actingAs($user) ->postJson(route('academy.prompts.save', ['prompt' => $prompt->id])) ->assertOk() ->assertJsonPath('saved', true); $this->assertDatabaseHas('academy_saved_prompts', [ 'prompt_template_id' => $prompt->id, 'user_id' => $user->id, ]); $this->actingAs($user) ->deleteJson(route('academy.prompts.unsave', ['prompt' => $prompt->id])) ->assertOk() ->assertJsonPath('saved', false); $this->assertDatabaseMissing('academy_saved_prompts', [ 'prompt_template_id' => $prompt->id, 'user_id' => $user->id, ]); } public function test_logged_in_user_can_submit_artwork_to_active_challenge(): void { $user = User::factory()->create(); $artwork = Artwork::factory()->create([ 'user_id' => $user->id, 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subMinute(), ]); $challenge = AcademyChallenge::query()->create([ 'title' => 'Open Challenge', 'slug' => 'open-challenge', 'access_level' => 'free', 'status' => 'active', 'active' => true, 'starts_at' => now()->subHour(), 'ends_at' => now()->addDay(), ]); $this->actingAs($user) ->post(route('academy.challenges.submit.store', ['slug' => $challenge->slug]), [ 'artwork_id' => $artwork->id, 'prompt_used' => 'Prompt used', 'workflow_notes' => 'Workflow notes', 'ai_tool_used' => 'ComfyUI', 'is_ai_generated' => true, 'is_ai_assisted' => true, ]) ->assertRedirect(route('academy.challenges.show', ['slug' => $challenge->slug])); $this->assertDatabaseHas('academy_challenge_submissions', [ 'challenge_id' => $challenge->id, 'user_id' => $user->id, 'artwork_id' => $artwork->id, ]); } public function test_guest_cannot_view_creator_or_pro_lesson_content(): void { $creatorLesson = AcademyLesson::query()->create([ 'title' => 'Creator Lesson', 'slug' => 'guest-creator-lesson', 'excerpt' => 'Creator preview', 'content' => 'CREATOR SECRET LESSON BODY', 'difficulty' => 'advanced', 'access_level' => 'creator', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $proLesson = AcademyLesson::query()->create([ 'title' => 'Pro Lesson', 'slug' => 'guest-pro-lesson', 'excerpt' => 'Pro preview', 'content' => 'PRO SECRET LESSON BODY', 'difficulty' => 'pro', 'access_level' => 'pro', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); foreach ([$creatorLesson, $proLesson] as $lesson) { $this->get(route('academy.lessons.show', ['slug' => $lesson->slug])) ->assertOk() ->assertDontSee($lesson->content) ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.content', null)); } } public function test_logged_in_free_user_cannot_view_creator_or_pro_prompt_content(): void { $creatorPrompt = AcademyPromptTemplate::query()->create([ 'title' => 'Creator Prompt', 'slug' => 'free-user-creator-prompt', 'excerpt' => 'Creator preview.', 'prompt' => 'CREATOR PREMIUM PROMPT', 'negative_prompt' => 'CREATOR PREMIUM NEGATIVE', 'difficulty' => 'beginner', 'access_level' => 'creator', 'active' => true, 'published_at' => now()->subMinute(), ]); $proPrompt = AcademyPromptTemplate::query()->create([ 'title' => 'Pro Prompt', 'slug' => 'free-user-pro-prompt', 'excerpt' => 'Pro preview.', 'prompt' => 'PRO PREMIUM PROMPT', 'negative_prompt' => 'PRO PREMIUM NEGATIVE', 'difficulty' => 'pro', 'access_level' => 'pro', 'active' => true, 'published_at' => now()->subMinute(), ]); $user = User::factory()->create(); foreach ([$creatorPrompt, $proPrompt] as $prompt) { $this->actingAs($user) ->get(route('academy.prompts.show', ['slug' => $prompt->slug])) ->assertOk() ->assertDontSee($prompt->prompt) ->assertDontSee((string) $prompt->negative_prompt) ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.prompt', null) ->where('item.negative_prompt', null)); } } public function test_creator_user_can_view_creator_prompt_but_not_pro_prompt(): void { $creatorPrompt = AcademyPromptTemplate::query()->create([ 'title' => 'Creator Prompt', 'slug' => 'creator-visible-prompt', 'excerpt' => 'Creator prompt.', 'prompt' => 'CREATOR ACCESS PROMPT', 'negative_prompt' => 'CREATOR ACCESS NEGATIVE', 'difficulty' => 'intermediate', 'access_level' => 'creator', 'active' => true, 'published_at' => now()->subMinute(), ]); $proPrompt = AcademyPromptTemplate::query()->create([ 'title' => 'Pro Prompt', 'slug' => 'creator-locked-pro-prompt', 'excerpt' => 'Pro prompt.', 'prompt' => 'PRO ONLY ACCESS PROMPT', 'negative_prompt' => 'PRO ONLY ACCESS NEGATIVE', 'difficulty' => 'pro', 'access_level' => 'pro', 'active' => true, 'published_at' => now()->subMinute(), ]); $creator = User::factory()->create(['role' => 'academy_creator']); $this->actingAs($creator) ->get(route('academy.prompts.show', ['slug' => $creatorPrompt->slug])) ->assertOk() ->assertSee('CREATOR ACCESS PROMPT') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', false) ->where('item.prompt', 'CREATOR ACCESS PROMPT')); $this->actingAs($creator) ->get(route('academy.prompts.show', ['slug' => $proPrompt->slug])) ->assertOk() ->assertDontSee('PRO ONLY ACCESS PROMPT') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', true) ->where('item.prompt', null)); } public function test_pro_and_admin_users_can_view_pro_prompt(): void { $prompt = AcademyPromptTemplate::query()->create([ 'title' => 'Pro Prompt', 'slug' => 'pro-visible-prompt', 'excerpt' => 'Visible to pro and admin.', 'prompt' => 'VISIBLE TO PRO AND ADMIN', 'negative_prompt' => 'VISIBLE NEGATIVE TO PRO AND ADMIN', 'difficulty' => 'pro', 'access_level' => 'pro', 'active' => true, 'published_at' => now()->subMinute(), ]); foreach ([ User::factory()->create(['role' => 'academy_pro']), User::factory()->create(['role' => 'admin']), ] as $user) { $this->actingAs($user) ->get(route('academy.prompts.show', ['slug' => $prompt->slug])) ->assertOk() ->assertSee('VISIBLE TO PRO AND ADMIN') ->assertInertia(fn (AssertableInertia $page) => $page ->where('item.locked', false) ->where('item.prompt', 'VISIBLE TO PRO AND ADMIN')); } } public function test_challenge_routes_are_hidden_when_challenges_are_disabled(): void { config(['academy.challenges_enabled' => false]); $this->get('/academy/challenges')->assertNotFound(); } public function test_checkout_returns_payment_disabled_response_when_payments_are_disabled(): void { config(['academy.payments_enabled' => false]); $user = User::factory()->create(); $this->actingAs($user) ->postJson(route('academy.checkout', ['plan' => 'creator-monthly'])) ->assertStatus(423) ->assertJsonPath('code', 'academy_payments_disabled'); } public function test_duplicate_prompt_save_is_idempotent(): void { $prompt = AcademyPromptTemplate::query()->create([ 'title' => 'Free Prompt', 'slug' => 'idempotent-save-prompt', 'prompt' => 'Save me twice', 'difficulty' => 'beginner', 'access_level' => 'free', 'active' => true, 'published_at' => now()->subMinute(), ]); $user = User::factory()->create(); $this->actingAs($user)->postJson(route('academy.prompts.save', ['prompt' => $prompt->id]))->assertOk(); $this->actingAs($user)->postJson(route('academy.prompts.save', ['prompt' => $prompt->id]))->assertOk(); $this->assertSame(1, DB::table('academy_saved_prompts')->where('user_id', $user->id)->where('prompt_template_id', $prompt->id)->count()); } public function test_duplicate_challenge_submission_updates_existing_record_instead_of_creating_duplicate(): void { $user = User::factory()->create(); $artwork = Artwork::factory()->create([ 'user_id' => $user->id, 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subMinute(), ]); $challenge = AcademyChallenge::query()->create([ 'title' => 'Duplicate Safe Challenge', 'slug' => 'duplicate-safe-challenge', 'access_level' => 'free', 'status' => 'active', 'active' => true, 'starts_at' => now()->subHour(), 'ends_at' => now()->addDay(), ]); $payload = [ 'artwork_id' => $artwork->id, 'prompt_used' => 'Prompt one', 'workflow_notes' => 'Workflow one', 'ai_tool_used' => 'ComfyUI', 'is_ai_generated' => true, 'is_ai_assisted' => true, ]; $this->actingAs($user)->post(route('academy.challenges.submit.store', ['slug' => $challenge->slug]), $payload)->assertRedirect(); $payload['workflow_notes'] = 'Workflow two'; $this->actingAs($user)->post(route('academy.challenges.submit.store', ['slug' => $challenge->slug]), $payload)->assertRedirect(); $this->assertSame(1, DB::table('academy_challenge_submissions')->where('challenge_id', $challenge->id)->where('user_id', $user->id)->where('artwork_id', $artwork->id)->count()); $this->assertDatabaseHas('academy_challenge_submissions', [ 'challenge_id' => $challenge->id, 'user_id' => $user->id, 'artwork_id' => $artwork->id, 'workflow_notes' => 'Workflow two', ]); } }