create(['role' => 'admin']); $admin->forceFill([ 'isAdmin' => true, 'activated' => true, ])->save(); AdminVerification::createForUser($admin->fresh()); return $admin->fresh(); } it('persists content preferences from the settings endpoint', function () { $user = User::factory()->create(); $this->actingAs($user) ->postJson('/settings/content/update', [ 'mature_content_visibility' => 'hide', 'mature_content_warning_enabled' => false, ]) ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('message', 'Content preferences saved successfully.'); $profile = DB::table('user_profiles') ->where('user_id', $user->id) ->first(['mature_content_visibility', 'mature_content_warning_enabled']); expect($profile)->not->toBeNull() ->and($profile->mature_content_visibility)->toBe('hide') ->and((int) $profile->mature_content_warning_enabled)->toBe(0); }); it('derives mature artwork presentation from viewer preferences', function () { $artwork = Artwork::factory()->create([ 'is_mature' => true, 'maturity_level' => ArtworkMaturityService::LEVEL_MATURE, 'maturity_status' => ArtworkMaturityService::STATUS_DECLARED, ]); $hideViewer = User::factory()->create(); $blurViewer = User::factory()->create(); $showViewer = User::factory()->create(); DB::table('user_profiles')->insert([ [ 'user_id' => $hideViewer->id, 'mature_content_visibility' => 'hide', 'mature_content_warning_enabled' => true, ], [ 'user_id' => $blurViewer->id, 'mature_content_visibility' => 'blur', 'mature_content_warning_enabled' => false, ], [ 'user_id' => $showViewer->id, 'mature_content_visibility' => 'show', 'mature_content_warning_enabled' => true, ], ]); $maturity = app(ArtworkMaturityService::class); $hidden = $maturity->presentation($artwork, $hideViewer); $blurred = $maturity->presentation($artwork, $blurViewer); $shown = $maturity->presentation($artwork, $showViewer); expect($hidden['should_hide'])->toBeTrue() ->and($hidden['should_blur'])->toBeFalse() ->and($hidden['requires_interstitial'])->toBeTrue() ->and($blurred['should_hide'])->toBeFalse() ->and($blurred['should_blur'])->toBeTrue() ->and($blurred['requires_interstitial'])->toBeFalse() ->and($shown['should_hide'])->toBeFalse() ->and($shown['should_blur'])->toBeFalse() ->and($shown['requires_interstitial'])->toBeTrue(); }); it('applies uploader mature declarations when publishing an existing artwork', function () { Queue::fake(); $user = User::factory()->create(); $artwork = Artwork::factory()->create([ 'user_id' => $user->id, 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_source' => ArtworkMaturityService::SOURCE_LEGACY, ]); $this->actingAs($user) ->postJson("/api/uploads/{$artwork->id}/publish", [ 'title' => 'Updated title', 'is_mature' => true, 'visibility' => 'private', ]) ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('status', 'published'); $artwork->refresh(); expect($artwork->is_mature)->toBeTrue() ->and($artwork->maturity_level)->toBe(ArtworkMaturityService::LEVEL_MATURE) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_DECLARED) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_USER); }); it('flags undeclared mature content from AI assessment', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_mismatch_count' => 0, ]); $assessment = app(ArtworkMaturityService::class)->applyAiAssessment($artwork, [ 'clip_tags' => [ ['tag' => 'nudity'], ], 'yolo_objects' => [], 'blip_caption' => 'topless portrait', ]); $artwork->refresh(); expect($assessment['flagged'])->toBeTrue() ->and($assessment['score'])->toBeGreaterThanOrEqual(0.68) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_SUSPECTED) ->and($artwork->maturity_flagged_at)->not->toBeNull() ->and($artwork->maturity_mismatch_count)->toBe(1) ->and($artwork->maturity_ai_labels)->toContain('nudity'); }); it('stores normalized maturity endpoint results and flags review-worthy content', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_mismatch_count' => 0, ]); $assessment = app(ArtworkMaturityService::class)->applyAiAssessment($artwork, [ 'status' => 'succeeded', 'maturity_label' => 'mature', 'confidence' => 0.9321, 'action_hint' => 'flag_high', 'threshold_used' => 0.7, 'analysis_time_ms' => 321, 'model' => 'vision-maturity-v2', 'advisory' => 'High confidence mature content.', 'labels' => ['nudity'], ]); $artwork->refresh(); expect($assessment['flagged'])->toBeTrue() ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_SUSPECTED) ->and($artwork->maturity_ai_status)->toBe(ArtworkMaturityService::AI_STATUS_SUCCEEDED) ->and($artwork->maturity_ai_label)->toBe(ArtworkMaturityService::LEVEL_MATURE) ->and($artwork->maturity_ai_confidence)->toBe(0.9321) ->and($artwork->maturity_ai_action_hint)->toBe(ArtworkMaturityService::AI_ACTION_FLAG_HIGH) ->and($artwork->maturity_ai_threshold_used)->toBe(0.7) ->and($artwork->maturity_ai_analysis_time_ms)->toBe(321) ->and($artwork->maturity_ai_model)->toBe('vision-maturity-v2') ->and($artwork->maturity_ai_advisory)->toBe('High confidence mature content.') ->and($artwork->maturity_mismatch_count)->toBe(1); }); it('normalizes safe action hints from the vision maturity contract', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, ]); $assessment = app(ArtworkMaturityService::class)->applyAiAssessment($artwork, [ 'status' => 'succeeded', 'maturity_label' => 'safe', 'confidence' => 0.1123, 'action_hint' => 'safe', 'threshold_used' => 0.7, 'model' => 'vision-maturity-v2', 'labels' => ['safe'], ]); $artwork->refresh(); expect($assessment['flagged'])->toBeFalse() ->and($assessment['action_hint'])->toBe(ArtworkMaturityService::AI_ACTION_SAFE) ->and($artwork->maturity_ai_action_hint)->toBe(ArtworkMaturityService::AI_ACTION_SAFE) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_CLEAR); }); it('records failed maturity AI analysis without implying safe content', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_NOT_REQUESTED, ]); $assessment = app(ArtworkMaturityService::class)->applyAiAssessment($artwork, [ 'status' => 'failed', 'advisory' => 'Gateway timeout.', ]); $artwork->refresh(); expect($assessment['flagged'])->toBeFalse() ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_CLEAR) ->and($artwork->is_mature)->toBeFalse() ->and($artwork->maturity_ai_status)->toBe(ArtworkMaturityService::AI_STATUS_FAILED) ->and($artwork->maturity_ai_advisory)->toBe('Gateway timeout.') ->and($artwork->maturity_ai_label)->toBeNull(); }); it('lets moderators review suspected artworks', function () { Queue::fake(); $moderator = User::factory()->create(['role' => 'moderator']); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_flag_reason' => 'AI suspected mature content from: nudity', ]); $this->actingAs($moderator) ->postJson("/cp/maturity/{$artwork->id}/review", [ 'action' => 'mark_mature', 'note' => 'Confirmed mature by moderation review.', ]) ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('artwork.id', $artwork->id); $artwork->refresh(); expect($artwork->is_mature)->toBeTrue() ->and($artwork->maturity_level)->toBe(ArtworkMaturityService::LEVEL_MATURE) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_REVIEWED) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_MODERATOR) ->and($artwork->maturity_reviewed_by)->toBe($moderator->id) ->and($artwork->maturity_reviewer_note)->toBe('Confirmed mature by moderation review.'); }); it('renders the moderation queue page and filters queue items by status', function () { Queue::fake(); $moderator = User::factory()->create(['role' => 'moderator']); $suspected = Artwork::factory()->create([ 'title' => 'Suspected Queue Artwork', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_flag_reason' => 'AI suspected mature content from: nudity', 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'maturity_ai_label' => ArtworkMaturityService::LEVEL_MATURE, 'maturity_ai_score' => 0.8812, 'maturity_ai_confidence' => 0.8812, 'maturity_ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'maturity_ai_labels' => ['nudity'], 'published_at' => now(), ]); $reviewed = Artwork::factory()->create([ 'title' => 'Reviewed Queue Artwork', 'is_mature' => true, 'maturity_level' => ArtworkMaturityService::LEVEL_MATURE, 'maturity_status' => ArtworkMaturityService::STATUS_REVIEWED, 'maturity_source' => ArtworkMaturityService::SOURCE_MODERATOR, 'maturity_reviewed_by' => $moderator->id, 'maturity_reviewed_at' => now(), 'maturity_reviewer_note' => 'Already reviewed.', 'published_at' => now(), ]); $this->actingAs($moderator) ->get('/cp/maturity') ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Moderation/ArtworkMaturityQueue') ->where('title', 'Artwork Maturity Queue') ->where('stats.suspected', 1) ->where('stats.reviewed', 1) ->where('initialItems.0.id', $suspected->id) ->where('initialItems.0.title', 'Suspected Queue Artwork') ->where('initialItems.0.maturity.ai_action_hint', ArtworkMaturityService::AI_ACTION_REVIEW) ); $this->actingAs($moderator) ->getJson('/cp/maturity/queue?status=reviewed') ->assertOk() ->assertJsonPath('meta.status', 'reviewed') ->assertJsonPath('meta.stats.suspected', 1) ->assertJsonPath('meta.stats.reviewed', 1) ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $reviewed->id) ->assertJsonPath('data.0.review.reviewer_note', 'Already reviewed.'); }); it('renders the artwork admin maturity queue for cpad admins', function () { Queue::fake(); $admin = createMaturityQueueAdmin(); $suspected = Artwork::factory()->create([ 'title' => 'Admin Surface Suspected Artwork', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'maturity_ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'published_at' => now(), ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.cp.artworks.maturity.main')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Moderation/ArtworkMaturityQueue') ->where('initialItems.0.id', $suspected->id) ->where('endpoints.list', route('admin.cp.artworks.maturity.queue')) ->where('endpoints.reviewPattern', route('admin.cp.artworks.maturity.review', ['artwork' => '__ARTWORK__'])) ); }); it('allows cpad admins to open the legacy maturity queue url', function () { Queue::fake(); $admin = createMaturityQueueAdmin(); $suspected = Artwork::factory()->create([ 'title' => 'Legacy Url Suspected Artwork', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'maturity_ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'published_at' => now(), ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get('/cp/maturity') ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Moderation/ArtworkMaturityQueue') ->where('initialItems.0.id', $suspected->id) ->where('endpoints.list', route('cp.maturity.list')) ->where('endpoints.reviewPattern', route('cp.maturity.review', ['artwork' => '__ARTWORK__'])) ); }); it('defaults to the audit queue when suspected items are empty but audit candidates exist', function () { Queue::fake(); $admin = createMaturityQueueAdmin(); $artwork = Artwork::factory()->create([ 'title' => 'Audit First Queue Candidate', 'hash' => 'audit-first-queue-candidate', 'thumb_ext' => 'webp', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_source' => ArtworkMaturityService::SOURCE_LEGACY, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_declared_at' => null, 'maturity_reviewed_at' => null, 'published_at' => now(), ]); DB::table('artwork_maturity_audit_findings')->insert([ 'artwork_id' => $artwork->id, 'status' => 'open', 'thumbnail_variant' => 'md', 'ai_label' => 'mature', 'ai_confidence' => 0.8442, 'ai_score' => 0.8442, 'ai_labels' => json_encode(['nudity'], JSON_UNESCAPED_SLASHES), 'ai_model' => 'vision-maturity-v2', 'ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'ai_advisory' => 'Needs manual review.', 'detected_at' => now(), 'last_scanned_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get('/cp/maturity') ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Moderation/ArtworkMaturityQueue') ->where('initialFilters.status', 'audit') ->where('initialItems.0.id', $artwork->id) ->where('stats.audit', 1) ->where('stats.suspected', 0) ); }); it('filters the moderation queue by AI action hint', function () { Queue::fake(); $moderator = User::factory()->create(['role' => 'moderator']); $flagHigh = Artwork::factory()->create([ 'title' => 'Flag High Artwork', 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'maturity_ai_action_hint' => ArtworkMaturityService::AI_ACTION_FLAG_HIGH, 'published_at' => now(), ]); Artwork::factory()->create([ 'title' => 'Review Artwork', 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'maturity_ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'published_at' => now(), ]); $this->actingAs($moderator) ->getJson('/cp/maturity/queue?status=suspected&ai_action=flag_high') ->assertOk() ->assertJsonPath('meta.filters.ai_action', ArtworkMaturityService::AI_ACTION_FLAG_HIGH) ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $flagHigh->id); }); it('records audit findings for legacy artworks without mutating artwork maturity fields', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'title' => 'Legacy Audit Candidate', 'hash' => 'abc123def456', 'thumb_ext' => 'webp', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_source' => ArtworkMaturityService::SOURCE_LEGACY, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_declared_at' => null, 'maturity_reviewed_at' => null, ]); config()->set('vision.enabled', true); config()->set('vision.maturity.base_url', 'https://vision.test'); config()->set('vision.maturity.endpoint', '/analyze/maturity'); Http::fake([ 'https://vision.test/*' => Http::response([ 'status' => 'succeeded', 'maturity_label' => 'mature', 'confidence' => 0.9123, 'score' => 0.9123, 'action_hint' => 'review', 'labels' => ['nudity'], 'model' => 'vision-maturity-v2', 'advisory' => 'Needs moderator confirmation.', ], 200), ]); $this->artisan('artworks:audit-thumbnail-maturity', ['--limit' => 1]) ->assertSuccessful(); $artwork->refresh(); expect($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_CLEAR) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_LEGACY) ->and($artwork->is_mature)->toBeFalse(); $this->assertDatabaseHas('artwork_maturity_audit_findings', [ 'artwork_id' => $artwork->id, 'status' => 'open', 'ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, ]); }); it('shows audit candidates in cpad and resolves them after moderator review', function () { Queue::fake(); $moderator = User::factory()->create(['role' => 'moderator']); $artwork = Artwork::factory()->create([ 'title' => 'Legacy Queue Candidate', 'hash' => 'queuecandidate123', 'thumb_ext' => 'webp', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_source' => ArtworkMaturityService::SOURCE_LEGACY, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_declared_at' => null, 'maturity_reviewed_at' => null, 'published_at' => now(), ]); DB::table('artwork_maturity_audit_findings')->insert([ 'artwork_id' => $artwork->id, 'status' => 'open', 'thumbnail_variant' => 'md', 'ai_label' => 'mature', 'ai_confidence' => 0.8442, 'ai_score' => 0.8442, 'ai_labels' => json_encode(['nudity'], JSON_UNESCAPED_SLASHES), 'ai_model' => 'vision-maturity-v2', 'ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'ai_advisory' => 'Needs manual review.', 'detected_at' => now(), 'last_scanned_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $this->actingAs($moderator) ->getJson('/cp/maturity/queue?status=audit') ->assertOk() ->assertJsonPath('meta.status', 'audit') ->assertJsonPath('meta.stats.audit', 1) ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $artwork->id) ->assertJsonPath('data.0.audit.ai_action_hint', ArtworkMaturityService::AI_ACTION_REVIEW) ->assertJsonPath('data.0.audit.legacy_unset', true); $this->actingAs($moderator) ->postJson("/cp/maturity/{$artwork->id}/review", [ 'action' => 'mark_mature', 'note' => 'Confirmed from audit queue.', ]) ->assertOk(); $artwork->refresh(); expect($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_REVIEWED) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_MODERATOR) ->and($artwork->is_mature)->toBeTrue(); $this->assertDatabaseHas('artwork_maturity_audit_findings', [ 'artwork_id' => $artwork->id, 'status' => 'reviewed', 'resolved_by' => $moderator->id, 'resolution_action' => 'mark_mature', ]); }); it('allows cpad admins to review artwork maturity from the artwork admin surface', function () { Queue::fake(); $admin = createMaturityQueueAdmin(); $artwork = Artwork::factory()->create([ 'title' => 'Artwork Admin Review Candidate', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_flag_reason' => 'AI suspected mature content from: nudity', ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->postJson(route('admin.cp.artworks.maturity.review', ['artwork' => $artwork->id]), [ 'action' => 'mark_mature', 'note' => 'Reviewed from artwork admin surface.', ]) ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('artwork.id', $artwork->id); $artwork->refresh(); expect($artwork->is_mature)->toBeTrue() ->and($artwork->maturity_level)->toBe(ArtworkMaturityService::LEVEL_MATURE) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_REVIEWED) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_MODERATOR) ->and($artwork->maturity_reviewer_note)->toBe('Reviewed from artwork admin surface.'); }); it('accepts controlpanel auth users when recording maturity reviews', function () { Queue::fake(); $admin = ControlPanelUser::query()->create([ 'name' => 'Legacy CP Admin', 'email' => 'legacy-cp-admin@example.test', 'password' => Hash::make('password'), 'isAdmin' => true, 'activated' => true, ]); $artwork = Artwork::factory()->create([ 'title' => 'Legacy CP Review Candidate', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_SUSPECTED, 'maturity_flag_reason' => 'AI suspected mature content from: nudity', ]); DB::table('artwork_maturity_audit_findings')->insert([ 'artwork_id' => $artwork->id, 'status' => 'open', 'thumbnail_variant' => 'md', 'ai_label' => 'mature', 'ai_confidence' => 0.8123, 'ai_score' => 0.8123, 'ai_labels' => json_encode(['nudity'], JSON_UNESCAPED_SLASHES), 'ai_model' => 'vision-maturity-v2', 'ai_action_hint' => ArtworkMaturityService::AI_ACTION_REVIEW, 'ai_status' => ArtworkMaturityService::AI_STATUS_SUCCEEDED, 'ai_advisory' => 'Needs manual review.', 'detected_at' => now(), 'last_scanned_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); app(ArtworkMaturityService::class)->review($artwork, 'mark_mature', $admin, 'Reviewed from legacy cp route.'); app(\App\Services\Maturity\ArtworkMaturityAuditService::class)->resolveFindingForReview( $artwork, $admin, 'mark_mature', 'Reviewed from legacy cp route.', ); $artwork->refresh(); expect($artwork->is_mature)->toBeTrue() ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_REVIEWED) ->and($artwork->maturity_source)->toBe(ArtworkMaturityService::SOURCE_MODERATOR) ->and($artwork->maturity_reviewed_by)->toBe($admin->id) ->and($artwork->maturity_reviewer_note)->toBe('Reviewed from legacy cp route.'); $this->assertDatabaseHas('artwork_maturity_audit_findings', [ 'artwork_id' => $artwork->id, 'status' => 'reviewed', 'resolved_by' => $admin->id, 'resolution_action' => 'mark_mature', ]); }); it('records a failed maturity detection job without marking the artwork safe by implication', function () { Queue::fake(); $artwork = Artwork::factory()->create([ 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'maturity_ai_status' => ArtworkMaturityService::AI_STATUS_PENDING, 'maturity_ai_label' => null, ]); $job = new DetectArtworkMaturityJob($artwork->id, 'fake-hash'); $job->failed(new RuntimeException('Vision gateway timeout.')); $artwork->refresh(); expect($artwork->is_mature)->toBeFalse() ->and($artwork->maturity_level)->toBe(ArtworkMaturityService::LEVEL_SAFE) ->and($artwork->maturity_status)->toBe(ArtworkMaturityService::STATUS_CLEAR) ->and($artwork->maturity_ai_status)->toBe(ArtworkMaturityService::AI_STATUS_FAILED) ->and($artwork->maturity_ai_advisory)->toBe('Vision gateway timeout.') ->and($artwork->maturity_ai_label)->toBeNull(); }); it('hides mature items from the daily uploads page for hide-mode viewers', function () { $viewer = User::factory()->create(); DB::table('user_profiles')->insert([ 'user_id' => $viewer->id, 'mature_content_visibility' => 'hide', 'mature_content_warning_enabled' => true, ]); $safeArtwork = Artwork::factory()->create([ 'title' => 'Daily Safe Artwork', 'is_public' => true, 'is_approved' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'published_at' => now(), 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, ]); $matureArtwork = Artwork::factory()->create([ 'title' => 'Daily Hidden Mature Artwork', 'is_public' => true, 'is_approved' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, 'published_at' => now(), 'is_mature' => true, 'maturity_level' => ArtworkMaturityService::LEVEL_MATURE, 'maturity_status' => ArtworkMaturityService::STATUS_DECLARED, ]); $this->actingAs($viewer) ->get(route('uploads.daily')) ->assertOk() ->assertSee('Daily Safe Artwork') ->assertDontSee('Daily Hidden Mature Artwork'); expect($safeArtwork->exists)->toBeTrue() ->and($matureArtwork->exists)->toBeTrue(); }); it('filters collection artworks and cover fallbacks for hide-mode viewers', function () { $viewer = User::factory()->create(); $owner = User::factory()->create(); DB::table('user_profiles')->insert([ 'user_id' => $viewer->id, 'mature_content_visibility' => 'hide', 'mature_content_warning_enabled' => true, ]); $collection = Collection::factory()->create([ 'user_id' => $owner->id, 'visibility' => Collection::VISIBILITY_PUBLIC, ]); $matureArtwork = Artwork::factory()->create([ 'user_id' => $owner->id, 'title' => 'Hidden Cover Artwork', 'hash' => 'mature-cover-hash', 'thumb_ext' => 'jpg', 'is_mature' => true, 'maturity_level' => ArtworkMaturityService::LEVEL_MATURE, 'maturity_status' => ArtworkMaturityService::STATUS_DECLARED, 'published_at' => now(), ]); $safeArtwork = Artwork::factory()->create([ 'user_id' => $owner->id, 'title' => 'Visible Safe Artwork', 'hash' => 'safe-cover-hash', 'thumb_ext' => 'jpg', 'is_mature' => false, 'maturity_level' => ArtworkMaturityService::LEVEL_SAFE, 'maturity_status' => ArtworkMaturityService::STATUS_CLEAR, 'published_at' => now(), ]); $collection->forceFill([ 'cover_artwork_id' => $matureArtwork->id, ])->save(); DB::table('collection_artwork')->insert([ [ 'collection_id' => $collection->id, 'artwork_id' => $matureArtwork->id, 'order_num' => 1, 'created_at' => now(), 'updated_at' => now(), ], [ 'collection_id' => $collection->id, 'artwork_id' => $safeArtwork->id, 'order_num' => 2, 'created_at' => now(), 'updated_at' => now(), ], ]); $service = app(CollectionService::class); $cardPayload = $service->mapCollectionCardPayloads( collect([$collection->fresh()->loadMissing(['user.profile', 'coverArtwork'])]), false, $viewer, )[0]; $artworks = $service->getCollectionDetailArtworks($collection->fresh(), false, 24, $viewer); expect($cardPayload['cover_artwork_id'])->toBe($safeArtwork->id) ->and($artworks->getCollection()->pluck('id')->all())->toBe([$safeArtwork->id]); });