From 0d777aaacab6f9d4ca77a28c409525df399f1463 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 16:14:16 +0200 Subject: [PATCH] test: cover MiddlewareRateLimitingTest and teardown --- test/test_middleware_rate_limiting.rb | 101 ++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 test/test_middleware_rate_limiting.rb diff --git a/test/test_middleware_rate_limiting.rb b/test/test_middleware_rate_limiting.rb new file mode 100644 index 0000000..c80821c --- /dev/null +++ b/test/test_middleware_rate_limiting.rb @@ -0,0 +1,101 @@ +require 'minitest/autorun' +require 'prism' +require 'support' + +class MiddlewareRateLimitingTest < Minitest::Test + include TestSupport + + def setup + Prism::RateLimiter.reset! + @downstream_calls = 0 + downstream = lambda do |_env| + @downstream_calls += 1 + [200, {}, ['ok']] + end + @app = Prism::Middleware::RateLimiting.new(downstream) + end + + def teardown + Prism::RateLimiter.reset! + end + + def request(ip: '1.1.1.1', path: '/login') + env = env_for('POST', path) + env['REMOTE_ADDR'] = ip + @app.call(env) + end + + def test_requests_under_the_limit_pass_through + Prism::RateLimiter.throttle('logins/ip', limit: 2, period: 60) { |req| req.env['REMOTE_ADDR'] } + + 2.times do + status, = request + assert_equal 200, status + end + assert_equal 2, @downstream_calls + end + + def test_request_over_the_limit_is_throttled_with_retry_after + Prism::RateLimiter.throttle('logins/ip', limit: 2, period: 60) { |req| req.env['REMOTE_ADDR'] } + + 2.times { request } + status, headers, body = request + + assert_equal 429, status + assert_equal '60', headers['Retry-After'] + assert_includes body.join, 'Too Many Requests' + assert_equal 2, @downstream_calls, 'the throttled request must not reach the app' + end + + def test_throttle_keys_are_independent_per_discriminator_value + Prism::RateLimiter.throttle('logins/ip', limit: 1, period: 60) { |req| req.env['REMOTE_ADDR'] } + + assert_equal 200, request(ip: '1.1.1.1').first + assert_equal 200, request(ip: '2.2.2.2').first + assert_equal 429, request(ip: '1.1.1.1').first + end + + def test_discriminator_returning_nil_skips_the_rule + Prism::RateLimiter.throttle('logins/ip', limit: 1, period: 60) { |_req| nil } + + 5.times { assert_equal 200, request.first } + end + + def test_blocklisted_request_is_rejected_with_403_before_reaching_app + Prism::RateLimiter.blocklist('scanners') { |req| req.path == '/etc/passwd' } + + status, = request(path: '/etc/passwd') + assert_equal 403, status + assert_equal 0, @downstream_calls + end + + def test_safelist_bypasses_an_otherwise_matching_blocklist + Prism::RateLimiter.blocklist('scanners') { |_req| true } + Prism::RateLimiter.safelist('trusted') { |req| req.env['REMOTE_ADDR'] == '9.9.9.9' } + + assert_equal 200, request(ip: '9.9.9.9').first + assert_equal 403, request(ip: '1.1.1.1').first + end + + def test_track_rule_observes_but_never_blocks + tracked = [] + Prism::RateLimiter.track('all-logins') { |req| req.env['REMOTE_ADDR'] } + Prism::RateLimiter.on_notify { |type, name, _req, payload| tracked << [type, name, payload] } + + assert_equal 200, request(ip: '3.3.3.3').first + assert_equal [[:track, 'all-logins', '3.3.3.3']], tracked + end + + def test_fail2ban_style_blocklist_bans_after_repeated_offenses + Prism::RateLimiter.blocklist('fail2ban pentesters') do |req| + Prism::RateLimiter.fail2ban_filter(req.env['REMOTE_ADDR'], maxretry: 2, findtime: 60, bantime: 300) do + req.path.include?('/etc/passwd') + end + end + + assert_equal 200, request(ip: '5.5.5.5', path: '/etc/passwd').first + assert_equal 403, request(ip: '5.5.5.5', path: '/etc/passwd').first + assert_equal 403, request(ip: '5.5.5.5', path: '/harmless').first, 'stays banned without a new offense' + assert_equal 200, request(ip: '6.6.6.6', path: '/etc/passwd').first, 'other IPs are unaffected' + end +end