set('forum_bot_protection.account_farm', [ 'window_minutes' => 10, 'register_attempt_threshold' => 10, 'same_ip_users_threshold' => 5, 'same_fingerprint_users_threshold' => 3, 'same_pattern_users_threshold' => 2, 'register_attempt_penalty' => 50, 'same_ip_penalty' => 35, 'same_fingerprint_penalty' => 40, 'same_pattern_penalty' => 45, ]); Schema::dropIfExists('forum_posts'); Schema::dropIfExists('forum_bot_device_fingerprints'); Schema::dropIfExists('forum_bot_logs'); Schema::create('forum_bot_logs', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('user_id')->nullable(); $table->string('ip_address', 45)->nullable(); $table->string('action', 80); $table->unsignedTinyInteger('risk_score')->default(0); $table->string('decision', 20)->default('allow'); $table->json('metadata')->nullable(); $table->timestamp('created_at')->nullable(); }); Schema::create('forum_bot_device_fingerprints', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('user_id')->nullable(); $table->string('fingerprint', 128)->nullable(); $table->timestamp('first_seen')->nullable(); $table->timestamp('last_seen')->nullable(); $table->unsignedTinyInteger('risk_score')->default(0); $table->string('user_agent_hash', 64)->nullable(); $table->timestamps(); }); Schema::create('forum_posts', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('thread_id')->nullable(); $table->unsignedBigInteger('topic_id')->nullable(); $table->string('source_ip_hash', 64)->nullable(); $table->unsignedBigInteger('user_id')->nullable(); $table->longText('content')->nullable(); $table->string('content_hash', 64)->nullable(); $table->boolean('is_edited')->default(false); $table->timestamp('edited_at')->nullable(); $table->unsignedInteger('spam_score')->default(0); $table->unsignedInteger('quality_score')->default(0); $table->unsignedInteger('ai_spam_score')->default(0); $table->unsignedInteger('ai_toxicity_score')->default(0); $table->unsignedInteger('behavior_score')->default(0); $table->unsignedInteger('link_score')->default(0); $table->integer('learning_score')->default(0); $table->unsignedInteger('risk_score')->default(0); $table->integer('trust_modifier')->default(0); $table->boolean('flagged')->default(false); $table->string('flagged_reason')->nullable(); $table->boolean('moderation_checked')->default(false); $table->string('moderation_status')->nullable(); $table->json('moderation_labels')->nullable(); $table->json('moderation_meta')->nullable(); $table->timestamp('last_ai_scan_at')->nullable(); $table->timestamps(); $table->softDeletes(); }); $hash = hash('sha256', 'buy cheap backlinks now'); foreach ([1, 2, 3] as $userId) { DB::table('forum_posts')->insert([ 'user_id' => $userId, 'content' => 'buy cheap backlinks now', 'content_hash' => $hash, 'created_at' => now()->subMinutes(2), 'updated_at' => now()->subMinutes(2), ]); } $result = app(AccountFarmDetector::class)->analyze(1, '203.0.113.10', null, 'forum_reply_create'); expect($result['score'])->toBe(45) ->and($result['reasons'])->toContain('Posting patterns or repeated content overlap across multiple accounts.'); });