insertGetId([ 'name' => 'World Submission Type', 'slug' => 'world-submission-type-' . Str::lower(Str::random(6)), 'description' => null, 'created_at' => now(), 'updated_at' => now(), ]); return DB::table('categories')->insertGetId([ 'content_type_id' => $contentTypeId, 'parent_id' => null, 'name' => 'World Submission Category', 'slug' => 'world-submission-category-' . Str::lower(Str::random(6)), 'description' => null, 'image' => null, 'is_active' => true, 'sort_order' => 0, 'created_at' => now(), 'updated_at' => now(), ]); } function acceptingWorld(?User $creator = null, array $attributes = []): World { $creator ??= User::factory()->create([ 'role' => 'moderator', 'username' => 'worldmoderator-' . Str::lower(Str::random(6)), 'name' => 'World Moderator', ]); return World::factory()->create(array_merge([ 'created_by_user_id' => $creator->id, 'status' => World::STATUS_PUBLISHED, 'published_at' => now()->subDay(), 'accepts_submissions' => true, 'participation_mode' => World::PARTICIPATION_MODE_MANUAL_APPROVAL, 'submission_note_enabled' => true, 'community_section_enabled' => true, 'allow_readd_after_removal' => true, 'submission_starts_at' => now()->subDay(), 'submission_ends_at' => now()->addDays(7), ], $attributes)); } it('creates pending world submissions when publishing an artwork draft', function (): void { $creator = User::factory()->create(); $world = acceptingWorld(); $categoryId = worldSubmissionCategoryId(); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Draft Upload', 'slug' => 'draft-upload', 'is_public' => false, 'visibility' => Artwork::VISIBILITY_PRIVATE, 'is_approved' => false, 'published_at' => null, 'artwork_status' => 'draft', ]); $this->actingAs($creator) ->postJson("/api/uploads/{$artwork->id}/publish", [ 'title' => 'World Upload', 'category' => $categoryId, 'tags' => ['world', 'submission'], 'world_submissions' => [ ['world_id' => $world->id, 'note' => 'Fits the active theme.'], ], ]) ->assertOk() ->assertJsonPath('status', 'published'); $this->assertDatabaseHas('world_submissions', [ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_PENDING, 'is_featured' => false, 'note' => 'Fits the active theme.', ]); }); it('creates live world participation immediately for auto-add worlds', function (): void { $creator = User::factory()->create(); $world = acceptingWorld(attributes: [ 'participation_mode' => World::PARTICIPATION_MODE_AUTO_ADD, ]); $categoryId = worldSubmissionCategoryId(); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Auto Add Upload', 'slug' => 'auto-add-upload', 'is_public' => false, 'visibility' => Artwork::VISIBILITY_PRIVATE, 'is_approved' => false, 'published_at' => null, 'artwork_status' => 'draft', ]); $this->actingAs($creator) ->postJson("/api/uploads/{$artwork->id}/publish", [ 'title' => 'Auto Add Upload', 'category' => $categoryId, 'tags' => ['world', 'auto-add'], 'world_submissions' => [ ['world_id' => $world->id, 'note' => 'Ship it.'], ], ]) ->assertOk(); $this->assertDatabaseHas('world_submissions', [ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => false, ]); }); it('syncs world submissions from the studio artwork editor update flow', function (): void { $creator = User::factory()->create(); $world = acceptingWorld(); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Studio Draft', 'slug' => 'studio-draft', 'is_public' => false, 'visibility' => Artwork::VISIBILITY_PRIVATE, 'published_at' => null, 'artwork_status' => 'draft', ]); $this->actingAs($creator) ->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [ 'world_submissions' => [ ['world_id' => $world->id, 'note' => 'Added after upload.'], ], ]) ->assertOk() ->assertJsonPath('world_submission_options.0.id', $world->id) ->assertJsonPath('world_submission_options.0.selected', true) ->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_PENDING); $this->assertDatabaseHas('world_submissions', [ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'status' => WorldSubmission::STATUS_PENDING, 'note' => 'Added after upload.', ]); }); it('allows removed submissions to be re-added when the world permits it', function (): void { $creator = User::factory()->create(); $world = acceptingWorld(); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Re Add Artwork', 'slug' => 're-add-artwork', 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'published_at' => now()->subDay(), 'artwork_status' => 'published', ]); WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_REMOVED, 'moderation_reason' => 'Needs tighter fit.', 'removed_at' => now()->subHour(), 'reviewed_at' => now()->subHour(), ]); $this->actingAs($creator) ->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [ 'world_submissions' => [ ['world_id' => $world->id, 'note' => 'Updated to fit the brief.'], ], ]) ->assertOk() ->assertJsonPath('world_submission_options.0.can_resubmit', false) ->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_PENDING); $this->assertDatabaseHas('world_submissions', [ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'status' => WorldSubmission::STATUS_PENDING, 'note' => 'Updated to fit the brief.', 'moderation_reason' => null, ]); }); it('keeps existing live submissions live when the artwork is updated in a manual approval world', function (): void { $creator = User::factory()->create(); $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'livereviewmod-' . Str::lower(Str::random(6)), 'name' => 'Live Review Moderator', ]); $world = acceptingWorld($moderator); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Live World Artwork', 'slug' => 'live-world-artwork', 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'published_at' => now()->subDay(), 'artwork_status' => 'published', ]); $submission = WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => true, 'note' => 'Original approved note.', 'reviewed_by_user_id' => $moderator->id, 'reviewed_at' => now()->subHour(), 'featured_at' => now()->subHour(), ]); $this->actingAs($creator) ->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [ 'world_submissions' => [ ['world_id' => $world->id, 'note' => 'Updated creator note after going live.'], ], ]) ->assertOk() ->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_LIVE) ->assertJsonPath('world_submission_options.0.selected', true) ->assertJsonPath('world_submission_options.0.note', 'Updated creator note after going live.'); $submission->refresh(); expect($submission->status)->toBe(WorldSubmission::STATUS_LIVE) ->and($submission->note)->toBe('Updated creator note after going live.') ->and($submission->is_featured)->toBeTrue() ->and((int) $submission->reviewed_by_user_id)->toBe($moderator->id) ->and($submission->reviewed_at)->not->toBeNull() ->and($submission->removed_at)->toBeNull() ->and($submission->blocked_at)->toBeNull(); }); it('does not expose closed worlds in creator submission options', function (): void { $creator = User::factory()->create(); $openWorld = acceptingWorld(attributes: ['title' => 'Open World']); $closedWorld = acceptingWorld(attributes: [ 'title' => 'Closed World', 'accepts_submissions' => false, 'participation_mode' => World::PARTICIPATION_MODE_CLOSED, ]); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Selector Artwork', 'slug' => 'selector-artwork', 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'published_at' => now()->subDay(), 'artwork_status' => 'published', ]); $this->actingAs($creator) ->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), []) ->assertOk() ->assertJsonCount(1, 'world_submission_options') ->assertJsonPath('world_submission_options.0.id', $openWorld->id); expect($closedWorld->id)->not->toBe($openWorld->id); }); it('shows and reviews world participation in the studio world editor', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'reviewmod', 'name' => 'Review Moderator', ]); $creator = User::factory()->create([ 'username' => 'queueartist', 'name' => 'Queue Artist', ]); $world = acceptingWorld($moderator); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Queue Artwork', 'slug' => 'queue-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $submission = WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_PENDING, 'note' => 'Please review this for the world.', ]); $this->actingAs($moderator) ->get(route('studio.worlds.edit', ['world' => $world->id])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldEditor') ->where('world.participation_mode', World::PARTICIPATION_MODE_MANUAL_APPROVAL) ->where('world.submission_review_queue.counts.pending', 1) ->where('world.submission_review_queue.items.0.artwork.title', 'Queue Artwork')); $this->actingAs($moderator) ->post(route('studio.worlds.submissions.approve', ['world' => $world->id, 'submission' => $submission->id])) ->assertRedirect(); $this->actingAs($moderator) ->post(route('studio.worlds.submissions.feature', ['world' => $world->id, 'submission' => $submission->id])) ->assertRedirect(); $this->assertDatabaseHas('world_submissions', [ 'id' => $submission->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => true, 'reviewed_by_user_id' => $moderator->id, ]); $this->actingAs($moderator) ->post(route('studio.worlds.submissions.block', ['world' => $world->id, 'submission' => $submission->id]), [ 'review_note' => 'Off brief for this world.', ]) ->assertRedirect(); $this->assertDatabaseHas('world_submissions', [ 'id' => $submission->id, 'status' => WorldSubmission::STATUS_BLOCKED, 'moderation_reason' => 'Off brief for this world.', 'is_featured' => false, ]); }); it('renders only live community submissions on public world pages and hides pending or blocked ones', function (): void { $world = acceptingWorld(attributes: [ 'title' => 'Public World', 'slug' => 'public-world', ]); $creator = User::factory()->create(); $featuredArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Featured Community Artwork', 'slug' => 'featured-community-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $approvedArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Approved Community Artwork', 'slug' => 'approved-community-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $pendingArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Pending Community Artwork', 'slug' => 'pending-community-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $matureArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Mature Community Artwork', 'slug' => 'mature-community-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'is_mature' => true, ]); foreach ([ [$featuredArtwork, WorldSubmission::STATUS_LIVE, true], [$approvedArtwork, WorldSubmission::STATUS_LIVE, false], [$pendingArtwork, WorldSubmission::STATUS_PENDING, false], [$matureArtwork, WorldSubmission::STATUS_LIVE, false], ] as [$artwork, $status, $isFeatured]) { WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => $status, 'is_featured' => $isFeatured, 'reviewed_at' => $status === WorldSubmission::STATUS_PENDING ? null : Carbon::now(), 'featured_at' => $isFeatured ? Carbon::now() : null, ]); } WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => Artwork::factory()->for($creator)->create([ 'title' => 'Blocked Community Artwork', 'slug' => 'blocked-community-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ])->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_BLOCKED, 'reviewed_at' => Carbon::now(), 'blocked_at' => Carbon::now(), ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('communitySubmissions.items.0.title', 'Featured Community Artwork') ->where('communitySubmissions.items.0.status', WorldSubmission::STATUS_LIVE) ->where('communitySubmissions.items.0.status_label', 'Featured') ->has('communitySubmissions.items', 2) ->where('communitySubmissions.items.1.title', 'Approved Community Artwork')); }); it('exposes world participation badges on the artwork page for curated and live world placements', function (): void { $world = acceptingWorld(attributes: [ 'title' => 'Retro Month', 'slug' => 'retro-month', ]); $creator = User::factory()->create(); $artwork = Artwork::factory()->for($creator)->create([ 'title' => 'Badge Artwork', 'slug' => 'badge-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); WorldRelation::query()->create([ 'world_id' => $world->id, 'related_type' => WorldRelation::TYPE_ARTWORK, 'related_id' => $artwork->id, 'section_key' => 'featured_artworks', 'context_label' => 'Curated spotlight', 'sort_order' => 1, 'is_featured' => true, ]); WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => true, 'reviewed_at' => Carbon::now(), 'featured_at' => Carbon::now(), ]); $this->get(route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug])) ->assertOk() ->assertViewHas('artworkData', function (array $artworkData): bool { $items = collect($artworkData['world_participation'] ?? []); return $items->count() === 1 && $items->contains(fn (array $item): bool => ($item['badge_label'] ?? null) === 'Featured in Retro Month'); }); });