create(['role' => 'admin']); $admin->forceFill([ 'isAdmin' => true, 'activated' => true, ])->save(); AdminVerification::createForUser($admin->fresh()); return $admin->fresh(); } it('loads the cpad moderation list and detail screens for admins', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id + 100, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::ConfirmedSpam->value, 'severity' => 'critical', 'score' => 120, 'content_hash' => hash('sha256', 'spam-description-prior'), 'scanner_version' => '2.0', 'content_snapshot' => 'Earlier spam finding', ]); ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id + 101, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'medium', 'score' => 45, 'content_hash' => hash('sha256', 'spam-description-pending'), 'scanner_version' => '2.0', 'content_snapshot' => 'Pending review finding', ]); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 95, 'content_hash' => hash('sha256', 'spam-description'), 'scanner_version' => '1.0', 'reasons_json' => ['Contains spam keywords'], 'matched_links_json' => ['https://promo.pornsite.com'], 'matched_domains_json' => ['promo.pornsite.com'], 'matched_keywords_json' => ['buy followers'], 'content_snapshot' => 'Buy followers now', ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.main')) ->assertOk() ->assertSee('Content Moderation') ->assertSee((string) $finding->id); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.show', $finding)) ->assertOk() ->assertSee('Buy followers now') ->assertSee('Contains spam keywords') ->assertSee('Related Findings') ->assertSee('Confirmed Spam') ->assertSee('Pending Findings'); }); it('supports sortable moderation list columns in cpad', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'critical', 'score' => 120, 'content_hash' => hash('sha256', 'first'), 'scanner_version' => '1.0', 'content_snapshot' => 'critical finding', ]); ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id + 1, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'medium', 'score' => 35, 'content_hash' => hash('sha256', 'second'), 'scanner_version' => '1.0', 'content_snapshot' => 'medium finding', ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.main', ['sort' => 'score', 'direction' => 'asc'])) ->assertOk() ->assertSee('Score') ->assertSee('ASC'); }); it('updates finding review status from the cpad moderation actions', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 95, 'content_hash' => hash('sha256', 'spam-description'), 'scanner_version' => '1.0', 'content_snapshot' => 'Buy followers now', ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.safe', $finding), ['admin_notes' => 'false positive']) ->assertRedirect(); $finding->refresh(); expect($finding->status)->toBe(ModerationStatus::ReviewedSafe) ->and($finding->admin_notes)->toBe('false positive') ->and($finding->reviewed_by)->toBe($admin->id); }); it('can hide flagged artwork comments from the cpad moderation screen', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $comment = ArtworkComment::factory()->create([ 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, ]); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkComment->value, 'content_id' => $comment->id, 'artwork_id' => $artwork->id, 'user_id' => $comment->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'critical', 'score' => 120, 'content_hash' => hash('sha256', 'spam-comment'), 'scanner_version' => '1.0', 'content_snapshot' => $comment->raw_content, ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.hide', $finding), ['admin_notes' => 'hidden by moderation']) ->assertRedirect(); expect($comment->fresh()->is_approved)->toBeFalse() ->and($finding->fresh()->status)->toBe(ModerationStatus::ConfirmedSpam) ->and($finding->fresh()->action_taken)->toBe('hide_comment'); }); it('loads the v2 moderation dashboard, domains, rules, and actions pages', function (): void { $admin = createControlPanelAdmin(); $domain = ContentModerationDomain::query()->create([ 'domain' => 'promo.pornsite.com', 'status' => ModerationDomainStatus::Blocked, 'times_seen' => 3, 'times_flagged' => 2, 'times_confirmed_spam' => 1, ]); $rule = ContentModerationRule::query()->create([ 'type' => ModerationRuleType::SuspiciousKeyword, 'value' => 'rare promo blast', 'enabled' => true, 'weight' => 20, 'created_by' => $admin->id, ]); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkComment->value, 'content_id' => 99, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 95, 'content_hash' => hash('sha256', 'finding'), 'scanner_version' => '2.0', 'content_snapshot' => 'spam snapshot', ]); ContentModerationActionLog::query()->create([ 'finding_id' => $finding->id, 'target_type' => 'finding', 'target_id' => $finding->id, 'action_type' => 'rescan', 'actor_type' => 'admin', 'actor_id' => $admin->id, 'created_at' => now(), ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.dashboard')) ->assertOk() ->assertSee('Content Moderation Dashboard') ->assertSee('Top Flagged Domains'); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.domains')) ->assertOk() ->assertSee($domain->domain); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.rules')) ->assertOk() ->assertSee($rule->value); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.actions')) ->assertOk() ->assertSee('rescan'); }); it('supports proactive domain creation and domain detail inspection from cpad', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $response = $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.domains.store'), [ 'domain' => 'promo.example.com', 'status' => ModerationDomainStatus::Blocked->value, 'notes' => 'Manual blocklist entry', ]); $domain = ContentModerationDomain::query()->where('domain', 'promo.example.com')->firstOrFail(); ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 95, 'content_hash' => hash('sha256', 'domain-linked-finding'), 'scanner_version' => '2.0', 'matched_domains_json' => ['promo.example.com'], 'content_snapshot' => 'Linked to promo domain', ]); $response->assertRedirect(route('admin.site.content-moderation.domains.show', $domain)); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.domains.show', $domain)) ->assertOk() ->assertSee('promo.example.com') ->assertSee('Linked Findings') ->assertSee('Manual blocklist entry'); }); it('applies admin managed domains to moderation analysis', function (): void { $admin = createControlPanelAdmin(); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.domains.store'), [ 'domain' => 'promo.example.com', 'status' => ModerationDomainStatus::Blocked->value, ]) ->assertRedirect(); $result = app(ContentModerationService::class)->analyze('Visit https://promo.example.com/deal right now.', [ 'user_id' => $admin->id, ]); expect($result->matchedDomains)->toContain('promo.example.com') ->and($result->ruleHits)->toHaveKey('blocked_domain') ->and($result->status)->toBe(ModerationStatus::Pending); }); it('applies admin managed rules to moderation analysis', function (): void { $admin = createControlPanelAdmin(); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.rules.store'), [ 'type' => ModerationRuleType::SuspiciousKeyword->value, 'value' => 'rare promo blast', 'enabled' => '1', 'weight' => 20, ]) ->assertRedirect(); $result = app(ContentModerationService::class)->analyze('This rare promo blast just dropped.'); expect($result->matchedKeywords)->toContain('rare promo blast') ->and($result->ruleHits)->toHaveKey('suspicious_keyword') ->and($result->score)->toBeGreaterThan(0) ->and($result->reasons)->toContain('Contains suspicious keyword(s): rare promo blast'); }); it('reports partial bulk action failures explicitly', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $domainFinding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 80, 'content_hash' => hash('sha256', 'bulk-domain-finding'), 'scanner_version' => '2.0', 'matched_domains_json' => ['promo.bulk-test.com'], 'content_snapshot' => 'Has a domain', ]); $noDomainFinding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkDescription->value, 'content_id' => $artwork->id + 1, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'medium', 'score' => 40, 'content_hash' => hash('sha256', 'bulk-no-domain-finding'), 'scanner_version' => '2.0', 'matched_domains_json' => [], 'content_snapshot' => 'No domain here', ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.bulk'), [ 'action' => 'block_domains', 'finding_ids' => [$domainFinding->id, $noDomainFinding->id], ]) ->assertRedirect(route('admin.site.content-moderation.main')) ->assertSessionHas('msg_success', 'Processed 1 moderation item(s).') ->assertSessionHas('msg_warning'); expect(ContentModerationDomain::query()->where('domain', 'promo.bulk-test.com')->where('status', ModerationDomainStatus::Blocked->value)->exists())->toBeTrue() ->and(ContentModerationActionLog::query()->where('target_type', 'domain')->where('target_id', ContentModerationDomain::query()->where('domain', 'promo.bulk-test.com')->value('id'))->exists())->toBeTrue(); }); it('restores auto hidden comments from the cpad moderation screen', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $comment = ArtworkComment::factory()->create([ 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'is_approved' => false, ]); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkComment->value, 'content_id' => $comment->id, 'artwork_id' => $artwork->id, 'user_id' => $comment->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'critical', 'score' => 120, 'content_hash' => hash('sha256', 'spam-comment-restored'), 'scanner_version' => '2.0', 'content_snapshot' => $comment->raw_content, 'is_auto_hidden' => true, 'auto_action_taken' => 'auto_hide_comment', 'auto_hidden_at' => now(), ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.restore', $finding), ['admin_notes' => 'restored after review']) ->assertRedirect(); expect($comment->fresh()->is_approved)->toBeTrue() ->and($finding->fresh()->is_auto_hidden)->toBeFalse() ->and($finding->fresh()->restored_by)->toBe($admin->id); }); it('loads the v3 moderation queue, cluster, user, policy, and feedback screens', function (): void { $admin = createControlPanelAdmin(); $flaggedUser = User::factory()->create(); $artwork = Artwork::factory()->create(['user_id' => $flaggedUser->id]); UserProfile::query()->create([ 'user_id' => $flaggedUser->id, 'about' => 'Profile with promotional links', ]); UserSocialLink::query()->create([ 'user_id' => $flaggedUser->id, 'platform' => 'website', 'url' => 'https://promo.cluster-test.com', ]); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::UserProfileLink->value, 'content_id' => UserSocialLink::query()->where('user_id', $flaggedUser->id)->value('id'), 'content_target_type' => UserSocialLink::class, 'content_target_id' => UserSocialLink::query()->where('user_id', $flaggedUser->id)->value('id'), 'artwork_id' => $artwork->id, 'user_id' => $flaggedUser->id, 'status' => ModerationStatus::Pending->value, 'severity' => 'critical', 'score' => 140, 'priority_score' => 99, 'review_bucket' => 'urgent', 'policy_name' => 'strict_seo_protection', 'campaign_key' => 'campaign:test-cluster', 'cluster_reason' => 'Shared promotional domain and keywords', 'cluster_score' => 88, 'content_hash' => hash('sha256', 'cluster-finding'), 'scanner_version' => '3.0', 'matched_domains_json' => ['promo.cluster-test.com'], 'matched_keywords_json' => ['promo blast'], 'content_snapshot' => 'Visit https://promo.cluster-test.com for promo blast offers', 'ai_label' => 'spam', 'ai_suggested_action' => 'review', 'ai_confidence' => 91, 'ai_provider' => 'heuristic', ]); ContentModerationCluster::query()->create([ 'campaign_key' => 'campaign:test-cluster', 'cluster_reason' => 'Shared promotional domain and keywords', 'cluster_score' => 88, ]); ContentModerationFeedback::query()->create([ 'finding_id' => $finding->id, 'actor_id' => $admin->id, 'feedback_type' => 'reviewed_safe', 'notes' => 'Operator reviewed queue behaviour', ]); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.priority')) ->assertOk() ->assertSee('Priority Findings') ->assertSee((string) $finding->id); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.clusters')) ->assertOk() ->assertSee('Campaign Clusters') ->assertSee('campaign:test-cluster'); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.clusters.show', ['campaignKey' => 'campaign:test-cluster'])) ->assertOk() ->assertSee('Cluster Findings') ->assertSee('Shared promotional domain and keywords'); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.users')) ->assertOk() ->assertSee('User Moderation Profiles') ->assertSee($flaggedUser->username ?? $flaggedUser->name); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.users.show', ['user' => $flaggedUser->id])) ->assertOk() ->assertSee('Profile Summary') ->assertSee($flaggedUser->username ?? $flaggedUser->name); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.policies')) ->assertOk() ->assertSee('Moderation Policies') ->assertSee('strict_seo_protection'); $this->actingAs($admin)->actingAs($admin, 'controlpanel') ->get(route('admin.site.content-moderation.feedback')) ->assertOk() ->assertSee('Reviewer Feedback') ->assertSee('Operator reviewed queue behaviour'); }); it('marks findings as false positives from the cpad moderation screen', function (): void { $admin = createControlPanelAdmin(); $artwork = Artwork::factory()->create(); $finding = ContentModerationFinding::query()->create([ 'content_type' => ModerationContentType::ArtworkTitle->value, 'content_id' => $artwork->id, 'content_target_type' => Artwork::class, 'content_target_id' => $artwork->id, 'artwork_id' => $artwork->id, 'user_id' => $artwork->user_id, 'status' => ModerationStatus::Pending->value, 'severity' => 'high', 'score' => 85, 'priority_score' => 80, 'content_hash' => hash('sha256', 'artwork-title-false-positive'), 'scanner_version' => '3.0', 'content_snapshot' => 'Totally legitimate artwork title', ]); $this->actingAs($admin) ->actingAs($admin, 'controlpanel') ->post(route('admin.site.content-moderation.false-positive', $finding), ['admin_notes' => 'Safe brand reference']) ->assertRedirect(route('admin.site.content-moderation.show', $finding)); $finding->refresh(); expect($finding->is_false_positive)->toBeTrue() ->and($finding->false_positive_count)->toBeGreaterThanOrEqual(1) ->and($finding->admin_notes)->toBe('Safe brand reference') ->and(ContentModerationFeedback::query()->where('finding_id', $finding->id)->where('feedback_type', 'false_positive')->exists())->toBeTrue(); });