create(array_merge([ 'slug' => 'admin-category-' . Str::lower(Str::random(6)), 'name' => 'Admin Category', 'description' => 'Admin category', 'active' => true, 'order_num' => 0, ], $attributes)); } function adminCardTemplate(array $attributes = []): NovaCardTemplate { return NovaCardTemplate::query()->create(array_merge([ 'slug' => 'admin-template-' . Str::lower(Str::random(6)), 'name' => 'Admin Template', 'description' => 'Admin template', 'config_json' => [ 'font_preset' => 'modern-sans', 'gradient_preset' => 'midnight-nova', 'layout' => 'quote_heavy', 'text_align' => 'center', 'text_color' => '#ffffff', 'overlay_style' => 'dark-soft', ], 'supported_formats' => ['square'], 'active' => true, 'official' => true, 'order_num' => 0, ], $attributes)); } function moderatedCard(User $user, array $attributes = []): NovaCard { $category = $attributes['category'] ?? adminCardCategory(); $template = $attributes['template'] ?? adminCardTemplate(); return NovaCard::query()->create(array_merge([ 'user_id' => $user->id, 'category_id' => $category->id, 'template_id' => $template->id, 'title' => 'Moderation Card', 'slug' => 'moderation-card-' . Str::lower(Str::random(6)), 'quote_text' => 'A card waiting for moderation.', 'format' => NovaCard::FORMAT_SQUARE, 'project_json' => [ 'content' => ['title' => 'Moderation Card', 'quote_text' => 'A card waiting for moderation.'], 'layout' => ['layout' => 'quote_heavy', 'position' => 'center', 'alignment' => 'center', 'padding' => 'comfortable', 'max_width' => 'balanced'], 'typography' => ['font_preset' => 'modern-sans', 'text_color' => '#ffffff', 'accent_color' => '#e0f2fe', 'quote_size' => 72, 'author_size' => 28, 'letter_spacing' => 0, 'line_height' => 1.2, 'shadow_preset' => 'soft'], 'background' => ['type' => 'gradient', 'gradient_preset' => 'midnight-nova', 'gradient_colors' => ['#0f172a', '#1d4ed8'], 'overlay_style' => 'dark-soft', 'focal_position' => 'center', 'blur_level' => 0, 'opacity' => 50], 'decorations' => [], ], 'render_version' => 1, 'background_type' => 'gradient', 'visibility' => NovaCard::VISIBILITY_PUBLIC, 'status' => NovaCard::STATUS_PUBLISHED, 'moderation_status' => NovaCard::MOD_PENDING, 'featured' => false, 'allow_download' => true, 'published_at' => now()->subMinutes(10), ], Arr::except($attributes, ['category', 'template']))); } it('blocks non staff users from the cards admin area', function (): void { $user = User::factory()->create(['role' => 'user']); $this->actingAs($user) ->get(route('cp.cards.index')) ->assertForbidden(); }); it('renders the cards admin index for admins', function (): void { $admin = User::factory()->create(['role' => 'admin']); $creator = User::factory()->create(['username' => 'spotlightmaker', 'nova_featured_creator' => true]); $card = moderatedCard($creator, [ 'title' => 'Needs Review', 'project_json' => [ 'content' => ['title' => 'Needs Review', 'quote_text' => 'A card waiting for moderation.'], 'moderation' => [ 'source' => 'publish_heuristics', 'flagged' => true, 'reasons' => ['duplicate_content'], 'override' => [ 'moderation_status' => 'flagged', 'disposition' => 'escalated_for_review', 'disposition_label' => 'Escalated for review', 'source' => 'report_queue', 'actor_username' => 'modone', 'updated_at' => now()->subMinutes(5)->toISOString(), ], 'override_history' => [ [ 'moderation_status' => 'flagged', 'disposition' => 'escalated_for_review', 'disposition_label' => 'Escalated for review', 'source' => 'report_queue', 'actor_username' => 'modone', 'updated_at' => now()->subMinutes(5)->toISOString(), ], [ 'moderation_status' => 'pending', 'disposition' => 'returned_to_pending', 'disposition_label' => 'Returned to pending', 'source' => 'admin_card_update', 'actor_username' => 'modtwo', 'updated_at' => now()->subMinutes(12)->toISOString(), ], ], ], ], ]); Report::query()->create([ 'reporter_id' => $creator->id, 'target_type' => 'nova_card', 'target_id' => $card->id, 'reason' => 'Spam remix bait', 'status' => 'open', ]); $this->actingAs($admin) ->get(route('cp.cards.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Collection/NovaCardsAdminIndex') ->where('cards.data.0.title', 'Needs Review') ->where('cards.data.0.moderation_reasons.0', 'duplicate_content') ->where('cards.data.0.moderation_reason_labels.0', 'Duplicate content') ->where('cards.data.0.moderation_override_history.1.disposition_label', 'Returned to pending') ->where('moderationDispositionOptions.approved.1.value', 'approved_with_watch') ->where('featuredCreators.0.username', 'spotlightmaker') ->where('featuredCreators.0.nova_featured_creator', true) ->where('reportingQueue.label', 'Nova Cards report queue') ->where('reportingQueue.pending', 1) ->where('endpoints.updateCreatorPattern', route('cp.cards.creators.update', ['user' => '__CREATOR__'])) ->where('endpoints.moderateReportTargetPattern', route('api.admin.reports.moderate-target', ['report' => '__REPORT__'])) ->where('endpoints.templates', route('cp.cards.templates.index'))); }); it('allows admins to update card moderation fields', function (): void { $admin = User::factory()->create(['role' => 'admin']); $creator = User::factory()->create(); $card = moderatedCard($creator); $this->actingAs($admin) ->patchJson(route('cp.cards.update', ['card' => $card->id]), [ 'status' => NovaCard::STATUS_PUBLISHED, 'moderation_status' => NovaCard::MOD_APPROVED, 'disposition' => 'approved_with_watch', 'featured' => true, 'allow_remix' => false, ]) ->assertOk() ->assertJsonPath('card.featured', true) ->assertJsonPath('card.moderation_status', NovaCard::MOD_APPROVED) ->assertJsonPath('card.moderation_override.source', 'admin_card_update') ->assertJsonPath('card.moderation_override.disposition', 'approved_with_watch') ->assertJsonPath('card.moderation_override.disposition_label', 'Approved with watch') ->assertJsonPath('card.moderation_override_history.0.disposition_label', 'Approved with watch') ->assertJsonPath('card.moderation_override.actor_user_id', $admin->id) ->assertJsonPath('card.allow_remix', false); $fresh = $card->fresh(); expect($fresh->featured)->toBeTrue(); expect($fresh->moderation_status)->toBe(NovaCard::MOD_APPROVED); expect($fresh->allow_remix)->toBeFalse(); expect($fresh->project_json['moderation']['override']['source'] ?? null)->toBe('admin_card_update') ->and($fresh->project_json['moderation']['override']['disposition'] ?? null)->toBe('approved_with_watch'); }); it('allows admins to feature creators for editorial surfacing', function (): void { $admin = User::factory()->create(['role' => 'admin']); $creator = User::factory()->create(['username' => 'creatorflag']); moderatedCard($creator, [ 'title' => 'Eligible Creator Card', 'status' => NovaCard::STATUS_PUBLISHED, 'moderation_status' => NovaCard::MOD_APPROVED, 'visibility' => NovaCard::VISIBILITY_PUBLIC, ]); $this->actingAs($admin) ->patchJson(route('cp.cards.creators.update', ['user' => $creator->id]), [ 'nova_featured_creator' => true, ]) ->assertOk() ->assertJsonPath('creator.username', 'creatorflag') ->assertJsonPath('creator.nova_featured_creator', true) ->assertJsonPath('creator.public_cards_count', 1); expect($creator->fresh()->nova_featured_creator)->toBeTrue(); }); it('allows admins to create categories and templates', function (): void { $admin = User::factory()->create(['role' => 'admin']); $this->actingAs($admin) ->postJson(route('cp.cards.categories.store'), [ 'slug' => 'serenity', 'name' => 'Serenity', 'description' => 'Peaceful cards', 'active' => true, 'order_num' => 3, ]) ->assertOk() ->assertJsonPath('category.slug', 'serenity'); $this->actingAs($admin) ->postJson(route('cp.cards.templates.store'), [ 'slug' => 'calm-quote', 'name' => 'Calm Quote', 'description' => 'Soft centered quote card', 'supported_formats' => ['square', 'story'], 'active' => true, 'official' => true, 'order_num' => 1, 'config_json' => [ 'font_preset' => 'modern-sans', 'gradient_preset' => 'midnight-nova', 'layout' => 'quote_heavy', 'text_align' => 'center', 'text_color' => '#ffffff', 'overlay_style' => 'dark-soft', ], ]) ->assertOk() ->assertJsonPath('template.slug', 'calm-quote') ->assertJsonPath('template.supported_formats.1', 'story'); expect(NovaCardCategory::query()->where('slug', 'serenity')->exists())->toBeTrue(); expect(NovaCardTemplate::query()->where('slug', 'calm-quote')->exists())->toBeTrue(); }); it('renders the template admin page for admins', function (): void { $admin = User::factory()->create(['role' => 'admin']); adminCardTemplate(['slug' => 'editorial', 'name' => 'Editorial']); $this->actingAs($admin) ->get(route('cp.cards.templates.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Collection/NovaCardsTemplateAdmin') ->where('templates.0.slug', 'editorial') ->where('endpoints.cards', route('cp.cards.index'))); }); it('renders and manages asset packs and challenges for admins', function (): void { $admin = User::factory()->create(['role' => 'admin']); $creator = User::factory()->create(); $winner = moderatedCard($creator, [ 'slug' => 'winner-card', 'title' => 'Winner Card', 'moderation_status' => NovaCard::MOD_APPROVED, ]); NovaCardAssetPack::query()->create([ 'slug' => 'official-pack', 'name' => 'Official Pack', 'description' => 'Admin managed pack', 'type' => 'asset', 'manifest_json' => ['items' => [['key' => 'spark', 'label' => 'Spark', 'glyph' => '✦']]], 'official' => true, 'active' => true, 'order_num' => 0, ]); NovaCardChallenge::query()->create([ 'user_id' => $admin->id, 'slug' => 'editorial-week', 'title' => 'Editorial Week', 'description' => 'Admin challenge', 'prompt' => 'Create a sharp editorial quote card.', 'status' => NovaCardChallenge::STATUS_ACTIVE, 'official' => true, 'featured' => true, 'winner_card_id' => $winner->id, 'starts_at' => now()->subDay(), 'ends_at' => now()->addWeek(), ]); $this->actingAs($admin) ->get(route('cp.cards.asset-packs.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Collection/NovaCardsAssetPackAdmin') ->where('packs.0.slug', 'official-pack')); $this->actingAs($admin) ->get(route('cp.cards.challenges.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Collection/NovaCardsChallengeAdmin') ->where('challenges.0.slug', 'editorial-week')); $this->actingAs($admin) ->postJson(route('cp.cards.asset-packs.store'), [ 'slug' => 'story-pack', 'name' => 'Story Pack', 'description' => 'Pack for stories', 'type' => 'template', 'manifest_json' => ['templates' => ['story-vertical']], 'official' => true, 'active' => true, 'order_num' => 1, ]) ->assertOk() ->assertJsonPath('pack.slug', 'story-pack'); $this->actingAs($admin) ->postJson(route('cp.cards.challenges.store'), [ 'slug' => 'clarity-run', 'title' => 'Clarity Run', 'description' => 'A clean clarity challenge.', 'prompt' => 'Use space and contrast.', 'rules_json' => ['max_entries_per_user' => 1], 'status' => 'active', 'official' => true, 'featured' => false, 'winner_card_id' => $winner->id, 'starts_at' => now()->toISOString(), 'ends_at' => now()->addWeek()->toISOString(), ]) ->assertOk() ->assertJsonPath('challenge.slug', 'clarity-run'); expect(NovaCardAssetPack::query()->where('slug', 'story-pack')->exists())->toBeTrue(); expect(NovaCardChallenge::query()->where('slug', 'clarity-run')->exists())->toBeTrue(); }); it('renders and manages official collections for admins', function (): void { $admin = User::factory()->create(['role' => 'admin']); $cardOwner = User::factory()->create(); $card = moderatedCard($cardOwner, ['slug' => 'collection-managed-card', 'title' => 'Collection Managed Card', 'moderation_status' => NovaCard::MOD_APPROVED]); $collection = NovaCardCollection::query()->create([ 'user_id' => $admin->id, 'slug' => 'official-launch', 'name' => 'Official Launch', 'description' => 'Initial official collection.', 'visibility' => NovaCardCollection::VISIBILITY_PUBLIC, 'official' => true, 'featured' => false, 'cards_count' => 0, ]); $this->actingAs($admin) ->get(route('cp.cards.collections.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Collection/NovaCardsCollectionAdmin') ->where('collections.0.slug', 'official-launch')); $this->actingAs($admin) ->postJson(route('cp.cards.collections.store'), [ 'user_id' => $admin->id, 'slug' => 'staff-picks', 'name' => 'Staff Picks', 'description' => 'Editorial picks.', 'visibility' => 'public', 'official' => true, 'featured' => true, ]) ->assertOk() ->assertJsonPath('collection.slug', 'staff-picks') ->assertJsonPath('collection.featured', true); $this->actingAs($admin) ->postJson(route('cp.cards.collections.cards.store', ['collection' => $collection->id]), [ 'card_id' => $card->id, 'note' => 'Lead card', ]) ->assertOk() ->assertJsonPath('collection.items.0.card.id', $card->id); expect(NovaCardCollectionItem::query()->where('collection_id', $collection->id)->where('card_id', $card->id)->exists())->toBeTrue(); expect(NovaCardCollection::query()->where('slug', 'staff-picks')->value('featured'))->toBeTrue(); });