Files
SkinbaseNova/tests/Unit/Moderation/ContentModerationServiceTest.php

208 lines
8.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Enums\ModerationDomainStatus;
use App\Enums\ModerationRuleType;
use App\Enums\ModerationContentType;
use App\Enums\ModerationSeverity;
use App\Enums\ModerationStatus;
use App\Models\ContentModerationDomain;
use App\Models\ContentModerationFinding;
use App\Models\ContentModerationRule;
use App\Models\User;
use App\Services\Moderation\ContentModerationService;
use App\Services\Moderation\ContentModerationSourceService;
use App\Services\Moderation\Rules\LinkPresenceRule;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
it('extracts explicit and www urls from content', function (): void {
$rule = app(LinkPresenceRule::class);
$urls = $rule->extractUrls('Visit https://spam.example.com and www.short.test/offer now.');
expect($urls)->toContain('https://spam.example.com')
->and($urls)->toContain('www.short.test/offer');
});
it('detects suspicious keywords and domains with a queued status', function (): void {
$result = app(ContentModerationService::class)->analyze(
'Buy followers today at https://promo.pornsite.com right now',
['content_type' => 'artwork_comment', 'content_id' => 1]
);
expect($result->score)->toBeGreaterThanOrEqual(110)
->and(in_array($result->severity, [ModerationSeverity::High, ModerationSeverity::Critical], true))->toBeTrue()
->and($result->matchedDomains)->toContain('promo.pornsite.com')
->and($result->matchedKeywords)->toContain('buy followers');
});
it('detects unicode obfuscation patterns', function (): void {
$result = app(ContentModerationService::class)->analyze(
'раypal giveaway click here',
['content_type' => 'artwork_comment', 'content_id' => 1]
);
expect($result->score)->toBeGreaterThan(0)
->and(collect($result->reasons)->implode(' '))->toContain('Unicode');
});
it('produces stable hashes for semantically identical whitespace variants', function (): void {
$service = app(ContentModerationService::class);
$first = $service->analyze("Visit my site\n\nnow", ['content_type' => 'artwork_comment', 'content_id' => 1]);
$second = $service->analyze(' visit my site now ', ['content_type' => 'artwork_comment', 'content_id' => 2]);
expect($first->contentHash)->toBe($second->contentHash);
});
it('detects keyword stuffing patterns', function (): void {
$result = app(ContentModerationService::class)->analyze(
'seo seo seo seo seo seo seo seo seo seo seo seo seo service service service service service cheap cheap cheap traffic traffic traffic',
['content_type' => 'artwork_description', 'content_id' => 1]
);
expect($result->score)->toBeGreaterThan(0)
->and($result->matchedKeywords)->toContain('seo');
});
it('maps score thresholds to the expected severities', function (): void {
expect(ModerationSeverity::fromScore(0))->toBe(ModerationSeverity::Low)
->and(ModerationSeverity::fromScore(30))->toBe(ModerationSeverity::Medium)
->and(ModerationSeverity::fromScore(60))->toBe(ModerationSeverity::High)
->and(ModerationSeverity::fromScore(90))->toBe(ModerationSeverity::Critical);
});
it('applies db managed moderation rules alongside config rules', function (): void {
ContentModerationRule::query()->create([
'type' => ModerationRuleType::SuspiciousKeyword,
'value' => 'rare promo blast',
'enabled' => true,
'weight' => 22,
]);
$result = app(ContentModerationService::class)->analyze(
'This rare promo blast just dropped.',
['content_type' => 'artwork_comment', 'content_id' => 1]
);
expect($result->matchedKeywords)->toContain('rare promo blast')
->and($result->score)->toBeGreaterThan(0);
});
it('uses domain reputation to escalate blocked domains and auto hide recommendations', function (): void {
ContentModerationDomain::query()->create([
'domain' => 'campaign.spam.test',
'status' => ModerationDomainStatus::Blocked,
]);
$result = app(ContentModerationService::class)->analyze(
'Buy followers now at https://campaign.spam.test and claim your giveaway',
['content_type' => 'artwork_comment', 'content_id' => 1]
);
expect($result->matchedDomains)->toContain('campaign.spam.test')
->and($result->autoHideRecommended)->toBeTrue()
->and($result->score)->toBeGreaterThanOrEqual(95);
});
it('applies user risk modifiers conservatively', function (): void {
$user = User::factory()->create();
ContentModerationFinding::query()->create([
'content_type' => 'artwork_comment',
'content_id' => 11,
'user_id' => $user->id,
'status' => ModerationStatus::ConfirmedSpam->value,
'severity' => 'high',
'score' => 85,
'content_hash' => hash('sha256', 'a'),
'scanner_version' => '2.0',
'content_snapshot' => 'spam one',
]);
ContentModerationFinding::query()->create([
'content_type' => 'artwork_comment',
'content_id' => 12,
'user_id' => $user->id,
'status' => ModerationStatus::ConfirmedSpam->value,
'severity' => 'critical',
'score' => 120,
'content_hash' => hash('sha256', 'b'),
'scanner_version' => '2.0',
'content_snapshot' => 'spam two',
]);
$base = app(ContentModerationService::class)->analyze(
'Visit https://safe.example.test for more info',
['content_type' => 'artwork_comment', 'content_id' => 1]
);
$risky = app(ContentModerationService::class)->analyze(
'Visit https://safe.example.test for more info',
['content_type' => 'artwork_comment', 'content_id' => 2, 'user_id' => $user->id]
);
expect($risky->score)->toBeGreaterThan($base->score)
->and($risky->userRiskScore)->toBeGreaterThan(0)
->and($risky->ruleHits)->toHaveKey('user_risk_modifier');
});
it('applies strict seo policy and v3 assistive fields for link-heavy profile content', function (): void {
$result = app(ContentModerationService::class)->analyze(
'Visit https://promo.cluster-test.com now for a limited time offer and boost your traffic',
[
'content_type' => ModerationContentType::UserProfileLink->value,
'content_id' => 77,
'content_target_type' => 'user_social_link',
'content_target_id' => 77,
'is_publicly_exposed' => true,
]
);
expect($result->policyName)->toBe('strict_seo_protection')
->and($result->status)->toBe(ModerationStatus::Pending)
->and($result->priorityScore)->toBeGreaterThan($result->score)
->and(in_array($result->reviewBucket, ['urgent', 'high'], true))->toBeTrue()
->and($result->aiProvider)->toBe('heuristic_assist')
->and($result->aiLabel)->not->toBeNull()
->and($result->contentTargetType)->toBe('user_social_link')
->and($result->contentTargetId)->toBe(77)
->and($result->scoreBreakdown)->not->toBeEmpty();
});
it('builds v3 moderation source context for profile links and card text', function (): void {
$service = app(ContentModerationSourceService::class);
$profileLinkContext = $service->buildContext(ModerationContentType::UserProfileLink, (object) [
'id' => 14,
'user_id' => 5,
'url' => 'https://promo.example.test',
]);
$cardTextContext = $service->buildContext(ModerationContentType::CardText, (object) [
'id' => 9,
'user_id' => 3,
'quote_text' => 'Promo headline',
'description' => 'Additional landing page copy',
'quote_author' => 'Campaign Bot',
'quote_source' => 'promo.example.test',
'visibility' => 'public',
]);
expect($profileLinkContext['content_type'])->toBe(ModerationContentType::UserProfileLink->value)
->and($profileLinkContext['content_target_type'])->toBe('user_social_link')
->and($profileLinkContext['content_target_id'])->toBe(14)
->and($profileLinkContext['user_id'])->toBe(5)
->and($profileLinkContext['is_publicly_exposed'])->toBeTrue()
->and($cardTextContext['content_type'])->toBe(ModerationContentType::CardText->value)
->and($cardTextContext['content_target_type'])->toBe('nova_card')
->and($cardTextContext['content_target_id'])->toBe(9)
->and($cardTextContext['user_id'])->toBe(3)
->and($cardTextContext['is_publicly_exposed'])->toBeTrue()
->and($cardTextContext['content_snapshot'])->toContain('Promo headline')
->and($cardTextContext['content_snapshot'])->toContain('promo.example.test');
});