361 lines
14 KiB
PHP
361 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Admin;
|
|
|
|
use App\Http\Middleware\ConditionalValidateCsrfToken;
|
|
use App\Models\AcademyAiComparisonResult;
|
|
use App\Models\AcademyCategory;
|
|
use App\Models\AcademyChallenge;
|
|
use App\Models\AcademyChallengeSubmission;
|
|
use App\Models\AcademyLesson;
|
|
use App\Models\AcademyLessonBlock;
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Tests\TestCase;
|
|
|
|
final class AcademyAdminTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->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' => '<p>Lesson body.</p>',
|
|
'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]);
|
|
}
|
|
}
|