create(['role' => 'admin']); $owner = User::factory()->create(['username' => 'mira']); $hash = 'aabbccddeeff1122'; $artwork = Artwork::factory()->for($owner)->unapproved()->unpublished()->create([ 'title' => 'Forest Spirit', 'description' => 'A mossy guardian in fog.', 'hash' => $hash, 'thumb_ext' => 'webp', 'artwork_status' => 'review', 'is_public' => false, 'moderation_note' => 'new_account, low_level', ]); $contentType = ContentType::query()->create([ 'name' => 'Illustration', 'slug' => 'illustration-moderation-type', 'description' => '', ]); $category = Category::query()->create([ 'content_type_id' => $contentType->id, 'parent_id' => null, 'name' => 'Illustration', 'slug' => 'illustration-moderation', 'description' => null, 'image' => null, 'is_active' => true, 'sort_order' => 0, ]); $artwork->categories()->attach($category->id); $tag = Tag::query()->create(['name' => 'forest', 'slug' => 'forest', 'is_active' => true]); $artwork->tags()->attach($tag->id, ['source' => 'user', 'confidence' => 1.0]); $response = $this->actingAs($admin)->getJson('/api/admin/artwork-review/pending'); $response->assertOk() ->assertJsonPath('data.0.id', $artwork->id) ->assertJsonPath('data.0.title', 'Forest Spirit') ->assertJsonPath('data.0.description', 'A mossy guardian in fog.') ->assertJsonPath('data.0.tags.0', 'forest') ->assertJsonPath('data.0.categories.0', 'Illustration') ->assertJsonPath('data.0.user.username', 'mira') ->assertJsonPath('data.0.preview_url', ThumbnailService::fromHash($hash, 'webp', 'md')) ->assertJsonPath('data.0.preview_lg_url', ThumbnailService::fromHash($hash, 'webp', 'lg')); }); it('approves a pending artwork', function () { Queue::fake(); $admin = User::factory()->create(['role' => 'moderator']); $artwork = Artwork::factory()->unapproved()->unpublished()->create([ 'artwork_status' => 'review', 'is_public' => false, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $response = $this->actingAs($admin)->postJson("/api/admin/artwork-review/{$artwork->id}/approve", [ 'note' => 'Looks good.', ]); $response->assertOk()->assertJsonPath('status', 'published'); $artwork->refresh(); expect($artwork->artwork_status)->toBe('published'); expect($artwork->is_approved)->toBeTrue(); expect($artwork->is_public)->toBeTrue(); expect($artwork->moderation_note)->toBe('Looks good.'); expect((int) $artwork->moderated_by)->toBe((int) $admin->id); $notice = Notification::query() ->where('user_id', $artwork->user_id) ->where('type', 'artwork_approved') ->first(); expect($notice)->not->toBeNull() ->and($notice->data['message'] ?? null)->toContain('was approved and is now live') ->and($notice->data['artwork_id'] ?? null)->toBe($artwork->id); Queue::assertPushed(IndexArtworkJob::class); }); it('rejects a pending artwork', function () { Queue::fake(); $admin = User::factory()->create(['role' => 'admin']); $artwork = Artwork::factory()->unapproved()->unpublished()->create([ 'artwork_status' => 'review', 'is_public' => false, ]); $response = $this->actingAs($admin)->postJson("/api/admin/artwork-review/{$artwork->id}/reject", [ 'note' => 'Low quality.', ]); $response->assertOk()->assertJsonPath('status', 'rejected'); $artwork->refresh(); expect($artwork->artwork_status)->toBe('rejected'); expect($artwork->is_approved)->toBeFalse(); expect($artwork->is_public)->toBeFalse(); expect($artwork->moderation_note)->toBe('Low quality.'); $notice = Notification::query() ->where('user_id', $artwork->user_id) ->where('type', 'artwork_rejected') ->first(); expect($notice)->not->toBeNull() ->and($notice->data['message'] ?? null)->toContain('was not approved') ->and($notice->data['message'] ?? null)->toContain('Low quality.'); Queue::assertPushed(IndexArtworkJob::class); }); it('denies artwork review access to regular users', function () { $user = User::factory()->create(['role' => 'user']); $this->actingAs($user) ->getJson('/api/admin/artwork-review/pending') ->assertStatus(403); });