Files
SkinbaseNova/tests/Feature/Admin/ContentModerationAdminTest.php

555 lines
21 KiB
PHP

<?php
declare(strict_types=1);
use App\Enums\ModerationContentType;
use App\Enums\ModerationDomainStatus;
use App\Enums\ModerationRuleType;
use App\Enums\ModerationStatus;
use App\Models\ContentModerationFeedback;
use App\Models\Artwork;
use App\Models\ArtworkComment;
use App\Models\ContentModerationActionLog;
use App\Models\ContentModerationCluster;
use App\Models\ContentModerationDomain;
use App\Models\ContentModerationFinding;
use App\Models\ContentModerationRule;
use App\Models\UserProfile;
use App\Models\UserSocialLink;
use App\Models\User;
use App\Services\Moderation\ContentModerationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Klevze\ControlPanel\Models\Admin\AdminVerification;
uses(RefreshDatabase::class);
function createControlPanelAdmin(): User
{
$admin = User::factory()->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();
});