set('forum_bot_protection.geo_behavior', [ 'enabled' => true, 'login_actions' => ['login'], 'country_headers' => ['CF-IPCountry'], 'recent_login_window_minutes' => 60, 'country_change_penalty' => 50, ]); Schema::dropIfExists('forum_bot_logs'); Schema::dropIfExists('users'); Schema::create('users', function (Blueprint $table): void { $table->id(); $table->string('email')->nullable(); $table->string('password')->nullable(); $table->timestamps(); $table->softDeletes(); }); 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(); }); DB::table('users')->insert([ 'id' => 1, 'email' => 'geo@example.com', 'password' => 'secret', 'created_at' => now(), 'updated_at' => now(), ]); $user = User::query()->findOrFail(1); ForumBotLog::query()->create([ 'user_id' => $user->id, 'ip_address' => '127.0.0.1', 'action' => 'login', 'risk_score' => 0, 'decision' => 'allow', 'metadata' => ['country_code' => 'SI'], 'created_at' => now()->subMinutes(10), ]); $request = Request::create('/login', 'POST'); $request->headers->set('CF-IPCountry', 'SI'); $unchanged = app(GeoBehaviorAnalyzer::class)->analyze($user, 'login', $request); expect($unchanged)->toMatchArray([ 'score' => 0, 'country_code' => 'SI', ])->and($unchanged['reasons'])->toBe([]); $request->headers->set('CF-IPCountry', 'JP'); $analysis = app(GeoBehaviorAnalyzer::class)->analyze($user, 'login', $request); expect($analysis['score'])->toBe(50) ->and($analysis['country_code'])->toBe('JP') ->and($analysis['reasons'])->toHaveCount(1) ->and($analysis['reasons'][0])->toContain('SI') ->and($analysis['reasons'][0])->toContain('JP'); });