create(array_merge([ 'user_id' => User::factory()->create()->id, 'hash' => str_repeat('a', 64), 'file_ext' => 'png', 'thumb_ext' => 'webp', ], $attributes)); } test('null featured_thumbnail_variants_json means not audited yet', function () { $artwork = makeAuditableArtwork(); expect($artwork->featured_thumbnail_variants_json)->toBeNull() ->and($artwork->featuredThumbnailAuditState())->toBe(['status' => 'not_audited', 'variants' => []]) ->and($artwork->hasFeaturedThumbnail())->toBeFalse() ->and($artwork->hasFeaturedThumbnail('desktop'))->toBeFalse(); }); test('empty array means audited with no featured variants available', function () { $artwork = makeAuditableArtwork(); $artwork->forceFill([ 'featured_thumbnail_variants_json' => [], 'featured_thumbnails_checked_at' => now(), ])->saveQuietly(); $fresh = $artwork->fresh(); expect($fresh->featured_thumbnail_variants_json)->toBe([]) ->and($fresh->featuredThumbnailAuditState())->toBe(['status' => 'no_variants', 'variants' => []]) ->and($fresh->hasFeaturedThumbnail())->toBeFalse(); }); test('a populated array means audited with those variants known to be available', function () { $artwork = makeAuditableArtwork(); $artwork->forceFill([ 'featured_thumbnail_variants_json' => ['desktop', 'desktop_xl'], 'featured_thumbnails_checked_at' => now(), ])->saveQuietly(); $fresh = $artwork->fresh(); expect($fresh->featuredThumbnailAuditState())->toBe(['status' => 'available', 'variants' => ['desktop', 'desktop_xl']]) ->and($fresh->hasFeaturedThumbnail('desktop'))->toBeTrue() ->and($fresh->hasFeaturedThumbnail('desktop_xl'))->toBeTrue() ->and($fresh->hasFeaturedThumbnail('mobile'))->toBeFalse(); }); test('malformed json in the column is treated as not audited rather than throwing', function () { $artwork = makeAuditableArtwork(); // Bypass the array cast to write a raw, non-JSON-array value directly, simulating // legacy or corrupted data that predates this column's introduction. DB::table('artworks')->where('id', $artwork->id)->update([ 'featured_thumbnail_variants_json' => '"not-an-array"', ]); $fresh = $artwork->fresh(); expect($fresh->featuredThumbnailAuditState())->toBe(['status' => 'not_audited', 'variants' => []]) ->and($fresh->hasFeaturedThumbnail())->toBeFalse(); }); test('non-string entries in the variants array are ignored rather than matched', function () { $artwork = makeAuditableArtwork(); DB::table('artworks')->where('id', $artwork->id)->update([ 'featured_thumbnail_variants_json' => json_encode(['desktop', 42, null, ['nested' => true]]), ]); $fresh = $artwork->fresh(); expect($fresh->featuredThumbnailAuditState())->toBe(['status' => 'available', 'variants' => ['desktop']]) ->and($fresh->hasFeaturedThumbnail('desktop'))->toBeTrue() ->and($fresh->hasFeaturedThumbnail('tablet'))->toBeFalse(); }); test('an artwork without a hash never reports a featured thumbnail regardless of audit state', function () { $artwork = makeAuditableArtwork(['hash' => null, 'file_ext' => null]); $artwork->forceFill([ 'featured_thumbnail_variants_json' => ['desktop'], 'featured_thumbnails_checked_at' => now(), ])->saveQuietly(); expect($artwork->fresh()->hasFeaturedThumbnail('desktop'))->toBeFalse(); }); test('changing an artwork hash resets stale featured thumbnail audit state and queues regeneration for featured artworks', function () { Queue::fake(); $artwork = makeAuditableArtwork([ 'hash' => str_repeat('b', 64), 'is_public' => true, 'is_approved' => true, 'published_at' => now()->subHour(), ]); $artwork->forceFill([ 'featured_thumbnail_variants_json' => ['desktop', 'desktop_xl'], 'featured_thumbnails_checked_at' => now(), ])->saveQuietly(); DB::table('artwork_features')->insert([ 'artwork_id' => $artwork->id, 'featured_at' => now()->subHour(), 'expires_at' => null, 'priority' => 500, 'label' => null, 'note' => null, 'is_active' => true, 'created_by' => null, 'created_at' => now(), 'updated_at' => now(), 'deleted_at' => null, ]); // Old hash reported 2 available variants — that state is now stale because the // object paths it referred to were computed from the old hash. expect($artwork->fresh()->hasFeaturedThumbnail('desktop'))->toBeTrue(); $artwork->hash = str_repeat('c', 64); $artwork->save(); $fresh = $artwork->fresh(); expect($fresh->featured_thumbnail_variants_json)->toBeNull() ->and($fresh->featured_thumbnails_checked_at)->toBeNull() ->and($fresh->hasFeaturedThumbnail('desktop'))->toBeFalse(); Queue::assertPushed(GenerateFeaturedArtworkThumbnailsJob::class); }); test('changing hash on an artwork with no active feature row does not queue regeneration', function () { Queue::fake(); $artwork = makeAuditableArtwork(['hash' => str_repeat('d', 64)]); $artwork->forceFill([ 'featured_thumbnail_variants_json' => ['desktop'], 'featured_thumbnails_checked_at' => now(), ])->saveQuietly(); $artwork->hash = str_repeat('e', 64); $artwork->save(); expect($artwork->fresh()->featured_thumbnail_variants_json)->toBeNull(); Queue::assertNotPushed(GenerateFeaturedArtworkThumbnailsJob::class); });