279 lines
9.8 KiB
PHP
279 lines
9.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Artwork;
|
|
use App\Models\Group;
|
|
use App\Models\GroupRelease;
|
|
use App\Models\GroupReleaseContributor;
|
|
use App\Models\User;
|
|
use App\Services\Profile\CreatorJourneyService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function seedCreatorJourneyFixture(): User
|
|
{
|
|
Cache::flush();
|
|
Queue::fake();
|
|
|
|
$creator = User::factory()->create([
|
|
'username' => 'journeymaker',
|
|
'is_active' => true,
|
|
'created_at' => Carbon::parse('2020-01-02 09:00:00'),
|
|
]);
|
|
|
|
$hiddenArtwork = Artwork::factory()->for($creator)->private()->create([
|
|
'title' => 'Hidden Draft',
|
|
'slug' => 'hidden-draft',
|
|
'published_at' => Carbon::parse('2020-02-01 12:00:00'),
|
|
]);
|
|
|
|
$firstArtwork = Artwork::factory()->for($creator)->create([
|
|
'title' => 'Sky One',
|
|
'slug' => 'sky-one',
|
|
'published_at' => Carbon::parse('2021-03-10 10:00:00'),
|
|
]);
|
|
|
|
$breakthroughArtwork = Artwork::factory()->for($creator)->create([
|
|
'title' => 'Neon Archive',
|
|
'slug' => 'neon-archive',
|
|
'published_at' => Carbon::parse('2024-05-10 09:00:00'),
|
|
]);
|
|
|
|
$lateYearArtwork = Artwork::factory()->for($creator)->create([
|
|
'title' => 'Terminal Bloom',
|
|
'slug' => 'terminal-bloom',
|
|
'published_at' => Carbon::parse('2024-11-20 19:00:00'),
|
|
]);
|
|
|
|
$latestArtwork = Artwork::factory()->for($creator)->create([
|
|
'title' => 'Afterglow Atlas',
|
|
'slug' => 'afterglow-atlas',
|
|
'published_at' => Carbon::parse('2025-02-14 18:30:00'),
|
|
]);
|
|
|
|
DB::table('artwork_stats')->insert([
|
|
[
|
|
'artwork_id' => $hiddenArtwork->id,
|
|
'views' => 5000,
|
|
'downloads' => 900,
|
|
'favorites' => 400,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
'comments_count' => 35,
|
|
'shares_count' => 12,
|
|
'downloads_1h' => 80,
|
|
'heat_score_updated_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $firstArtwork->id,
|
|
'views' => 120,
|
|
'downloads' => 18,
|
|
'favorites' => 9,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
'comments_count' => 2,
|
|
'shares_count' => 0,
|
|
'downloads_1h' => 2,
|
|
'heat_score_updated_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $breakthroughArtwork->id,
|
|
'views' => 1800,
|
|
'downloads' => 220,
|
|
'favorites' => 110,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
'comments_count' => 26,
|
|
'shares_count' => 9,
|
|
'downloads_1h' => 24,
|
|
'heat_score_updated_at' => Carbon::parse('2024-05-10 11:00:00'),
|
|
],
|
|
[
|
|
'artwork_id' => $lateYearArtwork->id,
|
|
'views' => 640,
|
|
'downloads' => 90,
|
|
'favorites' => 34,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
'comments_count' => 7,
|
|
'shares_count' => 2,
|
|
'downloads_1h' => 5,
|
|
'heat_score_updated_at' => null,
|
|
],
|
|
[
|
|
'artwork_id' => $latestArtwork->id,
|
|
'views' => 450,
|
|
'downloads' => 48,
|
|
'favorites' => 18,
|
|
'rating_avg' => 0,
|
|
'rating_count' => 0,
|
|
'comments_count' => 4,
|
|
'shares_count' => 1,
|
|
'downloads_1h' => 3,
|
|
'heat_score_updated_at' => null,
|
|
],
|
|
]);
|
|
|
|
DB::table('artwork_features')->insert([
|
|
'artwork_id' => $firstArtwork->id,
|
|
'featured_at' => Carbon::parse('2022-06-01 13:00:00'),
|
|
'priority' => 100,
|
|
'label' => 'Feature',
|
|
'is_active' => true,
|
|
'created_by' => $creator->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
DB::table('artwork_metric_snapshots_hourly')->insert([
|
|
[
|
|
'artwork_id' => $breakthroughArtwork->id,
|
|
'bucket_hour' => Carbon::parse('2024-05-10 10:00:00'),
|
|
'views_count' => 1200,
|
|
'downloads_count' => 40,
|
|
'favourites_count' => 60,
|
|
'comments_count' => 12,
|
|
'shares_count' => 3,
|
|
'created_at' => now(),
|
|
],
|
|
[
|
|
'artwork_id' => $breakthroughArtwork->id,
|
|
'bucket_hour' => Carbon::parse('2024-05-10 11:00:00'),
|
|
'views_count' => 1320,
|
|
'downloads_count' => 64,
|
|
'favourites_count' => 68,
|
|
'comments_count' => 13,
|
|
'shares_count' => 4,
|
|
'created_at' => now(),
|
|
],
|
|
[
|
|
'artwork_id' => $lateYearArtwork->id,
|
|
'bucket_hour' => Carbon::parse('2024-11-20 19:00:00'),
|
|
'views_count' => 300,
|
|
'downloads_count' => 10,
|
|
'favourites_count' => 12,
|
|
'comments_count' => 1,
|
|
'shares_count' => 0,
|
|
'created_at' => now(),
|
|
],
|
|
[
|
|
'artwork_id' => $lateYearArtwork->id,
|
|
'bucket_hour' => Carbon::parse('2024-11-20 20:00:00'),
|
|
'views_count' => 360,
|
|
'downloads_count' => 14,
|
|
'favourites_count' => 15,
|
|
'comments_count' => 2,
|
|
'shares_count' => 0,
|
|
'created_at' => now(),
|
|
],
|
|
]);
|
|
|
|
$groupOwner = User::factory()->create(['is_active' => true]);
|
|
$group = Group::factory()->for($groupOwner, 'owner')->create([
|
|
'name' => 'Nova Collective',
|
|
'slug' => 'nova-collective',
|
|
'visibility' => Group::VISIBILITY_PUBLIC,
|
|
'status' => Group::LIFECYCLE_ACTIVE,
|
|
]);
|
|
|
|
$release = GroupRelease::query()->create([
|
|
'group_id' => $group->id,
|
|
'title' => 'First Spectrum Pack',
|
|
'slug' => 'first-spectrum-pack',
|
|
'summary' => 'A first collaborative release.',
|
|
'status' => GroupRelease::STATUS_RELEASED,
|
|
'current_stage' => GroupRelease::STAGE_RELEASED,
|
|
'visibility' => GroupRelease::VISIBILITY_PUBLIC,
|
|
'released_at' => Carbon::parse('2023-08-15 16:00:00'),
|
|
'published_at' => Carbon::parse('2023-08-15 16:00:00'),
|
|
'created_by_user_id' => $groupOwner->id,
|
|
]);
|
|
|
|
GroupReleaseContributor::query()->create([
|
|
'group_release_id' => $release->id,
|
|
'user_id' => $creator->id,
|
|
'role_label' => 'Illustrator',
|
|
'sort_order' => 1,
|
|
]);
|
|
|
|
return $creator;
|
|
}
|
|
|
|
it('rebuilds persisted creator milestones from public source data', function () {
|
|
$creator = seedCreatorJourneyFixture();
|
|
|
|
$this->artisan('skinbase:rebuild-creator-journey', ['user_id' => $creator->id])
|
|
->assertExitCode(0);
|
|
|
|
$storedTypes = DB::table('creator_milestones')
|
|
->where('user_id', $creator->id)
|
|
->orderBy('type')
|
|
->pluck('type')
|
|
->all();
|
|
|
|
expect($storedTypes)->toContain(
|
|
'first_upload',
|
|
'first_featured_artwork',
|
|
'first_group_release',
|
|
'biggest_download_spike',
|
|
'best_performing_work',
|
|
'most_productive_year',
|
|
'yearly_recap',
|
|
);
|
|
|
|
$payload = app(CreatorJourneyService::class)->publicPayloadForUser($creator);
|
|
|
|
expect($payload['summary']['available'])->toBeTrue()
|
|
->and($payload['summary']['member_since_year'])->toBe(2020)
|
|
->and($payload['highlights'][0]['type'])->toBe('best_performing_work')
|
|
->and(collect($payload['timeline'])->pluck('headline')->all())->toContain('Sky One', 'First Spectrum Pack')
|
|
->and(collect($payload['timeline'])->pluck('headline')->all())->not->toContain('Hidden Draft')
|
|
->and($payload['yearly_recaps'][0]['metrics']['year'])->toBe(2025);
|
|
});
|
|
|
|
it('returns the public creator journey api payload without leaking private content', function () {
|
|
$creator = seedCreatorJourneyFixture();
|
|
|
|
app(CreatorJourneyService::class)->rebuildForUser($creator);
|
|
|
|
$response = $this->getJson(route('api.profile.journey', ['username' => $creator->username]));
|
|
|
|
$response
|
|
->assertOk()
|
|
->assertJsonPath('data.summary.available', true);
|
|
|
|
// v2: comeback milestones appear in timeline (fixture has a 3+ year gap → legendary comeback)
|
|
$highlightHeadlines = collect($response->json('data.highlights'))->pluck('headline')->filter()->all();
|
|
$headlines = collect($response->json('data.timeline'))->pluck('headline')->filter()->all();
|
|
$timelineTypes = collect($response->json('data.timeline'))->pluck('type')->filter()->values()->all();
|
|
|
|
expect($highlightHeadlines)->toContain('Neon Archive')
|
|
->and($headlines)->toContain('Sky One')
|
|
->and($headlines)->not->toContain('Hidden Draft')
|
|
->and($timelineTypes)->toContain('biggest_download_spike');
|
|
});
|
|
|
|
it('hydrates the public profile page with creator journey props', function () {
|
|
$creator = seedCreatorJourneyFixture();
|
|
|
|
app(CreatorJourneyService::class)->rebuildForUser($creator);
|
|
|
|
$this->get(route('profile.show', ['username' => strtolower((string) $creator->username)]))
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('Profile/ProfileShow')
|
|
->where('journey.summary.available', true)
|
|
->where('journey.summary.member_since_year', 2020)
|
|
->where('journey.highlights', fn ($highlights) => collect($highlights)->pluck('headline')->contains('Neon Archive'))
|
|
->where('journey.timeline', fn ($timeline) => collect($timeline)->pluck('type')->contains('biggest_download_spike'))
|
|
->where('journey.timeline', fn ($timeline) => collect($timeline)->pluck('headline')->contains('First Spectrum Pack'))
|
|
->where('journeyApiUrl', route('api.profile.journey', ['username' => strtolower((string) $creator->username)]))
|
|
);
|
|
}); |