625 lines
22 KiB
PHP
625 lines
22 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\User;
|
|
use App\Services\ArtworkService;
|
|
use App\Services\HomepageService;
|
|
use App\Services\Images\FeaturedArtworkThumbnailGenerator;
|
|
use App\Support\ArtworkFeaturedImagePath;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function makeFeaturedArtwork(array $attributes = []): Artwork
|
|
{
|
|
return Artwork::factory()->create(array_merge([
|
|
'is_public' => true,
|
|
'is_approved' => true,
|
|
'published_at' => now()->subDay(),
|
|
], $attributes));
|
|
}
|
|
|
|
test('featured hero ordering uses recent medal score as tie-break inside the same priority', function () {
|
|
$owner = User::factory()->create();
|
|
$artworkA = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'A']);
|
|
$artworkB = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'B']);
|
|
|
|
foreach ([$artworkA, $artworkB] as $artwork) {
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 100,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
]);
|
|
}
|
|
|
|
DB::table('artwork_medal_stats')->insert([
|
|
'artwork_id' => $artworkA->id,
|
|
'gold_count' => 0,
|
|
'silver_count' => 1,
|
|
'bronze_count' => 0,
|
|
'score_total' => 3,
|
|
'score_7d' => 3,
|
|
'score_30d' => 3,
|
|
'last_medaled_at' => now()->subDay(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
DB::table('artwork_medal_stats')->insert([
|
|
'artwork_id' => $artworkB->id,
|
|
'gold_count' => 1,
|
|
'silver_count' => 0,
|
|
'bronze_count' => 0,
|
|
'score_total' => 5,
|
|
'score_7d' => 5,
|
|
'score_30d' => 5,
|
|
'last_medaled_at' => now()->subHour(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$featured = app(ArtworkService::class)->getFeaturedArtworks(null, 1);
|
|
|
|
expect($featured->items()[0]->id)->toBe($artworkB->id);
|
|
});
|
|
|
|
test('featured query excludes inactive and expired feature rows', function () {
|
|
$owner = User::factory()->create();
|
|
$activeArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Active']);
|
|
$inactiveArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Inactive']);
|
|
$expiredArtwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Expired']);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
[
|
|
'artwork_id' => $activeArtwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 50,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $inactiveArtwork->id,
|
|
'featured_at' => now(),
|
|
'expires_at' => null,
|
|
'priority' => 999,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => false,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $expiredArtwork->id,
|
|
'featured_at' => now(),
|
|
'expires_at' => now()->subMinute(),
|
|
'priority' => 999,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
]);
|
|
|
|
$featuredIds = app(ArtworkService::class)
|
|
->getFeaturedArtworks(null, 10)
|
|
->getCollection()
|
|
->pluck('id')
|
|
->all();
|
|
|
|
expect($featuredIds)->toContain($activeArtwork->id)
|
|
->and($featuredIds)->not->toContain($inactiveArtwork->id)
|
|
->and($featuredIds)->not->toContain($expiredArtwork->id);
|
|
});
|
|
|
|
test('featured page ignores type filtering when the feature type column is absent', function () {
|
|
$owner = User::factory()->create();
|
|
$artwork = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Type Filter Fallback']);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 100,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
]);
|
|
|
|
$this->get(route('featured', ['type' => 3]))
|
|
->assertOk()
|
|
->assertSee('Type Filter Fallback', false);
|
|
});
|
|
|
|
test('featured hero sorts by priority before featured_at', function () {
|
|
$owner = User::factory()->create();
|
|
$higherPriority = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Higher Priority']);
|
|
$newerFeaturedAt = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Newer Featured']);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
[
|
|
'artwork_id' => $higherPriority->id,
|
|
'featured_at' => now()->subDays(2),
|
|
'expires_at' => null,
|
|
'priority' => 200,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $newerFeaturedAt->id,
|
|
'featured_at' => now(),
|
|
'expires_at' => null,
|
|
'priority' => 100,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
]);
|
|
|
|
$featured = app(ArtworkService::class)->getFeaturedArtworks(null, 1);
|
|
|
|
expect($featured->items()[0]->id)->toBe($higherPriority->id);
|
|
});
|
|
|
|
test('force hero overrides normal homepage eligibility filters without leaking into the public featured listing', function () {
|
|
$owner = User::factory()->create();
|
|
$forcedHero = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Forced Hero',
|
|
'has_missing_thumbnails' => true,
|
|
]);
|
|
$naturalWinner = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Natural Winner',
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
[
|
|
'artwork_id' => $forcedHero->id,
|
|
'featured_at' => now()->subDays(2),
|
|
'expires_at' => null,
|
|
'priority' => 50,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $naturalWinner->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 500,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => false,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
]);
|
|
|
|
$winner = app(ArtworkService::class)->getFeaturedArtworkWinner();
|
|
$featuredIds = app(ArtworkService::class)
|
|
->getFeaturedArtworks(null, 10)
|
|
->getCollection()
|
|
->pluck('id')
|
|
->all();
|
|
|
|
expect($winner?->id)->toBe($forcedHero->id)
|
|
->and($featuredIds)->toContain($naturalWinner->id)
|
|
->and($featuredIds)->not->toContain($forcedHero->id);
|
|
});
|
|
|
|
test('homepage hero payload uses the forced hero artwork when one is set', function () {
|
|
$owner = User::factory()->create();
|
|
$forcedHero = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Forced Homepage Hero',
|
|
'has_missing_thumbnails' => true,
|
|
]);
|
|
$naturalWinner = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Natural Homepage Winner',
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
[
|
|
'artwork_id' => $forcedHero->id,
|
|
'featured_at' => now()->subDays(2),
|
|
'expires_at' => null,
|
|
'priority' => 10,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $naturalWinner->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 500,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => false,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
],
|
|
]);
|
|
|
|
$hero = app(HomepageService::class)->getHeroArtwork();
|
|
|
|
expect($hero)->not->toBeNull()
|
|
->and($hero['id'])->toBe($forcedHero->id)
|
|
->and($hero['title'])->toBe('Forced Homepage Hero');
|
|
});
|
|
|
|
test('homepage renders featured hero picture and preload from dedicated featured thumbnails', function () {
|
|
Cache::flush();
|
|
app(HomepageService::class)->clearFeaturedAndMedalCaches();
|
|
Storage::fake('s3');
|
|
config([
|
|
'uploads.object_storage.disk' => 's3',
|
|
'cdn.files_url' => 'https://files.skinbase.org',
|
|
]);
|
|
|
|
$owner = User::factory()->create();
|
|
$artwork = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Hero With Dedicated Featured Images',
|
|
'hash' => str_repeat('a', 64),
|
|
'file_ext' => 'png',
|
|
'thumb_ext' => 'webp',
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 900,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
]);
|
|
|
|
$paths = app(ArtworkFeaturedImagePath::class);
|
|
|
|
// Featured-thumbnail existence is precomputed by FeaturedArtworkThumbnailGenerator
|
|
// (queued off the request path) and persisted on the artwork row; the homepage
|
|
// request only reads that state, so seed both the fake disk (for URL building)
|
|
// and the DB column here rather than relying on a live Storage::exists() check.
|
|
foreach ($paths->variantNames() as $variant) {
|
|
Storage::disk('s3')->put($paths->objectPath($artwork, $variant), 'featured-image');
|
|
}
|
|
|
|
$artwork->forceFill([
|
|
'featured_thumbnail_variants_json' => $paths->variantNames(),
|
|
'featured_thumbnails_checked_at' => now(),
|
|
])->saveQuietly();
|
|
|
|
$desktopUrl = $paths->url($artwork, 'desktop');
|
|
$desktopXlUrl = $paths->url($artwork, 'desktop_xl');
|
|
$mobileXsUrl = $paths->url($artwork, 'mobile_xs');
|
|
$mobileUrl = $paths->url($artwork, 'mobile');
|
|
|
|
$this->get(route('index'))
|
|
->assertOk()
|
|
->assertSee($desktopUrl, false)
|
|
->assertSee($desktopXlUrl, false)
|
|
->assertSee($mobileXsUrl, false)
|
|
->assertSee($mobileUrl, false)
|
|
->assertSee('rel="preload"', false)
|
|
->assertSee('type="image/webp"', false)
|
|
->assertSee('fetchpriority="high"', false);
|
|
});
|
|
|
|
test('featured hero image resolution never checks the remote disk', function () {
|
|
Cache::flush();
|
|
app(HomepageService::class)->clearFeaturedAndMedalCaches();
|
|
Storage::fake('s3');
|
|
config([
|
|
'uploads.object_storage.disk' => 's3',
|
|
'cdn.files_url' => 'https://files.skinbase.org',
|
|
]);
|
|
|
|
$owner = User::factory()->create();
|
|
$artwork = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Hero Without Live Storage Checks',
|
|
'hash' => str_repeat('e', 64),
|
|
'file_ext' => 'png',
|
|
'thumb_ext' => 'webp',
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 900,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
]);
|
|
|
|
$paths = app(ArtworkFeaturedImagePath::class);
|
|
|
|
$artwork->forceFill([
|
|
'featured_thumbnail_variants_json' => $paths->variantNames(),
|
|
'featured_thumbnails_checked_at' => now(),
|
|
])->saveQuietly();
|
|
|
|
// Note: no files are ever written to the fake "s3" disk. If hasFeaturedThumbnail()
|
|
// fell back to a live Storage::exists() check, these URLs would resolve to null;
|
|
// since it reads the DB column instead, they resolve correctly with zero disk I/O.
|
|
$hero = app(HomepageService::class)->getHeroArtwork();
|
|
|
|
expect($hero)->not->toBeNull()
|
|
->and($hero['id'])->toBe($artwork->id)
|
|
->and($hero['featured_image']['variants']['desktop'])->toBe($paths->url($artwork, 'desktop'))
|
|
->and($hero['featured_image']['variants']['desktop_xl'])->toBe($paths->url($artwork, 'desktop_xl'));
|
|
|
|
expect($artwork->fresh()->hasFeaturedThumbnail('desktop'))->toBeTrue();
|
|
});
|
|
|
|
test('generating featured thumbnail metadata invalidates the stale hero cache so the next resolution uses it', function () {
|
|
Cache::flush();
|
|
app(HomepageService::class)->clearFeaturedAndMedalCaches();
|
|
Storage::fake('s3');
|
|
config([
|
|
'uploads.object_storage.disk' => 's3',
|
|
'cdn.files_url' => 'https://files.skinbase.org',
|
|
]);
|
|
|
|
$owner = User::factory()->create();
|
|
$artwork = makeFeaturedArtwork([
|
|
'user_id' => $owner->id,
|
|
'title' => 'Hero Metadata Refresh',
|
|
'hash' => str_repeat('f', 64),
|
|
'file_ext' => 'png',
|
|
'thumb_ext' => 'webp',
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $artwork->id,
|
|
'featured_at' => now()->subHour(),
|
|
'expires_at' => null,
|
|
'priority' => 900,
|
|
'label' => null,
|
|
'note' => null,
|
|
'is_active' => true,
|
|
'force_hero' => true,
|
|
'created_by' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
'deleted_at' => null,
|
|
]);
|
|
|
|
$homepage = app(HomepageService::class);
|
|
$paths = app(ArtworkFeaturedImagePath::class);
|
|
|
|
// 1. Hero cache is built while featured metadata is unavailable: no variants have
|
|
// been persisted yet, so hasFeaturedThumbnail() reports false for every variant and
|
|
// the payload falls back to the non-featured thumbnail.
|
|
$heroBeforeGeneration = $homepage->getHeroArtwork();
|
|
expect($heroBeforeGeneration)->not->toBeNull()
|
|
->and($heroBeforeGeneration['id'])->toBe($artwork->id)
|
|
->and($heroBeforeGeneration['featured_image']['variants']['desktop'])->toBeNull();
|
|
|
|
// 2. Featured-thumbnail metadata is generated. The generator's plan() (also used by
|
|
// the skinbase:featured-thumbnails:generate command and GenerateFeaturedArtworkThumbnailsJob)
|
|
// discovers the now-present files and persists featured_thumbnail_variants_json via
|
|
// saveQuietly() — which does NOT fire ArtworkObserver/ArtworkFeatureObserver, so any
|
|
// cache invalidation has to come from the generator itself.
|
|
foreach ($paths->variantNames() as $variant) {
|
|
Storage::disk('s3')->put($paths->objectPath($artwork, $variant), 'featured-image');
|
|
}
|
|
app(FeaturedArtworkThumbnailGenerator::class)->plan($artwork->fresh());
|
|
|
|
// 3. The relevant hero cache (all three viewer-visibility segments) was invalidated as
|
|
// a side effect of that generation, without a full application cache flush.
|
|
foreach (['visibility-hide', 'visibility-blur', 'visibility-show'] as $segment) {
|
|
expect(Cache::has("homepage.hero.{$segment}"))->toBeFalse();
|
|
}
|
|
|
|
// 4. The next homepage resolution rebuilds the cache and now uses the featured thumbnail.
|
|
$heroAfterGeneration = $homepage->getHeroArtwork();
|
|
expect($heroAfterGeneration['featured_image']['variants']['desktop'])
|
|
->toBe($paths->url($artwork, 'desktop'));
|
|
});
|
|
|
|
test('community favorites returns artworks ordered by recent medal score', function () {
|
|
$owner = User::factory()->create();
|
|
$leader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Leader']);
|
|
$runnerUp = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Runner Up', 'published_at' => now()->subDays(2)]);
|
|
|
|
DB::table('artwork_medal_stats')->insert([
|
|
[
|
|
'artwork_id' => $runnerUp->id,
|
|
'gold_count' => 0,
|
|
'silver_count' => 2,
|
|
'bronze_count' => 0,
|
|
'score_total' => 6,
|
|
'score_7d' => 6,
|
|
'score_30d' => 6,
|
|
'last_medaled_at' => now()->subHour(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
],
|
|
[
|
|
'artwork_id' => $leader->id,
|
|
'gold_count' => 2,
|
|
'silver_count' => 0,
|
|
'bronze_count' => 0,
|
|
'score_total' => 10,
|
|
'score_7d' => 10,
|
|
'score_30d' => 10,
|
|
'last_medaled_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
],
|
|
]);
|
|
|
|
$results = app(HomepageService::class)->getCommunityFavorites(8);
|
|
|
|
expect($results)->toHaveCount(2)
|
|
->and($results[0]['id'])->toBe($leader->id)
|
|
->and($results[1]['id'])->toBe($runnerUp->id);
|
|
});
|
|
|
|
test('community favorites backfills with archive artworks when medal picks are sparse', function () {
|
|
Cache::flush();
|
|
|
|
$owner = User::factory()->create();
|
|
$leader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Leader']);
|
|
$archiveA = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Archive A', 'published_at' => now()->subDays(10)]);
|
|
$archiveB = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Archive B', 'published_at' => now()->subDays(20)]);
|
|
|
|
DB::table('artwork_medal_stats')->insert([
|
|
'artwork_id' => $leader->id,
|
|
'gold_count' => 1,
|
|
'silver_count' => 0,
|
|
'bronze_count' => 0,
|
|
'score_total' => 5,
|
|
'score_7d' => 5,
|
|
'score_30d' => 5,
|
|
'last_medaled_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$results = app(HomepageService::class)->getCommunityFavorites(3);
|
|
$resultIds = collect($results)->pluck('id')->all();
|
|
|
|
expect($results)->toHaveCount(3)
|
|
->and($resultIds[0])->toBe($leader->id)
|
|
->and($resultIds)->toContain($archiveA->id)
|
|
->and($resultIds)->toContain($archiveB->id);
|
|
});
|
|
|
|
test('hall of fame returns artworks ordered by all time medal score', function () {
|
|
$owner = User::factory()->create();
|
|
$leader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Legend']);
|
|
$runnerUp = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Veteran']);
|
|
|
|
DB::table('artwork_medal_stats')->insert([
|
|
[
|
|
'artwork_id' => $runnerUp->id,
|
|
'gold_count' => 1,
|
|
'silver_count' => 1,
|
|
'bronze_count' => 1,
|
|
'score_total' => 9,
|
|
'score_7d' => 0,
|
|
'score_30d' => 0,
|
|
'last_medaled_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
],
|
|
[
|
|
'artwork_id' => $leader->id,
|
|
'gold_count' => 3,
|
|
'silver_count' => 0,
|
|
'bronze_count' => 0,
|
|
'score_total' => 15,
|
|
'score_7d' => 0,
|
|
'score_30d' => 0,
|
|
'last_medaled_at' => now()->subDay(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
],
|
|
]);
|
|
|
|
$results = app(HomepageService::class)->getHallOfFame(8);
|
|
|
|
expect($results)->toHaveCount(2)
|
|
->and($results[0]['id'])->toBe($leader->id)
|
|
->and($results[1]['id'])->toBe($runnerUp->id);
|
|
});
|
|
|
|
test('trending backfills with archive artworks when the recent ranking pool is sparse', function () {
|
|
Cache::flush();
|
|
config(['scout.driver' => 'null']);
|
|
|
|
$owner = User::factory()->create();
|
|
$recentLeader = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Recent Leader', 'published_at' => now()->subDay()]);
|
|
$archiveA = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Archive A', 'published_at' => now()->subDays(45)]);
|
|
$archiveB = makeFeaturedArtwork(['user_id' => $owner->id, 'title' => 'Archive B', 'published_at' => now()->subDays(60)]);
|
|
|
|
DB::table('artwork_stats')->insert([
|
|
'artwork_id' => $recentLeader->id,
|
|
'views' => 100,
|
|
'downloads' => 10,
|
|
'favorites' => 5,
|
|
'comments_count' => 0,
|
|
'shares_count' => 0,
|
|
'ranking_score' => 500,
|
|
'engagement_velocity' => 100,
|
|
'heat_score' => 100,
|
|
]);
|
|
|
|
$results = app(HomepageService::class)->getTrending(3);
|
|
$resultIds = collect($results)->pluck('id')->all();
|
|
|
|
expect($results)->toHaveCount(3)
|
|
->and($resultIds[0])->toBe($recentLeader->id)
|
|
->and($resultIds)->toContain($archiveA->id)
|
|
->and($resultIds)->toContain($archiveB->id);
|
|
});
|