getFrom('/', '203.0.113.10')->assertOk()->assertSee('ok'); }); it('blocks ips on the list', function () { BadIpBlocker::block('203.0.113.10'); $this->getFrom('/', '203.0.113.10')->assertStatus(418)->assertSee('Blocked.'); $this->getFrom('/', '192.0.2.30')->assertOk(); }); it('uses the configured response', function () { config(['bad-ip-blocker.response' => ['status' => 403, 'message' => 'Nope']]); BadIpBlocker::block('203.0.113.10'); $this->getFrom('/', '203.0.113.10')->assertForbidden()->assertSee('Nope'); }); it('does nothing when disabled', function () { config(['bad-ip-blocker.enabled' => false]); BadIpBlocker::block('203.0.113.10'); $this->getFrom('/', '203.0.113.10')->assertOk(); }); it('never blocks whitelisted ranges, including private proxy addresses', function () { BadIpBlocker::block('192.168.1.10'); $this->getFrom('/', '192.168.1.10')->assertOk(); $this->getFrom('/.well-known/security.txt', '192.168.1.20')->assertNotFound(); expect(BadIpBlocker::isBlocked('192.168.1.20'))->toBeFalse(); }); it('auto-blocks clients probing for a missing security.txt', function (string $path) { $this->getFrom($path, '198.51.100.20')->assertStatus(418); expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeTrue() ->and(BadIpBlocker::all()['198.51.100.20']['reason'])->toBe('Auto: '.$path); $this->getFrom('/', '198.51.100.20')->assertStatus(418); })->with(['/.well-known/security.txt', '/security.txt', '/.env', '/wp-login.php']); it('does not auto-block when the path exists in the app', function () { Route::get('/.well-known/security.txt', fn () => 'Contact: mailto:security@example.com'); $this->getFrom('/.well-known/security.txt', '198.51.100.20')->assertOk(); expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse(); }); it('does not auto-block ordinary 404s', function () { foreach (['/about', '/products', '/missing-page'] as $path) { $this->getFrom($path, '203.0.113.10')->assertNotFound(); } expect(BadIpBlocker::isBlocked('203.0.113.10'))->toBeFalse(); }); it('respects max_hits', function () { config(['bad-ip-blocker.auto_block.max_hits' => 2]); $this->getFrom('/.well-known/security.txt', '198.51.100.20')->assertNotFound(); expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse(); $this->getFrom('/security.txt', '198.51.100.20')->assertStatus(418); expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeTrue(); }); it('expires auto-blocks after 7 days by default', function () { CarbonImmutable::setTestNow('2026-11-11 20:00:00'); $this->getFrom('/security.txt', '198.51.100.20'); expect(BadIpBlocker::all()['198.51.100.20']['expires_at'])->toBe('2026-11-18T20:00:00+00:00'); CarbonImmutable::setTestNow(); }); it('can make auto-blocks permanent', function () { config(['bad-ip-blocker.auto_block.ttl' => null]); $this->getFrom('/security.txt', '198.51.100.20'); expect(BadIpBlocker::all()['198.51.100.20']['expires_at'])->toBeNull(); }); it('can disable auto-blocking', function () { config(['bad-ip-blocker.auto_block.enabled' => false]); $this->getFrom('/security.txt', '198.51.100.20')->assertNotFound(); expect(BadIpBlocker::isBlocked('198.51.100.20'))->toBeFalse(); });