555 lines
21 KiB
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();
|
|
}); |