Skip to content

Commit

Permalink
BotDetectController specs, with re-usable turnstile mocks
Browse files Browse the repository at this point in the history
  • Loading branch information
jrochkind committed Jan 9, 2025
1 parent 5be41d3 commit 27a6d82
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 2 deletions.
16 changes: 14 additions & 2 deletions app/controllers/bot_detect_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,19 @@ class BotDetectController < ApplicationController
# key in rack env that says challenge is required
class_attribute :env_challenge_trigger_key, default: "bot_detect.should_challenge"

# for allowing unsubscribe for testing
class_attribute :_track_notification_subsription, instance_accessor: false

# perhaps in an initializer, and after changing any config, run:
#
# Rails.application.config.to_prepare do
# BotDetectController.rack_attack_init
# end
#
# Safe to call more than once if you change config and want to call again, say in testing.
def self.rack_attack_init
self._rack_attack_uninit # make it safe for calling multiple times

## Turnstile bot detection throttling
#
# for paths matched by `rate_limited_locations`, after over rate_limit count requests in rate_limit_period,
Expand All @@ -78,13 +85,12 @@ def self.rack_attack_init
Rack::Attack.track("bot_detect/rate_exceeded",
limit: self.rate_limit_count,
period: self.rate_limit_period) do |req|

if self.location_matcher.call(req)
self.rate_limit_discriminator.call(req)
end
end

ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, request_id, payload|
self._track_notification_subsription = ActiveSupport::Notifications.subscribe("track.rack_attack") do |_name, _start, _finish, request_id, payload|
rack_request = payload[:request]
rack_env = rack_request.env
match_name = rack_env["rack.attack.matched"] # name of rack-attack rule
Expand All @@ -99,6 +105,12 @@ def self.rack_attack_init
end
end

def self._rack_attack_uninit
Rack::Attack.track("bot_detect/rate_exceeded") {} # overwrite track name with empty proc
ActiveSupport::Notifications.unsubscribe(self._track_notification_subsription) if self._track_notification_subsription
self._track_notification_subsription = nil
end

# Usually in your ApplicationController,
#
# before_action { |controller| BotDetectController.bot_detection_enforce_filter(controller) }
Expand Down
28 changes: 28 additions & 0 deletions spec/controllers/bot_detect_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'rails_helper'

RSpec.describe BotDetectController, type: :controller do
include WebmockTurnstileHelperMethods

describe "#verify_challenge" do
it "handles turnstile success" do
turnstile_response = stub_turnstile_success

post :verify_challenge, params: { cf_turnstile_response: "XXXX.DUMMY.TOKEN.XXXX" }
expect(response.status).to be 200
expect(response.body).to eq turnstile_response.to_json

expect(session[BotDetectController.session_passed_key]).to be_present
expect(Time.new(session[BotDetectController.session_passed_key])).to be_within(60).of(Time.now.utc)
end

it "handles turnstile failure" do
turnstile_response = stub_turnstile_failure

post :verify_challenge, params: { cf_turnstile_response: "XXXX.DUMMY.TOKEN.XXXX" }
expect(response.status).to be 200
expect(response.body).to eq turnstile_response.to_json

expect(session[BotDetectController.session_passed_key]).not_to be_present
end
end
end
38 changes: 38 additions & 0 deletions spec/test_support/helper_ruby/webmock_turnstile_helper_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module WebmockTurnstileHelperMethods
def stub_turnstile_success(turnstile_response: {},
request_body: {"secret"=>BotDetectController.cf_turnstile_secret_key, "response"=>"XXXX.DUMMY.TOKEN.XXXX", "remoteip"=>"0.0.0.0"})

turnstile_response.reverse_merge!(
{"success"=>true, "error-codes"=>[], "challenge_ts"=>Time.now.utc.iso8601(3), "hostname"=>"example.com", "metadata"=>{"result_with_testing_key"=>true}}
)

stub_request(:post, BotDetectController.cf_turnstile_validation_url).
with(
body: request_body.to_json
).to_return(status: 200,
body: turnstile_response.to_json,
headers: { 'Content-Type'=>'application/json; charset=utf-8' }
)

return turnstile_response
end

def stub_turnstile_failure(turnstile_response: {},
request_body: {"secret"=>BotDetectController.cf_turnstile_secret_key, "response"=>"XXXX.DUMMY.TOKEN.XXXX", "remoteip"=>"0.0.0.0"})

turnstile_response.reverse_merge!(
{"success"=>false, "error-codes"=>["invalid-input-response"], "messages"=>[], "metadata"=>{"result_with_testing_key"=>true}}
)

stub_request(:post, BotDetectController.cf_turnstile_validation_url).
with(
body: request_body.to_json
).to_return(status: 200,
body: turnstile_response.to_json,
headers: { 'Content-Type'=>'application/json; charset=utf-8' }
)

return turnstile_response
end

end

0 comments on commit 27a6d82

Please sign in to comment.