Add comment spam classification and captcha checks.
Score artwork comments with local signatures plus Together AI, and optionally require Turnstile before posting.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\ArtworkComment;
|
||||
use App\Models\CommentSpamSignature;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('increments signature hit_count when a known spam comment is posted', function (): void {
|
||||
config([
|
||||
'comment_spam.enabled' => true,
|
||||
'comment_spam.mode' => 'enforce',
|
||||
'comment_spam.ai_enabled' => false,
|
||||
'comment_spam.captcha.enabled' => false,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$artwork = Artwork::factory()->create();
|
||||
$content = 'copy paste this known spam signature';
|
||||
$hash = hash('sha256', mb_strtolower((string) preg_replace('/\s+/u', ' ', trim($content))));
|
||||
|
||||
$signature = CommentSpamSignature::query()->create([
|
||||
'content_hash' => $hash,
|
||||
'source' => 'spam',
|
||||
'reason' => 'Known promotional spam.',
|
||||
'confidence' => 100,
|
||||
'hit_count' => 0,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->postJson("/api/artworks/{$artwork->id}/comments", [
|
||||
'content' => $content,
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
expect((int) $signature->fresh()->hit_count)->toBe(1);
|
||||
|
||||
$comment = ArtworkComment::query()->latest('id')->first();
|
||||
|
||||
expect($comment)->not->toBeNull()
|
||||
->and((bool) $comment->is_approved)->toBeFalse()
|
||||
->and((string) $comment->moderation_source)->toBe('signature');
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use App\Services\Moderation\TogetherCommentSpamClassifier;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
config([
|
||||
'comment_spam.ai.api_key' => 'test-key',
|
||||
'comment_spam.ai.base_url' => 'https://api.together.test/v1',
|
||||
'comment_spam.ai.model' => 'meta-llama/Llama-3.2-3B-Instruct-Turbo',
|
||||
]);
|
||||
});
|
||||
|
||||
it('classifies using the strict Together JSON contract', function (string $model): void {
|
||||
config(['comment_spam.ai.model' => $model]);
|
||||
Http::fake([
|
||||
'https://api.together.test/*' => Http::response([
|
||||
'choices' => [['message' => ['content' => '{"spam":true,"confidence":0.95,"reason":"Unsolicited promotion."}']]],
|
||||
]),
|
||||
]);
|
||||
|
||||
$result = app(TogetherCommentSpamClassifier::class)->classify('Buy now at example.test');
|
||||
|
||||
expect($result->spam)->toBeTrue()
|
||||
->and($result->confidence)->toBe(0.95)
|
||||
->and($result->provider)->toBe('together')
|
||||
->and($result->model)->toBe($model);
|
||||
})->with([
|
||||
'Llama' => ['meta-llama/Llama-3.2-3B-Instruct-Turbo'],
|
||||
'Bonsai' => ['Prism-ML/Ternary-Bonsai-27B'],
|
||||
]);
|
||||
|
||||
it('rejects an unsupported configured model before making an HTTP request', function (): void {
|
||||
config(['comment_spam.ai.model' => 'some/unsupported-model']);
|
||||
Http::fake();
|
||||
|
||||
expect(fn () => app(TogetherCommentSpamClassifier::class)->classify('hello'))
|
||||
->toThrow(RuntimeException::class, 'Unsupported Together AI comment spam model.');
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('rejects malformed or out of range classifier output', function (array $json): void {
|
||||
Http::fake([
|
||||
'https://api.together.test/*' => Http::response([
|
||||
'choices' => [['message' => ['content' => json_encode($json)]]],
|
||||
]),
|
||||
]);
|
||||
|
||||
expect(fn () => app(TogetherCommentSpamClassifier::class)->classify('hello'))
|
||||
->toThrow(RuntimeException::class);
|
||||
})->with([
|
||||
[['spam' => 'true', 'confidence' => 0.9, 'reason' => 'bad type']],
|
||||
[['spam' => true, 'confidence' => 1.1, 'reason' => 'out of range']],
|
||||
[['spam' => true, 'confidence' => 0.9]],
|
||||
]);
|
||||
|
||||
it('fails closed when Together is not configured', function (): void {
|
||||
config(['comment_spam.ai.api_key' => '']);
|
||||
|
||||
expect(fn () => app(TogetherCommentSpamClassifier::class)->classify('hello'))
|
||||
->toThrow(RuntimeException::class);
|
||||
});
|
||||
Reference in New Issue
Block a user