diff --git a/app/controllers/analytics_events_controller.rb b/app/controllers/analytics_events_controller.rb deleted file mode 100644 index 55a2070d69d..00000000000 --- a/app/controllers/analytics_events_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -# Serve a static file from Rails so that the CORS middleware can add the correct headers -class AnalyticsEventsController < ApplicationController - prepend_before_action :skip_session_load - prepend_before_action :skip_session_expiration - skip_before_action :disable_caching - - JSON_FILE = Rails.public_path.join('api', '_analytics-events.json').freeze - - def index - if File.exist?(JSON_FILE) - expires_in 15.minutes, public: true - - send_file JSON_FILE, type: 'application/json', disposition: 'inline' - else - render_not_found - end - end -end diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index c8f0ac1f157..cd3a11fdf5d 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -175,11 +175,6 @@ def async_state_done(current_async_state) }, ) - form_response.extra[:ssn_is_unique] = DuplicateSsnFinder.new( - ssn: idv_session.ssn, - user: current_user, - ).ssn_is_unique? - summarize_result_and_rate_limit(form_response) delete_async diff --git a/app/controllers/idv/by_mail/resend_letter_controller.rb b/app/controllers/idv/by_mail/resend_letter_controller.rb new file mode 100644 index 00000000000..8b1bd002ef7 --- /dev/null +++ b/app/controllers/idv/by_mail/resend_letter_controller.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +module Idv + module ByMail + class ResendLetterController < ApplicationController + include Idv::AvailabilityConcern + include IdvSessionConcern + include Idv::StepIndicatorConcern + + before_action :confirm_two_factor_authenticated + before_action :confirm_verification_needed + before_action :confirm_mail_not_rate_limited + before_action :confirm_profile_not_too_old + + def new + analytics.idv_resend_letter_visited + end + + def create + update_tracking + + if pii_locked? + redirect_to capture_password_url + elsif resend_requested? + resend_letter + flash[:success] = t('idv.messages.gpo.another_letter_on_the_way') + redirect_to idv_letter_enqueued_url + end + end + + def gpo_mail_service + @gpo_mail_service ||= Idv::GpoMail.new(current_user) + end + + private + + def confirm_verification_needed + return if current_user.gpo_verification_pending_profile? + redirect_to account_url + end + + def confirm_profile_not_too_old + redirect_to idv_verify_by_mail_enter_code_path if gpo_mail_service.profile_too_old? + end + + def confirm_mail_not_rate_limited + redirect_to idv_verify_by_mail_enter_code_path if gpo_mail_service.rate_limited? + end + + def update_tracking + analytics.idv_gpo_address_letter_requested( + resend: true, + first_letter_requested_at: first_letter_requested_at, + hours_since_first_letter: + hours_since_first_letter(first_letter_requested_at), + phone_step_attempts: RateLimiter.new( + user: current_user, + rate_limit_type: :proof_address, + ).attempts, + ) + create_user_event(:gpo_mail_sent, current_user) + end + + def resend_requested? + current_user.gpo_verification_pending_profile? + end + + def first_letter_requested_at + current_user.gpo_verification_pending_profile&.gpo_verification_pending_at + end + + def hours_since_first_letter(first_letter_requested_at) + first_letter_requested_at ? + (Time.zone.now - first_letter_requested_at).to_i.seconds.in_hours.to_i : 0 + end + + def resend_letter + analytics.idv_gpo_address_letter_enqueued( + enqueued_at: Time.zone.now, + resend: true, + first_letter_requested_at: first_letter_requested_at, + hours_since_first_letter: + hours_since_first_letter(first_letter_requested_at), + phone_step_attempts: RateLimiter.new( + user: current_user, + rate_limit_type: :proof_address, + ).attempts, + ) + confirmation_maker = confirmation_maker_perform + send_reminder + return unless FeatureManagement.reveal_gpo_code? + session[:last_gpo_confirmation_code] = confirmation_maker.otp + end + + def confirmation_maker_perform + confirmation_maker = GpoConfirmationMaker.new( + pii: pii, + service_provider: current_sp, + profile: current_user.pending_profile, + ) + confirmation_maker.perform + confirmation_maker + end + + def pii + Pii::Cacher.new(current_user, user_session). + fetch(current_user.gpo_verification_pending_profile.id) + end + + def send_reminder + current_user.send_email_to_all_addresses(:verify_by_mail_letter_requested) + end + + def pii_locked? + !Pii::Cacher.new(current_user, user_session).exists_in_session? + end + + def step_indicator_steps + if in_person_proofing? + Idv::Flows::InPersonFlow::STEP_INDICATOR_STEPS_GPO + else + StepIndicatorConcern::STEP_INDICATOR_STEPS_GPO + end + end + end + end +end diff --git a/app/jobs/reports/combined_invoice_supplement_report_v2.rb b/app/jobs/reports/combined_invoice_supplement_report_v2.rb index 0259180d9f2..d7d93969a7a 100644 --- a/app/jobs/reports/combined_invoice_supplement_report_v2.rb +++ b/app/jobs/reports/combined_invoice_supplement_report_v2.rb @@ -96,7 +96,7 @@ def combine_by_iaa_month( 'iaa_ial1_unique_users', 'iaa_ial2_unique_users', - 'iaa_ial1_plus_2_unique_users', + 'iaa_unique_users', 'partner_ial2_unique_user_events_year1', 'partner_ial2_unique_user_events_year2', 'partner_ial2_unique_user_events_year3', @@ -133,7 +133,7 @@ def combine_by_iaa_month( 'issuer_ial1_unique_users', 'issuer_ial2_unique_users', - 'issuer_ial1_plus_2_unique_users', + 'issuer_unique_users', ] by_issuer_iaa_issuer_year_months.each do |iaa_key, issuer_year_months| issuer_year_months.each do |issuer, year_months_data| @@ -167,9 +167,9 @@ def combine_by_iaa_month( year_month, year_month_start.strftime('%B %Y'), - (iaa_ial1_unique_users = extract(iaa_results, :unique_users, ial: 1)), - (iaa_ial2_unique_users = extract(iaa_results, :unique_users, ial: 2)), - iaa_ial1_unique_users + iaa_ial2_unique_users, + extract(iaa_results, :unique_users, ial: 1), + extract(iaa_results, :unique_users, ial: 2), + extract(iaa_results, :unique_users, ial: :all), partner_results[:partner_ial2_unique_user_events_year1] || 0, partner_results[:partner_ial2_unique_user_events_year2] || 0, partner_results[:partner_ial2_unique_user_events_year3] || 0, @@ -204,9 +204,9 @@ def combine_by_iaa_month( (ial2_total_auth_count = extract(issuer_results, :total_auth_count, ial: 2)), ial1_total_auth_count + ial2_total_auth_count, - (issuer_ial1_unique_users = extract(issuer_results, :unique_users, ial: 1)), - (issuer_ial2_unique_users = extract(issuer_results, :unique_users, ial: 2)), - issuer_ial1_unique_users + issuer_ial2_unique_users, + extract(issuer_results, :unique_users, ial: 1), + extract(issuer_results, :unique_users, ial: 2), + extract(issuer_results, :unique_users, ial: :all), ] end end diff --git a/app/models/user.rb b/app/models/user.rb index 97a88772a90..0721acc7bc3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -53,6 +53,7 @@ class User < ApplicationRecord has_many :sign_in_restrictions, dependent: :destroy has_many :in_person_enrollments, dependent: :destroy has_many :fraud_review_requests, dependent: :destroy + has_many :gpo_confirmation_codes, through: :profiles has_one :pending_in_person_enrollment, -> { where(status: :pending).order(created_at: :desc) }, @@ -424,7 +425,10 @@ def has_devices? def authenticated_device?(cookie_uuid:) return false if cookie_uuid.blank? - devices.joins(:events).exists?(cookie_uuid:, events: { event_type: :sign_in_after_2fa }) + devices.joins(:events).exists?( + cookie_uuid:, + events: { event_type: [:account_created, :sign_in_after_2fa] }, + ) end # Returns the number of times the user has signed in, corresponding to the `sign_in_before_2fa` diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index ce7d54fcd8e..699e56d39c0 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1309,9 +1309,113 @@ def idv_doc_auth_submitted_pii_validation( ) end - def idv_doc_auth_verify_proofing_results(**extra) - track_event('IdV: doc auth verify proofing results', **extra) + # rubocop:disable Layout/LineLength + # @param ab_tests [Hash] Object that holds A/B test data (legacy A/B tests may include attributes outside the scope of this object) + # @param acuant_sdk_upgrade_ab_test_bucket [String] A/B test bucket for Acuant document capture SDK upgrades + # @param address_edited [Boolean] Whether the user edited their address before submitting the "Verify your information" step + # @param address_line2_present [Boolean] Whether the user's address includes a second address line + # @param analytics_id [String] "Doc Auth" for remote unsupervised, "In Person Proofing" for IPP + # @param errors [Hash] Details about vendor-specific errors encountered during the stages of the identity resolution process + # @param flow_path [String] "hybrid" for hybrid handoff, "standard" otherwise + # @param irs_reproofing [Boolean] Whether the user is being forced to reproof to access the IRS + # @param lexisnexis_instant_verify_workflow_ab_test_bucket [String] A/B test bucket for Lexis Nexis InstantVerify workflow testing + # @param opted_in_to_in_person_proofing [Boolean] Whether this user explicitly opted into in-person proofing + # @param proofing_results [Hash] + # @option proofing_results [String,nil] exception If an exception occurred during any phase of proofing its message is provided here + # @option proofing_results [Boolean] timed_out true if any vendor API calls timed out during proofing + # @option proofing_results [String] threatmetrix_review_status Result of Threatmetrix assessment, either "review", "reject", or "pass" + # @option proofing_results [Hash] context Full context of the proofing process + # @option proofing_results [String] context.device_profiling_adjudication_reason Reason code describing how we arrived at the device profiling result + # @option proofing_results [String] context.resolution_adjudication_reason Reason code describing how we arrived at the identity resolution result + # @option proofing_results [Boolean] context.should_proof_state_id Whether we need to verify the user's PII with AAMVA. False if the user is using a document from a non-AAMVA jurisdiction + # @option proofing_results [Hash] context.stages Object holding details about each stage of the proofing process + # @option proofing_results [Hash] context.stages.resolution Object holding details about the call made to the identity resolution vendor + # @option proofing_results [Boolean] context.stages.resolution.success Whether identity resolution proofing was successful + # @option proofing_results [Hash] context.stages.resolution.errors Object describing errors encountered during identity resolution + # @option proofing_results [String,nil] context.stages.resolution.exception If an exception occured during identity resolution its message is provided here + # @option proofing_results [Boolean] context.stages.resolution.timed_out Whether the identity resolution API request timed out + # @option proofing_results [String] context.stages.resolution.transaction_id A unique id for the underlying vendor request + # @option proofing_results [Boolean] context.stages.resolution.can_pass_with_additional_verification Whether the PII could be verified if another vendor verified certain attributes + # @option proofing_results [Array] context.stages.resolution.attributes_requiring_additional_verification Attributes that need to be verified by another vendor + # @option proofing_results [String] context.stages.resolution.vendor_name Vendor used (e.g. lexisnexis:instant_verify) + # @option proofing_results [String] context.stages.resolution.vendor_workflow ID of workflow or configuration the vendor used for this transaction + # @option proofing_results [Boolean] context.stages.residential_address.success Whether the residential address passed proofing + # @option proofing_results [Hash] context.stages.residential_address.errors Object holding error details returned by the residential address proofing vendor. + # @option proofing_results [String,nil] context.stages.residential_address.exception If an exception occured during residential address verification its message is provided here + # @option proofing_results [Boolean] context.stages.residential_address.timed_out True if the request to the residential address proofing vendor timed out + # @option proofing_results [String] context.stages.residential_address.transaction_id Vendor-specific transaction ID for the request made to the residential address proofing vendor + # @option proofing_results [Boolean] context.stages.residential_address.can_pass_with_additional_verification Whether, if residential address proofing failed, it could pass with additional proofing from another vendor + # @option proofing_results [Array,nil] context.stages.residential_address.attributes_requiring_additional_verification List of PII attributes that require additional verification for residential address proofing to pass + # @option proofing_results [String] context.stages.residential_address.vendor_name Vendor used for residential address proofing + # @option proofing_results [String] context.stages.residential_address.vendor_workflow Vendor-specific workflow or configuration ID associated with the request made. + # @option proofing_results [Hash] context.stages.state_id Object holding details about the call made to the state ID proofing vendor + # @option proofing_results [Boolean] context.stages.state_id.success Whether the PII associated with the user's state ID document passed proofing + # @option proofing_results [Hash] context.stages.state_id.errors Object describing errors encountered while proofing the user's state ID PII + # @option proofing_results [String,nil] context.stages.state_id.exception If an exception occured during state ID PII verification its message is provided here + # @option proofing_results [Boolean] context.stages.state_id.mva_exception For AAMVA, whether the exception that occurred was due to an error on the state MVA side + # @option proofing_results [Hash] context.stages.state_id.requested_attributes An object whose keys are field names and values are "1" representing PII attributes sent to the state ID proofing vendor for verification. + # @option proofing_results [Boolean] context.stages.state_id.timed_out Whether the request to the state ID verification vendor timed out + # @option proofing_results [String] context.stages.state_id.transaction_id Vendor-specific transaction ID for the request made to the state id proofing vendor + # @option proofing_results [String] context.stages.state_id.vendor_name Name of the vendor used for state ID PII verification. If the ID was not from a supported jurisdiction, it will be "UnsupportedJurisdiction". It MAY also be "UnsupportedJurisdiction" if state ID verification was not needed because other vendor calls did not succeed. + # @option proofing_results [String] context.stages.state_id.state The state that was listed as the user's address on their state ID. Note that this may differ from state_id_jurisdiction. + # @option proofing_results [String] context.stages.state_id.state_id_jurisdiction The state that issued the drivers license or ID card being used for proofing. + # @option proofing_results [String] context.stages.state_id.state_id_number A string describing the _format_ of the state ID number provided. + # @option proofing_results [Hash] context.stages.threatmetrix Object holding details about the call made to the device profiling vendor + # @option proofing_results [String] context.stages.threatmetrix.client Identifier string indicating which client was used. + # @option proofing_results [Boolean] context.stages.threatmetrix.success Whether the request to the vendor succeeded. + # @option proofing_results [Hash] context.stages.threatmetrix.errors Hash describing errors encountered when making the request. + # @option proofing_results [String,nil] context.stages.threatmetrix.exception If an exception was encountered making the request to the vendor, its message is provided here. + # @option proofing_results [Boolean] context.stages.threatmetrix.timed_out Whether the request to the vendor timed out. + # @option proofing_results [String] context.stages.threatmetrix.transaction_id Vendor-specific transaction ID for the request. + # @option proofing_results [Hash] context.stages.threatmetrix.response_body JSON body of the response returned from the vendor. PII has been redacted. + # @option proofing_results [String] context.stages.threatmetrix.response_body.account_lex_id LexID associated with the response. + # @option proofing_results [String] context.stages.threatmetrix.response_body.session_id Session ID associated with the response. + # @option proofing_results [String] context.stages.threatmetrix.review_status One of "pass", "review", "reject". + # @param skip_hybrid_handoff [Boolean] Whether the user should skip hybrid handoff (i.e. because they are already on a mobile device) + # @param ssn_is_unique [Boolean] Whether another Profile existed with the same SSN at the time the profile associated with the current IdV session was minted. + # @param step [String] Always "verify" (leftover from flow state machine days) + # @param success [Boolean] Whether identity resolution succeeded overall + def idv_doc_auth_verify_proofing_results( + ab_tests: nil, + acuant_sdk_upgrade_ab_test_bucket: nil, + address_edited: nil, + address_line2_present: nil, + analytics_id: nil, + errors: nil, + flow_path: nil, + irs_reproofing: nil, + lexisnexis_instant_verify_workflow_ab_test_bucket: nil, + opted_in_to_in_person_proofing: nil, + proofing_results: nil, + skip_hybrid_handoff: nil, + ssn_is_unique: nil, + step: nil, + success: nil, + **extra + ) + track_event( + 'IdV: doc auth verify proofing results', + { + ab_tests:, + acuant_sdk_upgrade_ab_test_bucket:, + address_edited:, + address_line2_present:, + analytics_id:, + errors:, + flow_path:, + irs_reproofing:, + lexisnexis_instant_verify_workflow_ab_test_bucket:, + opted_in_to_in_person_proofing:, + proofing_results:, + skip_hybrid_handoff:, + ssn_is_unique:, + step:, + success:, + **extra, + }.compact, + ) end + # rubocop:enable Layout/LineLength # @identity.idp.previous_event_name IdV: in person proofing verify submitted def idv_doc_auth_verify_submitted(**extra) @@ -3171,6 +3275,17 @@ def idv_request_letter_visited( ) end + # GPO "resend letter" page visited + # @identity.idp.previous_event_name IdV: request letter visited + def idv_resend_letter_visited( + **extra + ) + track_event( + :idv_resend_letter_visited, + **extra, + ) + end + # Acuant SDK errored after loading but before initialization # @param [Boolean] success # @param [String] error_message diff --git a/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb b/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb index ecc6c25f46b..fd874de2235 100644 --- a/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb +++ b/app/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window.rb @@ -11,6 +11,7 @@ module TotalMonthlyAuthCountsWithinIaaWindow module_function + # rubocop:disable Metrics/BlockLength # @param [String] issuer # @param [String] iaa # @param [Date] iaa_start_date @@ -30,8 +31,11 @@ def call(issuer:, iaa:, iaa_start_date:, iaa_end_date:) ial_h[ial_k] = Hash.new { |ym_h, ym_k| ym_h[ym_k] = Multiset.new } end + all_year_month_to_users = Hash.new { |h, ym_k| h[ym_k] = Set.new } + queries.each do |query| - temp_copy = ial_to_year_month_to_users.deep_dup + by_ial_temp_copy = ial_to_year_month_to_users.deep_dup + all_temp_copy = all_year_month_to_users.deep_dup with_retries( max_tries: 3, @@ -42,7 +46,8 @@ def call(issuer:, iaa:, iaa_start_date:, iaa_end_date:) PG::UnableToSend, ], handler: proc do - ial_to_year_month_to_users = temp_copy + ial_to_year_month_to_users = by_ial_temp_copy + all_year_month_to_users = all_temp_copy ActiveRecord::Base.connection.reconnect! end, ) do @@ -54,6 +59,7 @@ def call(issuer:, iaa:, iaa_start_date:, iaa_end_date:) ial = row['ial'] ial_to_year_month_to_users[ial][year_month].add(user_id, auth_count) + all_year_month_to_users[year_month] << user_id end end end @@ -89,8 +95,21 @@ def call(issuer:, iaa:, iaa_start_date:, iaa_end_date:) end end + all_year_month_to_users.each do |year_month, users| + rows << { + issuer: issuer, + iaa: iaa, + iaa_end_date: iaa_range.end.to_s, + iaa_start_date: iaa_range.begin.to_s, + ial: :all, + unique_users: users.count, + year_month: year_month, + } + end + rows end + # rubocop:enable Metrics/BlockLength # @param [String] issuer # @param [Array>] months ranges of dates by month that are included in this iaa, diff --git a/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb b/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb index bfc24384669..7743bf0ec0a 100644 --- a/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb +++ b/app/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa.rb @@ -7,6 +7,7 @@ module UniqueMonthlyAuthCountsByIaa module_function + # rubocop:disable Metrics/BlockLength # @param [String] key label for billing (IAA + order number) # @param [Array] issuers issuers for the iaa # @param [Date] start_date iaa start date @@ -26,8 +27,11 @@ def call(key:, issuers:, start_date:, end_date:) ial_h[ial_k] = Hash.new { |ym_h, ym_k| ym_h[ym_k] = Multiset.new } end + all_year_month_to_users = Hash.new { |h, ym_k| h[ym_k] = Set.new } + queries.each do |query| - temp_copy = ial_to_year_month_to_users.deep_dup + by_ial_temp_copy = ial_to_year_month_to_users.deep_dup + all_temp_copy = all_year_month_to_users.deep_dup with_retries( max_tries: 3, @@ -38,7 +42,8 @@ def call(key:, issuers:, start_date:, end_date:) PG::UnableToSend, ], handler: proc do - ial_to_year_month_to_users = temp_copy + ial_to_year_month_to_users = by_ial_temp_copy + all_year_month_to_users = all_temp_copy ActiveRecord::Base.connection.reconnect! end, ) do @@ -50,6 +55,7 @@ def call(key:, issuers:, start_date:, end_date:) ial = row['ial'] ial_to_year_month_to_users[ial][year_month].add(user_id, auth_count) + all_year_month_to_users[year_month] << user_id end end end @@ -84,8 +90,20 @@ def call(key:, issuers:, start_date:, end_date:) end end + all_year_month_to_users.each do |year_month, users| + rows << { + key: key, + ial: :all, + year_month: year_month, + iaa_start_date: date_range.begin.to_s, + iaa_end_date: date_range.end.to_s, + unique_users: users.count, + } + end + rows end + # rubocop:enable Metrics/BlockLength # @param [Array] issuers all the issuers for this iaa # @param [Array>] months ranges of dates by month that are included in this iaa, diff --git a/app/services/idv/agent.rb b/app/services/idv/agent.rb index b18208c4a03..6a0cb8bedd5 100644 --- a/app/services/idv/agent.rb +++ b/app/services/idv/agent.rb @@ -29,8 +29,6 @@ def proof_resolution( threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - # This argument is intended to be removed - should_proof_state_id: false, } if IdentityConfig.store.ruby_workers_idv_enabled diff --git a/app/services/idv/gpo_mail.rb b/app/services/idv/gpo_mail.rb index 4408ce8ebf4..1d0359d8451 100644 --- a/app/services/idv/gpo_mail.rb +++ b/app/services/idv/gpo_mail.rb @@ -2,6 +2,8 @@ module Idv class GpoMail + attr_reader :current_user + def initialize(current_user) @current_user = current_user end @@ -30,39 +32,23 @@ def last_not_too_recent_enabled? IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours != 0 end - attr_reader :current_user - def too_many_letter_requests_within_window? return false unless window_limit_enabled? - - number_of_letter_requests_within( - IdentityConfig.store.max_mail_events_window_in_days.days, - maximum: IdentityConfig.store.max_mail_events, - ) >= IdentityConfig.store.max_mail_events + current_user.gpo_confirmation_codes.where( + created_at: IdentityConfig.store.max_mail_events_window_in_days.days.ago..Time.zone.now, + ).count >= IdentityConfig.store.max_mail_events end def last_letter_request_too_recent? return false unless last_not_too_recent_enabled? return false unless current_user.gpo_verification_pending_profile? - number_of_letter_requests_within( - IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours.hours, - maximum: 1, - for_profile: current_user.gpo_verification_pending_profile, - ) > 0 - end - - def number_of_letter_requests_within(time_window, maximum:, for_profile: nil) - profile_query_conditions = { user: current_user } - profile_query_conditions[:id] = for_profile.id if for_profile - - GpoConfirmationCode.joins(:profile). - where( - updated_at: (time_window.ago..), - profile: profile_query_conditions, - ). - limit(maximum). - count + current_user.gpo_verification_pending_profile.gpo_confirmation_codes.exists?( + [ + 'created_at > ?', + IdentityConfig.store.minimum_wait_before_another_usps_letter_in_hours.hours.ago, + ], + ) end end end diff --git a/app/views/idv/by_mail/resend_letter/new.html.erb b/app/views/idv/by_mail/resend_letter/new.html.erb new file mode 100644 index 00000000000..57f0692c4b3 --- /dev/null +++ b/app/views/idv/by_mail/resend_letter/new.html.erb @@ -0,0 +1,35 @@ +<% self.title = t('titles.idv.get_letter') %> + +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: step_indicator_steps, + current_step: :verify_address, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> +<% end %> + +<%= render PageHeadingComponent.new.with_content(t('idv.gpo.request_another_letter.title')) %> +

+ <%= t('idv.gpo.request_another_letter.instructions_html') %> +

+

+ <%= new_tab_link_to( + t('idv.gpo.request_another_letter.learn_more_link'), + help_center_redirect_url( + category: 'verify-your-identity', + article: 'verify-your-address-by-mail', + flow: :idv, + step: :gpo_send_letter, + ), + ) %> +

+ +
+ <%= button_to t('idv.gpo.request_another_letter.button'), + idv_resend_letter_path, + method: 'put', + class: 'usa-button usa-button--big usa-button--wide' %> +
+ +<%= render 'idv/shared/back', fallback_path: idv_verify_by_mail_enter_code_url %> diff --git a/config/application.rb b/config/application.rb index 1f10c9c2eb1..47a424275c6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -156,7 +156,6 @@ class Application < Rails::Application allow do origins IdentityCors.allowed_origins_static_sites - resource '/api/analytics-events', headers: :any, methods: [:get] resource '/api/country-support', headers: :any, methods: [:get] if Identity::Hostdata.config.in_person_public_address_search_enabled resource '/api/usps_locations', headers: :any, methods: %i[post options] diff --git a/config/routes.rb b/config/routes.rb index 478efd2cd7e..cd0391f6206 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ Rails.application.routes.draw do # Non i18n routes. Alphabetically sorted. - get '/api/analytics-events' => 'analytics_events#index' get '/api/country-support' => 'country_support#index' get '/api/health' => 'health/health#index' get '/api/health/database' => 'health/database#index' @@ -429,6 +428,8 @@ if FeatureManagement.gpo_verification_enabled? get '/by_mail/request_letter' => 'by_mail/request_letter#index', as: :request_letter put '/by_mail/request_letter' => 'by_mail/request_letter#create' + get '/by_mail/resend_letter' => 'by_mail/resend_letter#new', as: :resend_letter + put '/by_mail/resend_letter' => 'by_mail/resend_letter#create' end get '/by_mail/letter_enqueued' => 'by_mail/letter_enqueued#show', as: :letter_enqueued diff --git a/lib/reporting/fraud_metrics_lg99_report.rb b/lib/reporting/fraud_metrics_lg99_report.rb index 734772e7447..8dfbae7d342 100644 --- a/lib/reporting/fraud_metrics_lg99_report.rb +++ b/lib/reporting/fraud_metrics_lg99_report.rb @@ -106,6 +106,7 @@ def query name , properties.user_id as user_id | filter name in %{event_names} + | limit 10000 QUERY end diff --git a/lib/reporting/mfa_report.rb b/lib/reporting/mfa_report.rb index 15523728c9d..b833eedc53e 100644 --- a/lib/reporting/mfa_report.rb +++ b/lib/reporting/mfa_report.rb @@ -118,7 +118,7 @@ def query def cloudwatch_client @cloudwatch_client ||= Reporting::CloudwatchClient.new( num_threads: @threads, - ensure_complete_logs: true, + ensure_complete_logs: false, slice_interval: @slice, progress: progress?, logger: verbose? ? Logger.new(STDERR) : nil, diff --git a/lib/reporting/protocols_report.rb b/lib/reporting/protocols_report.rb index f6a5ecf347f..c0cdfc62905 100644 --- a/lib/reporting/protocols_report.rb +++ b/lib/reporting/protocols_report.rb @@ -181,7 +181,7 @@ def saml_signature_query def cloudwatch_client @cloudwatch_client ||= Reporting::CloudwatchClient.new( num_threads: @threads, - ensure_complete_logs: true, + ensure_complete_logs: false, slice_interval: @slice, progress: progress?, logger: verbose? ? Logger.new(STDERR) : nil, diff --git a/lib/tasks/backfill_sponsor_id.rake b/lib/tasks/backfill_sponsor_id.rake new file mode 100644 index 00000000000..f3463f13440 --- /dev/null +++ b/lib/tasks/backfill_sponsor_id.rake @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +namespace :in_person_enrollments do + desc 'Backfill the sponsor_id column.' + + ## + # Usage: + # + # bundle exec rake in_person_enrollments:backfill_sponsor_id + # + task backfill_sponsor_id: :environment do |_task, _args| + with_timeout do + ipp_sponsor_id = IdentityConfig.store.usps_ipp_sponsor_id + enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil) + enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count + + warn("Found #{enrollments_without_sponsor_id_count} in_person_enrollments needing backfill") + + tally = 0 + enrollments_without_sponsor_id.in_batches(of: batch_size) do |batch| + tally += batch.update_all(sponsor_id: ipp_sponsor_id) # rubocop:disable Rails/SkipsModelValidations + warn("set sponsor_id for #{tally} in_person_enrollments") + end + warn("COMPLETE: Updated #{tally} in_person_enrollments") + + enrollments_without_sponsor_id = InPersonEnrollment.where(sponsor_id: nil) + enrollments_without_sponsor_id_count = enrollments_without_sponsor_id.count + warn("#{enrollments_without_sponsor_id_count} enrollments without a sponsor id") + end + end + + def batch_size + ENV['BATCH_SIZE'] ? ENV['BATCH_SIZE'].to_i : 1000 + end + + def with_timeout + timeout_in_seconds ||= if ENV['STATEMENT_TIMEOUT_IN_SECONDS'] + ENV['STATEMENT_TIMEOUT_IN_SECONDS'].to_i.seconds + else + 60.seconds + end + ActiveRecord::Base.transaction do + quoted_timeout = ActiveRecord::Base.connection.quote(timeout_in_seconds.in_milliseconds) + ActiveRecord::Base.connection.execute("SET statement_timeout = #{quoted_timeout}") + yield + end + end +end diff --git a/spec/bin/oncall/download-piv-certs_spec.rb b/spec/bin/oncall/download-piv-certs_spec.rb index 99a3bc5589a..1344be5b149 100644 --- a/spec/bin/oncall/download-piv-certs_spec.rb +++ b/spec/bin/oncall/download-piv-certs_spec.rb @@ -68,15 +68,6 @@ end end - let(:cloudwatch_client) do - instance_double( - 'Reporting::CloudwatchClient', - fetch: [ - { 'user_id' => 'abc123', 'key_id' => 'key123' }, - ], - ) - end - before do Aws.config[:sts] = { stub_responses: { @@ -99,7 +90,11 @@ }, } - allow(instance).to receive(:cloudwatch_client).and_return(cloudwatch_client) + stub_cloudwatch_logs( + [ + { 'user_id' => 'abc123', 'key_id' => 'key123' }, + ], + ) end it 'writes certs to the tmpdir' do diff --git a/spec/bin/oncall/otp-deliveries_spec.rb b/spec/bin/oncall/otp-deliveries_spec.rb index 09199d8b5cf..834f8877d87 100644 --- a/spec/bin/oncall/otp-deliveries_spec.rb +++ b/spec/bin/oncall/otp-deliveries_spec.rb @@ -86,12 +86,8 @@ let(:stdout) { StringIO.new } subject(:run) { instance.run(out: stdout) } - let(:cloudwatch_client) { instance_double('Reporting::CloudwatchClient') } - before do - allow(instance).to receive(:cloudwatch_client).and_return(cloudwatch_client) - - allow(cloudwatch_client).to receive(:fetch).and_return( + stub_cloudwatch_logs( [ { 'properties.user_id' => 'aaa', diff --git a/spec/controllers/analytics_events_controller_spec.rb b/spec/controllers/analytics_events_controller_spec.rb deleted file mode 100644 index 1557323afdd..00000000000 --- a/spec/controllers/analytics_events_controller_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'rails_helper' - -RSpec.describe AnalyticsEventsController do - describe '#index' do - subject(:action) { get :index } - - context 'when the JSON file exists' do - let(:json_content) { { events: [] }.to_json } - - around do |ex| - Tempfile.create do |json_file| - @json_file = json_file - json_file.rewind - json_file << json_content - json_file.close - - ex.run - end - end - - before do - stub_const('AnalyticsEventsController::JSON_FILE', @json_file.path) - end - - it 'renders the file' do - action - - expect(response).to be_ok - expect(response.body).to eq(json_content) - expect(response.content_type).to eq('application/json') - end - end - - context 'when the JSON file does not exist' do - it '404s' do - action - - expect(response).to be_not_found - end - end - end -end diff --git a/spec/controllers/idv/by_mail/resend_letter_controller_spec.rb b/spec/controllers/idv/by_mail/resend_letter_controller_spec.rb new file mode 100644 index 00000000000..15670d3f2f6 --- /dev/null +++ b/spec/controllers/idv/by_mail/resend_letter_controller_spec.rb @@ -0,0 +1,119 @@ +require 'rails_helper' + +RSpec.describe Idv::ByMail::ResendLetterController do + let(:user) { create(:user) } + + before do + stub_sign_in(user) + stub_analytics + end + + describe '#new' do + context 'the user has a gpo pending pending profile' do + before do + create(:profile, :verify_by_mail_pending, user: user) + end + + it 'renders the confirmation page' do + get(:new) + + expect(response).to have_http_status(200) + expect(@analytics).to have_logged_event(:idv_resend_letter_visited) + end + end + + context 'the user does not have a gpo pending profile' do + it 'redirects to the account page' do + get(:new) + + expect(response).to redirect_to(account_url) + end + end + + context 'the user has a profile that is too old to request a new letter' do + before do + create(:profile, :verify_by_mail_pending, created_at: 100.days.ago, user: user) + end + + it 'redirects to the enter OTP page' do + get(:new) + + expect(response).to redirect_to(idv_verify_by_mail_enter_code_path) + end + end + + context 'the user has sent to much mail' do + before do + profile = create(:profile, :verify_by_mail_pending, user: user) + create_list(:gpo_confirmation_code, 3, profile: profile) + end + + it 'redirects to the enter OTP page' do + get(:new) + + expect(response).to redirect_to(idv_verify_by_mail_enter_code_path) + end + end + end + + describe '#new' do + before do + create(:profile, :verify_by_mail_pending, :with_pii, user: user) + end + + it 'uses the GPO confirmation maker to send another letter and redirects', :freeze_time do + expect_to_resend_letter_and_redirect + + expect(@analytics).to have_logged_event( + 'IdV: USPS address letter requested', + hash_including( + resend: true, + first_letter_requested_at: user.pending_profile.gpo_verification_pending_at, + hours_since_first_letter: 24, + ), + ) + + expect(@analytics).to have_logged_event( + 'IdV: USPS address letter enqueued', + hash_including( + resend: true, + first_letter_requested_at: user.pending_profile.gpo_verification_pending_at, + hours_since_first_letter: 24, + enqueued_at: Time.zone.now, + proofing_components: nil, + ), + ) + end + + it 'redirects to capture password controller if the PII is locked' do + pii_cacher = instance_double(Pii::Cacher) + allow(pii_cacher).to receive(:fetch).and_return(nil) + allow(pii_cacher).to receive(:exists_in_session?).and_return(false) + allow(Pii::Cacher).to receive(:new).and_return(pii_cacher) + + put :create + + expect(response).to redirect_to capture_password_path + end + end + + def expect_to_resend_letter_and_redirect + pii = user.pending_profile.decrypt_pii(user.password).to_h + pii_cacher = instance_double(Pii::Cacher) + allow(pii_cacher).to receive(:fetch).with(user.pending_profile.id).and_return(pii) + allow(pii_cacher).to receive(:exists_in_session?).and_return(true) + allow(Pii::Cacher).to receive(:new).and_return(pii_cacher) + + service_provider = create(:service_provider, issuer: '123abc') + session[:sp] = { issuer: service_provider.issuer, vtr: ['C1'] } + + gpo_confirmation_maker = instance_double(GpoConfirmationMaker) + allow(GpoConfirmationMaker).to receive(:new). + with(pii: pii, service_provider: service_provider, profile: user.pending_profile). + and_return(gpo_confirmation_maker) + + expect(gpo_confirmation_maker).to receive(:perform) + expect { put :create }.to change { ActionMailer::Base.deliveries.count }.by(1) + expect(response).to redirect_to idv_letter_enqueued_path + end +end diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index bbb2960b85b..deaecf1309a 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -41,5 +41,9 @@ trait :with_notification_phone_configuration do association :notification_phone_configuration end + + trait :with_sponsor_id do + sponsor_id { '123458' } + end end end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 9b4cc3cd9b1..2da550bb912 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -198,7 +198,7 @@ flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth' }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, proofing_results: base_proofing_results }, 'IdV: phone of record visited' => { @@ -324,7 +324,7 @@ flow_path: 'hybrid', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth' }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'hybrid', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, proofing_results: base_proofing_results }, 'IdV: phone of record visited' => { @@ -447,7 +447,7 @@ flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth' }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, proofing_results: base_proofing_results }, 'IdV: phone of record visited' => { @@ -565,7 +565,7 @@ analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', same_address_as_id: false, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: false, skip_hybrid_handoff: nil, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, same_address_as_id: false, proofing_results: in_person_path_proofing_results }, 'IdV: phone confirmation form' => { @@ -698,7 +698,7 @@ flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, analytics_id: 'Doc Auth' }, 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, proofing_results: base_proofing_results }, 'IdV: phone of record visited' => { diff --git a/spec/features/new_device_tracking_spec.rb b/spec/features/new_device_tracking_spec.rb index a64a8e1179f..83fe171aca6 100644 --- a/spec/features/new_device_tracking_spec.rb +++ b/spec/features/new_device_tracking_spec.rb @@ -197,6 +197,26 @@ end end end + + context 'authenticating after new account creation from the same device' do + let(:user) do + user = sign_up_and_2fa_ial1_user + click_on t('links.sign_out') + user + end + + before do + user + reset_email + end + + it 'does not send a second user notification' do + visit new_user_session_path + sign_in_live_with_2fa(user) + + expect_delivered_email_count(0) + end + end end context 'user does not have existing devices' do diff --git a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb index 52db27cf4d5..0ffda29936f 100644 --- a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb +++ b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb @@ -15,9 +15,6 @@ end context 'with data generates reports by iaa + order number, issuer and year_month' do - let(:user1) { create(:user, profiles: [profile1]) } - let(:profile1) { build(:profile, verified_at: DateTime.new(2019, 10, 15).utc) } - context 'with an IAA with a single issuer in April 2020' do let(:partner_account1) { create(:partner_account) } let(:iaa1_range) { DateTime.new(2020, 4, 15).utc..DateTime.new(2021, 4, 14).utc } @@ -49,12 +46,12 @@ let(:inside_iaa1) { iaa1_range.begin + 1.day } + let(:user1) { create(:user, profiles: [profile1]) } + let(:profile1) { build(:profile, verified_at: DateTime.new(2018, 6, 1).utc) } + let(:user2) { create(:user, profiles: [profile2]) } let(:profile2) { build(:profile, verified_at: DateTime.new(2018, 6, 1).utc) } - let(:user3) { create(:user, profiles: [profile3]) } - let(:profile3) { build(:profile, verified_at: DateTime.new(2018, 6, 1).utc) } - let(:csv) { CSV.parse(report.perform(Time.zone.today), headers: true) } before do @@ -67,16 +64,21 @@ # 1 new unique user in month 1 at IAA 1 sp @ IAL 1 7.times do create_sp_return_log( - user: user1, issuer: iaa1_sp.issuer, ial: 1, - returned_at: inside_iaa1 + user: user1, + issuer: iaa1_sp.issuer, + ial: 1, + returned_at: inside_iaa1, ) end # 2 new unique users in month 1 at IAA 1 sp @ IAL 2 with profile age 2 - [user2, user3].each do |user| + # user1 is both IAL1 and IAL2 + [user1, user2].each do |user| create_sp_return_log( - user: user, issuer: iaa1_sp.issuer, ial: 2, - returned_at: inside_iaa1 + user: user, + issuer: iaa1_sp.issuer, + ial: 2, + returned_at: inside_iaa1, ) end end @@ -98,7 +100,7 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(1) expect(row['iaa_ial2_unique_users'].to_i).to eq(2) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(3) + expect(row['iaa_unique_users'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(0) expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) @@ -135,7 +137,7 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(1) expect(row['issuer_ial2_unique_users'].to_i).to eq(2) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(3) + expect(row['issuer_unique_users'].to_i).to eq(2) end end end @@ -180,6 +182,9 @@ let(:inside_iaa2) { iaa2_range.begin + 1.day } + let(:user1) { create(:user, profiles: [profile1]) } + let(:profile1) { build(:profile, verified_at: DateTime.new(2019, 10, 15).utc) } + let(:user4) { create(:user, profiles: [profile4]) } let(:profile4) { build(:profile, verified_at: nil) } @@ -278,7 +283,7 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(8) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(8) + expect(row['iaa_unique_users'].to_i).to eq(8) expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(1) @@ -315,7 +320,7 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(4) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(4) + expect(row['issuer_unique_users'].to_i).to eq(4) end aggregate_failures do @@ -334,7 +339,7 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(8) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(8) + expect(row['iaa_unique_users'].to_i).to eq(8) expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(1) @@ -371,7 +376,7 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(4) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(4) + expect(row['issuer_unique_users'].to_i).to eq(4) end end end @@ -473,7 +478,7 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(1) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(1) + expect(row['iaa_unique_users'].to_i).to eq(1) expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(0) expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) @@ -510,7 +515,7 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(1) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(1) + expect(row['issuer_unique_users'].to_i).to eq(1) end aggregate_failures do @@ -529,7 +534,7 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(2) - expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(2) + expect(row['iaa_unique_users'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(2) expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(1) expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) @@ -566,7 +571,7 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(2) - expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(2) + expect(row['issuer_unique_users'].to_i).to eq(2) end end end diff --git a/spec/lib/reporting/authentication_report_spec.rb b/spec/lib/reporting/authentication_report_spec.rb index 3337375f26a..f2e87ac3938 100644 --- a/spec/lib/reporting/authentication_report_spec.rb +++ b/spec/lib/reporting/authentication_report_spec.rb @@ -8,9 +8,8 @@ subject(:report) { Reporting::AuthenticationReport.new(issuers: [issuer], time_range:) } before do - cloudwatch_client = double( - 'Reporting::CloudwatchClient', - fetch: [ + stub_cloudwatch_logs( + [ # finishes funnel { 'user_id' => 'user1', 'name' => 'OpenID Connect: authorization request' }, { 'user_id' => 'user1', 'name' => 'User Registration: Email Confirmation' }, @@ -38,8 +37,6 @@ { 'user_id' => 'user5', 'name' => 'SP redirect initiated' }, ], ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#as_tables' do diff --git a/spec/lib/reporting/cloudwatch_client_spec.rb b/spec/lib/reporting/cloudwatch_client_spec.rb index 66986b08b2e..5c371a59e51 100644 --- a/spec/lib/reporting/cloudwatch_client_spec.rb +++ b/spec/lib/reporting/cloudwatch_client_spec.rb @@ -58,33 +58,15 @@ subject(:fetch) { client.fetch(query:, from:, to:, time_slices:) } - # Helps mimic Array - # @return [Array] - def to_result_fields(hsh) - hsh.map do |key, value| - { field: key, value: value } - end - end - def stub_single_page - query_id = SecureRandom.hex - - Aws.config[:cloudwatchlogs] = { - stub_responses: { - start_query: { query_id: query_id }, - get_query_results: { - status: 'Complete', - results: [ - # rubocop:disable Layout/LineLength - to_result_fields('@message' => 'aaa', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex), - to_result_fields('@message' => 'bbb', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex), - to_result_fields('@message' => 'ccc', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex), - to_result_fields('@message' => 'ddd', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex), - # rubocop:enable Layout/LineLength - ], - }, - }, - } + stub_cloudwatch_logs( + [ + { '@message' => 'aaa', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex }, + { '@message' => 'bbb', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex }, + { '@message' => 'ccc', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex }, + { '@message' => 'ddd', '@timestamp' => now.iso8601, '@ptr' => SecureRandom.hex }, + ], + ) end context ':slice_interval is falsy' do diff --git a/spec/lib/reporting/drop_off_report_spec.rb b/spec/lib/reporting/drop_off_report_spec.rb index b5e6ad5b62a..d8bf43dc49a 100644 --- a/spec/lib/reporting/drop_off_report_spec.rb +++ b/spec/lib/reporting/drop_off_report_spec.rb @@ -8,9 +8,8 @@ subject(:report) { Reporting::DropOffReport.new(issuers: [issuer], time_range:) } before do - cloudwatch_client = double( - 'Reporting::CloudwatchClient', - fetch: [ + stub_cloudwatch_logs( + [ # finishes funnel { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' }, { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome submitted' }, @@ -101,8 +100,6 @@ { 'user_id' => 'user8', 'name' => 'IdV: final resolution' }, ], ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#as_tables' do diff --git a/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb b/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb index 7c11894bb7e..2e743eb223e 100644 --- a/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb +++ b/spec/lib/reporting/fraud_metrics_lg99_report_spec.rb @@ -2,7 +2,7 @@ require 'reporting/fraud_metrics_lg99_report' RSpec.describe Reporting::FraudMetricsLg99Report do - let(:time_range) { Date.new(2022, 1, 1).all_month } + let(:time_range) { Date.new(2022, 1, 1).in_time_zone('UTC').all_month } let(:expected_lg99_metrics_table) do [ ['Metric', 'Total'], @@ -13,9 +13,8 @@ subject(:report) { Reporting::FraudMetricsLg99Report.new(time_range:) } before do - cloudwatch_client = double( - 'Reporting::CloudwatchClient', - fetch: [ + stub_cloudwatch_logs( + [ { 'user_id' => 'user1', 'name' => 'IdV: Verify please call visited' }, { 'user_id' => 'user1', 'name' => 'IdV: Verify please call visited' }, { 'user_id' => 'user1', 'name' => 'IdV: Verify setup errors visited' }, @@ -32,8 +31,6 @@ { 'user_id' => 'user5', 'name' => 'IdV: Verify setup errors visited' }, ], ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#lg99_metrics_table' do diff --git a/spec/lib/reporting/identity_verification_report_spec.rb b/spec/lib/reporting/identity_verification_report_spec.rb index 1fc617d509a..863dd6774d2 100644 --- a/spec/lib/reporting/identity_verification_report_spec.rb +++ b/spec/lib/reporting/identity_verification_report_spec.rb @@ -11,9 +11,8 @@ # rubocop:disable Layout/LineLength before do - cloudwatch_client = double( - 'Reporting::CloudwatchClient', - fetch: [ + stub_cloudwatch_logs( + [ # Online verification user (failed each vendor once, then succeeded once) { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' }, { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome submitted' }, @@ -62,8 +61,6 @@ { 'user_id' => 'user6', 'name' => 'IdV: doc auth image upload vendor submitted', 'doc_auth_failed_non_fraud' => '1' }, ], ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#as_csv' do diff --git a/spec/lib/reporting/mfa_report_spec.rb b/spec/lib/reporting/mfa_report_spec.rb index ac936374712..10ab7582077 100644 --- a/spec/lib/reporting/mfa_report_spec.rb +++ b/spec/lib/reporting/mfa_report_spec.rb @@ -8,9 +8,8 @@ subject(:report) { Reporting::MfaReport.new(issuers: [issuer], time_range:) } before do - cloudwatch_client = double( - 'Reporting::CloudwatchClient', - fetch: [ + stub_cloudwatch_logs( + [ { 'personal_key_total' => '2', 'sms_total' => '5', @@ -33,8 +32,6 @@ }, ], ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#as_tables' do @@ -75,7 +72,7 @@ let(:default_args) do { num_threads: 10, - ensure_complete_logs: true, + ensure_complete_logs: false, slice_interval: 1.day, progress: false, logger: nil, diff --git a/spec/lib/reporting/protocols_report_spec.rb b/spec/lib/reporting/protocols_report_spec.rb index 394b074964c..0a47332ef1b 100644 --- a/spec/lib/reporting/protocols_report_spec.rb +++ b/spec/lib/reporting/protocols_report_spec.rb @@ -72,14 +72,12 @@ 'issuer' => 'Issuer3', }, ] - cloudwatch_client = instance_double('Reporting::CloudwatchClient') - allow(cloudwatch_client).to receive(:fetch).and_return( + + stub_multiple_cloudwatch_logs( protocol_query_response, saml_signature_query_response, loa_issuers_query_response, ) - - allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client) end describe '#as_tables' do @@ -120,7 +118,7 @@ let(:default_args) do { num_threads: 10, - ensure_complete_logs: true, + ensure_complete_logs: false, slice_interval: 1.day, progress: false, logger: nil, diff --git a/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb b/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb new file mode 100644 index 00000000000..5a5d3a60f07 --- /dev/null +++ b/spec/lib/tasks/backfill_sponsor_id_rake_spec.rb @@ -0,0 +1,71 @@ +require 'rails_helper' +require 'rake' + +RSpec.describe 'in_person_enrollments:backfill_sponsor_id rake task' do + let!(:task) do + Rake.application.rake_require 'tasks/backfill_sponsor_id' + Rake::Task.define_task(:environment) + Rake::Task['in_person_enrollments:backfill_sponsor_id'] + end + + subject(:invoke_task) do + actual_stderr = $stderr + proxy_stderr = StringIO.new + begin + $stderr = proxy_stderr + task.reenable + task.invoke + proxy_stderr.string + ensure + $stderr = actual_stderr + end + end + + let(:pending_enrollment) { create(:in_person_enrollment, :pending) } + let(:expired_enrollment) { create(:in_person_enrollment, :expired) } + let(:failed_enrollment) { create(:in_person_enrollment, :failed) } + let(:enrollment_with_service_provider) { create(:in_person_enrollment, :with_service_provider) } + let(:enrollment_with_sponsor_id) { create(:in_person_enrollment, :with_sponsor_id) } + + before do + allow(IdentityConfig.store).to receive(:usps_ipp_sponsor_id).and_return('31459') + expect(pending_enrollment.sponsor_id).to be_nil + expect(expired_enrollment.sponsor_id).to be_nil + expect(failed_enrollment.sponsor_id).to be_nil + expect(enrollment_with_service_provider.sponsor_id).to be_nil + expect(enrollment_with_sponsor_id.sponsor_id).not_to be_nil + end + + it 'does not change the value of an existing sponsor id' do + original_sponsor_id = enrollment_with_sponsor_id.sponsor_id + subject + expect(enrollment_with_sponsor_id.sponsor_id).to eq(original_sponsor_id) + end + + it 'sets a sponsor id for every enrollment with a nil sponsor id' do + enrollments_with_nil_sponsor_id_count = InPersonEnrollment.where(sponsor_id: nil).count + expect(enrollments_with_nil_sponsor_id_count).to eq(4) + subject + enrollments_with_nil_sponsor_id_count = InPersonEnrollment.where(sponsor_id: nil).count + expect(enrollments_with_nil_sponsor_id_count).to eq(0) + end + + it 'sets a sponsor id that is a string' do + subject + enrollments = InPersonEnrollment.all + enrollments.each do |enrollment| + expect(enrollment.sponsor_id).to be_a String + end + end + + it 'outputs what it did' do + expect(invoke_task.to_s).to eql( + <<~END, + Found 4 in_person_enrollments needing backfill + set sponsor_id for 4 in_person_enrollments + COMPLETE: Updated 4 in_person_enrollments + 0 enrollments without a sponsor id + END + ) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 349819d5753..fa8143236da 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1564,6 +1564,14 @@ def it_should_not_send_survey end it { expect(result).to eq(false) } + + context 'with account_created event' do + before do + create(:event, device:, event_type: :account_created) + end + + it { expect(result).to eq(true) } + end end context 'with existing device with sign_in_after_2fa event' do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 09c22c7a6e5..95b8bcb97e6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -42,6 +42,7 @@ config.include Capybara::RSpecMatchers, type: :component config.include AgreementsHelper config.include AnalyticsHelper + config.include AwsCloudwatchHelper config.include AwsKmsClientHelper config.include KeyRotationHelper config.include OtpHelper diff --git a/spec/requests/api_cors_spec.rb b/spec/requests/api_cors_spec.rb index a471c028918..fab34bd7a1c 100644 --- a/spec/requests/api_cors_spec.rb +++ b/spec/requests/api_cors_spec.rb @@ -109,20 +109,4 @@ it_behaves_like 'static API with correct CORS headers' end - - describe '/api/analytics-events' do - before do - Tempfile.create do |json_file| - json_file.rewind - json_file << '{}' - json_file.close - - stub_const('AnalyticsEventsController::JSON_FILE', json_file.path) - - get api_analytics_events_path, headers: { 'HTTP_ORIGIN' => http_origin } - end - end - - it_behaves_like 'static API with correct CORS headers' - end end diff --git a/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb b/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb index 572c060f9e2..8b88d7deba6 100644 --- a/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb +++ b/spec/services/db/monthly_sp_auth_count/total_monthly_auth_counts_within_iaa_window_spec.rb @@ -102,6 +102,24 @@ iaa_start_date: iaa_range.begin.to_s, iaa_end_date: iaa_range.end.to_s, }, + { + year_month: partial_month_date.strftime('%Y%m'), + ial: :all, + issuer: service_provider.issuer, + iaa: service_provider.iaa, + iaa_start_date: iaa_range.begin.to_s, + iaa_end_date: iaa_range.end.to_s, + unique_users: 1, + }, + { + year_month: full_month_date.strftime('%Y%m'), + ial: :all, + issuer: service_provider.issuer, + iaa: service_provider.iaa, + iaa_start_date: iaa_range.begin.to_s, + iaa_end_date: iaa_range.end.to_s, + unique_users: 1, + }, ] expect(result).to match_array(rows) diff --git a/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb b/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb index d2eb0074992..cda5811209c 100644 --- a/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb +++ b/spec/services/db/monthly_sp_auth_count/unique_monthly_auth_counts_by_iaa_spec.rb @@ -157,6 +157,14 @@ unique_users: 2, new_unique_users: 2, }, + { + ial: :all, + key: key, + year_month: '202009', + iaa_start_date: iaa_range.begin.to_s, + iaa_end_date: iaa_range.end.to_s, + unique_users: 2, + }, { ial: 1, key: key, @@ -177,6 +185,14 @@ unique_users: 3, new_unique_users: 1, }, + { + ial: :all, + key: key, + year_month: '202010', + iaa_start_date: iaa_range.begin.to_s, + iaa_end_date: iaa_range.end.to_s, + unique_users: 3, + }, ] expect(results).to match_array(rows) diff --git a/spec/support/aws_cloudwatch_helper.rb b/spec/support/aws_cloudwatch_helper.rb new file mode 100644 index 00000000000..ec29136eef4 --- /dev/null +++ b/spec/support/aws_cloudwatch_helper.rb @@ -0,0 +1,46 @@ +module AwsCloudwatchHelper + # Helps mimic Array + # @return [Array] + def to_result_fields(hsh) + hsh.map do |key, value| + { field: key, value: value } + end + end + + # @param rows [Array] + def stub_cloudwatch_logs(rows) + query_id = SecureRandom.hex + + stub_const('Reporting::CloudwatchClient::DEFAULT_WAIT_DURATION', 0) + + Aws.config[:cloudwatchlogs] = { + stub_responses: { + start_query: { query_id: query_id }, + get_query_results: { + status: 'Complete', + results: rows.map { |row| to_result_fields(row) }, + }, + }, + } + end + + # Stubs multiple separate Cloudwatch queries (in order) to have differente response + # @param responses [Array>] + def stub_multiple_cloudwatch_logs(*responses) + stub_const('Reporting::CloudwatchClient::DEFAULT_WAIT_DURATION', 0) + + query_ids = responses.map { SecureRandom.hex } + + Aws.config[:cloudwatchlogs] = { + stub_responses: { + start_query: query_ids.map { |query_id| { query_id: query_id } }, + get_query_results: responses.map do |rows| + { + status: 'Complete', + results: rows.map { |row| to_result_fields(row) }, + } + end, + }, + } + end +end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 4658d9ef97a..d8dc562604b 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -137,9 +137,9 @@ def fill_in_password_and_submit(password) end def sign_up - user = create(:user, :unconfirmed) + email = Faker::Internet.safe_email + sign_up_with(email) confirm_last_user - user end def sign_up_and_set_password @@ -232,15 +232,18 @@ def user_with_piv_cac end def confirm_last_user + user = User.last @raw_confirmation_token, = Devise.token_generator.generate(EmailAddress, :confirmation_token) - User.last.email_addresses.first.update( + user.email_addresses.first.update( confirmation_token: @raw_confirmation_token, confirmation_sent_at: Time.zone.now, ) visit sign_up_create_email_confirmation_path( confirmation_token: @raw_confirmation_token, ) + + user end def click_send_one_time_code