withoutMiddleware(ConditionalValidateCsrfToken::class); } public function test_admin_can_open_academy_dashboard(): void { $admin = User::factory()->create(['role' => 'admin']); $this->actingAs($admin) ->get('/moderation/academy/dashboard') ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Admin/Academy/Dashboard') ->where('stats.lessons', 0) ->where('stats.prompts', 0)); } public function test_admin_can_approve_and_reject_challenge_submission(): void { $admin = User::factory()->create(['role' => 'admin']); $user = User::factory()->create(); $artwork = Artwork::factory()->create(['user_id' => $user->id]); $challenge = AcademyChallenge::query()->create([ 'title' => 'Moderated Challenge', 'slug' => 'moderated-challenge', 'access_level' => 'free', 'status' => 'active', 'active' => true, ]); $submission = AcademyChallengeSubmission::query()->create([ 'challenge_id' => $challenge->id, 'user_id' => $user->id, 'artwork_id' => $artwork->id, 'moderation_status' => 'pending', 'submitted_at' => now(), ]); $this->from('/moderation/academy/submissions') ->actingAs($admin) ->post(route('admin.academy.submissions.approve', ['academyChallengeSubmission' => $submission])) ->assertRedirect('/moderation/academy/submissions'); $this->assertDatabaseHas('academy_challenge_submissions', [ 'id' => $submission->id, 'moderation_status' => 'approved', ]); $this->from('/moderation/academy/submissions') ->actingAs($admin) ->post(route('admin.academy.submissions.reject', ['academyChallengeSubmission' => $submission])) ->assertRedirect('/moderation/academy/submissions'); $this->assertDatabaseHas('academy_challenge_submissions', [ 'id' => $submission->id, 'moderation_status' => 'rejected', ]); } public function test_admin_can_open_all_academy_modules(): void { $admin = User::factory()->create(['role' => 'admin']); foreach ([ '/moderation/academy/dashboard', '/moderation/academy/categories', '/moderation/academy/lessons', '/moderation/academy/prompts', '/moderation/academy/packs', '/moderation/academy/challenges', '/moderation/academy/submissions', '/moderation/academy/badges', ] as $path) { $this->actingAs($admin)->get($path)->assertOk(); } } public function test_admin_category_update_clears_academy_cache(): void { $admin = User::factory()->create(['role' => 'admin']); $category = AcademyCategory::query()->create([ 'type' => 'lesson', 'name' => 'Prompting Basics', 'slug' => 'prompting-basics', 'order_num' => 10, 'active' => true, ]); Cache::put('academy.home', ['stale' => true], 600); Cache::put('academy.categories.lesson', ['stale' => true], 600); $this->actingAs($admin) ->patch(route('admin.academy.categories.update', ['academyCategory' => $category]), [ 'type' => 'lesson', 'name' => 'Prompting Basics Updated', 'slug' => 'prompting-basics', 'description' => 'Updated description', 'icon' => 'fa-wand-magic-sparkles', 'order_num' => 11, 'active' => true, ]) ->assertRedirect(route('admin.academy.categories.edit', ['academyCategory' => $category])); $this->assertNull(Cache::get('academy.home')); $this->assertNull(Cache::get('academy.categories.lesson')); } public function test_admin_can_create_a_lesson_with_ai_comparison_block(): void { $admin = User::factory()->create(['role' => 'admin']); $response = $this->actingAs($admin) ->post(route('admin.academy.lessons.store'), [ 'title' => 'AI Comparison Lesson', 'slug' => 'ai-comparison-lesson', 'excerpt' => 'Testing comparison block creation.', 'content' => '
Lesson body.
', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'cover_image' => '', 'video_url' => '', 'reading_minutes' => 5, 'featured' => false, 'active' => true, 'published_at' => now()->subMinute()->toDateTimeString(), 'seo_title' => '', 'seo_description' => '', 'blocks' => [ [ 'type' => 'ai_comparison', 'title' => 'Same Prompt, Different AI Models', 'payload' => [ 'title' => 'Same Prompt, Different AI Models', 'intro' => 'Compare multiple tools.', 'prompt' => 'A peaceful fantasy forest wallpaper.', 'negative_prompt' => 'text, watermark', 'aspect_ratio' => '16:9', 'criteria' => ['Composition', 'Lighting'], ], 'sort_order' => 0, 'active' => true, 'comparison_results' => [], ], ], ]); $lesson = AcademyLesson::query()->where('slug', 'ai-comparison-lesson')->firstOrFail(); $response->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson])); $this->assertDatabaseHas('academy_lesson_blocks', [ 'lesson_id' => $lesson->id, 'type' => 'ai_comparison', 'active' => true, ]); } public function test_admin_can_add_ai_comparison_result_to_existing_lesson(): void { $admin = User::factory()->create(['role' => 'admin']); $lesson = AcademyLesson::query()->create([ 'title' => 'Existing Lesson', 'slug' => 'existing-lesson', 'content' => 'Body', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $this->actingAs($admin) ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [ 'title' => $lesson->title, 'slug' => $lesson->slug, 'excerpt' => '', 'content' => $lesson->content, 'difficulty' => $lesson->difficulty, 'access_level' => $lesson->access_level, 'lesson_type' => $lesson->lesson_type, 'cover_image' => '', 'video_url' => '', 'reading_minutes' => 5, 'featured' => false, 'active' => true, 'published_at' => now()->subMinute()->toDateTimeString(), 'seo_title' => '', 'seo_description' => '', 'blocks' => [ [ 'type' => 'ai_comparison', 'title' => 'Same Prompt, Different AI Models', 'payload' => [ 'title' => 'Same Prompt, Different AI Models', 'intro' => 'Compare multiple tools.', 'prompt' => 'A peaceful fantasy forest wallpaper.', 'negative_prompt' => '', 'aspect_ratio' => '16:9', 'criteria' => ['Composition'], ], 'sort_order' => 0, 'active' => true, 'comparison_results' => [ [ 'provider' => 'OpenAI', 'model_name' => 'ChatGPT Images', 'image_path' => 'academy/lessons/body/aa/bb/example.webp', 'thumb_path' => 'academy/lessons/body/aa/bb/example-thumb.webp', 'settings' => 'Default quality', 'strengths' => 'Strong composition', 'weaknesses' => 'Slightly over-polished', 'best_for' => 'Wallpaper concepts', 'score' => 9, 'sort_order' => 0, 'active' => true, ], ], ], ], ]) ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson])); $block = AcademyLessonBlock::query()->where('lesson_id', $lesson->id)->firstOrFail(); $this->assertDatabaseHas('academy_ai_comparison_results', [ 'lesson_block_id' => $block->id, 'provider' => 'OpenAI', 'model_name' => 'ChatGPT Images', 'score' => 9, ]); } public function test_ai_comparison_score_must_stay_in_range(): void { $admin = User::factory()->create(['role' => 'admin']); $lesson = AcademyLesson::query()->create([ 'title' => 'Validation Lesson', 'slug' => 'validation-lesson', 'content' => 'Body', 'difficulty' => 'beginner', 'access_level' => 'free', 'lesson_type' => 'article', 'active' => true, 'published_at' => now()->subMinute(), ]); $this->from(route('admin.academy.lessons.edit', ['academyLesson' => $lesson])) ->actingAs($admin) ->patch(route('admin.academy.lessons.update', ['academyLesson' => $lesson]), [ 'title' => $lesson->title, 'slug' => $lesson->slug, 'excerpt' => '', 'content' => $lesson->content, 'difficulty' => $lesson->difficulty, 'access_level' => $lesson->access_level, 'lesson_type' => $lesson->lesson_type, 'cover_image' => '', 'video_url' => '', 'reading_minutes' => 5, 'featured' => false, 'active' => true, 'published_at' => now()->subMinute()->toDateTimeString(), 'seo_title' => '', 'seo_description' => '', 'blocks' => [ [ 'type' => 'ai_comparison', 'title' => 'Invalid score block', 'payload' => [ 'title' => 'Invalid score block', 'prompt' => 'Prompt', 'criteria' => ['Composition'], ], 'sort_order' => 0, 'active' => true, 'comparison_results' => [ [ 'provider' => 'OpenAI', 'model_name' => 'ChatGPT Images', 'image_path' => 'academy/lessons/body/aa/bb/example.webp', 'score' => 11, 'sort_order' => 0, 'active' => true, ], ], ], ], ]) ->assertRedirect(route('admin.academy.lessons.edit', ['academyLesson' => $lesson])) ->assertSessionHasErrors(['blocks.0.comparison_results.0.score']); } public function test_lesson_delete_soft_deletes_ai_comparison_children(): void { $admin = User::factory()->create(['role' => 'admin']); $lesson = AcademyLesson::query()->create([ 'title' => 'Delete Lesson', 'slug' => 'delete-lesson', 'content' => 'Body', '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' => 'Delete Block', 'payload' => ['title' => 'Delete Block', 'prompt' => 'Prompt'], 'sort_order' => 0, 'active' => true, ]); $result = AcademyAiComparisonResult::query()->create([ 'lesson_block_id' => $block->id, 'provider' => 'OpenAI', 'model_name' => 'ChatGPT Images', 'image_path' => 'academy/lessons/body/aa/bb/example.webp', 'score' => 8, 'sort_order' => 0, 'active' => true, ]); $this->actingAs($admin) ->delete(route('admin.academy.lessons.destroy', ['academyLesson' => $lesson])) ->assertRedirect(route('admin.academy.lessons.index')); $this->assertSoftDeleted('academy_lessons', ['id' => $lesson->id]); $this->assertSoftDeleted('academy_lesson_blocks', ['id' => $block->id]); $this->assertSoftDeleted('academy_ai_comparison_results', ['id' => $result->id]); } }