diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index c3a5c3bc4ab..ea5ce481adf 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -41,7 +41,10 @@ def idv_session end def idv_attempter_throttled? - Throttler::IsThrottled.call(effective_user.id, :idv_resolution) + Throttle.for( + user: effective_user, + throttle_type: :idv_resolution, + ).throttled? end def redirect_unless_effective_user diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 5f7fa437124..f7c7307c482 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -50,7 +50,10 @@ def document_capture_session end def throttled? - Throttler::IsThrottled.call(document_capture_session.user_id, :idv_acuant) + Throttle.for( + user: document_capture_session.user, + throttle_type: :idv_acuant, + ).throttled? end end end diff --git a/app/controllers/idv/gpo_controller.rb b/app/controllers/idv/gpo_controller.rb index c7c97d21641..e07ba7c2d2e 100644 --- a/app/controllers/idv/gpo_controller.rb +++ b/app/controllers/idv/gpo_controller.rb @@ -137,15 +137,18 @@ def form_response(result, success) end def idv_throttle_params - [idv_session.current_user.id, :idv_resolution] + { + user: idv_session.current_user, + throttle_type: :idv_resolution, + } end def idv_attempter_increment - Throttler::Increment.call(*idv_throttle_params) + Throttle.for(**idv_throttle_params).increment end def idv_attempter_throttled? - Throttler::IsThrottled.call(*idv_throttle_params) + Throttle.for(**idv_throttle_params).throttled? end def throttle_failure diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 54b3fd93de1..9de7e8ca805 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -1,6 +1,7 @@ module Idv class SessionErrorsController < ApplicationController include IdvSession + include EffectiveUser before_action :confirm_two_factor_authenticated_or_user_id_in_session before_action :confirm_idv_session_step_needed @@ -12,7 +13,10 @@ def warning private def remaining_step_attempts - Throttler::RemainingCount.call(user_id, :idv_resolution) + Throttle.for( + user: effective_user, + throttle_type: :idv_resolution, + ).remaining_count end def confirm_two_factor_authenticated_or_user_id_in_session @@ -25,12 +29,5 @@ def confirm_idv_session_step_needed return unless user_fully_authenticated? redirect_to idv_phone_url if idv_session.profile_confirmation == true end - - def user_id - doc_capture_user_id = session[:doc_capture_user_id] - return doc_capture_user_id if doc_capture_user_id.present? - - current_user.id - end end end diff --git a/app/controllers/users/verify_account_controller.rb b/app/controllers/users/verify_account_controller.rb index 4284739f92c..d7161395114 100644 --- a/app/controllers/users/verify_account_controller.rb +++ b/app/controllers/users/verify_account_controller.rb @@ -12,7 +12,7 @@ def index @verify_account_form = VerifyAccountForm.new(user: current_user) @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code? - if Throttler::IsThrottled.call(current_user.id, :verify_gpo_key) + if throttle.throttled? render_throttled else render :index @@ -22,12 +22,7 @@ def index def create @verify_account_form = build_verify_account_form - throttled = Throttler::IsThrottledElseIncrement.call( - current_user.id, - :verify_gpo_key, - ) - - if throttled + if throttle.throttled_else_increment? render_throttled else result = @verify_account_form.submit @@ -52,6 +47,13 @@ def create private + def throttle + @throttle ||= Throttle.for( + user: current_user, + throttle_type: :verify_gpo_key, + ) + end + def render_throttled analytics.track_event( Analytics::THROTTLER_RATE_LIMIT_TRIGGERED, diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 90a60bce344..1a288824e93 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -13,7 +13,7 @@ def new personal_key: '', ) - if Throttler::IsThrottled.call(current_user.id, :verify_personal_key) + if throttle.throttled? render_throttled else render :new @@ -21,12 +21,7 @@ def new end def create - throttled = Throttler::IsThrottledElseIncrement.call( - current_user.id, - :verify_personal_key, - ) - - if throttled + if throttle.throttled_else_increment? render_throttled else result = personal_key_form.submit @@ -42,6 +37,13 @@ def create private + def throttle + @throttle ||= Throttle.for( + user: current_user, + throttle_type: :verify_personal_key, + ) + end + def render_throttled analytics.track_event( Analytics::THROTTLER_RATE_LIMIT_TRIGGERED, diff --git a/app/forms/idv/api_document_verification_form.rb b/app/forms/idv/api_document_verification_form.rb index 6094718cfd6..c60d89ebce3 100644 --- a/app/forms/idv/api_document_verification_form.rb +++ b/app/forms/idv/api_document_verification_form.rb @@ -39,7 +39,7 @@ def submit def remaining_attempts return unless document_capture_session - Throttler::RemainingCount.call(document_capture_session.user_id, :idv_acuant) + throttle.remaining_count end def liveness_checking_enabled? @@ -95,9 +95,13 @@ def throttle_if_rate_limited def throttled_else_increment return unless document_capture_session - @throttled = Throttler::IsThrottledElseIncrement.call( - document_capture_session.user_id, - :idv_acuant, + @throttled = throttle.throttled_else_increment? + end + + def throttle + @throttle ||= Throttle.for( + user: document_capture_session.user, + throttle_type: :idv_acuant, ) end diff --git a/app/forms/idv/api_document_verification_status_form.rb b/app/forms/idv/api_document_verification_status_form.rb index f6735c4d103..251c6359f1a 100644 --- a/app/forms/idv/api_document_verification_status_form.rb +++ b/app/forms/idv/api_document_verification_status_form.rb @@ -23,7 +23,10 @@ def submit def remaining_attempts return unless @document_capture_session - Throttler::RemainingCount.call(@document_capture_session.user_id, :idv_acuant) + Throttle.for( + user: @document_capture_session.user, + throttle_type: :idv_acuant, + ).remaining_count end def timeout_error diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index 2f64ffa1410..60c78ec5734 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -45,10 +45,7 @@ def submit def throttled_else_increment return unless document_capture_session - @throttled = Throttler::IsThrottledElseIncrement.call( - document_capture_session.user_id, - :idv_acuant, - ) + @throttled = throttle.throttled_else_increment? end def validate_form @@ -104,7 +101,7 @@ def extra_attributes def remaining_attempts return nil unless document_capture_session - Throttler::RemainingCount.call(document_capture_session.user_id, :idv_acuant) + throttle.remaining_count end def determine_response(form_response:, client_response:, doc_pii_response:) @@ -247,5 +244,12 @@ def user_id def user_uuid document_capture_session&.user&.uuid end + + def throttle + @throttle ||= Throttle.for( + user: document_capture_session.user, + throttle_type: :idv_acuant, + ) + end end end diff --git a/app/forms/register_user_email_form.rb b/app/forms/register_user_email_form.rb index 89b2054c965..79b4c58f5b2 100644 --- a/app/forms/register_user_email_form.rb +++ b/app/forms/register_user_email_form.rb @@ -133,7 +133,8 @@ def process_errors(request_id) end def send_sign_up_unconfirmed_email(request_id) - @throttled = Throttler::IsThrottledElseIncrement.call(existing_user.id, :reg_unconfirmed_email) + throttler = Throttle.for(user: existing_user, throttle_type: :reg_unconfirmed_email) + @throttled = throttler.throttled_else_increment? if @throttled @analytics.track_event( @@ -146,7 +147,8 @@ def send_sign_up_unconfirmed_email(request_id) end def send_sign_up_confirmed_email - @throttled = Throttler::IsThrottledElseIncrement.call(existing_user.id, :reg_confirmed_email) + throttler = Throttle.for(user: existing_user, throttle_type: :reg_confirmed_email) + @throttled = throttler.throttled_else_increment? if @throttled @analytics.track_event( diff --git a/app/jobs/document_proofing_job.rb b/app/jobs/document_proofing_job.rb index 6880123b396..5746537a25a 100644 --- a/app/jobs/document_proofing_job.rb +++ b/app/jobs/document_proofing_job.rb @@ -67,10 +67,7 @@ def perform( pii: proofer_result.pii_from_doc, ) - remaining_attempts = Throttler::RemainingCount.call( - user.id, - :idv_acuant, - ) + remaining_attempts = Throttle.for(user: user, throttle_type: :idv_acuant).remaining_count analytics.track_event( Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, diff --git a/app/models/throttle.rb b/app/models/throttle.rb index 6830b93ee63..8913989e7a9 100644 --- a/app/models/throttle.rb +++ b/app/models/throttle.rb @@ -1,6 +1,7 @@ class Throttle < ApplicationRecord belongs_to :user - validates :user_id, presence: true + validates :user_id, presence: true, unless: :target? + validates :target, presence: true, unless: :user_id? enum throttle_type: { idv_acuant: 1, @@ -48,10 +49,49 @@ class Throttle < ApplicationRecord }, }.freeze + # Either target or user must be supplied + # @param [Symbol] throttle_type + # @param [String] target + # @param [User] user + # @return [Throttle] + def self.for(throttle_type:, user: nil, target: nil) + throttle = if user + find_or_create_by(user: user, throttle_type: throttle_type) + elsif target + find_or_create_by(target: target, throttle_type: throttle_type) + else + raise 'Throttle must have a user or a target, but neither were provided' + end + + throttle.reset_if_expired_and_maxed + throttle + end + + # @return [Integer] + def increment + return attempts if maxed? + update(attempts: attempts + 1, attempted_at: Time.zone.now) + attempts + end + def throttled? !expired? && maxed? end + def throttled_else_increment? + if throttled? + true + else + update(attempts: attempts + 1, attempted_at: Time.zone.now) + false + end + end + + def reset + update(attempts: 0) + self + end + def remaining_count return 0 if throttled? max_attempts, _attempt_window_in_minutes = Throttle.config_values(throttle_type) @@ -73,4 +113,10 @@ def self.config_values(throttle_type) config = THROTTLE_CONFIG.with_indifferent_access[throttle_type] [config[:max_attempts], config[:attempt_window]] end + + # @api private + def reset_if_expired_and_maxed + return unless expired? && maxed? + update(attempts: 0, throttled_count: throttled_count.to_i + 1) + end end diff --git a/app/services/idv/actions/verify_document_status_action.rb b/app/services/idv/actions/verify_document_status_action.rb index edbccd27b65..e537d524fcf 100644 --- a/app/services/idv/actions/verify_document_status_action.rb +++ b/app/services/idv/actions/verify_document_status_action.rb @@ -97,7 +97,10 @@ def async_state def remaining_attempts return nil unless verify_document_capture_session - Throttler::RemainingCount.call(verify_document_capture_session.user_id, :idv_acuant) + Throttle.for( + user: verify_document_capture_session.user, + throttle_type: :idv_acuant, + ).remaining_count end def timed_out diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index 95f3f988263..bc3423ae045 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -8,15 +8,18 @@ def initialize(flow) private def idv_throttle_params - [current_user.id, :idv_resolution] + { + user: current_user, + throttle_type: :idv_resolution, + } end def attempter_increment - Throttler::Increment.call(*idv_throttle_params) + Throttle.for(**idv_throttle_params).increment end def attempter_throttled? - Throttler::IsThrottled.call(*idv_throttle_params) + Throttle.for(**idv_throttle_params).throttled? end def idv_failure(result) @@ -87,7 +90,10 @@ def throttled_url end def throttled_else_increment - Throttler::IsThrottledElseIncrement.call(user_id, :idv_acuant) + Throttle.for( + target: user_id, + throttle_type: :idv_acuant, + ).throttled_else_increment? end def user_id diff --git a/app/services/idv/steps/send_link_step.rb b/app/services/idv/steps/send_link_step.rb index 6dd935bd677..d5283d8164b 100644 --- a/app/services/idv/steps/send_link_step.rb +++ b/app/services/idv/steps/send_link_step.rb @@ -53,7 +53,10 @@ def link(session_uuid) end def throttled_else_increment - Throttler::IsThrottledElseIncrement.call(user_id, :idv_send_link) + Throttle.for( + user: current_user, + throttle_type: :idv_send_link, + ).throttled_else_increment? end end end diff --git a/app/services/request_password_reset.rb b/app/services/request_password_reset.rb index 771e726b072..3f86f5a2f85 100644 --- a/app/services/request_password_reset.rb +++ b/app/services/request_password_reset.rb @@ -16,8 +16,7 @@ def perform private def send_reset_password_instructions - throttled = Throttler::IsThrottledElseIncrement.call(user.id, :reset_password_email) - if throttled + if Throttle.for(user: user, throttle_type: :reset_password_email).throttled_else_increment? analytics.track_event( Analytics::THROTTLER_RATE_LIMIT_TRIGGERED, throttle_type: :reset_password_email, diff --git a/app/services/throttler/find_or_create.rb b/app/services/throttler/find_or_create.rb deleted file mode 100644 index 18abe2333a2..00000000000 --- a/app/services/throttler/find_or_create.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Throttler - class FindOrCreate - def self.call(user_id, throttle_type) - throttle = Throttle.find_or_create_by(user_id: user_id, throttle_type: throttle_type) - reset_if_expired_and_maxed(throttle) - end - - def self.reset_if_expired_and_maxed(throttle) - return throttle unless throttle.expired? && throttle.maxed? - throttle.update(attempts: 0, throttled_count: throttle.throttled_count.to_i + 1) - throttle - end - private_class_method :reset_if_expired_and_maxed - end -end diff --git a/app/services/throttler/increment.rb b/app/services/throttler/increment.rb deleted file mode 100644 index 0f50a36e551..00000000000 --- a/app/services/throttler/increment.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Throttler - class Increment - def self.call(user_id, throttle_type) - throttle = Throttler::FindOrCreate.call(user_id, throttle_type) - return throttle if throttle.maxed? - throttle.update(attempts: throttle.attempts + 1, attempted_at: Time.zone.now) - throttle - end - end -end diff --git a/app/services/throttler/is_throttled.rb b/app/services/throttler/is_throttled.rb deleted file mode 100644 index c128e08e828..00000000000 --- a/app/services/throttler/is_throttled.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Throttler - class IsThrottled - def self.call(user_id, throttle_type) - throttle = FindOrCreate.call(user_id, throttle_type) - throttle.throttled? ? throttle : false - end - end -end diff --git a/app/services/throttler/is_throttled_else_increment.rb b/app/services/throttler/is_throttled_else_increment.rb deleted file mode 100644 index 3250ecc0401..00000000000 --- a/app/services/throttler/is_throttled_else_increment.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Throttler - class IsThrottledElseIncrement - def self.call(user_id, throttle_type) - throttle = FindOrCreate.call(user_id, throttle_type) - return throttle if throttle.throttled? - throttle.update(attempts: throttle.attempts + 1, attempted_at: Time.zone.now) - false - end - end -end diff --git a/app/services/throttler/remaining_count.rb b/app/services/throttler/remaining_count.rb deleted file mode 100644 index f87a2d7c3ce..00000000000 --- a/app/services/throttler/remaining_count.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Throttler - class RemainingCount - def self.call(user_id, throttle_type) - throttle = FindOrCreate.call(user_id, throttle_type) - throttle.remaining_count - end - end -end diff --git a/app/services/throttler/reset.rb b/app/services/throttler/reset.rb deleted file mode 100644 index 2221b48a422..00000000000 --- a/app/services/throttler/reset.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Throttler - class Reset - def self.call(user_id, throttle_type) - throttle = Throttle.find_or_create_by(user_id: user_id, throttle_type: throttle_type) - throttle.update(attempts: 0) - throttle - end - end -end diff --git a/db/migrate/20210817162607_add_target_to_throttles.rb b/db/migrate/20210817162607_add_target_to_throttles.rb new file mode 100644 index 00000000000..4dc7e0da0b8 --- /dev/null +++ b/db/migrate/20210817162607_add_target_to_throttles.rb @@ -0,0 +1,7 @@ +class AddTargetToThrottles < ActiveRecord::Migration[6.1] + def change + change_column_null :throttles, :user_id, true + + add_column :throttles, :target, :string, null: true + end +end diff --git a/db/migrate/20210817200558_add_target_index_to_throttles.rb b/db/migrate/20210817200558_add_target_index_to_throttles.rb new file mode 100644 index 00000000000..8898e43d7e3 --- /dev/null +++ b/db/migrate/20210817200558_add_target_index_to_throttles.rb @@ -0,0 +1,7 @@ +class AddTargetIndexToThrottles < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :throttles, [:target, :throttle_type], algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index b7c520684fc..69971489046 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_08_11_191107) do +ActiveRecord::Schema.define(version: 2021_08_17_200558) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -613,11 +613,13 @@ end create_table "throttles", force: :cascade do |t| - t.integer "user_id", null: false + t.integer "user_id" t.integer "throttle_type", null: false t.datetime "attempted_at" t.integer "attempts", default: 0 t.integer "throttled_count" + t.string "target" + t.index ["target", "throttle_type"], name: "index_throttles_on_target_and_throttle_type" t.index ["user_id", "throttle_type"], name: "index_throttles_on_user_id_and_throttle_type" end diff --git a/spec/controllers/idv/capture_doc_status_controller_spec.rb b/spec/controllers/idv/capture_doc_status_controller_spec.rb index 49befabc339..3e137cff221 100644 --- a/spec/controllers/idv/capture_doc_status_controller_spec.rb +++ b/spec/controllers/idv/capture_doc_status_controller_spec.rb @@ -8,7 +8,7 @@ end describe '#show' do - let(:document_capture_session) { DocumentCaptureSession.create! } + let(:document_capture_session) { DocumentCaptureSession.create!(user: user) } let(:flow_session) { { document_capture_session_uuid: document_capture_session.uuid } } before do @@ -61,7 +61,7 @@ context 'when the user is throttled' do before do - allow(Throttler::IsThrottled).to receive(:call).with(user.id, :idv_acuant).and_return(true) + create(:throttle, :with_throttled, user: user, throttle_type: :idv_acuant) end it 'returns throttled with redirect' do diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb index 01a4eb01935..9603bd082ac 100644 --- a/spec/controllers/idv/doc_auth_controller_spec.rb +++ b/spec/controllers/idv/doc_auth_controller_spec.rb @@ -411,10 +411,10 @@ def mock_next_step(step) allow_any_instance_of(Idv::Flows::DocAuthFlow).to receive(:next_step).and_return(step) end - let(:verify_document_action_session_uuid) { SecureRandom.uuid } + let(:user) { create(:user, :signed_up) } + let(:verify_document_action_session_uuid) { DocumentCaptureSession.create!(user: user).uuid } def mock_document_capture_step - user = create(:user, :signed_up) stub_sign_in(user) DocumentCaptureSession.create_by_user_id(user.id, @analytics, result_id: 1, uuid: 'foo') allow_any_instance_of(Flow::BaseFlow).to \ diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index e5f6d96cd7a..951b11f1090 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -5,7 +5,7 @@ subject(:action) { post :create, params: params } let(:user) { create(:user) } - let!(:document_capture_session) { user.document_capture_sessions.create! } + let!(:document_capture_session) { user.document_capture_sessions.create!(user: user) } let(:params) do { front: DocAuthImageFixtures.document_front_image_multipart, @@ -141,7 +141,13 @@ context 'throttling' do it 'returns remaining_attempts with error' do params.delete(:front) - allow(Throttler::RemainingCount).to receive(:call).and_return(3) + create( + :throttle, + :with_throttled, + attempts: IdentityConfig.store.acuant_max_attempts - 4, + user: user, + throttle_type: :idv_acuant, + ) action @@ -156,8 +162,7 @@ end it 'returns an error when throttled' do - allow(Throttler::IsThrottledElseIncrement).to receive(:call).once.and_return(true) - allow(Throttler::RemainingCount).to receive(:call).and_return(0) + create(:throttle, :with_throttled, user: user, throttle_type: :idv_acuant) action @@ -171,8 +176,13 @@ end it 'tracks analytics' do - allow(Throttler::IsThrottledElseIncrement).to receive(:call).once.and_return(true) - allow(Throttler::RemainingCount).to receive(:call).and_return(0) + create( + :throttle, + :with_throttled, + attempts: IdentityConfig.store.acuant_max_attempts, + user: user, + throttle_type: :idv_acuant, + ) stub_analytics diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb index 72953985b0f..7d0adbc605d 100644 --- a/spec/controllers/idv/session_errors_controller_spec.rb +++ b/spec/controllers/idv/session_errors_controller_spec.rb @@ -3,7 +3,7 @@ shared_examples_for 'an idv session errors controller action' do context 'the user is authenticated and has not confirmed their profile' do it 'renders the error' do - stub_sign_in + stub_sign_in(create(:user)) get action diff --git a/spec/controllers/users/verify_account_controller_spec.rb b/spec/controllers/users/verify_account_controller_spec.rb index bbcf565e008..9bc118e47df 100644 --- a/spec/controllers/users/verify_account_controller_spec.rb +++ b/spec/controllers/users/verify_account_controller_spec.rb @@ -36,7 +36,7 @@ end it 'shows throttled page is user is throttled' do - allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + create(:throttle, :with_throttled, user: user, throttle_type: :verify_gpo_key) action @@ -56,7 +56,7 @@ context 'with throttle reached' do before do - allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + create(:throttle, :with_throttled, user: user, throttle_type: :verify_gpo_key) end it 'renders throttled page' do diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 7d59db2a0d8..17c641e38cc 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -37,7 +37,8 @@ end it 'shows throttled page after being throttled' do - allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + create(:throttle, :with_throttled, user: user, throttle_type: :verify_personal_key) + get :new expect(response).to render_template(:throttled) @@ -48,7 +49,7 @@ let(:profiles) { [create(:profile, deactivation_reason: :password_reset)] } before do - allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + create(:throttle, :with_throttled, user: user, throttle_type: :verify_personal_key) end it 'renders throttled page' do diff --git a/spec/factories/throttles.rb b/spec/factories/throttles.rb new file mode 100644 index 00000000000..c8cbb58f3ec --- /dev/null +++ b/spec/factories/throttles.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :throttle do + trait :with_throttled do + attempts { 9999 } + attempted_at { Time.zone.now } + end + end +end diff --git a/spec/forms/idv/api_document_verification_form_spec.rb b/spec/forms/idv/api_document_verification_form_spec.rb index 7dc064795a5..792b4694288 100644 --- a/spec/forms/idv/api_document_verification_form_spec.rb +++ b/spec/forms/idv/api_document_verification_form_spec.rb @@ -25,7 +25,7 @@ let(:back_image_iv) { 'back-iv' } let(:selfie_image_url) { 'http://example.com/selfie' } let(:selfie_image_iv) { 'selfie-iv' } - let!(:document_capture_session) { DocumentCaptureSession.create! } + let!(:document_capture_session) { DocumentCaptureSession.create!(user: create(:user)) } let(:document_capture_session_uuid) { document_capture_session.uuid } let(:analytics) { FakeAnalytics.new } let(:liveness_checking_enabled?) { true } @@ -113,7 +113,12 @@ context 'when throttled from submission' do before do - allow(Throttler::IsThrottledElseIncrement).to receive(:call).once.and_return(true) + create( + :throttle, + :with_throttled, + user: document_capture_session.user, + throttle_type: :idv_acuant, + ) form.submit end diff --git a/spec/forms/idv/api_document_verification_status_form_spec.rb b/spec/forms/idv/api_document_verification_status_form_spec.rb index b326e86ebfc..7aa305ab651 100644 --- a/spec/forms/idv/api_document_verification_status_form_spec.rb +++ b/spec/forms/idv/api_document_verification_status_form_spec.rb @@ -9,7 +9,7 @@ end let(:async_state) { DocumentCaptureSessionAsyncResult.new } - let(:document_capture_session) { DocumentCaptureSession.create! } + let(:document_capture_session) { DocumentCaptureSession.create!(user: build(:user)) } describe '#valid?' do context 'with timeout async state' do diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index ecb8666ae11..f17dd4954c4 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -26,7 +26,7 @@ { width: 20, height: 20, mimeType: 'image/png', source: 'upload' }.to_json end let(:selfie_image) { DocAuthImageFixtures.selfie_image_multipart } - let!(:document_capture_session) { DocumentCaptureSession.create! } + let!(:document_capture_session) { DocumentCaptureSession.create!(user: create(:user)) } let(:document_capture_session_uuid) { document_capture_session.uuid } let(:liveness_checking_enabled?) { true } let(:fake_analytics) { FakeAnalytics.new } @@ -81,7 +81,12 @@ context 'when throttled from submission' do before do - allow(Throttler::IsThrottledElseIncrement).to receive(:call).once.and_return(true) + create( + :throttle, + :with_throttled, + user: document_capture_session.user, + throttle_type: :idv_acuant, + ) form.submit end @@ -104,9 +109,9 @@ exception: nil, doc_auth_result: 'Passed', billed: true, - remaining_attempts: IdentityConfig.store.acuant_max_attempts, + remaining_attempts: IdentityConfig.store.acuant_max_attempts - 1, state: 'MT', - user_id: nil, + user_id: document_capture_session.user.uuid, client_image_metrics: { front: JSON.parse(front_image_metadata, symbolize_names: true), back: JSON.parse(back_image_metadata, symbolize_names: true), @@ -143,8 +148,8 @@ exception: nil, doc_auth_result: 'Passed', billed: true, - remaining_attempts: IdentityConfig.store.acuant_max_attempts, - user_id: nil, + remaining_attempts: IdentityConfig.store.acuant_max_attempts - 1, + user_id: document_capture_session.user.uuid, client_image_metrics: { front: JSON.parse(front_image_metadata, symbolize_names: true), }, diff --git a/spec/models/throttle_spec.rb b/spec/models/throttle_spec.rb new file mode 100644 index 00000000000..c105a78332f --- /dev/null +++ b/spec/models/throttle_spec.rb @@ -0,0 +1,158 @@ +require 'rails_helper' + +RSpec.describe Throttle do + let(:throttle_type) { :idv_acuant } + + describe '.for' do + context 'when target is a user' do + let(:user) { create(:user) } + subject(:for_target) { Throttle.for(user: user, throttle_type: throttle_type) } + + context 'throttle does not exist yet' do + it 'creates a new throttle row' do + expect { for_target }.to change { Throttle.count }.by(1) + end + end + + context 'throttle already exists' do + let!(:existing) { create(:throttle, user: user, throttle_type: throttle_type) } + + it 'does not create a new throttle row' do + expect { for_target }.to_not change { Throttle.count } + + expect(for_target).to eq(existing) + end + end + end + + context 'when target is a string' do + let(:target) { Digest::SHA256.hexdigest(SecureRandom.hex) } + subject(:for_target) { Throttle.for(target: target, throttle_type: throttle_type) } + + context 'throttle does not exist yet' do + it 'creates a new throttle row' do + expect { for_target }.to change { Throttle.count }.by(1) + end + end + + context 'throttle already exists' do + let!(:existing) { create(:throttle, target: target, throttle_type: throttle_type) } + + it 'does not create a new throttle row' do + expect { for_target }.to_not change { Throttle.count } + + expect(for_target).to eq(existing) + end + end + end + + context 'when target and user are missing' do + it 'throws an error' do + expect { Throttle.for(throttle_type: throttle_type) }. + to raise_error(/Throttle must have a user or a target/) + end + end + end + + describe '#increment' do + subject(:throttle) { Throttle.for(target: 'aaa', throttle_type: :idv_acuant) } + + it 'increments attempts' do + expect { throttle.increment }.to change { throttle.reload.attempts }.by(1) + end + end + + describe '#throttled?' do + let(:user) { create(:user) } + let(:throttle_type) { :idv_acuant } + let(:throttle) { Throttle.all.first } + let(:max_attempts) { IdentityConfig.store.acuant_max_attempts } + let(:attempt_window_in_minutes) { IdentityConfig.store.acuant_attempt_window_in_minutes } + + subject(:throttle) { Throttle.for(user: user, throttle_type: throttle_type) } + + it 'returns true if throttled' do + create( + :throttle, + user: user, + throttle_type: throttle_type, + attempts: max_attempts, + attempted_at: Time.zone.now, + ) + + expect(throttle.throttled?).to eq(true) + end + + it 'returns false if the attempts < max_attempts' do + create( + :throttle, + user: user, + throttle_type: throttle_type, + attempts: max_attempts - 1, + attempted_at: Time.zone.now, + ) + + expect(throttle.throttled?).to eq(false) + end + + it 'returns false if the attempts <= max_attempts but the window is expired' do + create( + :throttle, + user: user, + throttle_type: throttle_type, + attempts: max_attempts, + attempted_at: Time.zone.now - attempt_window_in_minutes.minutes, + ) + + expect(throttle.throttled?).to eq(false) + end + end + + describe '#throttled_else_increment?' do + subject(:throttle) { Throttle.for(target: 'aaaa', throttle_type: :idv_acuant) } + + context 'throttle has hit limit' do + before do + throttle.update( + attempts: IdentityConfig.store.acuant_max_attempts + 1, + attempted_at: Time.zone.now, + ) + end + + it 'is true' do + expect(throttle.throttled_else_increment?).to eq(true) + end + end + + context 'throttle has not hit limit' do + it 'is false' do + expect(throttle.throttled_else_increment?).to eq(false) + end + + it 'increments the throttle' do + expect { throttle.throttled_else_increment? }.to change { throttle.reload.attempts }.by(1) + end + end + end + + describe '#reset' do + let(:user) { create(:user) } + let(:throttle_type) { :idv_acuant } + let(:max_attempts) { 3 } + let(:subject) { described_class } + + subject(:throttle) { Throttle.for(user: user, throttle_type: throttle_type) } + + it 'resets attempt count to 0' do + create( + :throttle, + user: user, + throttle_type: throttle_type, + attempts: max_attempts, + attempted_at: Time.zone.now, + ) + + expect { throttle.reset }.to change { throttle.reload.attempts }.to(0) + end + end +end diff --git a/spec/services/idv/actions/verify_document_status_action_spec.rb b/spec/services/idv/actions/verify_document_status_action_spec.rb index 5d722ea2dda..14edac581e8 100644 --- a/spec/services/idv/actions/verify_document_status_action_spec.rb +++ b/spec/services/idv/actions/verify_document_status_action_spec.rb @@ -60,6 +60,7 @@ verify_document_capture_session = DocumentCaptureSession.new( uuid: 'uuid', result_id: 'result_id', + user: create(:user), ) expect(subject).to receive(:verify_document_capture_session). diff --git a/spec/services/throttler/increment_spec.rb b/spec/services/throttler/increment_spec.rb deleted file mode 100644 index 77dc1c18d0d..00000000000 --- a/spec/services/throttler/increment_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -describe Throttler::Increment do - let(:user_id) { 1 } - let(:throttle_type) { :idv_acuant } - let(:subject) { described_class } - let(:throttle) { Throttle.all.first } - - it 'creates and increments a throttle if one does not exist' do - subject.call(user_id, throttle_type) - - expect(throttle.attempts).to eq(1) - expect(throttle.attempted_at).to be_present - end - - it 'it increments a throttle if one exists' do - Throttle.create( - user_id: user_id, - throttle_type: throttle_type, - attempts: 1, - attempted_at: Time.zone.now, - ) - subject.call(user_id, throttle_type) - - expect(throttle.attempts).to eq(2) - expect(throttle.attempted_at).to be_present - end -end diff --git a/spec/services/throttler/is_throttled_spec.rb b/spec/services/throttler/is_throttled_spec.rb deleted file mode 100644 index 070d38e3a85..00000000000 --- a/spec/services/throttler/is_throttled_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe Throttler::IsThrottled do - let(:user_id) { 1 } - let(:throttle_type) { :idv_acuant } - let(:subject) { described_class } - let(:throttle) { Throttle.all.first } - let(:max_attempts) { IdentityConfig.store.acuant_max_attempts } - let(:attempt_window_in_minutes) { IdentityConfig.store.acuant_attempt_window_in_minutes } - - it 'returns throttle if throttled' do - Throttle.create( - user_id: user_id, - throttle_type: throttle_type, - attempts: max_attempts, - attempted_at: Time.zone.now, - ) - result = subject.call(user_id, throttle_type) - - expect(result.class).to eq(Throttle) - end - - it 'returns nil if the attempts < max_attempts' do - Throttle.create( - user_id: user_id, - throttle_type: throttle_type, - attempts: max_attempts - 1, - attempted_at: Time.zone.now, - ) - result = subject.call(user_id, throttle_type) - - expect(result).to be(false) - end - - it 'returns nil if the attempts <= max_attempts but the window is expired' do - Throttle.create( - user_id: user_id, - throttle_type: throttle_type, - attempts: max_attempts, - attempted_at: Time.zone.now - attempt_window_in_minutes.minutes, - ) - result = subject.call(user_id, throttle_type) - - expect(result).to be(false) - end -end diff --git a/spec/services/throttler/reset_spec.rb b/spec/services/throttler/reset_spec.rb deleted file mode 100644 index 9eda2a7d4b2..00000000000 --- a/spec/services/throttler/reset_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'rails_helper' - -describe Throttler::Reset do - let(:user_id) { 1 } - let(:throttle_type) { :idv_acuant } - let(:max_attempts) { 3 } - let(:subject) { described_class } - let(:throttle) { Throttle.all.first } - - it 'resets attempt count to 0' do - Throttle.create( - user_id: user_id, - throttle_type: throttle_type, - attempts: max_attempts, - attempted_at: Time.zone.now, - ) - subject.call(user_id, throttle_type) - - expect(throttle.attempts).to eq(0) - end -end