Artwork::factory()->create($attrs)); } beforeEach(function () { // Register GREATEST() polyfill for SQLite (used by observers on user_statistics) if (DB::connection()->getDriverName() === 'sqlite') { DB::connection()->getPdo()->sqliteCreateFunction('GREATEST', function (...$args) { return max($args); }, -1); } $this->user = User::factory()->create(); $this->actingAs($this->user); }); // ── Route Auth Tests ────────────────────────────────────────────────────────── test('studio routes require authentication', function () { auth()->logout(); $routes = [ '/studio', '/studio/content', '/studio/artworks', '/studio/cards', '/studio/collections', '/studio/stories', '/studio/drafts', '/studio/scheduled', '/studio/calendar', '/studio/archived', '/studio/assets', '/studio/activity', '/studio/inbox', '/studio/challenges', '/studio/analytics', '/studio/growth', '/studio/search', '/studio/comments', '/studio/followers', '/studio/profile', '/studio/featured', '/studio/preferences', '/studio/settings', ]; foreach ($routes as $route) { $this->get($route)->assertRedirect('/login'); } }); test('studio dashboard loads for authenticated user', function () { $this->get('/studio') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioDashboard') ->has('overview.active_challenges') ->has('overview.creator_health') ->has('overview.featured_status')); }); test('studio dashboard honors saved landing page preference', function () { DB::table('dashboard_preferences')->insert([ 'user_id' => $this->user->id, 'studio_preferences' => json_encode([ 'default_landing_page' => 'scheduled', ], JSON_THROW_ON_ERROR), 'created_at' => now(), 'updated_at' => now(), ]); $this->get('/studio') ->assertRedirect('/studio/scheduled'); }); test('studio artworks page loads', function () { $this->get('/studio/artworks') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page->component('Studio/StudioArtworks')->where('title', 'Artworks')); }); test('studio drafts page loads', function () { $this->get('/studio/artworks/drafts') ->assertRedirect('/studio/drafts'); $this->get('/studio/drafts') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page->component('Studio/StudioDrafts')->where('title', 'Drafts')); }); test('studio archived page loads', function () { $this->get('/studio/artworks/archived') ->assertRedirect('/studio/archived'); $this->get('/studio/archived') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page->component('Studio/StudioArchived')->where('title', 'Archived')); }); test('studio scheduled page loads', function () { $this->get('/studio/scheduled') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioScheduled') ->where('title', 'Scheduled') ->has('listing.items') ->has('listing.summary') ->has('listing.agenda') ->has('listing.range_options') ->where('endpoints.publishNowPattern', route('api.studio.schedule.publishNow', ['module' => '__MODULE__', 'id' => '__ID__'])) ->where('endpoints.unschedulePattern', route('api.studio.schedule.unschedule', ['module' => '__MODULE__', 'id' => '__ID__']))); }); test('studio calendar page loads', function () { $this->get('/studio/calendar') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioCalendar') ->where('title', 'Calendar') ->has('calendar.month.days') ->has('calendar.week.days') ->has('calendar.agenda') ->has('calendar.unscheduled_items') ->where('endpoints.publishNowPattern', route('api.studio.schedule.publishNow', ['module' => '__MODULE__', 'id' => '__ID__']))); }); test('studio activity page loads', function () { $this->get('/studio/activity') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioActivity') ->where('title', 'Activity') ->has('listing.items') ->has('listing.summary') ->has('listing.module_options') ->where('endpoints.markAllRead', route('api.studio.activity.readAll'))); }); test('studio inbox page loads', function () { $this->get('/studio/inbox') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioInbox') ->where('title', 'Inbox') ->has('inbox.items') ->has('inbox.panels.attention_now') ->has('inbox.read_state_options') ->has('inbox.priority_options') ->where('endpoints.markAllRead', route('api.studio.activity.readAll'))); }); test('studio challenges page exposes challenge workflow payload', function () { $this->get('/studio/challenges') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioChallenges') ->where('title', 'Challenges') ->has('summary') ->has('activeChallenges') ->has('recentEntries') ->has('cardLeaders') ->has('reminders')); }); test('studio search page loads', function () { $this->get('/studio/search') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioSearch') ->where('title', 'Search') ->has('search.filters') ->has('search.empty_state.continue_working') ->has('quickCreate')); }); test('studio followers page loads', function () { $this->get('/studio/followers') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page->component('Studio/StudioFollowers')->where('title', 'Followers')); }); test('studio assets page exposes asset library payload', function () { studioArtwork([ 'user_id' => $this->user->id, ]); $this->get('/studio/assets') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioAssets') ->where('title', 'Assets') ->has('assets.items') ->has('assets.meta') ->has('assets.filters') ->has('assets.type_options') ->has('assets.source_options') ->has('assets.sort_options') ->has('assets.summary') ->has('assets.items.0.usage_references')); }); test('studio comments page exposes unified comments contract', function () { $this->get('/studio/comments') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioComments') ->where('title', 'Comments') ->has('listing.items') ->has('listing.meta') ->has('listing.filters') ->has('listing.module_options') ->where('endpoints.replyPattern', route('api.studio.comments.reply', ['module' => '__MODULE__', 'commentId' => '__COMMENT__'])) ->where('endpoints.moderatePattern', route('api.studio.comments.moderate', ['module' => '__MODULE__', 'commentId' => '__COMMENT__'])) ->where('endpoints.reportPattern', route('api.studio.comments.report', ['module' => '__MODULE__', 'commentId' => '__COMMENT__']))); }); test('studio profile page exposes editable profile contract', function () { $this->get('/studio/profile') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioProfile') ->where('title', 'Profile') ->where('profile.name', $this->user->name) ->where('profile.username', $this->user->username) ->has('profile.social_links') ->has('moduleSummaries') ->has('featuredModules') ->has('featuredContent') ->where('endpoints.profile', route('api.studio.preferences.profile')) ->where('endpoints.avatarUpload', route('avatar.upload')) ->where('endpoints.coverUpload', route('api.profile.cover.upload')) ->where('endpoints.coverPosition', route('api.profile.cover.position')) ->where('endpoints.coverDelete', route('api.profile.cover.destroy'))); }); test('studio featured page exposes selection contract', function () { $this->get('/studio/featured') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioFeatured') ->where('title', 'Featured') ->has('items') ->has('selected') ->has('featuredModules') ->where('endpoints.save', route('api.studio.preferences.featured'))); }); test('studio preferences page exposes preference contract', function () { $this->get('/studio/preferences') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioPreferences') ->where('title', 'Preferences') ->has('preferences') ->where('preferences.default_landing_page', 'overview') ->has('preferences.widget_visibility') ->has('preferences.widget_order') ->has('links') ->where('endpoints.save', route('api.studio.preferences.settings'))); }); test('studio settings page exposes settings handoff payload', function () { $this->get('/studio/settings') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioSettings') ->where('title', 'Settings') ->has('links') ->has('sections')); }); test('studio analytics page exposes trend and comparison payloads', function () { $this->get('/studio/analytics') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioAnalytics') ->has('totals') ->has('topContent') ->has('moduleBreakdown') ->has('viewsTrend') ->has('engagementTrend') ->has('publishingTimeline') ->has('comparison') ->has('insightBlocks') ->where('rangeDays', 30) ->has('recentComments')); }); test('studio analytics page respects requested date range', function () { $this->get('/studio/analytics?range_days=14') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioAnalytics') ->where('rangeDays', 14)); }); test('studio growth page exposes growth workspace payloads', function () { $this->get('/studio/growth') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioGrowth') ->where('title', 'Growth') ->has('summary') ->has('moduleFocus') ->has('checkpoints') ->has('opportunities') ->has('milestones') ->has('momentum.views_trend') ->has('momentum.engagement_trend') ->has('momentum.publishing_timeline') ->where('rangeDays', 30)); }); test('studio growth page respects requested date range', function () { $this->get('/studio/growth?range_days=14') ->assertStatus(200) ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioGrowth') ->where('rangeDays', 14)); }); // ── API Tests ───────────────────────────────────────────────────────────────── test('studio api requires authentication', function () { auth()->logout(); $this->getJson('/api/studio/artworks') ->assertStatus(401); $this->postJson('/api/studio/events', [ 'event_type' => 'studio_opened', ])->assertStatus(401); }); test('studio events api accepts known creator studio hooks', function () { $this->postJson('/api/studio/events', [ 'event_type' => 'studio_filter_used', 'module' => 'artworks', 'surface' => '/studio/artworks', 'item_module' => 'artworks', 'item_id' => 42, 'meta' => [ 'filter' => 'bucket', 'value' => 'drafts', ], ]) ->assertStatus(202) ->assertJsonPath('ok', true); }); test('studio api returns artworks for authenticated user', function () { // Create artworks for this user $artwork = studioArtwork([ 'user_id' => $this->user->id, 'is_public' => true, 'is_approved' => true, ]); ArtworkStats::create([ 'artwork_id' => $artwork->id, 'views' => 100, 'downloads' => 10, 'favorites' => 5, ]); $this->getJson('/api/studio/artworks') ->assertStatus(200) ->assertJsonStructure([ 'data' => [['id', 'title', 'slug', 'views', 'favourites']], 'meta' => ['current_page', 'last_page', 'per_page', 'total'], ]); }); test('studio api does not return other users artworks', function () { $otherUser = User::factory()->create(); studioArtwork([ 'user_id' => $otherUser->id, 'is_public' => true, 'is_approved' => true, ]); $this->getJson('/api/studio/artworks') ->assertStatus(200) ->assertJsonCount(0, 'data'); }); // ── Bulk Action Tests ───────────────────────────────────────────────────────── test('bulk archive works on owned artworks', function () { $artwork = studioArtwork([ 'user_id' => $this->user->id, 'is_public' => true, ]); $this->postJson('/api/studio/artworks/bulk', [ 'action' => 'archive', 'artwork_ids' => [$artwork->id], ]) ->assertStatus(200) ->assertJsonPath('success', 1); expect($artwork->fresh()->trashed())->toBeTrue(); }); test('bulk delete requires confirmation', function () { $artwork = studioArtwork(['user_id' => $this->user->id]); $this->postJson('/api/studio/artworks/bulk', [ 'action' => 'delete', 'artwork_ids' => [$artwork->id], ]) ->assertStatus(422); }); test('bulk delete with confirmation works', function () { $artwork = studioArtwork(['user_id' => $this->user->id]); $this->postJson('/api/studio/artworks/bulk', [ 'action' => 'delete', 'artwork_ids' => [$artwork->id], 'confirm' => 'DELETE', ]) ->assertStatus(200) ->assertJsonPath('success', 1); }); test('bulk publish on owned artworks', function () { $artwork = studioArtwork([ 'user_id' => $this->user->id, 'is_public' => false, ]); $this->postJson('/api/studio/artworks/bulk', [ 'action' => 'publish', 'artwork_ids' => [$artwork->id], ]) ->assertStatus(200) ->assertJsonPath('success', 1); expect($artwork->fresh()->is_public)->toBeTrue(); }); test('bulk action cannot modify other users artworks', function () { $otherUser = User::factory()->create(); $artwork = studioArtwork(['user_id' => $otherUser->id]); $this->postJson('/api/studio/artworks/bulk', [ 'action' => 'archive', 'artwork_ids' => [$artwork->id], ]) ->assertStatus(422) ->assertJsonPath('success', 0) ->assertJsonPath('failed', 1); }); // ── Toggle Tests ────────────────────────────────────────────────────────────── test('toggle publish on single artwork', function () { $artwork = studioArtwork([ 'user_id' => $this->user->id, 'is_public' => false, ]); $this->postJson("/api/studio/artworks/{$artwork->id}/toggle", [ 'action' => 'publish', ]) ->assertStatus(200) ->assertJsonPath('success', true); expect($artwork->fresh()->is_public)->toBeTrue(); }); test('toggle on non-owned artwork returns 404', function () { $otherUser = User::factory()->create(); $artwork = studioArtwork(['user_id' => $otherUser->id]); $this->postJson("/api/studio/artworks/{$artwork->id}/toggle", [ 'action' => 'archive', ]) ->assertStatus(404); }); // ── Analytics API Tests ─────────────────────────────────────────────────────── test('analytics api returns artwork stats', function () { $artwork = studioArtwork(['user_id' => $this->user->id]); ArtworkStats::create([ 'artwork_id' => $artwork->id, 'views' => 500, 'downloads' => 20, 'favorites' => 30, 'shares_count' => 10, 'comments_count' => 5, 'ranking_score' => 42.5, 'heat_score' => 8.3, ]); $this->getJson("/api/studio/artworks/{$artwork->id}/analytics") ->assertStatus(200) ->assertJsonStructure([ 'artwork' => ['id', 'title', 'slug'], 'analytics' => ['views', 'favourites', 'shares', 'comments', 'downloads', 'ranking_score', 'heat_score'], ]); }); test('analytics api denies access to other users artwork', function () { $otherUser = User::factory()->create(); $artwork = studioArtwork(['user_id' => $otherUser->id]); $this->getJson("/api/studio/artworks/{$artwork->id}/analytics") ->assertStatus(404); });