diff --git a/.rubocop.yml b/.rubocop.yml index 30a60d3f67c..20ec9a534fe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1081,6 +1081,9 @@ Style/ClassEqualityComparison: Style/ClassMethods: Enabled: true +Style/CollectionMethods: + Enabled: true + Style/ColonMethodCall: Enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index f3ef1394fae..36ec53aae08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -579,7 +579,7 @@ GEM actionpack (>= 5.0) railties (>= 5.0) retries (0.0.5) - rexml (3.3.4) + rexml (3.3.6) strscan rotp (6.3.0) rouge (4.2.0) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index cd3a11fdf5d..24248b2ea94 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -135,6 +135,7 @@ def process_async_state(current_async_state) end if current_async_state.in_progress? + analytics.idv_doc_auth_verify_polling_wait_visited render 'shared/wait' return end @@ -142,6 +143,7 @@ def process_async_state(current_async_state) return if confirm_not_rate_limited_after_doc_auth if current_async_state.none? + analytics.idv_doc_auth_verify_visited(**analytics_arguments) render :show elsif current_async_state.missing? analytics.idv_proofing_resolution_result_missing diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index 50e06f35e39..d4f9d4cdbfa 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -10,6 +10,7 @@ class VerifyInfoController < ApplicationController include VerifyInfoConcern before_action :confirm_not_rate_limited_after_doc_auth, except: [:show] + before_action :confirm_pii_data_present before_action :confirm_ssn_step_complete def show @@ -17,7 +18,6 @@ def show @ssn = idv_session.ssn @pii = pii - analytics.idv_doc_auth_verify_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('verify', :view, true) # specify in_person? @@ -74,7 +74,8 @@ def prev_url end def pii - user_session.dig('idv/in_person', :pii_from_user).merge(ssn: idv_session.ssn) + pii_from_user = user_session.dig('idv/in_person', :pii_from_user) || {} + pii_from_user.merge(ssn: idv_session.ssn) end # override IdvSessionConcern @@ -95,6 +96,12 @@ def confirm_ssn_step_complete return if pii.present? && idv_session.ssn.present? redirect_to prev_url end + + def confirm_pii_data_present + unless user_session.dig('idv/in_person').present? + redirect_to idv_path + end + end end end end diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index 10b2221c345..c69563bb272 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -16,7 +16,6 @@ def show @ssn = idv_session.ssn @pii = pii - analytics.idv_doc_auth_verify_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('verify', :view, true) diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 8e86694c638..69234ac992b 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -4,6 +4,37 @@ class SocureWebhookController < ApplicationController skip_before_action :verify_authenticity_token def create - render json: { message: 'Got here.' } + if token_valid? + render json: { message: 'Secret token is valid.' } + else + render status: :unauthorized, json: { message: 'Invalid secret token.' } + end + end + + private + + def token_valid? + authorization_header = request.headers['Authorization']&.split&.last + + return false if authorization_header.nil? + + verify_current_key(authorization_header: authorization_header) || + verify_queue(authorization_header: authorization_header) + end + + def verify_current_key(authorization_header:) + ActiveSupport::SecurityUtils.secure_compare( + authorization_header, + IdentityConfig.store.socure_webhook_secret_key, + ) + end + + def verify_queue(authorization_header:) + IdentityConfig.store.socure_webhook_secret_key_queue.any? do |key| + ActiveSupport::SecurityUtils.secure_compare( + authorization_header, + key, + ) + end end end diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 6a7bf111812..8e5e0778869 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -432,17 +432,17 @@ function AcuantCapture( } /** - * Given a click source, returns a higher-order function that, when called, will log an event + * Given a clickSource, returns a higher-order function that, when called, will log an event * before calling the original function. */ - function withLoggedClick(source: string, metadata: { isDrop: boolean } = { isDrop: false }) { + function withLoggedClick(clickSource: string, metadata: { isDrop: boolean } = { isDrop: false }) { return any>(fn: T) => (...args: Parameters) => { if (!isSuppressingClickLogging.current) { trackEvent( name === 'selfie' ? 'idv_selfie_image_clicked' : `IdV: ${name} image clicked`, { - source, + click_source: clickSource, ...metadata, liveness_checking_required: isSelfieCaptureEnabled, captureAttempts, @@ -810,7 +810,7 @@ function AcuantCapture( {children} ), 'lg-upload': ({ children }) => ( - ), diff --git a/app/javascript/packs/ssn-field.ts b/app/javascript/packs/ssn-field.ts index 322cf9520bd..63f69cd7216 100644 --- a/app/javascript/packs/ssn-field.ts +++ b/app/javascript/packs/ssn-field.ts @@ -1,49 +1,42 @@ import Cleave from 'cleave.js'; -function formatSSNFieldAndLimitLength() { - const inputs = document.querySelectorAll('input.ssn-toggle[type="password"]'); - - if (inputs) { - inputs.forEach((input) => { - const toggle = document.querySelector(`[aria-controls="${input.id}"]`)!; - - let cleave: Cleave | undefined; - - function sync() { - const { value } = input; - cleave?.destroy(); - if (toggle.checked) { - cleave = new Cleave(input, { - numericOnly: true, - blocks: [3, 2, 4], - delimiter: '-', - }); - } else { - const nextValue = value.replace(/-/g, ''); - if (nextValue !== value) { - input.value = nextValue; - } - } - const didFormat = input.value !== value; - if (didFormat) { - input.checkValidity(); - } +const inputs = document.querySelectorAll('input.ssn-toggle[type="password"]'); +inputs.forEach((input) => { + const toggle = document.querySelector(`[aria-controls="${input.id}"]`)!; + + let cleave: Cleave | undefined; + + function sync() { + const { value } = input; + cleave?.destroy(); + if (toggle.checked) { + cleave = new Cleave(input, { + numericOnly: true, + blocks: [3, 2, 4], + delimiter: '-', + }); + } else { + const nextValue = value.replace(/-/g, ''); + if (nextValue !== value) { + input.value = nextValue; } + } + const didFormat = input.value !== value; + if (didFormat) { + input.checkValidity(); + } + } - sync(); - toggle.addEventListener('change', sync); - - function limitLength(this: HTMLInputElement) { - const maxLength = 9 + (this.value.match(/-/g) || []).length; - if (this.value.length > maxLength) { - this.value = this.value.slice(0, maxLength); - this.checkValidity(); - } - } + sync(); + toggle.addEventListener('change', sync); - input.addEventListener('input', limitLength.bind(input)); - }); + function limitLength(this: HTMLInputElement) { + const maxLength = 9 + (this.value.match(/-/g) || []).length; + if (this.value.length > maxLength) { + this.value = this.value.slice(0, maxLength); + this.checkValidity(); + } } -} -document.addEventListener('DOMContentLoaded', formatSSNFieldAndLimitLength); + input.addEventListener('input', limitLength.bind(input)); +}); diff --git a/app/javascript/packs/state-guidance.ts b/app/javascript/packs/state-guidance.ts index a317fd668da..bc9bfa10710 100644 --- a/app/javascript/packs/state-guidance.ts +++ b/app/javascript/packs/state-guidance.ts @@ -57,8 +57,6 @@ function onIdentityDocJurisdictionSelection() { document.getElementById('idv_form_state')?.addEventListener('change', onStateSelectionChange); -document.addEventListener('DOMContentLoaded', () => { - onStateSelectionChange(); - onIdentityDocStateSelection(); - onIdentityDocJurisdictionSelection(); -}); +onStateSelectionChange(); +onIdentityDocStateSelection(); +onIdentityDocJurisdictionSelection(); diff --git a/app/jobs/reports/monthly_gpo_letter_requests_report.rb b/app/jobs/reports/monthly_gpo_letter_requests_report.rb index 4776b8be807..93e51d42b92 100644 --- a/app/jobs/reports/monthly_gpo_letter_requests_report.rb +++ b/app/jobs/reports/monthly_gpo_letter_requests_report.rb @@ -24,7 +24,7 @@ def perform(_date, start_time: first_of_this_month, end_time: end_of_today) private def calculate_totals(daily_results) - daily_results.inject(0) { |sum, rec| sum + rec['letter_requests_count'].to_i } + daily_results.reduce(0) { |sum, rec| sum + rec['letter_requests_count'].to_i } end end end diff --git a/app/jobs/reports/monthly_key_metrics_report.rb b/app/jobs/reports/monthly_key_metrics_report.rb index d6e82944165..e560e9495d4 100644 --- a/app/jobs/reports/monthly_key_metrics_report.rb +++ b/app/jobs/reports/monthly_key_metrics_report.rb @@ -106,9 +106,7 @@ def total_user_count_report end def active_users_count_report - @active_users_count_report ||= Reporting::ActiveUsersCountReport.new( - report_date, - ) + @active_users_count_report ||= Reporting::ActiveUsersCountReport.new(report_date) end def agency_and_sp_report diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 5d5ca2f8a2f..edad5db7413 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -521,10 +521,12 @@ def email_sent(action:, ses_message_id:, email_address_id:, **extra) # @param [Integer, nil] event_id events table id # @param [String, nil] event_type (see Event#event_type) # @param [String, nil] event_ip ip address for the event + # @param [String, nil] user_id UUID of the user # Tracks disavowed event def event_disavowal( success:, errors:, + user_id:, error_details: nil, event_created_at: nil, disavowed_device_last_used_at: nil, @@ -547,6 +549,7 @@ def event_disavowal( event_id:, event_type:, event_ip:, + user_id:, **extra, ) end @@ -561,10 +564,12 @@ def event_disavowal( # @param [Integer, nil] event_id events table id # @param [String, nil] event_type (see Event#event_type) # @param [String, nil] event_ip ip address for the event + # @param [String, nil] user_id UUID of the user # Event disavowal password reset was performed def event_disavowal_password_reset( success:, errors:, + user_id:, error_details: nil, event_created_at: nil, disavowed_device_last_used_at: nil, @@ -587,6 +592,7 @@ def event_disavowal_password_reset( event_id:, event_type:, event_ip:, + user_id:, **extra, ) end @@ -876,7 +882,7 @@ def idv_back_image_added( # @param [String] acuant_version # @param ["hybrid","standard"] flow_path Document capture user flow # @param [Boolean] isDrop - # @param [Boolean] source + # @param [Boolean] click_source # @param [Boolean] use_alternate_sdk # @param [Number] captureAttempts count of image capturing attempts # @param [String] liveness_checking_required Whether or not the selfie is required @@ -885,7 +891,7 @@ def idv_back_image_clicked( acuant_version:, flow_path:, isDrop:, - source:, + click_source:, use_alternate_sdk:, captureAttempts:, liveness_checking_required:, @@ -897,7 +903,7 @@ def idv_back_image_clicked( acuant_version: acuant_version, flow_path: flow_path, isDrop: isDrop, - source: source, + click_source: click_source, use_alternate_sdk: use_alternate_sdk, liveness_checking_required: liveness_checking_required, captureAttempts: captureAttempts, @@ -1640,6 +1646,12 @@ def idv_doc_auth_submitted_pii_validation( ) end + # User visits IdV verify step waiting on a resolution proofing job result + # @identity.idp.previous_event_name IdV: doc auth verify visited + def idv_doc_auth_verify_polling_wait_visited(**extra) + track_event(:idv_doc_auth_verify_polling_wait_visited, **extra) + end + # 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 @@ -2121,7 +2133,7 @@ def idv_front_image_added( # @param [String] acuant_version # @param ["hybrid","standard"] flow_path Document capture user flow # @param [Boolean] isDrop - # @param [String] source + # @param [String] click_source # @param [String] use_alternate_sdk # @param [Number] captureAttempts count of image capturing attempts # @param [Boolean] liveness_checking_required @@ -2130,7 +2142,7 @@ def idv_front_image_clicked( acuant_version:, flow_path:, isDrop:, - source:, + click_source:, use_alternate_sdk:, captureAttempts:, liveness_checking_required: nil, @@ -2142,7 +2154,7 @@ def idv_front_image_clicked( acuant_version: acuant_version, flow_path: flow_path, isDrop: isDrop, - source: source, + click_source: click_source, use_alternate_sdk: use_alternate_sdk, liveness_checking_required: liveness_checking_required, captureAttempts: captureAttempts, @@ -4137,7 +4149,7 @@ def idv_selfie_image_added( # @param [String] acuant_version # @param ["hybrid","standard"] flow_path Document capture user flow # @param [Boolean] isDrop - # @param [String] source + # @param [String] click_source # @param [String] use_alternate_sdk # @param [Number] captureAttempts # @param [Boolean] liveness_checking_required @@ -4149,7 +4161,7 @@ def idv_selfie_image_clicked( acuant_version:, flow_path:, isDrop:, - source:, + click_source:, use_alternate_sdk:, captureAttempts:, liveness_checking_required: nil, @@ -4164,7 +4176,7 @@ def idv_selfie_image_clicked( acuant_version: acuant_version, flow_path: flow_path, isDrop: isDrop, - source: source, + click_source: click_source, use_alternate_sdk: use_alternate_sdk, captureAttempts: captureAttempts, liveness_checking_required: liveness_checking_required, diff --git a/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb b/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb index 96dfa2bf1ed..535698ca40a 100644 --- a/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb +++ b/app/services/doc_auth/lexis_nexis/image_metrics_reader.rb @@ -11,7 +11,7 @@ def read_image_metrics(true_id_product) true_id_product[:ParameterDetails].each do |detail| next unless detail[:Group] == 'IMAGE_METRICS_RESULT' - inner_val = detail.dig(:Values).collect { |value| value.dig(:Value) } + inner_val = detail.dig(:Values).map { |value| value.dig(:Value) } image_metrics[detail[:Name]] = inner_val end diff --git a/app/services/encryption/encryptors/attribute_encryptor.rb b/app/services/encryption/encryptors/attribute_encryptor.rb index 5698e11aaff..c6923ec957a 100644 --- a/app/services/encryption/encryptors/attribute_encryptor.rb +++ b/app/services/encryption/encryptors/attribute_encryptor.rb @@ -50,7 +50,7 @@ def current_key end def all_keys - [current_key].concat(old_keys.collect { |hash| hash['key'] }) + [current_key].concat(old_keys.map { |hash| hash['key'] }) end def old_keys diff --git a/app/services/event_disavowal/build_disavowed_event_analytics_attributes.rb b/app/services/event_disavowal/build_disavowed_event_analytics_attributes.rb index a5d26252cfa..b5f0334f811 100644 --- a/app/services/event_disavowal/build_disavowed_event_analytics_attributes.rb +++ b/app/services/event_disavowal/build_disavowed_event_analytics_attributes.rb @@ -11,6 +11,7 @@ def self.call(event) event_type: event.event_type, event_created_at: event.created_at, event_ip: event.ip, + user_id: event.user&.uuid, disavowed_device_user_agent: device&.user_agent, disavowed_device_last_ip: device&.last_ip, disavowed_device_last_used_at: device&.last_used_at, diff --git a/app/services/flow/base_flow.rb b/app/services/flow/base_flow.rb index 2abf1843902..8bd5a08e8df 100644 --- a/app/services/flow/base_flow.rb +++ b/app/services/flow/base_flow.rb @@ -19,7 +19,7 @@ def initialize(controller, steps, actions, session) def next_step return @redirect if @redirect - step, _klass = steps.detect do |_step, klass| + step, _klass = steps.find do |_step, klass| !@flow_session[klass.to_s] end step diff --git a/app/services/proofing/ddp_result.rb b/app/services/proofing/ddp_result.rb index d00e5e1109e..9670d570078 100644 --- a/app/services/proofing/ddp_result.rb +++ b/app/services/proofing/ddp_result.rb @@ -70,8 +70,16 @@ def to_h timed_out: timed_out?, transaction_id: transaction_id, review_status: review_status, - response_body: Proofing::LexisNexis::Ddp::ResponseRedacter.redact(response_body), + response_body: redacted_response_body, } end + + private + + def redacted_response_body + return response_body if response_body.blank? + + Proofing::LexisNexis::Ddp::ResponseRedacter.redact(response_body) + end end end diff --git a/app/services/reporting/active_users_count_report.rb b/app/services/reporting/active_users_count_report.rb index ed1bda940ed..7ba3fd9f871 100644 --- a/app/services/reporting/active_users_count_report.rb +++ b/app/services/reporting/active_users_count_report.rb @@ -74,6 +74,7 @@ def generate_apg_report [ ['Active Users (APG)', 'IAL1', 'IDV', 'Total', 'Range start', 'Range end'], + monthly_active_users_apg.as_csv(title: 'Current month'), q1.as_csv(title: 'Fiscal year Q1'), q2.as_csv(title: 'Fiscal year Q2 cumulative'), q3.as_csv(title: 'Fiscal year Q3 cumulative'), @@ -94,6 +95,19 @@ def monthly_active_users end end + # @return [ReportRow] + def monthly_active_users_apg + @monthly_active_users_apg ||= Reports::BaseReport.transaction_with_timeout do + ReportRow.from_hash_time_range( + time_range: monthly_range, + hash: Db::Identity::SpActiveUserCounts.overall_apg( + monthly_range.begin, + monthly_range.end, + ).first, + ) + end + end + # @return [Array] def fiscal_year_active_users_per_quarter_cumulative @fiscal_year_active_users_per_quarter_cumulative ||= begin diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 2f2d4d52b6b..50915cbc1f7 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -7,7 +7,7 @@ <%= tag.div id: 'document-capture-form', data: { app_name: APP_NAME, liveness_required: nil, - mock_client: mock_client, + mock_client: mock_client.presence, help_center_redirect_url: help_center_redirect_url( flow: :idv, step: :document_capture, diff --git a/config/application.yml.default b/config/application.yml.default index a294ce55604..017ef81a2d7 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -335,6 +335,8 @@ sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1 sign_in_user_id_per_ip_attempt_window_in_minutes: 720 sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200 sign_in_user_id_per_ip_max_attempts: 50 +socure_webhook_secret_key: '' +socure_webhook_secret_key_queue: '[]' sp_handoff_bounce_max_seconds: 2 sp_issuer_user_counts_report_configs: '[]' team_ada_email: '' @@ -425,6 +427,8 @@ development: show_unsupported_passkey_platform_authentication_setup: true sign_in_recaptcha_score_threshold: 0.3 skip_encryption_allowed_list: '["urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost"]' + socure_webhook_secret_key: 'secret-key' + socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true @@ -548,6 +552,8 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 short_term_phone_otp_max_attempts: 100 skip_encryption_allowed_list: '[]' + socure_webhook_secret_key: 'secret-key' + socure_webhook_secret_key_queue: '["old-key-one", "old-key-two"]' state_tracking_enabled: true team_ada_email: 'ada@example.com' team_all_login_emails: '["b@example.com", "c@example.com"]' diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 9e00efaa556..2b5f2c7c323 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -4,9 +4,10 @@ cron_12m = '0/12 * * * *' cron_1h = '0 * * * *' cron_24h = '0 0 * * *' +cron_24h_and_a_bit = '12 0 * * *' # 0000 UTC + 12 min, staggered from whatever else runs at 0000 UTC cron_24h_1am = '0 1 * * *' # 1am UTC is 8pm EST/9pm EDT gpo_cron_24h = '0 10 * * *' # 10am UTC is 5am EST/6am EDT -cron_every_monday = 'every Monday at 0:00 UTC' # equivalent to '0 0 * * 1' +cron_every_monday = 'every Monday at 0:25 UTC' # equivalent to '25 0 * * 1' cron_every_monday_1am = 'every Monday at 1:00 UTC' # equivalent to '0 1 * * 1' cron_every_monday_2am = 'every Monday at 2:00 UTC' # equivalent to '0 2 * * 1' @@ -177,7 +178,7 @@ # Send Identity Verification report to S3 identity_verification_report: { class: 'Reports::IdentityVerificationReport', - cron: cron_24h, + cron: cron_24h_and_a_bit, args: -> { [Time.zone.yesterday] }, }, # Refresh USPS auth tokens @@ -219,7 +220,7 @@ # Send fraud metrics to Team Judy fraud_metrics_report: { class: 'Reports::FraudMetricsReport', - cron: cron_24h, + cron: cron_24h_and_a_bit, args: -> { [Time.zone.yesterday.end_of_day] }, }, # Previous week's drop of report diff --git a/db/primary_migrate/20240822122355_add_selected_email_to_identity.rb b/db/primary_migrate/20240822122355_add_selected_email_to_identity.rb new file mode 100644 index 00000000000..9a4db5a4bc0 --- /dev/null +++ b/db/primary_migrate/20240822122355_add_selected_email_to_identity.rb @@ -0,0 +1,5 @@ +class AddSelectedEmailToIdentity < ActiveRecord::Migration[7.1] + def change + add_column :identities, :email_address_id, :bigint + end +end diff --git a/db/schema.rb b/db/schema.rb index e1cdc08131f..ef682f62526 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[7.1].define(version: 2024_08_07_202012) do +ActiveRecord::Schema[7.1].define(version: 2024_08_22_122355) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -287,6 +287,7 @@ t.text "requested_aal_value" t.string "vtr" t.string "acr_values" + t.bigint "email_address_id" t.index ["access_token"], name: "index_identities_on_access_token", unique: true t.index ["session_uuid"], name: "index_identities_on_session_uuid", unique: true t.index ["user_id", "service_provider"], name: "index_identities_on_user_id_and_service_provider", unique: true diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 79d29a1d847..51d032a7b1e 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -383,6 +383,8 @@ def self.store config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer) config.add(:sign_in_recaptcha_score_threshold, type: :float) config.add(:skip_encryption_allowed_list, type: :json) + config.add(:socure_webhook_secret_key, type: :string) + config.add(:socure_webhook_secret_key_queue, type: :json) config.add(:sp_handoff_bounce_max_seconds, type: :integer) config.add(:sp_issuer_user_counts_report_configs, type: :json) config.add(:state_tracking_enabled, type: :boolean) diff --git a/lib/reporting/drop_off_report.rb b/lib/reporting/drop_off_report.rb index 27bdf0bfac5..e7159e11811 100644 --- a/lib/reporting/drop_off_report.rb +++ b/lib/reporting/drop_off_report.rb @@ -49,10 +49,15 @@ module Events OLD_IDV_ENTER_PASSWORD_SUBMITTED = 'IdV: review complete' IDV_PERSONAL_KEY_SUBMITTED = 'IdV: personal key submitted' IDV_FINAL_RESOLUTION = 'IdV: final resolution' + IPP_ENROLLMENT_UPDATE = 'GetUspsProofingResultsJob: Enrollment status updated' def self.all_events constants.map { |c| const_get(c) } end + + def self.must_pass_events + [IPP_ENROLLMENT_UPDATE] + end end module Results @@ -63,7 +68,7 @@ def as_emailable_reports [ Reporting::EmailableReport.new( title: 'Step Definitions', - table: step_definition_table, + table: STEP_DEFINITIONS, ), Reporting::EmailableReport.new( title: 'Overview', @@ -234,6 +239,10 @@ def dropoff_metrics_table 'Workflow Complete - Total Pending', idv_final_resolution_total_pending, ], + [ + 'Successfully verified via in-person proofing', + ipp_verification_total, + ], ] end @@ -246,60 +255,66 @@ def idv_final_resolution_verified data[Results::IDV_FINAL_RESOLUTION_VERIFIED].count end - def step_definition_table - [ - ['Step', 'Definition'], - [ - 'Welcome (page viewed)', - 'Start of proofing process', - ], - [ - 'User agreement (page viewer)', - 'Users who clicked "Continue" on the welcome page', - ], - [ - 'Capture Document (page viewed)', - 'Users who check the consent checkbox and click "Continue"', - ], - [ - 'Document submitted (event)', - 'Users who upload a front and back image and click "Submit" ', - ], - [ - 'SSN (page view)', - 'Users whose ID is authenticated by Acuant', - ], - [ - 'Verify Info (page view)', - 'Users who enter an SSN and continue', - ], - [ - 'Verify submit (event)', - 'Users who verify their information and submit it for Identity Verification (LN)', - ], - [ - 'Phone finder (page view)', - 'Users who successfuly had their identities verified by LN', - ], - [ - 'Encrypt account: enter password (page view)', - 'Users who were able to complete the physicality check using PhoneFinder', - ], - [ - 'Personal key input (page view)', - 'Users who enter their password to encrypt their PII', - ], - [ - 'Verified (event)', - 'Users who confirm their personal key and complete setting up their verified account', - ], - [ - 'Workflow Complete - Total Pending', - 'Total count of users who are pending IDV', - ], - ] + def ipp_verification_total + @ipp_verification_total ||= data[Events::IPP_ENROLLMENT_UPDATE].count end + STEP_DEFINITIONS = [ + ['Step', 'Definition'], + [ + 'Welcome (page viewed)', + 'Start of proofing process', + ], + [ + 'User agreement (page viewer)', + 'Users who clicked "Continue" on the welcome page', + ], + [ + 'Capture Document (page viewed)', + 'Users who check the consent checkbox and click "Continue"', + ], + [ + 'Document submitted (event)', + 'Users who upload a front and back image and click "Submit" ', + ], + [ + 'SSN (page view)', + 'Users whose ID is authenticated by Acuant', + ], + [ + 'Verify Info (page view)', + 'Users who enter an SSN and continue', + ], + [ + 'Verify submit (event)', + 'Users who verify their information and submit it for Identity Verification (LN)', + ], + [ + 'Phone finder (page view)', + 'Users who successfuly had their identities verified by LN', + ], + [ + 'Encrypt account: enter password (page view)', + 'Users who were able to complete the physicality check using PhoneFinder', + ], + [ + 'Personal key input (page view)', + 'Users who enter their password to encrypt their PII', + ], + [ + 'Verified (event)', + 'Users who confirm their personal key and complete setting up their verified account', + ], + [ + 'Workflow Complete - Total Pending', + 'Total count of users who are pending IDV', + ], + [ + 'Successfully verified via in-person proofing', + 'The count of users who successfully verified their identity in-person at a USPS location within the report period', # rubocop:disable Layout/LineLength + ], + ].freeze + def idv_started data[Events::IDV_DOC_AUTH_WELCOME].count end @@ -352,7 +367,7 @@ def idv_pending_gpo def as_tables [ - step_definition_table, + STEP_DEFINITIONS, overview_table, dropoff_metrics_table, ] @@ -370,10 +385,12 @@ def to_csvs # @return [Float] def percent(numerator:, denominator:) - (numerator.to_f / denominator.to_f) + result = (numerator.to_f / denominator.to_f) + result.nan? ? 0 : result end - def fetch_results + def fetch_results(query: nil) + query ||= self.query cloudwatch_client.fetch(query:, from: time_range.begin, to: time_range.end) end @@ -381,20 +398,22 @@ def query params = { issuers: issuers.present? && quote(issuers), event_names: quote(Events.all_events), + must_pass_event_names: quote(Events.must_pass_events), } format(<<~QUERY, params) fields name , properties.user_id AS user_id - , coalesce(properties.event_properties.success, 0) AS success + , coalesce(properties.event_properties.success, properties.event_properties.passed, 0) AS success , coalesce(properties.event_properties.fraud_review_pending, 0) AS fraud_review_pending , coalesce(properties.event_properties.gpo_verification_pending, 0) AS gpo_verification_pending , coalesce(properties.event_properties.in_person_verification_pending, 0) AS in_person_verification_pending , ispresent(properties.event_properties.deactivation_reason) AS has_other_deactivation_reason , !fraud_review_pending and !gpo_verification_pending and !in_person_verification_pending and !has_other_deactivation_reason AS identity_verified - #{issuers.present? ? '| filter properties.service_provider IN %{issuers}' : ''} | filter name in %{event_names} + #{issuers.present? ? '| filter properties.service_provider IN %{issuers} or properties.event_properties.issuer IN %{issuers}' : ''} + | filter (name in %{must_pass_event_names} and properties.event_properties.passed = 1) or (name not in %{must_pass_event_names}) | limit 10000 QUERY end diff --git a/lib/reporting/mfa_report.rb b/lib/reporting/mfa_report.rb index b833eedc53e..6bfcc4b66ce 100644 --- a/lib/reporting/mfa_report.rb +++ b/lib/reporting/mfa_report.rb @@ -135,7 +135,7 @@ def overview_table end def totals(key) - data.inject(0) { |sum, slice| slice[key].to_i + sum } + data.reduce(0) { |sum, slice| slice[key].to_i + sum } end def multi_factor_auth_table diff --git a/spec/config/initializers/job_configurations_spec.rb b/spec/config/initializers/job_configurations_spec.rb index bcd432593e1..b36d463d7cf 100644 --- a/spec/config/initializers/job_configurations_spec.rb +++ b/spec/config/initializers/job_configurations_spec.rb @@ -13,9 +13,10 @@ end describe 'weekly reporting' do - %w[drop_off_report authentication_report].each do |job_name| + %w[drop_off_report authentication_report protocols_report].each do |job_name| it "schedules the #{job_name} to run after the end of the week with yesterday's date" do report = GoodJob.configuration.cron[:"weekly_#{job_name}"] + expect(report).to be, "Missing report weekly_#{job_name}" expect(report[:class]).to eq("Reports::#{job_name.camelize}") freeze_time do @@ -26,10 +27,23 @@ now = Time.zone.now next_time = Fugit.parse(report[:cron]).next_time expect(next_time.utc). - to be_within(2.hours).of(now.utc.end_of_week) + to be_within(2.hours + 1.minute).of(now.utc.end_of_week) expect(next_time.utc).to be > now.utc.end_of_week end end end + it 'has each report scheduled at a different time' do + next_times = freeze_time do + %w[drop_off_report authentication_report protocols_report].map do |job_name| + report = GoodJob.configuration.cron[:"weekly_#{job_name}"] + expect(report).to be, "Missing report weekly_#{job_name}" + expect(report[:class]).to eq("Reports::#{job_name.camelize}") + expect(report[:args].call).to eq([Time.zone.yesterday.end_of_day]) + Fugit.parse(report[:cron]).next_time.to_i + end + end + expect(next_times.count).to be(3) + expect(next_times.uniq.count).to be(3) + end end end diff --git a/spec/controllers/event_disavowal_controller_spec.rb b/spec/controllers/event_disavowal_controller_spec.rb index a291cda55c3..e1e6c8b337a 100644 --- a/spec/controllers/event_disavowal_controller_spec.rb +++ b/spec/controllers/event_disavowal_controller_spec.rb @@ -22,7 +22,7 @@ expect(@analytics).to have_logged_event( 'Event disavowal visited', - build_analytics_hash, + build_analytics_hash(user_id: event.user.uuid), ) end @@ -31,7 +31,7 @@ expect(@analytics).to have_logged_event( 'Event disavowal visited', - build_analytics_hash, + build_analytics_hash(user_id: event.user.uuid), ) expect(assigns(:forbidden_passwords)).to all(be_a(String)) end @@ -79,7 +79,7 @@ expect(@analytics).to have_logged_event( 'Event disavowal password reset', - build_analytics_hash, + build_analytics_hash(user_id: event.user.uuid), ) end end @@ -172,7 +172,7 @@ end end - def build_analytics_hash(success: true, errors: {}) + def build_analytics_hash(success: true, errors: {}, user_id: nil) hash_including( { event_created_at: event.created_at, @@ -184,6 +184,7 @@ def build_analytics_hash(success: true, errors: {}) event_ip: event.ip, disavowed_device_user_agent: event.device.user_agent, disavowed_device_last_ip: event.device.last_ip, + user_id: user_id, }.compact, ) end diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index ab4e5bbe6ee..93a24901d47 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -48,6 +48,13 @@ :confirm_ssn_step_complete, ) end + + it 'confirms idv/in_person data is present' do + expect(subject).to have_actions( + :before, + :confirm_pii_data_present, + ) + end end before do @@ -55,15 +62,6 @@ end describe '#show' do - let(:analytics_name) { 'IdV: doc auth verify visited' } - let(:analytics_args) do - { - analytics_id: 'In Person Proofing', - flow_path: 'standard', - step: 'verify', - }.merge(ab_test_args) - end - it 'renders the show template' do get :show @@ -75,10 +73,32 @@ expect(@analytics).to have_logged_event( 'IdV: doc auth verify visited', - hash_including(**analytics_args, same_address_as_id: true), + { + analytics_id: 'In Person Proofing', + flow_path: 'standard', + step: 'verify', + same_address_as_id: true, + }.merge(ab_test_args), ) end + context 'when the user is rate limited' do + before do + RateLimiter.new( + user: subject.current_user, + rate_limit_type: :idv_resolution, + ).increment_to_limited! + end + + it 'redirects to rate limited url' do + get :show + + expect(response).to redirect_to idv_session_errors_failure_url + + expect(@analytics).to have_logged_event('Rate Limit Reached', limiter_type: :idv_resolution) + end + end + context 'when done' do let(:review_status) { 'review' } let(:async_state) { instance_double(ProofingSessionAsyncResult) } @@ -111,10 +131,76 @@ expect(@analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_including(**analytics_args, success: true), + hash_including( + { + success: true, + analytics_id: 'In Person Proofing', + flow_path: 'standard', + step: 'verify', + same_address_as_id: true, + }.merge(ab_test_args), + ), ) end end + + context 'when the resolution proofing job has not completed' do + let(:async_state) do + ProofingSessionAsyncResult.new(status: ProofingSessionAsyncResult::IN_PROGRESS) + end + + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + it 'renders the wait template' do + get :show + + expect(response).to render_template 'shared/wait' + expect(@analytics).to have_logged_event(:idv_doc_auth_verify_polling_wait_visited) + end + end + + context 'when the resolution proofing job result is missing' do + let(:async_state) do + ProofingSessionAsyncResult.new(status: ProofingSessionAsyncResult::MISSING) + end + + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + it 'renders a timeout error' do + get :show + + expect(response).to render_template :show + expect(controller.flash[:error]).to eq(I18n.t('idv.failure.timeout')) + expect(@analytics).to have_logged_event('IdV: proofing resolution result missing') + end + end + + context 'when idv/in_person data is present' do + before do + subject.user_session['idv/in_person'] = flow_session + end + + it 'renders the show template without errors' do + get :show + + expect(response).to render_template :show + end + end + + context 'when idv/in_person data is missing' do + before do + subject.user_session['idv/in_person'] = {} + end + + it 'redirects to idv_path' do + get :show + expect(response).to redirect_to(idv_path) + end + end end describe '#update' do diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 5bef7b24c89..a5d376d91c5 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -46,15 +46,6 @@ end describe '#show' do - let(:analytics_name) { 'IdV: doc auth verify visited' } - let(:analytics_args) do - { - analytics_id: 'Doc Auth', - flow_path: 'standard', - step: 'verify', - }.merge(ab_test_args) - end - it 'renders the show template' do get :show @@ -64,7 +55,14 @@ it 'sends analytics_visited event' do get :show - expect(@analytics).to have_logged_event(analytics_name, analytics_args) + expect(@analytics).to have_logged_event( + 'IdV: doc auth verify visited', + { + analytics_id: 'Doc Auth', + flow_path: 'standard', + step: 'verify', + }.merge(ab_test_args), + ) end it 'updates DocAuthLog verify_view_count' do @@ -126,6 +124,8 @@ get :show expect(response).to redirect_to idv_session_errors_ssn_failure_url + + expect(@analytics).to have_logged_event('Rate Limit Reached', limiter_type: :proof_ssn) end end @@ -141,6 +141,8 @@ get :show expect(response).to redirect_to idv_session_errors_failure_url + + expect(@analytics).to have_logged_event('Rate Limit Reached', limiter_type: :idv_resolution) end end @@ -292,7 +294,13 @@ expect(@analytics).to have_logged_event( 'IdV: doc auth verify proofing results', - hash_including(**analytics_args, success: true, analytics_id: 'Doc Auth'), + hash_including( + { + analytics_id: 'Doc Auth', + flow_path: 'standard', + step: 'verify', + }.merge(ab_test_args), + ), ) end end @@ -326,6 +334,41 @@ end end end + + context 'when the resolution proofing job has not completed' do + let(:async_state) do + ProofingSessionAsyncResult.new(status: ProofingSessionAsyncResult::IN_PROGRESS) + end + + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + it 'renders the wait template' do + get :show + + expect(response).to render_template 'shared/wait' + expect(@analytics).to have_logged_event(:idv_doc_auth_verify_polling_wait_visited) + end + end + + context 'when the reolution proofing job result is missing' do + let(:async_state) do + ProofingSessionAsyncResult.new(status: ProofingSessionAsyncResult::MISSING) + end + + before do + allow(controller).to receive(:load_async_state).and_return(async_state) + end + + it 'renders a timeout error' do + get :show + + expect(response).to render_template :show + expect(controller.flash[:error]).to eq(I18n.t('idv.failure.timeout')) + expect(@analytics).to have_logged_event('IdV: proofing resolution result missing') + end + end end describe '#update' do diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index cd2c4c1027a..04a06d36d03 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -4,9 +4,41 @@ RSpec.describe SocureWebhookController do describe 'POST /api/webhooks/socure/event' do - it 'returns OK' do + let(:socure_secret_key) { 'this-is-a-secret' } + let(:socure_secret_key_queue) { ['this-is-an-old-secret', 'this-is-an-older-secret'] } + + before do + allow(IdentityConfig.store).to receive(:socure_webhook_secret_key). + and_return(socure_secret_key) + allow(IdentityConfig.store).to receive(:socure_webhook_secret_key_queue). + and_return(socure_secret_key_queue) + end + + it 'returns OK with a correct secret key' do + request.headers['Authorization'] = socure_secret_key post :create + + expect(response).to have_http_status(:ok) + end + + it 'returns OK with an older secret key' do + request.headers['Authorization'] = socure_secret_key_queue.last + post :create + expect(response).to have_http_status(:ok) end + + it 'returns unauthorized with a bad secret key' do + request.headers['Authorization'] = 'ABC123' + post :create + + expect(response).to have_http_status(:unauthorized) + end + + it 'returns unauthorized with no secret key' do + post :create + + expect(response).to have_http_status(:unauthorized) + end end end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index afc18f7c86b..b321848299e 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -771,7 +771,7 @@ timed_out: false, transaction_id: nil, review_status: 'pass', - response_body: { error: 'TMx response body was empty' } } + response_body: nil } end it 'records all of the events' do @@ -847,7 +847,7 @@ timed_out: false, transaction_id: nil, review_status: 'pass', - response_body: { error: 'TMx response body was empty' } } + response_body: nil } end it 'records all of the events' do @@ -892,7 +892,7 @@ timed_out: false, transaction_id: nil, review_status: 'pass', - response_body: { error: 'TMx response body was empty' } } + response_body: nil } end it 'records all of the events' do @@ -949,7 +949,7 @@ timed_out: false, transaction_id: nil, review_status: 'pass', - response_body: { error: 'TMx response body was empty' } } + response_body: nil } end it 'records all of the events', allow_browser_log: true do @@ -1019,7 +1019,7 @@ def wait_for_event(event, wait) timed_out: false, transaction_id: nil, review_status: 'pass', - response_body: { error: 'TMx response body was empty' } } + response_body: nil } end it 'records all of the events' do diff --git a/spec/features/phone/confirmation_spec.rb b/spec/features/phone/confirmation_spec.rb index 04478724e63..737db27c687 100644 --- a/spec/features/phone/confirmation_spec.rb +++ b/spec/features/phone/confirmation_spec.rb @@ -85,7 +85,7 @@ def expect_failed_otp_confirmation(_delivery_method) end def phone_configuration - user.reload.phone_configurations.detect do |phone_configuration| + user.reload.phone_configurations.find do |phone_configuration| phone_configuration.phone == formatted_phone end end diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx index 6581424ebf7..5c8e5b3caee 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx @@ -1358,19 +1358,19 @@ describe('document-capture/components/acuant-capture', () => { expect(trackEvent.callCount).to.be.at.least(3); expect(trackEvent).to.have.been.calledWith('IdV: test image clicked', { - source: 'placeholder', + click_source: 'placeholder', isDrop: false, liveness_checking_required: false, captureAttempts: 1, }); expect(trackEvent).to.have.been.calledWith('IdV: test image clicked', { - source: 'button', + click_source: 'button', isDrop: false, liveness_checking_required: false, captureAttempts: 1, }); expect(trackEvent).to.have.been.calledWith('IdV: test image clicked', { - source: 'upload', + click_source: 'button', isDrop: false, liveness_checking_required: false, captureAttempts: 1, @@ -1391,7 +1391,7 @@ describe('document-capture/components/acuant-capture', () => { fireEvent.drop(input); expect(trackEvent).to.have.been.calledWith('IdV: test image clicked', { - source: 'placeholder', + click_source: 'placeholder', isDrop: true, liveness_checking_required: false, captureAttempts: 1, diff --git a/spec/lib/reporting/drop_off_report_spec.rb b/spec/lib/reporting/drop_off_report_spec.rb index d8bf43dc49a..a2e2fb5f903 100644 --- a/spec/lib/reporting/drop_off_report_spec.rb +++ b/spec/lib/reporting/drop_off_report_spec.rb @@ -8,7 +8,7 @@ subject(:report) { Reporting::DropOffReport.new(issuers: [issuer], time_range:) } before do - stub_cloudwatch_logs( + stub_multiple_cloudwatch_logs( [ # finishes funnel { 'user_id' => 'user1', 'name' => 'IdV: doc auth welcome visited' }, @@ -99,6 +99,21 @@ { 'user_id' => 'user8', 'name' => 'IdV: personal key submitted' }, { 'user_id' => 'user8', 'name' => 'IdV: final resolution' }, ], + [ + # IPP successes + { 'user_id' => 'user9', + 'name' => 'GetUspsProofingResultsJob: Enrollment status updated', + 'success' => '1' }, + { 'user_id' => 'user10', + 'name' => 'GetUspsProofingResultsJob: Enrollment status updated', + 'success' => '1' }, + { 'user_id' => 'user11', + 'name' => 'GetUspsProofingResultsJob: Enrollment status updated', + 'success' => '1' }, + { 'user_id' => 'user12', + 'name' => 'GetUspsProofingResultsJob: Enrollment status updated', + 'success' => '1' }, + ], ) end @@ -178,10 +193,41 @@ end end - def expected_tables(strings: false) + context 'no available events' do + before do + stub_multiple_cloudwatch_logs([], []) + end + + it 'tries its best' do + expect(report.as_tables).to eq(empty_tables) + expect(report.as_emailable_reports.map(&:table)).to eq(empty_tables) + end + end + + def empty_tables(strings: false) + data = [ + [0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0], + [0], + ] + expected_tables(strings:, values: data) + end + + def expected_tables(strings: false, values: nil) + iterator = values&.each [ - # these two tables are static - report.step_definition_table, + # the first two tables are relatively static + Reporting::DropOffReport::STEP_DEFINITIONS, [ ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"], ['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date @@ -190,18 +236,55 @@ def expected_tables(strings: false) [ ['Step', 'Unique user count', 'Users lost', 'Dropoff from last step', 'Users left from start'], - ['Welcome (page viewed)'] + string_or_num(strings, 8), - ['User agreement (page viewed)'] + string_or_num(strings, 8, 0, 0.0, 1.0), - ['Capture Document (page viewed)'] + string_or_num(strings, 7, 1, 0.125, 0.875), - ['Document submitted (event)'] + string_or_num(strings, 7, 0, 0.0, 0.875), - ['SSN (page view)'] + string_or_num(strings, 6, 1, 1.0 / 7, 0.75), - ['Verify Info (page view)'] + string_or_num(strings, 5, 1, 1.0 / 6, 0.625), - ['Verify submit (event)'] + string_or_num(strings, 5, 0, 0.0, 0.625), - ['Phone finder (page view)'] + string_or_num(strings, 5, 0, 0.0, 0.625), - ['Encrypt account: enter password (page view)'] + string_or_num(strings, 4, 1, 0.2, 0.5), - ['Personal key input (page view)'] + string_or_num(strings, 4, 0, 0.0, 0.5), - ['Verified (event)'] + string_or_num(strings, 4, 0, 0.0, 0.5), - ['Workflow Complete - Total Pending'] + string_or_num(strings, 3), + ['Welcome (page viewed)'] + string_or_num(strings, *(values ? iterator.next : [8])), + ['User agreement (page viewed)'] + string_or_num( + strings, + *(values ? iterator.next : [8, 0, 0.0, 1.0]), + ), + ['Capture Document (page viewed)'] + string_or_num( + strings, + *(values ? iterator.next : [7, 1, 0.125, 0.875]), + ), + ['Document submitted (event)'] + string_or_num( + strings, + *(values ? iterator.next : [7, 0, 0.0, 0.875]), + ), + ['SSN (page view)'] + string_or_num( + strings, + *(values ? iterator.next : [6, 1, 1.0 / 7, 0.75]), + ), + ['Verify Info (page view)'] + string_or_num( + strings, + *(values ? iterator.next : [5, 1, 1.0 / 6, 0.625]), + ), + ['Verify submit (event)'] + string_or_num( + strings, + *(values ? iterator.next : [5, 0, 0.0, 0.625]), + ), + ['Phone finder (page view)'] + string_or_num( + strings, + *(values ? iterator.next : [5, 0, 0.0, 0.625]), + ), + ['Encrypt account: enter password (page view)'] + string_or_num( + strings, + *(values ? iterator.next : [4, 1, 0.2, 0.5]), + ), + ['Personal key input (page view)'] + string_or_num( + strings, + *(values ? iterator.next : [4, 0, 0.0, 0.5]), + ), + ['Verified (event)'] + string_or_num( + strings, + *(values ? iterator.next : [4, 0, 0.0, 0.5]), + ), + ['Workflow Complete - Total Pending'] + string_or_num( + strings, + *(values ? iterator.next : [3]), + ), + ['Successfully verified via in-person proofing'] + string_or_num( + strings, + *(values ? iterator.next : [4]), + ), ], ] end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 17e4ea85785..5958a803ed2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1315,7 +1315,7 @@ def it_should_not_send_survey describe '#visible_email_addresses' do let(:user) { create(:user) } - let(:confirmed_email_address) { user.email_addresses.detect(&:confirmed?) } + let(:confirmed_email_address) { user.email_addresses.find(&:confirmed?) } let!(:unconfirmed_expired_email_address) do create( :email_address, diff --git a/spec/services/proofing/aamva/verification_client_spec.rb b/spec/services/proofing/aamva/verification_client_spec.rb index 611aa3721a0..47f7df59d00 100644 --- a/spec/services/proofing/aamva/verification_client_spec.rb +++ b/spec/services/proofing/aamva/verification_client_spec.rb @@ -167,7 +167,7 @@ it 'throws an error which complains about the invalid response' do expect { response }.to raise_error( Proofing::Aamva::VerificationError, - /No close tag for \/h1/, + /Missing end tag for '\/h1'/, ) end diff --git a/spec/services/proofing/ddp_result_spec.rb b/spec/services/proofing/ddp_result_spec.rb index ca358fde2db..d5f10950330 100644 --- a/spec/services/proofing/ddp_result_spec.rb +++ b/spec/services/proofing/ddp_result_spec.rb @@ -117,4 +117,31 @@ end end end + + describe '#to_h' do + context 'when response_body is present' do + it 'is redacted' do + response_body = { first_name: 'Jonny Proofs' } + result = Proofing::DdpResult.new(response_body:) + + expect(result.to_h[:response_body]).to eq({ first_name: '[redacted]' }) + end + end + + context 'when response_body is nil' do + it 'is nil' do + result = Proofing::DdpResult.new(response_body: nil) + + expect(result.to_h[:response_body]).to be_nil + end + end + + context 'when response_body is empty' do + it 'responds with an empty string is the response body is empty' do + result = Proofing::DdpResult.new(response_body: '') + + expect(result.to_h[:response_body]).to eq('') + end + end + end end diff --git a/spec/services/reporting/active_users_count_report_spec.rb b/spec/services/reporting/active_users_count_report_spec.rb index 9a6b09452d1..29e7edd7fdf 100644 --- a/spec/services/reporting/active_users_count_report_spec.rb +++ b/spec/services/reporting/active_users_count_report_spec.rb @@ -96,6 +96,7 @@ expected_table = [ ['Active Users (APG)', 'IAL1', 'IDV', 'Total', 'Range start', 'Range end'], + ['Current month', 0, 0, 0, Date.new(2023, 3, 1), Date.new(2023, 3, 31)], ['Fiscal year Q1', 0, 0, 0, Date.new(2022, 10, 1), Date.new(2022, 12, 31)], ['Fiscal year Q2 cumulative', 2, 0, 2, Date.new(2022, 10, 1), Date.new(2023, 3, 31)], ['Fiscal year Q3 cumulative', 2, 0, 2, Date.new(2022, 10, 1), Date.new(2023, 3, 31)], @@ -106,7 +107,7 @@ emailable_report = report.active_users_count_apg_emailable_report - expect(Db::Identity::SpActiveUserCounts).to have_received(:overall_apg).exactly(2).times + expect(Db::Identity::SpActiveUserCounts).to have_received(:overall_apg).exactly(3).times aggregate_failures do emailable_report.table.zip(expected_table).each do |actual, expected| diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb index f11ae053a58..00a34431f46 100644 --- a/spec/support/matchers/accessibility.rb +++ b/spec/support/matchers/accessibility.rb @@ -182,7 +182,7 @@ def ids(page) failure_message do |page| page_ids = ids(page) - duplicate = page_ids.detect { |id| page_ids.count(id) > 1 } + duplicate = page_ids.find { |id| page_ids.count(id) > 1 } "Expected no duplicate element IDs. Found duplicate: #{duplicate}" end end diff --git a/spec/support/shared_examples_for_email_validation.rb b/spec/support/shared_examples_for_email_validation.rb index e1b70535ff6..88ae7c8583c 100644 --- a/spec/support/shared_examples_for_email_validation.rb +++ b/spec/support/shared_examples_for_email_validation.rb @@ -1,7 +1,7 @@ RSpec.shared_examples 'email validation' do it 'uses the valid_email gem with mx and ban_disposable options' do email_validator = subject._validators.values.flatten. - detect { |v| v.instance_of?(EmailValidator) } + find { |v| v.instance_of?(EmailValidator) } expect(email_validator.options). to eq(mx_with_fallback: true, ban_disposable_email: true, partial: true) diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index f147c8ddd3f..ceddb2af071 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -20,6 +20,7 @@ let(:skip_doc_auth_from_handoff) { false } let(:opted_in_to_in_person_proofing) { false } let(:presenter) { Idv::InPerson::UspsFormPresenter.new } + let(:mock_client) { false } before do decorated_sp_session = instance_double( @@ -55,7 +56,7 @@ skip_doc_auth_from_how_to_verify: skip_doc_auth_from_how_to_verify, skip_doc_auth_from_handoff: skip_doc_auth_from_handoff, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, - mock_client: nil, + mock_client: mock_client, } end @@ -129,5 +130,24 @@ ) end end + + context 'when not using doc auth mock client' do + it 'contains mock-client-data in metadata' do + render_partial + expect(rendered).not_to have_css( + '#document-capture-form[data-mock-client]', + ) + end + end + + context 'when using doc auth mock client' do + let(:mock_client) { true } + it 'contains mock-client-data in metadata' do + render_partial + expect(rendered).to have_css( + '#document-capture-form[data-mock-client]', + ) + end + end end end