Files
SkinbaseNova/tests/Feature/FeaturedArtworkAdminTest.php

316 lines
13 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Artwork;
use App\Models\ArtworkFeature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Inertia\Testing\AssertableInertia;
use Klevze\ControlPanel\Models\Admin\AdminVerification;
use Klevze\ControlPanel\Core\Structs\MenuRootItem;
use Klevze\ControlPanel\Framework\Core\Menu as ControlPanelMenu;
uses(RefreshDatabase::class);
function createControlPanelAdmin(): User
{
$admin = User::factory()->create(['role' => 'admin']);
$admin->forceFill([
'isAdmin' => true,
'activated' => true,
])->save();
AdminVerification::createForUser($admin->fresh());
return $admin->fresh();
}
function adminArtwork(array $attributes = []): Artwork
{
return Artwork::factory()->create(array_merge([
'title' => 'Featured Artwork ' . fake()->unique()->words(2, true),
'slug' => 'featured-artwork-' . fake()->unique()->numberBetween(1000, 9999),
'is_public' => true,
'is_approved' => true,
'published_at' => now()->subHour(),
'has_missing_thumbnails' => false,
], $attributes));
}
function featureRow(Artwork $artwork, array $attributes = []): ArtworkFeature
{
return ArtworkFeature::query()->create(array_merge([
'artwork_id' => $artwork->id,
'priority' => 100,
'featured_at' => now()->subHour(),
'expires_at' => null,
'is_active' => true,
], $attributes));
}
function medalScore(Artwork $artwork, int $score30d): void
{
DB::table('artwork_medal_stats')->insert([
'artwork_id' => $artwork->id,
'gold_count' => 0,
'silver_count' => 0,
'bronze_count' => 0,
'score_total' => $score30d,
'score_7d' => $score30d,
'score_30d' => $score30d,
'last_medaled_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
}
it('blocks non staff users from the featured artworks admin area', function (): void {
$user = User::factory()->create(['role' => 'user']);
$user->forceFill([
'isAdmin' => false,
'activated' => true,
])->save();
$this->actingAs($user)->actingAs($user, 'controlpanel')
->get(route('admin.cp.artworks.featured.main'))
->assertRedirect(route('cp.login'));
});
it('registers the featured artworks entry in the cpad menu', function (): void {
$sidebarMenu = collect(app(ControlPanelMenu::class)->getSidebarMenu());
$editorialRoot = $sidebarMenu
->first(fn ($item): bool => $item instanceof MenuRootItem && $item->getName() === 'Artworks');
expect($editorialRoot)->toBeInstanceOf(MenuRootItem::class);
$featuredItem = collect($editorialRoot->getItems())
->first(fn ($item): bool => ($item->name ?? null) === 'Featured Artworks');
expect($featuredItem)->not->toBeNull()
->and($featuredItem->mainRoute)->toBe('admin.cp.artworks.featured.main')
->and($featuredItem->icon)->toBe('fas fa-star');
});
it('renders the featured artworks admin index with the current winner summary', function (): void {
$admin = createControlPanelAdmin();
$owner = User::factory()->create(['username' => 'winnermaker']);
$higherMedal = adminArtwork(['user_id' => $owner->id, 'title' => 'Higher Medal Winner']);
$runnerUp = adminArtwork(['user_id' => $owner->id, 'title' => 'Runner Up']);
featureRow($higherMedal, ['priority' => 100, 'featured_at' => now()->subHour()]);
featureRow($runnerUp, ['priority' => 100, 'featured_at' => now()->subHour()]);
medalScore($higherMedal, 12);
medalScore($runnerUp, 3);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->get(route('admin.cp.artworks.featured.main'))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Collection/FeaturedArtworksAdmin')
->where('winner.artwork.id', $higherMedal->id)
->where('winner.medals.score_30d', 12)
->where('winner.selection_reason', 'Tied on priority, won on higher 30-day medal score.')
->where('entries.0.is_winner', true)
->where('entries.0.artwork.id', $higherMedal->id)
->where('endpoints.store', route('admin.cp.artworks.featured.store')));
});
it('allows admins to create featured rows', function (): void {
$admin = createControlPanelAdmin();
$artwork = adminArtwork();
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->postJson(route('admin.cp.artworks.featured.store'), [
'artwork_id' => $artwork->id,
'priority' => 220,
'featured_at' => now()->toISOString(),
'expires_at' => now()->addDay()->toISOString(),
'is_active' => true,
])
->assertOk()
->assertJsonPath('winner.artwork.id', $artwork->id)
->assertJsonPath('stats.total', 1);
$feature = ArtworkFeature::query()->firstOrFail();
expect((int) $feature->artwork_id)->toBe($artwork->id)
->and((int) $feature->priority)->toBe(220)
->and((int) $feature->created_by)->toBe($admin->id)
->and((bool) $feature->is_active)->toBeTrue();
});
it('allows admins to update featured rows', function (): void {
$admin = createControlPanelAdmin();
$feature = featureRow(adminArtwork(), [
'priority' => 50,
'featured_at' => now()->subDays(2),
'is_active' => true,
]);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.update', ['feature' => $feature->id]), [
'priority' => 180,
'featured_at' => now()->subHour()->toISOString(),
'expires_at' => now()->addHours(6)->toISOString(),
'is_active' => false,
])
->assertOk()
->assertJsonPath('entries.0.priority', 180)
->assertJsonPath('entries.0.is_active', false);
$fresh = $feature->fresh();
expect((int) $fresh->priority)->toBe(180)
->and((bool) $fresh->is_active)->toBeFalse()
->and($fresh->expires_at)->not->toBeNull();
});
it('allows admins to activate and deactivate featured rows', function (): void {
$admin = createControlPanelAdmin();
$feature = featureRow(adminArtwork(), ['is_active' => true]);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.toggle', ['feature' => $feature->id]))
->assertOk()
->assertJsonPath('entries.0.is_active', false);
expect($feature->fresh()->is_active)->toBeFalse();
});
it('allows admins to force and unforce the homepage hero from the featured pool', function (): void {
$admin = createControlPanelAdmin();
$owner = User::factory()->create(['username' => 'forcehero']);
$naturalWinner = adminArtwork(['user_id' => $owner->id, 'title' => 'Natural Winner']);
$forcedArtwork = adminArtwork(['user_id' => $owner->id, 'title' => 'Forced Winner']);
$naturalFeature = featureRow($naturalWinner, ['priority' => 300, 'featured_at' => now()->subHour()]);
$forcedFeature = featureRow($forcedArtwork, ['priority' => 100, 'featured_at' => now()->subDays(2)]);
medalScore($naturalWinner, 50);
medalScore($forcedArtwork, 1);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.force-hero', ['feature' => $forcedFeature->id]))
->assertOk()
->assertJsonPath('winner.artwork.id', $forcedArtwork->id)
->assertJsonPath('winner.is_force_hero', true)
->assertJsonPath('winner.selection_reason', 'Forced hero override is enabled for this featured artwork.');
expect($forcedFeature->fresh()->force_hero)->toBeTrue()
->and($naturalFeature->fresh()->force_hero)->toBeFalse()
->and(ArtworkFeature::query()->where('force_hero', true)->count())->toBe(1);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.force-hero', ['feature' => $forcedFeature->id]))
->assertOk()
->assertJsonPath('winner.artwork.id', $naturalWinner->id)
->assertJsonPath('winner.is_force_hero', false)
->assertJsonPath('winner.selection_reason', 'Highest priority among active, eligible featured artworks.');
expect($forcedFeature->fresh()->force_hero)->toBeFalse()
->and(ArtworkFeature::query()->where('force_hero', true)->count())->toBe(0);
});
it('returns a forced hero as the admin winner even when standard artwork eligibility fails', function (): void {
$admin = createControlPanelAdmin();
$owner = User::factory()->create(['username' => 'forceheromissingpreview']);
$naturalWinner = adminArtwork(['user_id' => $owner->id, 'title' => 'Natural Winner']);
$forcedArtwork = adminArtwork(['user_id' => $owner->id, 'title' => 'Forced Missing Preview', 'has_missing_thumbnails' => true]);
featureRow($naturalWinner, ['priority' => 300, 'featured_at' => now()->subHour()]);
$forcedFeature = featureRow($forcedArtwork, ['priority' => 100, 'featured_at' => now()->subDays(2)]);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.force-hero', ['feature' => $forcedFeature->id]))
->assertOk()
->assertJsonPath('winner.artwork.id', $forcedArtwork->id)
->assertJsonPath('winner.is_force_hero', true)
->assertJsonPath('winner.selection_reason', 'Forced hero override is enabled for this featured artwork.');
expect(ArtworkFeature::query()->where('force_hero', true)->count())->toBe(1);
});
it('allows admins to delete featured rows', function (): void {
$admin = createControlPanelAdmin();
$feature = featureRow(adminArtwork());
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->deleteJson(route('admin.cp.artworks.featured.delete', ['feature' => $feature->id]))
->assertOk()
->assertJsonPath('stats.total', 0);
expect(ArtworkFeature::query()->count())->toBe(0)
->and(ArtworkFeature::withTrashed()->count())->toBe(1);
});
it('marks expired and ineligible rows on the index page', function (): void {
$admin = createControlPanelAdmin();
$privateArtwork = adminArtwork([
'title' => 'Private Artwork',
'is_public' => false,
'visibility' => Artwork::VISIBILITY_PRIVATE,
]);
$expiredArtwork = adminArtwork(['title' => 'Expired Artwork']);
featureRow($privateArtwork, ['priority' => 300]);
featureRow($expiredArtwork, ['priority' => 200, 'expires_at' => now()->subMinute()]);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->get(route('admin.cp.artworks.featured.main'))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Collection/FeaturedArtworksAdmin')
->where('entries.0.artwork.id', $privateArtwork->id)
->where('entries.0.eligibility.is_eligible', false)
->where('entries.0.eligibility.reasons.0', 'Private')
->where('entries.1.artwork.id', $expiredArtwork->id)
->where('entries.1.is_expired', true)
->where('stats.expired', 1)
->where('stats.ineligible', 2));
});
it('clears homepage hero cache after create update toggle and delete actions', function (): void {
$admin = createControlPanelAdmin();
$artwork = adminArtwork();
Cache::put('homepage.hero', ['stale' => true], 600);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->postJson(route('admin.cp.artworks.featured.store'), [
'artwork_id' => $artwork->id,
'priority' => 100,
'featured_at' => now()->toISOString(),
'expires_at' => null,
'is_active' => true,
])
->assertOk();
expect(Cache::has('homepage.hero'))->toBeFalse();
$feature = ArtworkFeature::query()->firstOrFail();
Cache::put('homepage.hero', ['stale' => true], 600);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.update', ['feature' => $feature->id]), [
'priority' => 110,
'featured_at' => now()->addMinute()->toISOString(),
'expires_at' => now()->addDay()->toISOString(),
'is_active' => true,
])
->assertOk();
expect(Cache::has('homepage.hero'))->toBeFalse();
Cache::put('homepage.hero', ['stale' => true], 600);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->patchJson(route('admin.cp.artworks.featured.toggle', ['feature' => $feature->id]))
->assertOk();
expect(Cache::has('homepage.hero'))->toBeFalse();
Cache::put('homepage.hero', ['stale' => true], 600);
$this->actingAs($admin)->actingAs($admin, 'controlpanel')
->deleteJson(route('admin.cp.artworks.featured.delete', ['feature' => $feature->id]))
->assertOk();
expect(Cache::has('homepage.hero'))->toBeFalse();
});