394 lines
16 KiB
PHP
394 lines
16 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\NovaCard;
|
|
use App\Models\NovaCardAssetPack;
|
|
use App\Models\NovaCardCategory;
|
|
use App\Models\NovaCardChallenge;
|
|
use App\Models\NovaCardCollection;
|
|
use App\Models\NovaCardCollectionItem;
|
|
use App\Models\NovaCardTemplate;
|
|
use App\Models\Report;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
function adminCardCategory(array $attributes = []): NovaCardCategory
|
|
{
|
|
return NovaCardCategory::query()->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();
|
|
});
|