From 0e2ecc8b83065c89cccd7d19b3542e1c673706c5 Mon Sep 17 00:00:00 2001 From: Colter <59977618+colter-nattrass@users.noreply.github.com> Date: Thu, 24 Oct 2024 15:04:43 -0600 Subject: [PATCH 01/16] LG-14548 dw stale data check idp to analytics (#11386) * LG-14548: DW stale data check job changelog: Internal, Reporting, Set up DW Stale Data Check * brakeman fixes * linting * specs fixes * using transaction_with_timeout * job name change * job name change * removing private --------- Co-authored-by: Osman Latif --- app/jobs/data_warehouse/base_job.rb | 46 ++++++++ .../table_summary_stats_export_job.rb | 66 +++++++++++ config/application.yml.default | 3 + config/initializers/job_configurations.rb | 6 + lib/identity_config.rb | 1 + .../table_summary_stats_export_job_spec.rb | 103 ++++++++++++++++++ 6 files changed, 225 insertions(+) create mode 100644 app/jobs/data_warehouse/base_job.rb create mode 100644 app/jobs/data_warehouse/table_summary_stats_export_job.rb create mode 100644 spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb diff --git a/app/jobs/data_warehouse/base_job.rb b/app/jobs/data_warehouse/base_job.rb new file mode 100644 index 00000000000..5db5f079a27 --- /dev/null +++ b/app/jobs/data_warehouse/base_job.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'identity/hostdata' + +module DataWarehouse + class BaseJob < ApplicationJob + queue_as :long_running + + private + + def bucket_name + bucket_name = IdentityConfig.store.s3_data_warehouse_bucket_prefix + env = Identity::Hostdata.env + aws_account_id = Identity::Hostdata.aws_account_id + aws_region = Identity::Hostdata.aws_region + "#{bucket_name}-#{env}-#{aws_account_id}-#{aws_region}" + end + + def generate_s3_paths(name, extension, subname: nil, now: Time.zone.now) + name_subdir_ext = "#{name}#{subname ? '/' : ''}#{subname}.#{extension}" + latest = "#{name}/latest.#{name_subdir_ext}" + [latest, "#{name}/#{now.year}/#{now.strftime('%F')}_#{name_subdir_ext}"] + end + + def logger + Rails.logger + end + + def class_name + self.class.name + end + + def s3_client + @s3_client ||= JobHelpers::S3Helper.new.s3_client + end + + def upload_file_to_s3_bucket(path:, body:, content_type:, bucket: bucket_name) + url = "s3://#{bucket}/#{path}" + logger.info("#{class_name}: uploading to #{url}") + obj = Aws::S3::Resource.new(client: s3_client).bucket(bucket).object(path) + obj.put(body: body, acl: 'private', content_type: content_type) + logger.debug("#{class_name}: upload completed to #{url}") + url + end + end +end diff --git a/app/jobs/data_warehouse/table_summary_stats_export_job.rb b/app/jobs/data_warehouse/table_summary_stats_export_job.rb new file mode 100644 index 00000000000..b6040808793 --- /dev/null +++ b/app/jobs/data_warehouse/table_summary_stats_export_job.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module DataWarehouse + class TableSummaryStatsExportJob < BaseJob + REPORT_NAME = 'idp_max_ids' + + def perform(timestamp) + data = fetch_table_max_ids_and_counts(timestamp) + upload_to_s3(data, timestamp) + end + + def fetch_table_max_ids_and_counts(timestamp) + Reports::BaseReport.transaction_with_timeout do + max_ids_and_counts(timestamp) + end + end + + private + + def max_ids_and_counts(timestamp) + active_tables = {} + ActiveRecord::Base.connection.tables.each do |table| + if table_has_id_column?(table) + active_tables[table] = fetch_max_id_and_count(table, timestamp) + end + end + + active_tables + end + + def table_has_id_column?(table) + ActiveRecord::Base.connection.columns(table).map(&:name).include?('id') + end + + def fetch_max_id_and_count(table, timestamp) + quoted_table = ActiveRecord::Base.connection.quote_table_name(table) + query = <<-SQL + SELECT COALESCE(MAX(id), 0) AS max_id, COUNT(*) AS row_count + FROM #{quoted_table} + SQL + if table_has_column?(table, 'created_at') + quoted_timestamp = ActiveRecord::Base.connection.quote(timestamp) + query += " WHERE created_at <= #{quoted_timestamp}" + end + + ActiveRecord::Base.connection.execute(query).first + end + + def table_has_column?(table, column_name) + ActiveRecord::Base.connection.columns(table).map(&:name).include?(column_name) + end + + def upload_to_s3(data, timestamp) + _latest, path = generate_s3_paths(REPORT_NAME, 'json', now: timestamp) + + if bucket_name.present? + upload_file_to_s3_bucket( + path: path, + body: data.to_json, + content_type: 'application/json', + bucket: bucket_name, + ) + end + end + end +end diff --git a/config/application.yml.default b/config/application.yml.default index ada79aca234..899e69e50a7 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -337,6 +337,7 @@ risc_notifications_request_timeout: 3 ruby_workers_idv_enabled: true rules_of_use_horizon_years: 5 rules_of_use_updated_at: '2022-01-19T00:00:00Z' # Production has a newer timestamp than this, update directly in S3 +s3_data_warehouse_bucket_prefix: 'login-gov-analytics-export' s3_public_reports_enabled: false s3_report_bucket_prefix: login-gov.reports s3_report_public_bucket_prefix: login-gov-pubdata @@ -467,6 +468,7 @@ development: rails_mailer_previews_enabled: true raise_on_missing_title: true risc_notifications_local_enabled: true + s3_data_warehouse_bucket_prefix: '' s3_report_bucket_prefix: '' s3_report_public_bucket_prefix: '' saml_endpoint_configs: '[{"suffix":"2023","secret_key_passphrase":"trust-but-verify"},{"suffix":"2024","secret_key_passphrase":"trust-but-verify"}]' @@ -568,6 +570,7 @@ test: requests_per_ip_period: 60 reset_password_email_max_attempts: 5 reset_password_email_window_in_minutes: 80 + s3_data_warehouse_bucket_prefix: '' s3_report_bucket_prefix: '' s3_report_public_bucket_prefix: '' saml_endpoint_configs: '[{"suffix":"2024","secret_key_passphrase":"trust-but-verify"},{"suffix":"2023","secret_key_passphrase":"trust-but-verify","comment":"this extra year is needed to demonstrate how handling multiple live years works in spec/requests/saml_requests_spec.rb"}]' diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index be5602d67ce..b856f61768b 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -175,6 +175,12 @@ cron: cron_24h, args: -> { [Time.zone.today] }, }, + # Data warehouse stale data check + table_summary_stats_export_job: { + class: 'DataWarehouse::TableSummaryStatsExportJob', + cron: gpo_cron_24h, + args: -> { [Time.zone.now.yesterday.end_of_day] }, + }, # Send Duplicate SSN report to S3 duplicate_ssn: { class: 'Reports::DuplicateSsnReport', diff --git a/lib/identity_config.rb b/lib/identity_config.rb index b64e7e5fa77..b1f5f0b572d 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -367,6 +367,7 @@ def self.store config.add(:s3_public_reports_enabled, type: :boolean) config.add(:s3_report_bucket_prefix, type: :string) config.add(:s3_report_public_bucket_prefix, type: :string) + config.add(:s3_data_warehouse_bucket_prefix, type: :string) config.add(:s3_reports_enabled, type: :boolean) config.add(:saml_endpoint_configs, type: :json, options: { symbolize_names: true }) config.add(:saml_secret_rotation_enabled, type: :boolean) diff --git a/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb new file mode 100644 index 00000000000..2f947d4dcf9 --- /dev/null +++ b/spec/jobs/data_warehouse/table_summary_stats_export_job_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +RSpec.describe DataWarehouse::TableSummaryStatsExportJob, type: :job do + let(:timestamp) { Date.new(2024, 10, 10).in_time_zone('UTC').end_of_day } + let(:job) { described_class.new } + let(:expected_bucket) { 'login-gov-analytics-export-test-1234-us-west-2' } + let(:test_on_tables) { ['users'] } + let(:s3_data_warehouse_bucket_prefix) { 'login-gov-analytics-export' } + + let(:expected_json) do + { + 'users' => { + 'max_id' => 2, + 'row_count' => 2, + }, + }.to_json + end + + let(:s3_metadata) do + { + body: anything, + content_type: 'application/json', + bucket: 'login-gov-analytics-export-int-1234-us-west-1', + } + end + + before do + allow(Identity::Hostdata).to receive(:env).and_return('int') + allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') + allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') + allow(IdentityConfig.store).to receive(:s3_data_warehouse_bucket_prefix). + and_return(s3_data_warehouse_bucket_prefix) + + Aws.config[:s3] = { + stub_responses: { + put_object: {}, + }, + } + end + + describe '#perform' do + before do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return(test_on_tables) + add_data_to_tables + end + + context 'when database tables contain data' do + it 'generates correct JSON from database tables' do + json_data = job.fetch_table_max_ids_and_counts(timestamp) + + expect(json_data.to_json).to eq(expected_json) + end + end + + context 'when tables are empty' do + let(:expected_empty_json) { { 'users' => { 'max_id' => 0, 'row_count' => 0 } }.to_json } + + before do + User.delete_all # Clear the User table to simulate emptiness + end + + it 'returns nil max_id and 0 row_count for empty tables' do + json_data = job.fetch_table_max_ids_and_counts(timestamp) + + expect { job.perform(timestamp) }.not_to raise_error + expect(json_data.to_json).to eq(expected_empty_json) + end + end + + context 'when tables are missing the id column' do + let(:expected_empty_json) { {}.to_json } + + before do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['non_id_table']) + allow(ActiveRecord::Base.connection).to receive(:columns).with('non_id_table'). + and_return([double(name: 'name')]) + end + + it 'skips tables without an id column' do + json_data = job.fetch_table_max_ids_and_counts(timestamp) + + expect { job.perform(timestamp) }.not_to raise_error + expect(json_data.to_json).to eq(expected_empty_json) + end + end + + context 'when uploading to S3' do + it 'uploads a file to S3 based on the report date' do + expect(job).to receive(:upload_file_to_s3_bucket).with( + path: 'idp_max_ids/2024/2024-10-10_idp_max_ids.json', + **s3_metadata, + ).exactly(1).time.and_call_original + + job.perform(timestamp) + end + end + end + + def add_data_to_tables + User.create!(id: 1, created_at: (timestamp - 1.hour)) + User.create!(id: 2, created_at: (timestamp - 1.day)) + end +end From eca5f8d592e4be1dc89294274c3c455f4535bd20 Mon Sep 17 00:00:00 2001 From: "Davida (she/they)" Date: Fri, 25 Oct 2024 08:03:32 -0400 Subject: [PATCH 02/16] Ignore unknown authncontext (#11362) (#11396) changelog: User-Facing Improvements, Integration Experience, Allowing and ignoring unknown authn_context values --- .../authorization_controller.rb | 8 + app/controllers/saml_idp_controller.rb | 25 ++- app/services/analytics_events.rb | 9 ++ app/services/saml_request_validator.rb | 5 +- app/services/vot/parser.rb | 15 +- .../application_controller_spec.rb | 27 +++- .../authorization_controller_spec.rb | 59 +++++++ spec/controllers/saml_idp_controller_spec.rb | 53 +++++- .../openid_connect_authorize_form_spec.rb | 151 +++++++++++++----- spec/services/attribute_asserter_spec.rb | 14 ++ spec/services/authn_context_resolver_spec.rb | 124 +++++++++----- spec/services/saml_request_validator_spec.rb | 97 ++++++++--- spec/services/vot/parser_spec.rb | 96 +++++++++-- 13 files changed, 551 insertions(+), 132 deletions(-) diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 930f7e25b42..01944cf0858 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -174,6 +174,7 @@ def pre_validate_authorize_form user_fully_authenticated: user_fully_authenticated?, referer: request.referer, vtr_param: params[:vtr], + unknown_authn_contexts:, ), ) return if result.success? @@ -258,5 +259,12 @@ def redirect_user(redirect_uri, issuer, user_uuid) def sp_handoff_bouncer @sp_handoff_bouncer ||= SpHandoffBouncer.new(sp_session) end + + def unknown_authn_contexts + return nil if params[:vtr].present? || params[:acr_values].blank? + + (params[:acr_values].split - Saml::Idp::Constants::VALID_AUTHN_CONTEXTS). + join(' ').presence + end end end diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index b4278026379..a945f66259f 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -131,6 +131,7 @@ def capture_analytics request_signed: saml_request.signed?, matching_cert_serial:, requested_nameid_format: saml_request.name_id_format, + unknown_authn_contexts:, ) if result.success? && saml_request.signed? @@ -151,12 +152,13 @@ def log_external_saml_auth_request analytics.saml_auth_request( requested_ial: requested_ial, - authn_context: saml_request&.requested_authn_contexts, + authn_context: requested_authn_contexts, requested_aal_authn_context: FederatedProtocols::Saml.new(saml_request).aal, requested_vtr_authn_contexts: saml_request&.requested_vtr_authn_contexts.presence, force_authn: saml_request&.force_authn?, final_auth_request: sp_session[:final_auth_request], service_provider: saml_request&.issuer, + unknown_authn_contexts:, user_fully_authenticated: user_fully_authenticated?, ) end @@ -227,4 +229,25 @@ def resolved_authn_context_int_ial def require_path_year render_not_found if params[:path_year].blank? end + + def unknown_authn_contexts + return nil if saml_request.requested_vtr_authn_contexts.present? + return nil if requested_authn_contexts.blank? + + unmatched_authn_contexts.reject do |authn_context| + authn_context.match(req_attrs_regexp) + end.join(' ').presence + end + + def unmatched_authn_contexts + requested_authn_contexts - Saml::Idp::Constants::VALID_AUTHN_CONTEXTS + end + + def requested_authn_contexts + @request_authn_contexts || saml_request&.requested_authn_contexts + end + + def req_attrs_regexp + Regexp.escape(Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF) + end end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index d7175ac29ce..c0f90702f37 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -5498,6 +5498,7 @@ def openid_connect_bearer_token(success:, ial:, client_id:, errors:, error_detai # @param [String, nil] vtr_param # @param [Boolean] unauthorized_scope # @param [Boolean] user_fully_authenticated + # @param [String] unknown_authn_contexts space separated list of unknown contexts def openid_connect_request_authorization( success:, errors:, @@ -5514,6 +5515,7 @@ def openid_connect_request_authorization( unauthorized_scope:, user_fully_authenticated:, error_details: nil, + unknown_authn_contexts: nil, **extra ) track_event( @@ -5533,6 +5535,7 @@ def openid_connect_request_authorization( vtr_param:, unauthorized_scope:, user_fully_authenticated:, + unknown_authn_contexts:, **extra, ) end @@ -6334,6 +6337,7 @@ def rules_of_use_visit # matches the request certificate in a successful, signed request # @param [Hash] cert_error_details Details for errors that occurred because of an invalid # signature + # @param [String] unknown_authn_contexts space separated list of unknown contexts def saml_auth( success:, errors:, @@ -6350,6 +6354,7 @@ def saml_auth( matching_cert_serial:, error_details: nil, cert_error_details: nil, + unknown_authn_contexts: nil, **extra ) track_event( @@ -6369,6 +6374,7 @@ def saml_auth( request_signed:, matching_cert_serial:, cert_error_details:, + unknown_authn_contexts:, **extra, ) end @@ -6380,6 +6386,7 @@ def saml_auth( # @param [Boolean] force_authn # @param [Boolean] final_auth_request # @param [String] service_provider + # @param [String] unknown_authn_contexts space separated list of unknown contexts # @param [Boolean] user_fully_authenticated # An external request for SAML Authentication was received def saml_auth_request( @@ -6390,6 +6397,7 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + unknown_authn_contexts:, user_fully_authenticated:, **extra ) @@ -6402,6 +6410,7 @@ def saml_auth_request( force_authn:, final_auth_request:, service_provider:, + unknown_authn_contexts:, user_fully_authenticated:, **extra, ) diff --git a/app/services/saml_request_validator.rb b/app/services/saml_request_validator.rb index 740d67a5f60..44826eb879c 100644 --- a/app/services/saml_request_validator.rb +++ b/app/services/saml_request_validator.rb @@ -88,7 +88,10 @@ def valid_authn_context? next true if classref.match?(SamlIdp::Request::VTR_REGEXP) && IdentityConfig.store.use_vot_in_sp_requests end - authn_contexts.all? do |classref| + # SAML requests are allowed to "default" to the integration's IAL default. + return true if authn_contexts.empty? + + authn_contexts.any? do |classref| valid_contexts.include?(classref) end end diff --git a/app/services/vot/parser.rb b/app/services/vot/parser.rb index 3f135fad867..4b768f3ab7b 100644 --- a/app/services/vot/parser.rb +++ b/app/services/vot/parser.rb @@ -4,8 +4,6 @@ module Vot class Parser class ParseException < StandardError; end - class UnsupportedComponentsException < ParseException; end - class DuplicateComponentsException < ParseException; end Result = Data.define( @@ -87,8 +85,7 @@ def initial_components @initial_components ||= component_string.split(component_separator).map do |component_name| component_map.fetch(component_name) rescue KeyError - raise_unsupported_component_exception(component_name) - end + end.compact end def component_separator @@ -113,16 +110,6 @@ def validate_component_uniqueness!(component_values) end end - def raise_unsupported_component_exception(component_value_name) - if vector_of_trust.present? - raise UnsupportedComponentsException, - "'#{vector_of_trust}' contains unknown component '#{component_value_name}'" - else - raise UnsupportedComponentsException, - "'#{acr_values}' contains unknown acr value '#{component_value_name}'" - end - end - def raise_duplicate_component_exception if vector_of_trust.present? raise DuplicateComponentsException, "'#{vector_of_trust}' contains duplicate components" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index be7775ef85d..d3a0a0bff8f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -477,7 +477,7 @@ def index let(:vtr) { nil } let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ].join(' ') end @@ -486,6 +486,31 @@ def index expect(result.identity_proofing?).to eq(true) end + context 'when an unknown acr value is passed in' do + let(:acr_values) { 'unknown-acr-value' } + + it 'raises an exception' do + expect { result }.to raise_exception( + Vot::Parser::ParseException, + 'VoT parser called without VoT or ACR values', + ) + end + + context 'with a known acr value' do + let(:acr_values) do + [ + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'unknown-acr-value', + ].join(' ') + end + + it 'returns a resolved authn context result' do + expect(result.aal2?).to eq(true) + expect(result.identity_proofing?).to eq(true) + end + end + end + context 'without an SP' do let(:sp) { nil } let(:sp_session) { nil } diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 16686795bc6..0590bca9983 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -2029,6 +2029,65 @@ end end + context 'when there are unknown acr_values params' do + let(:unknown_value) { 'unknown-acr-value' } + let(:acr_values) { unknown_value } + + context 'when there is only an unknown acr_value' do + it 'tracks the event with errors' do + stub_analytics + + action + + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: false, + client_id:, + prompt:, + allow_prompt_login: true, + unauthorized_scope: false, + errors: hash_including(:acr_values), + error_details: hash_including(:acr_values), + user_fully_authenticated: true, + acr_values: '', + code_challenge_present: false, + scope: 'openid profile', + unknown_authn_contexts: unknown_value, + ) + end + + context 'when there is also a valid acr_value' do + let(:known_value) { Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF } + let(:acr_values) do + [ + unknown_value, + known_value, + ].join(' ') + end + + it 'tracks the event' do + stub_analytics + + action + expect(@analytics).to have_logged_event( + 'OpenID Connect: authorization request', + success: true, + client_id:, + prompt:, + allow_prompt_login: true, + unauthorized_scope: false, + user_fully_authenticated: true, + acr_values: known_value, + code_challenge_present: false, + scope: 'openid profile', + unknown_authn_contexts: unknown_value, + errors: {}, + ) + end + end + end + end + context 'vtr with invalid params that do not interfere with the redirect_uri' do let(:acr_values) { nil } let(:vtr) { ['C1'].to_json } diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 5a19104dccb..9777cfbc734 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -970,15 +970,22 @@ def name_id_version(format_urn) end context 'authn_context is invalid' do - it 'renders an error page' do + let(:unknown_value) do + 'http://idmanagement.gov/ns/assurance/loa/5' + end + let(:authn_context) { unknown_value } + + before do stub_analytics saml_get_auth( saml_settings( - overrides: { authn_context: 'http://idmanagement.gov/ns/assurance/loa/5' }, + overrides: { authn_context: }, ), ) + end + it 'renders an error page' do expect(controller).to render_template('saml_idp/auth/error') expect(response.status).to eq(400) expect(response.body).to include(t('errors.messages.unauthorized_authn_context')) @@ -989,7 +996,7 @@ def name_id_version(format_urn) errors: { authn_context: [t('errors.messages.unauthorized_authn_context')] }, error_details: { authn_context: { unauthorized_authn_context: true } }, nameid_format: Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT, - authn_context: ['http://idmanagement.gov/ns/assurance/loa/5'], + authn_context: [unknown_value], authn_context_comparison: 'exact', service_provider: 'http://localhost:3000', request_signed: true, @@ -998,9 +1005,49 @@ def name_id_version(format_urn) idv: false, finish_profile: false, matching_cert_serial: saml_test_sp_cert_serial, + unknown_authn_contexts: unknown_value, ), ) end + + context 'there is also a valid authn_context' do + let(:authn_context) do + [ + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + unknown_value, + ] + end + + it 'logs the unknown authn_context value' do + expect(response.status).to eq(302) + expect(@analytics).to have_logged_event( + 'SAML Auth Request', + hash_including( + unknown_authn_contexts: unknown_value, + ), + ) + end + + context 'when it includes the ReqAttributes AuthnContext' do + let(:authn_context) do + [ + Saml::Idp::Constants::REQUESTED_ATTRIBUTES_CLASSREF, + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + unknown_value, + ] + end + + it 'logs the unknown authn_context value' do + expect(response.status).to eq(302) + expect(@analytics).to have_logged_event( + 'SAML Auth Request', + hash_including( + unknown_authn_contexts: unknown_value, + ), + ) + end + end + end end context 'authn_context scenarios' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 6fb26fda4d7..380037b22f4 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -187,6 +187,35 @@ end end + context 'with unknown acr_values' do + let(:acr_values) { 'unknown-value' } + let(:vtr) { nil } + + it 'has errors' do + expect(valid?).to eq(false) + expect(form.errors[:acr_values]). + to include(t('openid_connect.authorization.errors.no_valid_acr_values')) + end + + context 'with a known IAL value' do + before do + allow(IdentityConfig.store).to receive( + :allowed_valid_authn_contexts_semantic_providers, + ).and_return(client_id) + end + let(:acr_values) do + [ + 'unknown-value', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + ].join(' ') + end + + it 'is valid' do + expect(valid?).to eq(true) + end + end + end + context 'with ialmax requested' do let(:acr_values) { Saml::Idp::Constants::IALMAX_AUTHN_CONTEXT_CLASSREF } let(:vtr) { nil } @@ -211,42 +240,53 @@ end end - shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| - let(:acr_values) { facial_match_ial } + context 'when facial match is requested' do + shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| + let(:acr_values) { facial_match_ial } + + context "when the IAL requested is #{facial_match_ial}" do + context 'when the service provider is allowed to use facial match ials' do + before do + allow(IdentityConfig.store).to receive( + :allowed_biometric_ial_providers, + ).and_return([client_id]) + end - context "when the IAL requested is #{facial_match_ial}" do - context 'when the service provider is allowed to use facial match ials' do - before do - allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). - and_return(true) + it 'succeeds validation' do + expect(form).to be_valid + end end - it 'succeeds validation' do - expect(form).to be_valid + context 'when the service provider is not allowed to use facial match ials' do + it 'fails with a not authorized error' do + expect(form).not_to be_valid + expect(form.errors[:acr_values]). + to include(t('openid_connect.authorization.errors.no_auth')) + end end end + end - context 'when the service provider is not allowed to use facial match ials' do - before do - allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). - and_return(false) - end + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - it 'fails with a not authorized error' do - expect(form).not_to be_valid - expect(form.errors[:acr_values]). - to include(t('openid_connect.authorization.errors.no_auth')) - end + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF + + context 'when using semantic acr_values' do + before do + allow(IdentityConfig.store).to receive( + :allowed_valid_authn_contexts_semantic_providers, + ).and_return([client_id]) end + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR + + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR end end - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - - it_behaves_like 'allows facial match IAL only if sp is authorized', - Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF - context 'with aal but not ial requested via acr_values' do let(:acr_values) { Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF } let(:vtr) { nil } @@ -433,22 +473,39 @@ end describe '#acr_values' do - let(:acr_values) do + let(:vtr) { nil } + let(:acr_value_list) do [ - 'http://idmanagement.gov/ns/assurance/loa/1', - 'http://idmanagement.gov/ns/assurance/aal/3', - 'fake_value', - ].join(' ') + Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, + ] end - let(:vtr) { nil } + let(:acr_values) { acr_value_list.join(' ') } it 'is parsed into an array of valid ACR values' do - expect(form.acr_values).to eq( - %w[ - http://idmanagement.gov/ns/assurance/loa/1 - http://idmanagement.gov/ns/assurance/aal/3 - ], - ) + expect(form.acr_values).to eq acr_value_list + end + + context 'when an unknown acr value is included' do + let(:acr_value_list) do + [ + Saml::Idp::Constants::LOA1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, + ] + end + let(:acr_values) { (acr_value_list + ['fake-value']).join(' ') } + + it 'is parsed into an array of valid ACR values' do + expect(form.acr_values).to eq acr_value_list + end + end + + context 'when the only value is an unknown acr value' do + let(:acr_values) { 'fake_value' } + + it 'returns an empty array for acr_values' do + expect(form.acr_values).to eq([]) + end end end @@ -546,6 +603,26 @@ expect(requested_aal_value).to eq(phishing_resistant) end end + + context 'when no values are passed in' do + let(:acr_values) { '' } + + it 'returns the default AAL value' do + expect(form.requested_aal_value).to eq( + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ) + end + end + + context 'when only an unknown value is passed in' do + let(:acr_values) { 'fake-value' } + + it 'returns the default AAL value' do + expect(form.requested_aal_value).to eq( + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + ) + end + end end end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index e74d4328d20..3903cbf66fb 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -107,6 +107,20 @@ it 'gets UUID from Service Provider' do expect(get_asserted_attribute(user, :uuid)).to eq user.last_identity.uuid end + + context 'when authn_context includes an unknown value' do + let(:authn_context) do + [ + ial_value, + 'unknown/authn/context', + ] + end + + it 'includes all requested attributes + uuid' do + expect(user.asserted_attributes.keys). + to eq(%i[uuid email phone first_name verified_at aal ial]) + end + end end context 'custom bundle includes dob' do diff --git a/spec/services/authn_context_resolver_spec.rb b/spec/services/authn_context_resolver_spec.rb index 60e4f4f6417..d3bb6b20570 100644 --- a/spec/services/authn_context_resolver_spec.rb +++ b/spec/services/authn_context_resolver_spec.rb @@ -48,8 +48,8 @@ vtr = ['C2.Pb'] acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/2', - 'http://idmanagement.gov/ns/assurance/ial/2', + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL_VERIFIED_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -152,8 +152,8 @@ context 'with no service provider' do it 'parses an ACR value into requirements' do acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/2', - 'http://idmanagement.gov/ns/assurance/ial/1', + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -175,7 +175,7 @@ it 'properly parses an ACR value without an AAL ACR' do acr_values = [ - 'http://idmanagement.gov/ns/assurance/ial/1', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -197,7 +197,7 @@ it 'properly parses an ACR value without an IAL ACR' do acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/2', + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, ].join(' ') result = AuthnContextResolver.new( @@ -223,8 +223,8 @@ service_provider = build(:service_provider, default_aal: 2) acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/1', - 'http://idmanagement.gov/ns/assurance/ial/1', + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -241,7 +241,7 @@ service_provider = build(:service_provider, default_aal: 2) acr_values = [ - 'http://idmanagement.gov/ns/assurance/ial/1', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -259,7 +259,7 @@ service_provider = build(:service_provider, default_aal: 3) acr_values = [ - 'http://idmanagement.gov/ns/assurance/ial/1', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ].join(' ') result = AuthnContextResolver.new( @@ -295,10 +295,10 @@ let(:service_provider) { build(:service_provider, ial: 2) } subject do AuthnContextResolver.new( - user: user, - service_provider: service_provider, + user:, + service_provider:, vtr: nil, - acr_values: acr_values, + acr_values:, ) end @@ -307,8 +307,8 @@ context 'if IAL ACR value is present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/ial/1', - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ].join(' ') end @@ -318,12 +318,12 @@ end end - context 'if multiple IAL ACR values are present' do + context 'when multiple IAL ACR values are present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/ial/1', - 'http://idmanagement.gov/ns/assurance/ial/2', - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ].join(' ') end @@ -331,12 +331,28 @@ expect(result.identity_proofing?).to be true expect(result.aal2?).to be true end + + context 'when one of the acr values is unknown' do + let(:acr_values) do + [ + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'unknown-acr-value', + ].join(' ') + end + + it 'ignores the unknown value and uses the highest IAL ACR' do + expect(result.identity_proofing?).to eq(true) + expect(result.aal2?).to eq(true) + end + end end context 'if No IAL ACR is present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ].join(' ') end @@ -346,12 +362,23 @@ end end + context 'when the only ACR value is unknown' do + let(:acr_values) { 'unknown-acr-value' } + + it 'errors out as if there were no values' do + expect { result }.to raise_error Vot::Parser::ParseException + end + end + context 'if requesting facial match comparison' do - let(:bio_value) { 'required' } + let(:bio_acr_value) do + Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF + end + let(:acr_values) do [ - "http://idmanagement.gov/ns/assurance/ial/2?bio=#{bio_value}", - 'http://idmanagement.gov/ns/assurance/aal/1', + bio_acr_value, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ].join(' ') end @@ -391,7 +418,9 @@ end context 'with facial match comparison is preferred' do - let(:bio_value) { 'preferred' } + let(:bio_acr_value) do + Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF + end context 'when the user is already verified' do context 'without facial match comparison' do @@ -478,7 +507,7 @@ context 'with no service provider' do it 'parses an ACR value into requirements' do acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/2', + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, ] @@ -525,7 +554,7 @@ it 'properly parses an ACR value without an IAL ACR' do acr_values = [ - 'http://idmanagement.gov/ns/assurance/aal/2', + Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, ] resolver = AuthnContextResolver.new( user: user, @@ -561,8 +590,8 @@ context 'if IAL ACR value is present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/ial/1', - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ] end @@ -572,12 +601,12 @@ end end - context 'if multiple IAL ACR values are present' do + context 'when multiple IAL ACR values are present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/ial/1', - 'urn:acr.login.gov:verified', - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + Saml::Idp::Constants::IAL_VERIFIED_ACR, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ] end @@ -585,12 +614,28 @@ expect(result.identity_proofing?).to be true expect(result.aal2?).to be true end + + context 'when one of the acr values is unknown' do + let(:acr_values) do + [ + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + Saml::Idp::Constants::IAL_VERIFIED_ACR, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, + 'unknown-acr-value', + ] + end + + it 'ignores the unknown value and uses the highest IAL ACR' do + expect(result.identity_proofing?).to eq(true) + expect(result.aal2?).to eq(true) + end + end end context 'if No IAL ACR is present' do let(:acr_values) do [ - 'http://idmanagement.gov/ns/assurance/aal/1', + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ] end @@ -616,11 +661,14 @@ end context 'if requesting facial match comparison' do - let(:bio_value) { 'required' } + let(:bio_acr_value) do + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR + end + let(:acr_values) do [ - "urn:acr.login.gov:verified-facial-match-#{bio_value}", - 'http://idmanagement.gov/ns/assurance/aal/1', + bio_acr_value, + Saml::Idp::Constants::DEFAULT_AAL_AUTHN_CONTEXT_CLASSREF, ] end @@ -674,7 +722,9 @@ end context 'with facial match comparison is preferred' do - let(:bio_value) { 'preferred' } + let(:bio_acr_value) do + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR + end context 'when the user is already verified' do context 'without facial match comparison' do diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index c675f3a0b43..73ceaedc9a2 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -2,7 +2,8 @@ RSpec.describe SamlRequestValidator do describe '#call' do - let(:sp) { ServiceProvider.find_by(issuer: 'http://localhost:3000') } + let(:issuer) { 'http://localhost:3000' } + let(:sp) { ServiceProvider.find_by(issuer:) } let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT } let(:authn_context) { [Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF] } let(:comparison) { 'exact' } @@ -31,15 +32,27 @@ ).and_return( use_vot_in_sp_requests, ) + allow(IdentityConfig.store).to receive( + :allowed_biometric_ial_providers, + ).and_return([issuer]) + allow(IdentityConfig.store).to receive( + :allowed_valid_authn_contexts_semantic_providers, + ).and_return([issuer]) end context 'valid authn context and sp and authorized nameID format' do - it 'returns FormResponse with success: true' do - expect(response.to_h).to include( - success: true, - errors: {}, - **extra, - ) + [ + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + ].each do |ial_value| + let(:authn_context) { [ial_value] } + it 'returns FormResponse with success: true' do + expect(response.to_h).to include( + success: true, + errors: {}, + **extra, + ) + end end context 'ialmax authncontext and ialmax provider' do @@ -59,6 +72,17 @@ end end + context 'no authn context and valid sp and authorized nameID format' do + let(:authn_context) { [] } + it 'returns FormResponse with success: true' do + expect(response.to_h).to include( + success: true, + errors: {}, + **extra, + ) + end + end + context 'valid authn context and invalid sp and authorized nameID format' do let(:sp) { ServiceProvider.find_by(issuer: 'foo') } @@ -180,8 +204,8 @@ end end - context 'invalid authn context and valid sp and authorized nameID format' do - context 'unknown auth context' do + context 'unknown context and valid sp and authorized nameID format' do + context 'only the unknown authn_context is requested' do let(:authn_context) { ['IAL1'] } it 'returns FormResponse with success: false' do @@ -196,22 +220,39 @@ **extra, ) end - end - context 'authn context is ial2 when sp is ial 1' do - let(:authn_context) { [Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF] } + context 'unknown authn_context requested along with a valid one' do + let(:authn_context) { ['IAL1', Saml::Idp::Constants::IAL_AUTH_ONLY_ACR] } - it 'returns FormResponse with success: false' do - errors = { - authn_context: [t('errors.messages.unauthorized_authn_context')], - } + it 'returns FormResponse with success: true' do + expect(response.to_h).to include( + success: true, + errors: {}, + **extra, + ) + end + end + end - expect(response.to_h).to include( - success: false, - errors: errors, - error_details: hash_including(*errors.keys), - **extra, - ) + context 'authn context is ial2 when sp is ial 1' do + [ + Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, + Saml::Idp::Constants::IAL_VERIFIED_ACR, + ].each do |ial_value| + let(:authn_context) { [ial_value] } + + it 'returns FormResponse with success: false' do + errors = { + authn_context: [t('errors.messages.unauthorized_authn_context')], + } + + expect(response.to_h).to include( + success: false, + errors: errors, + error_details: hash_including(*errors.keys), + **extra, + ) + end end end @@ -237,9 +278,8 @@ context "when the IAL requested is #{facial_match_ial}" do context 'when the service provider is allowed to use facial match ials' do - let(:sp) { create(:service_provider, :idv) } - before do + sp.update(ial: 2) allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(true) end @@ -281,14 +321,19 @@ it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR + + it_behaves_like 'allows facial match IAL only if sp is authorized', + Saml::Idp::Constants::IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR + shared_examples 'allows semantic IAL only if sp is authorized' do |semantic_ial| let(:authn_context) { [semantic_ial] } context "when the IAL requested is #{semantic_ial}" do context 'when the service provider is allowed to use semantic ials' do - let(:sp) { create(:service_provider, :idv) } - before do + sp.update(ial: 2) allow_any_instance_of(ServiceProvider). to receive(:semantic_authn_contexts_allowed?). and_return(true) diff --git a/spec/services/vot/parser_spec.rb b/spec/services/vot/parser_spec.rb index 43e301f3ade..6065776e08b 100644 --- a/spec/services/vot/parser_spec.rb +++ b/spec/services/vot/parser_spec.rb @@ -97,22 +97,94 @@ end context 'when input includes unrecognized components' do - let(:acr_values) { 'i-am-not-an-acr-value' } - it 'raises an exception' do - expect { Vot::Parser.new(acr_values:).parse }.to raise_exception( - Vot::Parser::UnsupportedComponentsException, - /'i-am-not-an-acr-value'$/, - ) + let(:acr_values) { 'unknown-acr-value' } + + context 'only an unknown acr_value is passed in' do + it 'raises an exception' do + expect { Vot::Parser.new(acr_values:).parse }.to raise_exception( + Vot::Parser::ParseException, + 'VoT parser called without VoT or ACR values', + ) + end + + context 'when a known and valid acr_value is passed in as well' do + let(:acr_values) do + [ + 'unknown-acr-value', + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + ].join(' ') + end + + it 'parses ACR values to component values' do + result = Vot::Parser.new(acr_values:).parse + + expect(result.component_values.map(&:name).join(' ')).to eq( + Saml::Idp::Constants::IAL1_AUTHN_CONTEXT_CLASSREF, + ) + expect(result.aal2?).to eq(false) + expect(result.phishing_resistant?).to eq(false) + expect(result.hspd12?).to eq(false) + expect(result.identity_proofing?).to eq(false) + expect(result.facial_match?).to eq(false) + expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) + end + + context 'with semantic acr_values' do + let(:acr_values) do + [ + 'unknown-acr-value', + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + ].join(' ') + end + + it 'parses ACR values to component values' do + result = Vot::Parser.new(acr_values:).parse + + expect(result.component_values.map(&:name).join(' ')).to eq( + Saml::Idp::Constants::IAL_AUTH_ONLY_ACR, + ) + expect(result.aal2?).to eq(false) + expect(result.phishing_resistant?).to eq(false) + expect(result.hspd12?).to eq(false) + expect(result.identity_proofing?).to eq(false) + expect(result.facial_match?).to eq(false) + expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) + end + end + end end context 'with vectors of trust' do - it 'raises an exception' do - vector_of_trust = 'C1.C2.Xx' + context 'only an unknown VoT is passed in' do + it 'raises an exception' do + vector_of_trust = 'Xx' + + expect { Vot::Parser.new(vector_of_trust:).parse }.to raise_exception( + Vot::Parser::ParseException, + 'VoT parser called without VoT or ACR values', + ) + end + end - expect { Vot::Parser.new(vector_of_trust:).parse }.to raise_exception( - Vot::Parser::UnsupportedComponentsException, - /'Xx'$/, - ) + context 'along with a known vector' do + it 'parses the vector' do + vector_of_trust = 'C1.C2.Xx' + + result = Vot::Parser.new(vector_of_trust:).parse + + expect(result.component_values.map(&:name).join(' ')).to eq( + 'C1 C2', + ) + expect(result.aal2?).to eq(true) + expect(result.phishing_resistant?).to eq(false) + expect(result.hspd12?).to eq(false) + expect(result.identity_proofing?).to eq(false) + expect(result.facial_match?).to eq(false) + expect(result.ialmax?).to eq(false) + expect(result.enhanced_ipp?).to eq(false) + end end end end From 9986f9abb35f7d32103529389a676cd1a7729992 Mon Sep 17 00:00:00 2001 From: Shane Chesnutt Date: Fri, 25 Oct 2024 09:32:02 -0400 Subject: [PATCH 03/16] LG-14591 Cancel enrollments with deactivated profiles in proofing job (#11363) changelog: Internal, In-person proofing, Cancel enrollments with deactivated profiles during the proofing job Additional Work: Fix activate after passing in person profile method Update the profile activate_after_passing_in_person method to no longer nil out fields other than in_person_verification_pending_at before attempting to activate the profile. The previous implementation would allow profiles with deactivation reasons or with fraud pending reasons to be activated. --- app/jobs/get_usps_proofing_results_job.rb | 24 ++- app/models/in_person_enrollment.rb | 5 + app/models/profile.rb | 4 - app/services/analytics_events.rb | 6 +- ...get_proofing_results_job_scenarios_spec.rb | 42 ++-- .../get_usps_proofing_results_job_spec.rb | 62 ++++++ spec/models/in_person_enrollment_spec.rb | 29 +++ spec/models/profile_spec.rb | 183 ++++++++++++------ 8 files changed, 269 insertions(+), 86 deletions(-) diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index ea870cb815b..dfae07ef11f 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -31,6 +31,7 @@ def perform(_now) enrollments_network_error: 0, enrollments_expired: 0, enrollments_failed: 0, + enrollments_cancelled: 0, enrollments_in_progress: 0, enrollments_passed: 0, } @@ -106,6 +107,20 @@ def check_enrollment(enrollment) status_check_attempted_at = Time.zone.now enrollment_outcomes[:enrollments_checked] += 1 + profile_deactivation_reason = enrollment.profile_deactivation_reason + + if profile_deactivation_reason.present? + log_enrollment_updated_analytics( + enrollment: enrollment, + enrollment_passed: false, + enrollment_completed: true, + response: nil, + reason: "Profile has a deactivation reason of #{profile_deactivation_reason}", + ) + cancel_enrollment(enrollment) + return + end + response = proofer.request_proofing_results( enrollment, ) @@ -125,6 +140,12 @@ def check_enrollment(enrollment) enrollment.update(status_check_attempted_at: status_check_attempted_at) end + def cancel_enrollment(enrollment) + enrollment_outcomes[:enrollments_cancelled] += 1 + enrollment.cancelled! + enrollment.profile.deactivate_due_to_in_person_verification_cancelled + end + def passed_with_unsupported_secondary_id_type?(enrollment, response) return false if enrollment.enhanced_ipp? @@ -374,8 +395,7 @@ def handle_fraud_review_pending(enrollment) def handle_unexpected_response(enrollment, response_message, reason:, cancel: true) if cancel - enrollment.cancelled! - enrollment.profile.deactivate_due_to_in_person_verification_cancelled + cancel_enrollment(enrollment) end analytics(user: enrollment.user). idv_in_person_usps_proofing_results_job_unexpected_response( diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 2490bf46db2..27aa547860e 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -155,6 +155,11 @@ def enhanced_ipp? IdentityConfig.store.usps_eipp_sponsor_id == sponsor_id end + # @return [String, nil] The enrollment's profile deactivation reason or nil. + def profile_deactivation_reason + profile&.deactivation_reason + end + private def days_to_expire diff --git a/app/models/profile.rb b/app/models/profile.rb index 1e29f7dada1..be986e03cf4 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -159,10 +159,6 @@ def activate_after_fraud_review_unnecessary def activate_after_passing_in_person transaction do update!( - fraud_review_pending_at: nil, - fraud_rejection_at: nil, - fraud_pending_reason: nil, - deactivation_reason: nil, in_person_verification_pending_at: nil, ) activate diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index c0f90702f37..25ed4fb4c79 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3238,20 +3238,20 @@ def idv_in_person_usps_proofing_results_job_enrollment_incomplete( # @param [String] enrollment_code # @param [String] enrollment_id # @param [Float] minutes_since_established - # @param [Boolean] fraud_suspected # @param [Boolean] passed did this enrollment pass or fail? # @param [String] reason why did this enrollment pass or fail? # @param [String] tmx_status the tmx_status of the enrollment profile profile # @param [Integer] profile_age_in_seconds How many seconds have passed since profile created + # @param [Boolean] fraud_suspected def idv_in_person_usps_proofing_results_job_enrollment_updated( enrollment_code:, enrollment_id:, minutes_since_established:, - fraud_suspected:, passed:, reason:, tmx_status:, profile_age_in_seconds:, + fraud_suspected: nil, **extra ) track_event( @@ -3259,11 +3259,11 @@ def idv_in_person_usps_proofing_results_job_enrollment_updated( enrollment_code: enrollment_code, enrollment_id: enrollment_id, minutes_since_established: minutes_since_established, - fraud_suspected: fraud_suspected, passed: passed, reason: reason, tmx_status: tmx_status, profile_age_in_seconds: profile_age_in_seconds, + fraud_suspected: fraud_suspected, **extra, ) end diff --git a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb index e98b057d677..08be64fae88 100644 --- a/spec/features/idv/get_proofing_results_job_scenarios_spec.rb +++ b/spec/features/idv/get_proofing_results_job_scenarios_spec.rb @@ -65,19 +65,19 @@ # When the user logs out logout(@user) # And the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) - # And the user has an InPersonEnrollment with status "passed" + # Then the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'cancelled', ) - # And the user has a Profile that is active + # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( - active: true, - deactivation_reason: nil, + active: false, + deactivation_reason: 'encryption_error', in_person_verification_pending_at: nil, ) @@ -86,9 +86,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "passed" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -149,9 +149,9 @@ # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) - # Then the user has an InPersonEnrollment with status "failed|cancelled|expired" + # Then the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: status, + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -165,9 +165,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "failed|cancelled|expired" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: status, + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" expect(@user.in_person_enrollments.first.profile).to have_attributes( @@ -205,7 +205,7 @@ ) # And the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) @@ -313,7 +313,7 @@ ) # When the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) @@ -378,7 +378,7 @@ ) # When the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) @@ -554,14 +554,14 @@ # When the user logs out logout(@user) # And the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) - # And the user has an InPersonEnrollment with status "passed" + # Then the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" and # pending fraud review @@ -578,9 +578,9 @@ # Then the user is taken to the /verify/welcome page expect(current_path).to eq(idv_welcome_path) - # And the user has an InPersonEnrollment with status "passed" + # And the user has an InPersonEnrollment with status "cancelled" expect(@user.in_person_enrollments.first).to have_attributes( - status: 'passed', + status: 'cancelled', ) # And the user has a Profile that is deactivated with reason "encryption_error" and # pending fraud review @@ -634,7 +634,7 @@ ) # And the user visits USPS to complete their enrollment - # And USPS enrollment passed + # And USPS enrollment "passed" stub_request_passed_proofing_results # And GetUspsProofingResultsJob is performed perform_get_usps_proofing_results_job(@user) diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index aa7a524833e..9b4b2a4716e 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -15,6 +15,7 @@ enrollments_network_error: 0, enrollments_expired: 0, enrollments_failed: 0, + enrollments_cancelled: 0, enrollments_in_progress: 0, enrollments_passed: 0, duration_seconds: 0.0, @@ -794,6 +795,7 @@ ).with( **default_job_completion_analytics, enrollments_checked: 1, + enrollments_cancelled: 1, ) end end @@ -872,6 +874,7 @@ ).with( **default_job_completion_analytics, enrollments_checked: 1, + enrollments_cancelled: 1, ) end end @@ -2666,6 +2669,65 @@ end end + context 'when the enrollment has a profile with a deactivation reason' do + let(:deactivation_reason) { 'encryption_error' } + + before do + enrollment.profile.update(deactivation_reason: deactivation_reason) + allow(analytics).to receive(:idv_in_person_usps_proofing_results_job_enrollment_updated) + subject.perform(current_time) + end + + it 'logs the job started analytic' do + expect(analytics).to have_received( + :idv_in_person_usps_proofing_results_job_started, + ).with( + enrollments_count: 1, + reprocess_delay_minutes: 5, + job_name: described_class.name, + ) + end + + it 'logs the job enrollment updated analytic' do + expect(analytics).to have_received( + :idv_in_person_usps_proofing_results_job_enrollment_updated, + ).with( + **enrollment_analytics, + response_present: false, + passed: false, + reason: "Profile has a deactivation reason of #{deactivation_reason}", + job_name: described_class.name, + tmx_status: nil, + profile_age_in_seconds: enrollment.profile&.profile_age_in_seconds, + enhanced_ipp: false, + ) + end + + it 'cancels the enrollment' do + expect(enrollment.reload).to have_attributes( + status: 'cancelled', + ) + end + + it "deactivates the enrollment's profile" do + expect(enrollment.reload.profile).to have_attributes( + active: false, + deactivation_reason: 'encryption_error', + in_person_verification_pending_at: nil, + ) + end + + it 'logs the job completed analytic' do + expect(analytics).to have_received( + :idv_in_person_usps_proofing_results_job_completed, + ).with( + **default_job_completion_analytics, + enrollments_checked: 1, + enrollments_cancelled: 1, + ) + end + end + context 'when the enrollment does not have a unique_id' do before do enrollment.update(unique_id: nil) diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index 751be21ee86..0a6e70ac233 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -533,4 +533,33 @@ end end end + + describe '#profile_deactivation_reason' do + let(:profile) { create(:profile) } + let(:enrollment) { create(:in_person_enrollment, user: profile.user, profile: profile) } + + context "when the enrollment's profile has a deactivation reason" do + before do + profile.update!(deactivation_reason: 'encryption_error') + end + + it "returns the profile's deactivation reason" do + expect(enrollment.profile_deactivation_reason).to eq('encryption_error') + end + end + + context 'when the profile does not have a deactivation reason' do + it 'returns nil' do + expect(enrollment.profile_deactivation_reason).to be_nil + end + end + + context 'when the enrollment does not have a profile' do + let(:enrollment) { create(:in_person_enrollment, user: profile.user, profile: nil) } + + it 'returns nil' do + expect(enrollment.profile_deactivation_reason).to be_nil + end + end + end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 7e709aba761..c2972bd3bce 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -801,74 +801,145 @@ end describe '#activate_after_passing_in_person' do - it 'activates a profile if it passes in person proofing' do - profile = create( - :profile, - :fraud_review_pending, - :in_person_verification_pending, - user: user, - ) + let(:current_time) { Time.zone.now } - expect(profile.activated_at).to be_nil # to change - expect(profile.active).to eq(false) # to change - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(true) # to change - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.in_person_verification_pending_at).to be_present - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_nil # to change - - profile.activate_after_passing_in_person + context 'when the profile does not have any reason not to activate' do + let(:profile) do + create( + :profile, + :in_person_verification_pending, + user: user, + ) + end - expect(profile.activated_at).to be_present # changed - expect(profile.active).to eq(true) # changed - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(false) # changed - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.in_person_verification_pending_at).to be_nil - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_present # changed + before do + freeze_time do + travel_to(current_time) + profile.activate_after_passing_in_person + end + end - expect(profile.fraud_review_pending_at).to be_nil - expect(profile.activated_at).not_to be_nil - expect(profile.deactivation_reason).to be_nil - expect(profile).to be_active # changed + it 'activates a profile' do + expect(profile).to have_attributes( + activated_at: current_time, + active: true, + deactivation_reason: nil, + gpo_verification_pending_at: nil, + in_person_verification_pending_at: nil, + initiating_service_provider: nil, + verified_at: current_time, + fraud_review_pending_at: nil, + fraud_rejection_at: nil, + fraud_pending_reason: nil, + ) + end end - it 'does not activate a profile if transaction raises an error' do - profile = create( - :profile, - :in_person_verification_pending, - :fraud_review_pending, - user: user, - ) + context 'when the profile has a pending reason not to activate' do + let(:profile) do + create( + :profile, + :fraud_review_pending, + :in_person_verification_pending, + user: user, + ) + end - allow(profile).to receive(:update!).and_raise(RuntimeError) + before do + profile.activate_after_passing_in_person + rescue => err + @error = err + ensure + profile.reload + end - expect(profile.activated_at).to be_nil - expect(profile.active).to eq(false) - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(true) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.in_person_verification_pending_at).to be_present - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_nil + it 'throws an "Attempting to activate a profile with pending reason:" error' do + expect(@error.message).to eq( + 'Attempting to activate profile with pending reasons: fraud_check_pending', + ) + end - suppress(RuntimeError) do + it 'does not activate the profile' do + expect(profile).to have_attributes( + active: false, + activated_at: nil, + deactivation_reason: nil, + gpo_verification_pending_at: nil, + in_person_verification_pending_at: kind_of(Time), + initiating_service_provider: nil, + verified_at: nil, + fraud_review_pending_at: kind_of(Time), + fraud_rejection_at: nil, + fraud_pending_reason: 'threatmetrix_review', + ) + end + end + + context 'when the profile has a deactivation reason not to activate' do + let(:profile) do + create( + :profile, + :encryption_error, + :in_person_verification_pending, + user: user, + ) + end + + before do profile.activate_after_passing_in_person + rescue => err + @error = err + ensure + profile.reload end - expect(profile.activated_at).to be_nil - expect(profile.active).to eq(false) - expect(profile.deactivation_reason).to be_nil - expect(profile.fraud_review_pending?).to eq(true) - expect(profile.gpo_verification_pending_at).to be_nil - expect(profile.in_person_verification_pending_at).to be_present - expect(profile.initiating_service_provider).to be_nil - expect(profile.verified_at).to be_nil + it 'throws an "Attempting to activate a profile with deactivation reason:" error' do + expect(@error.message).to eq( + 'Attempting to activate profile with deactivation reason: encryption_error', + ) + end - expect(profile.deactivation_reason).to be_nil - expect(profile).to_not be_active + it 'does not activate the profile' do + expect(profile).to have_attributes( + active: false, + activated_at: nil, + deactivation_reason: 'encryption_error', + gpo_verification_pending_at: nil, + in_person_verification_pending_at: kind_of(Time), + initiating_service_provider: nil, + verified_at: nil, + fraud_review_pending_at: nil, + fraud_rejection_at: nil, + fraud_pending_reason: nil, + ) + end + end + + context 'when an update error occurs' do + let(:profile) { create(:profile, :in_person_verification_pending, user: user) } + + before do + allow(profile).to receive(:update!).and_raise(RuntimeError) + + suppress(RuntimeError) do + profile.activate_after_passing_in_person + end + end + + it 'does not activate the profile' do + expect(profile).to have_attributes( + active: false, + activated_at: nil, + deactivation_reason: nil, + gpo_verification_pending_at: nil, + in_person_verification_pending_at: kind_of(Time), + initiating_service_provider: nil, + verified_at: nil, + fraud_review_pending_at: nil, + fraud_rejection_at: nil, + fraud_pending_reason: nil, + ) + end end end From e2bfded29f1d08af075aa236b265428a5a4c33e1 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Fri, 25 Oct 2024 08:49:07 -0500 Subject: [PATCH 04/16] Upgrade to good_job v4 (#11377) changelog: Internal, Maintenance, Upgrade to good_job v4 --- Gemfile | 2 +- Gemfile.lock | 22 +++++++++---------- app/jobs/good_job_v4_ready_job.rb | 16 -------------- config/initializers/job_configurations.rb | 5 ----- ...dd_jobs_finished_at_to_good_job_batches.rb | 19 ++++++++++++++++ db/worker_jobs_schema.rb | 3 ++- spec/jobs/good_job_v4_ready_job_spec.rb | 19 ---------------- 7 files changed, 33 insertions(+), 53 deletions(-) delete mode 100644 app/jobs/good_job_v4_ready_job.rb create mode 100644 db/worker_jobs_migrate/20241022162624_add_jobs_finished_at_to_good_job_batches.rb delete mode 100644 spec/jobs/good_job_v4_ready_job_spec.rb diff --git a/Gemfile b/Gemfile index 70131c68b55..d865a112c2e 100644 --- a/Gemfile +++ b/Gemfile @@ -34,7 +34,7 @@ gem 'faker' gem 'faraday-retry' gem 'fugit' gem 'foundation_emails' -gem 'good_job', '~> 3.0' +gem 'good_job', '~> 4.0' gem 'http_accept_language' gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v4.0.0' gem 'identity-logging', github: '18F/identity-logging', tag: 'v0.1.1' diff --git a/Gemfile.lock b/Gemfile.lock index 149c90bb090..be007c550d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -339,13 +339,13 @@ GEM ffi (~> 1.0) globalid (1.2.1) activesupport (>= 6.1) - good_job (3.99.1) - activejob (>= 6.0.0) - activerecord (>= 6.0.0) - concurrent-ruby (>= 1.0.2) - fugit (>= 1.1) - railties (>= 6.0.0) - thor (>= 0.14.1) + good_job (4.4.2) + activejob (>= 6.1.0) + activerecord (>= 6.1.0) + concurrent-ruby (>= 1.3.1) + fugit (>= 1.11.0) + railties (>= 6.1.0) + thor (>= 1.0.0) google-protobuf (4.28.2) bigdecimal rake (>= 13) @@ -357,7 +357,7 @@ GEM htmlbeautifier (1.4.3) htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -372,7 +372,7 @@ GEM terminal-table (>= 1.5.1) ice_nine (0.11.2) io-console (0.7.2) - irb (1.13.2) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) @@ -743,7 +743,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.36) - zeitwerk (2.6.16) + zeitwerk (2.7.1) zlib (3.0.0) zonebie (0.6.1) zxcvbn (0.1.9) @@ -789,7 +789,7 @@ DEPENDENCIES faraday-retry foundation_emails fugit - good_job (~> 3.0) + good_job (~> 4.0) http_accept_language i18n-tasks (~> 1.0) identity-hostdata! diff --git a/app/jobs/good_job_v4_ready_job.rb b/app/jobs/good_job_v4_ready_job.rb deleted file mode 100644 index 0c8b9a41fd4..00000000000 --- a/app/jobs/good_job_v4_ready_job.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class GoodJobV4ReadyJob < ApplicationJob - queue_as :default - - def perform - IdentityJobLogSubscriber.new.logger.info( - { - name: 'good_job_v4_ready', - ready: GoodJob.v4_ready?, - }.to_json, - ) - - true - end -end diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index b856f61768b..a184672380a 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -164,11 +164,6 @@ class: 'ThreatMetrixJsVerificationJob', cron: cron_1h, }, - # Periodically check whether we can upgrade to GoodJob V4 - good_job_v4_ready: { - class: 'GoodJobV4ReadyJob', - cron: cron_1h, - }, # Reject profiles that have been in fraud_review_pending for 30 days fraud_rejection: { class: 'FraudRejectionDailyJob', diff --git a/db/worker_jobs_migrate/20241022162624_add_jobs_finished_at_to_good_job_batches.rb b/db/worker_jobs_migrate/20241022162624_add_jobs_finished_at_to_good_job_batches.rb new file mode 100644 index 00000000000..dc0d978bb7e --- /dev/null +++ b/db/worker_jobs_migrate/20241022162624_add_jobs_finished_at_to_good_job_batches.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddJobsFinishedAtToGoodJobBatches < ActiveRecord::Migration[7.1] + def change + reversible do |dir| + dir.up do + # Ensure this incremental update migration is idempotent + # with monolithic install migration. + return if connection.column_exists?(:good_job_batches, :jobs_finished_at) + end + end + + safety_assured do + change_table :good_job_batches do |t| + t.datetime :jobs_finished_at + end + end + end +end diff --git a/db/worker_jobs_schema.rb b/db/worker_jobs_schema.rb index 3e6fa136f59..2ce7f8fc09c 100644 --- a/db/worker_jobs_schema.rb +++ b/db/worker_jobs_schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_10_21_192437) do +ActiveRecord::Schema[7.2].define(version: 2024_10_22_162624) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -28,6 +28,7 @@ t.datetime "enqueued_at" t.datetime "discarded_at" t.datetime "finished_at" + t.datetime "jobs_finished_at" end create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/spec/jobs/good_job_v4_ready_job_spec.rb b/spec/jobs/good_job_v4_ready_job_spec.rb deleted file mode 100644 index 0a5c60645bb..00000000000 --- a/spec/jobs/good_job_v4_ready_job_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'rails_helper' - -RSpec.describe GoodJobV4ReadyJob, type: :job do - describe '#perform' do - it 'logs goodjob v4 readiness' do - expect(Rails.logger).to receive(:info) do |str| - msg = JSON.parse(str, symbolize_names: true) - expect(msg).to eq( - { - name: 'good_job_v4_ready', - ready: GoodJob.v4_ready?, - }, - ) - end - - GoodJobV4ReadyJob.new.perform - end - end -end From 3e0d513902056fea867dc149803017a776222999 Mon Sep 17 00:00:00 2001 From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:02:05 -0400 Subject: [PATCH 05/16] LG-14353 Re-label selected email session key (#11395) * changelog: Internal, tech debt, rename session key * update remaining keys --- app/controllers/concerns/saml_idp_auth_concern.rb | 4 +++- .../openid_connect/authorization_controller.rb | 4 +++- app/controllers/sign_up/completions_controller.rb | 6 +++--- app/controllers/sign_up/select_email_controller.rb | 6 +++--- spec/controllers/sign_up/select_email_controller_spec.rb | 8 ++++++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index e209ad0037f..f31c2f6c992 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -142,7 +142,9 @@ def link_identity_from_session_data def email_address_id return nil unless IdentityConfig.store.feature_select_email_to_share_enabled - return user_session[:selected_email_id] if user_session[:selected_email_id].present? + if user_session[:selected_email_id_for_linked_identity].present? + return user_session[:selected_email_id_for_linked_identity] + end identity = current_user.identities.find_by(service_provider: sp_session['issuer']) email_id = identity&.email_address_id return email_id if email_id.is_a? Integer diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 01944cf0858..f2d4f45a8b8 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -90,7 +90,9 @@ def link_identity_to_service_provider def email_address_id return nil unless IdentityConfig.store.feature_select_email_to_share_enabled - return user_session[:selected_email_id] if user_session[:selected_email_id].present? + if user_session[:selected_email_id_for_linked_identity].present? + return user_session[:selected_email_id_for_linked_identity] + end identity = current_user.identities.find_by(service_provider: sp_session['issuer']) identity&.email_address_id end diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 007e1609c95..dd9800dd9e2 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -21,8 +21,8 @@ def update track_completion_event('agency-page') update_verified_attributes send_in_person_completion_survey - if user_session[:selected_email_id].nil? - user_session[:selected_email_id] = EmailContext.new(current_user). + if user_session[:selected_email_id_for_linked_identity].nil? + user_session[:selected_email_id_for_linked_identity] = EmailContext.new(current_user). last_sign_in_email_address.id end if decider.go_back_to_mobile_app? @@ -53,7 +53,7 @@ def completions_presenter requested_attributes: decorated_sp_session.requested_attributes.map(&:to_sym), ial2_requested: ial2_requested?, completion_context: needs_completion_screen_reason, - selected_email_id: user_session[:selected_email_id], + selected_email_id: user_session[:selected_email_id_for_linked_identity], ) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb index 48e0ff0a821..c98f451027e 100644 --- a/app/controllers/sign_up/select_email_controller.rb +++ b/app/controllers/sign_up/select_email_controller.rb @@ -24,7 +24,7 @@ def create analytics.sp_select_email_submitted(**result.to_h, needs_completion_screen_reason:) if result.success? - user_session[:selected_email_id] = form_params[:selected_email_id] + user_session[:selected_email_id_for_linked_identity] = form_params[:selected_email_id] redirect_to sign_up_completed_path else flash[:error] = result.first_error_message @@ -47,8 +47,8 @@ def form_params end def last_email - if user_session[:selected_email_id] - user_emails.find(user_session[:selected_email_id]).email + if user_session[:selected_email_id_for_linked_identity] + user_emails.find(user_session[:selected_email_id_for_linked_identity]).email else EmailContext.new(current_user).last_sign_in_email_address.email end diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb index 8257d6f127d..2e9a841af3d 100644 --- a/spec/controllers/sign_up/select_email_controller_spec.rb +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -70,7 +70,9 @@ it 'updates selected email address' do response - expect(controller.user_session[:selected_email_id]).to eq(selected_email.id.to_s) + expect( + controller.user_session[:selected_email_id_for_linked_identity], + ).to eq(selected_email.id.to_s) end it 'logs analytics event' do @@ -91,7 +93,9 @@ it 'rejects email not belonging to the user' do expect(response).to redirect_to(sign_up_select_email_path) - expect(controller.user_session[:selected_email_id]).to eq(nil) + expect( + controller.user_session[:selected_email_id_for_linked_identity], + ).to eq(nil) end it 'logs analytics event' do From 774fa2d736cce4cb7e0cd854c492b090f8c7dcf4 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Fri, 25 Oct 2024 10:56:49 -0500 Subject: [PATCH 06/16] Enable rubocop rule to disallow OpenStruct usage (#11397) * Enable rubocop rule to disallow OpenStruct usage changelog: Internal, Performance, Enable rubocop rule to disallow OpenStruct usage * Update spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --------- Co-authored-by: Andrew Duthie <1779930+aduth@users.noreply.github.com> --- .rubocop.yml | 3 +++ Gemfile.lock | 2 +- app/presenters/idv/welcome_presenter.rb | 7 ++++++- app/services/proofing/aamva/proofer.rb | 3 +-- .../idv/in_person/usps_locations_controller_spec.rb | 2 +- spec/lib/custom_devise_failure_app_spec.rb | 2 +- spec/models/profile_spec.rb | 4 ++-- .../piv_cac_authentication_setup_presenter_spec.rb | 2 +- spec/services/proofing/aamva/proofer_spec.rb | 2 +- spec/services/proofing/lexis_nexis/request_signer_spec.rb | 2 +- .../piv_cac_authentication_setup/new.html.erb_spec.rb | 2 +- 11 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index c882e675be8..46eb823c2b6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1292,6 +1292,9 @@ Style/NumericLiteralPrefix: Style/OneLineConditional: Enabled: true +Style/OpenStructUse: + Enabled: true + Style/OptionalArguments: Enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index be007c550d2..49dc0f9ceb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -742,7 +742,7 @@ GEM nokogiri (~> 1.11) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.36) + yard (0.9.37) zeitwerk (2.7.1) zlib (3.0.0) zonebie (0.6.1) diff --git a/app/presenters/idv/welcome_presenter.rb b/app/presenters/idv/welcome_presenter.rb index c9a3c2c57b2..d2a1fc3f7af 100644 --- a/app/presenters/idv/welcome_presenter.rb +++ b/app/presenters/idv/welcome_presenter.rb @@ -2,6 +2,11 @@ module Idv class WelcomePresenter + BulletPoint = Struct.new( + :bullet, + :text, + keyword_init: true, + ) include ActionView::Helpers::TranslationHelper include Rails.application.routes.url_helpers include LinkHelper @@ -70,7 +75,7 @@ def current_user end def bullet_point(bullet, text) - OpenStruct.new(bullet: bullet, text: text) + BulletPoint.new(bullet: bullet, text: text) end def first_time_idv? diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index 0ce39e7d144..fd7493632bc 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -50,8 +50,7 @@ def initialize(config) end def proof(applicant) - aamva_applicant = - Aamva::Applicant.from_proofer_applicant(OpenStruct.new(applicant)) + aamva_applicant = Aamva::Applicant.from_proofer_applicant(applicant) response = Aamva::VerificationClient.new( config, diff --git a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb index 10572bc74f8..9ba5c554b0b 100644 --- a/spec/controllers/idv/in_person/usps_locations_controller_spec.rb +++ b/spec/controllers/idv/in_person/usps_locations_controller_spec.rb @@ -455,7 +455,7 @@ context 'with failed doc_auth_result' do before do allow(controller).to receive(:document_capture_session).and_return( - OpenStruct.new({ last_doc_auth_result: 'Failed' }), + DocumentCaptureSession.new(last_doc_auth_result: 'Failed'), ) end diff --git a/spec/lib/custom_devise_failure_app_spec.rb b/spec/lib/custom_devise_failure_app_spec.rb index 7e37caa8660..e0dc6f791ef 100644 --- a/spec/lib/custom_devise_failure_app_spec.rb +++ b/spec/lib/custom_devise_failure_app_spec.rb @@ -5,7 +5,7 @@ subject(:failure_app) { CustomDeviseFailureApp.new } let(:message) { :invalid } - let(:env) { { 'warden' => OpenStruct.new(message:) } } + let(:env) { { 'warden' => double('warden_proxy', message:) } } let(:request) { ActionDispatch::Request.new(env) } before do diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index c2972bd3bce..57f9631c173 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -1199,8 +1199,8 @@ # controller's params. As this is a model spec, we have to fake # the params object. fake_params = ActionController::Parameters.new( - user: OpenStruct.new(id: 'fake_user_id'), - email_address: OpenStruct.new(user_id: 'fake_user_id', email: 'fake_user@test.com'), + user: User.new(id: 'fake_user_id'), + email_address: EmailAddress.new(user_id: 'fake_user_id', email: 'fake_user@test.com'), ) allow_any_instance_of(UserMailer).to receive(:params).and_return(fake_params) end diff --git a/spec/presenters/piv_cac_authentication_setup_presenter_spec.rb b/spec/presenters/piv_cac_authentication_setup_presenter_spec.rb index 69d0e04d7cd..465a86ea98c 100644 --- a/spec/presenters/piv_cac_authentication_setup_presenter_spec.rb +++ b/spec/presenters/piv_cac_authentication_setup_presenter_spec.rb @@ -4,7 +4,7 @@ let(:user) { create(:user) } let(:presenter) { described_class.new(user, false, form) } let(:form) do - OpenStruct.new + UserPivCacSetupForm.new end describe '#title' do diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index cab58477c75..e14c2f44aa0 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Proofing::Aamva::Proofer do let(:aamva_applicant) do - Aamva::Applicant.from_proofer_applicant(OpenStruct.new(state_id_data)) + Aamva::Applicant.from_proofer_applicant(state_id_data) end let(:state_id_data) do diff --git a/spec/services/proofing/lexis_nexis/request_signer_spec.rb b/spec/services/proofing/lexis_nexis/request_signer_spec.rb index c0dc555654d..c585da3b1b3 100644 --- a/spec/services/proofing/lexis_nexis/request_signer_spec.rb +++ b/spec/services/proofing/lexis_nexis/request_signer_spec.rb @@ -6,7 +6,7 @@ let(:timestamp) { Time.zone.now.strftime('%s%L') } let(:nonce) { SecureRandom.uuid } let(:config) do - OpenStruct.new( + Proofing::LexisNexis::Config.new( base_url: 'https://example.gov', hmac_key_id: 'KEY_ID', hmac_secret_key: 'SECRET_KEY', diff --git a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb index 4409370f49f..bf21e0b8ffa 100644 --- a/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb +++ b/spec/views/users/piv_cac_authentication_setup/new.html.erb_spec.rb @@ -11,7 +11,7 @@ allow(view).to receive(:user_session).and_return(user_session) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:in_multi_mfa_selection_flow?).and_return(in_multi_mfa_selection_flow) - form = OpenStruct.new + form = UserPivCacSetupForm.new @presenter = PivCacAuthenticationSetupPresenter.new(user, true, form) end From e11ea7ddc249fa1e1f77755fe0374752767f34a5 Mon Sep 17 00:00:00 2001 From: Stephen Shelton Date: Fri, 25 Oct 2024 12:21:41 -0400 Subject: [PATCH 07/16] Changing LOGIN_ENV to properly construct piv url (#11401) * changelog: Internal, CI, Changing LOGIN_ENV to match review app environment --- dockerfiles/application.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dockerfiles/application.yaml b/dockerfiles/application.yaml index 94447e9e008..36d224df518 100644 --- a/dockerfiles/application.yaml +++ b/dockerfiles/application.yaml @@ -47,7 +47,7 @@ spec: value: "prefer" - op: add path: /data/LOGIN_ENV - value: "{{ENVIRONMENT}}" + value: "reviewapps" - op: add path: /data/LOGIN_HOST_ROLE value: "idp" @@ -89,7 +89,7 @@ spec: value: "true" - op: add path: /data/LOGIN_ENV - value: "review-app" + value: "reviewapps" - op: add path: /data/LOGIN_DOMAIN value: "identitysandbox.gov" @@ -120,7 +120,7 @@ spec: value: "prefer" - op: add path: /data/LOGIN_ENV - value: "{{ENVIRONMENT}}" + value: "reviewapps" - op: add path: /data/LOGIN_HOST_ROLE value: "idp" @@ -162,7 +162,7 @@ spec: value: "true" - op: add path: /data/LOGIN_ENV - value: "review-app" + value: "reviewapps" - op: add path: /data/LOGIN_DOMAIN value: "identitysandbox.gov" @@ -188,7 +188,7 @@ spec: value: "{{ENVIRONMENT}}-idp-pg.review-apps" - op: add path: /data/LOGIN_ENV - value: "{{ENVIRONMENT}}" + value: "reviewapps" - op: add path: /data/LOGIN_HOST_ROLE value: "worker" @@ -225,7 +225,7 @@ spec: value: "{{ENVIRONMENT}}-idp-pg.review-apps" - op: add path: /data/LOGIN_ENV - value: "{{ENVIRONMENT}}" + value: "reviewapps" - op: add path: /data/LOGIN_HOST_ROLE value: "worker" From 0e2e6c51f1c4f7a086624bf4be67fc9aa13323f8 Mon Sep 17 00:00:00 2001 From: Matt Wagner Date: Fri, 25 Oct 2024 12:45:25 -0400 Subject: [PATCH 08/16] LG-14641 | Update "letter on the way" screen (#11346) * LG-14651 | Update "letter on the way" screen changelog: User-Facing Improvements, GPO flow, UI tweaks --- .../redirect/marketing_site_controller.rb | 9 +++++++ .../idv/by_mail/letter_enqueued_presenter.rb | 18 ++++++------- .../idv/by_mail/letter_enqueued/show.html.erb | 26 ++++++++++++++----- config/locales/en.yml | 5 ++-- config/locales/es.yml | 5 ++-- config/locales/fr.yml | 5 ++-- config/locales/zh.yml | 5 ++-- config/routes.rb | 1 + .../marketing_site_controller_spec.rb | 21 +++++++++++++++ .../by_mail/letter_enqueued_presenter_spec.rb | 20 ++------------ .../letter_enqueued/show.html.erb_spec.rb | 10 +++---- 11 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 app/controllers/redirect/marketing_site_controller.rb create mode 100644 spec/controllers/redirect/marketing_site_controller_spec.rb diff --git a/app/controllers/redirect/marketing_site_controller.rb b/app/controllers/redirect/marketing_site_controller.rb new file mode 100644 index 00000000000..cc21dfd5d9d --- /dev/null +++ b/app/controllers/redirect/marketing_site_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Redirect + class MarketingSiteController < RedirectController + def show + redirect_to_and_log(MarketingSite.base_url) + end + end +end diff --git a/app/presenters/idv/by_mail/letter_enqueued_presenter.rb b/app/presenters/idv/by_mail/letter_enqueued_presenter.rb index 1f0e4ae2401..fc1592e56cc 100644 --- a/app/presenters/idv/by_mail/letter_enqueued_presenter.rb +++ b/app/presenters/idv/by_mail/letter_enqueued_presenter.rb @@ -23,22 +23,22 @@ def address_lines ].compact end - def button_text - if sp - t('idv.cancel.actions.exit', app_name: APP_NAME) - else - t('idv.buttons.continue_plain') - end - end - def button_destination if sp return_to_sp_cancel_path(step: :verify_address, location: :come_back_later) else - account_path + marketing_site_redirect_path end end + def sp_name + sp&.friendly_name + end + + def show_sp_contact_instructions? + sp_name.present? + end + private attr_accessor :idv_session, :user_session, :current_user diff --git a/app/views/idv/by_mail/letter_enqueued/show.html.erb b/app/views/idv/by_mail/letter_enqueued/show.html.erb index 9beba0a11f7..3462515bd61 100644 --- a/app/views/idv/by_mail/letter_enqueued/show.html.erb +++ b/app/views/idv/by_mail/letter_enqueued/show.html.erb @@ -19,7 +19,7 @@ <%= render PageHeadingComponent.new.with_content(t('idv.titles.come_back_later')) %> -

<%= t('idv.gpo.will_send_to') %>

+

<%= t('idv.gpo.will_send_to', app_name: APP_NAME) %>

<% @presenter.address_lines.each do |address_line| %> @@ -27,13 +27,27 @@ <% end %>
-

- <%= t('idv.messages.come_back_later_html') %> -

+<% if @presenter.show_sp_contact_instructions? %> +
    +
  • + <%= t('idv.messages.come_back_later') %> +
  • +
  • + <%= t( + 'idv.messages.contact_sp_html', + sp_link_html: link_to(@presenter.sp_name, @presenter.button_destination), + ) %> +
  • +
+<% else %> +

+ <%= t('idv.messages.come_back_later') %> +

+<% end %> <%= link_to( - @presenter.button_text, + t('idv.cancel.actions.exit', app_name: APP_NAME), @presenter.button_destination, - class: 'usa-button usa-button--big usa-button--wide', + class: 'usa-button usa-button--big usa-button--wide margin-top-2', ) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index c4e56d5d3c2..1b9f639c3e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1107,12 +1107,13 @@ idv.gpo.request_another_letter.learn_more_link: Learn more about verifying your idv.gpo.request_another_letter.title: Request another letter? idv.gpo.return_to_profile: Return to your profile idv.gpo.title: Enter your verification code -idv.gpo.will_send_to: 'We’ll mail a letter to:' +idv.gpo.will_send_to: 'You’ll get a letter in the mail from %{app_name} in 5 to 10 days at the address we verified and associated with you:' idv.images.come_back_later: Letter with a check mark idv.messages.activated_html: Your identity has been verified. If you need to change your verified information, please %{link_html}. idv.messages.activated_link: contact us -idv.messages.come_back_later_html: 'Letters take 5 to 10 days to arrive. Sign back in to enter your verification code once you get your letter.' +idv.messages.come_back_later: Sign back in and enter the verification code when your letter arrives. idv.messages.confirm: We secured your verified information +idv.messages.contact_sp_html: Contact %{sp_link_html} if you need to access their services before your letter arrives. idv.messages.enter_password.by_mail_password_reminder_html: 'Remember your password. The verification code in your letter won’t work if you reset your password later.' idv.messages.enter_password.message: '%{app_name} will encrypt your information with your password. This means that your information is secure and only you will be able to access or change it.' idv.messages.enter_password.phone_verified: We verified your phone number diff --git a/config/locales/es.yml b/config/locales/es.yml index 5ab154e9ebd..6c642f86249 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1118,12 +1118,13 @@ idv.gpo.request_another_letter.learn_more_link: Obtenga más información sobre idv.gpo.request_another_letter.title: '¿Solicitar otra carta?' idv.gpo.return_to_profile: Vuelva a su perfil idv.gpo.title: Introduzca su código de verificación -idv.gpo.will_send_to: 'La hemos enviado a:' +idv.gpo.will_send_to: 'En un plazo de 5 a 10 días, recibirá por correo una carta de %{app_name} en la dirección que verificamos y asociamos con usted:' idv.images.come_back_later: Carta con una marca de verificación idv.messages.activated_html: Se verificó su identidad. Si necesita cambiar la información verificada, %{link_html}. idv.messages.activated_link: contáctenos -idv.messages.come_back_later_html: 'Las cartas tardan entre 5 y 10 días en llegar. Vuelva a iniciar sesión para introducir el código de verificación cuando reciba la carta.' +idv.messages.come_back_later: Cuando reciba su carta, vuelva a iniciar sesión e ingrese el código de verificación. idv.messages.confirm: Hemos protegido su información verificada +idv.messages.contact_sp_html: Contacte con %{sp_link_html} si necesita acceder a los servicios de esa agencia antes de que llegue su carta. idv.messages.enter_password.by_mail_password_reminder_html: 'Recuerde su contraseña. El código de verificación de su carta no funcionará si restablece su contraseña más adelante.' idv.messages.enter_password.message: '%{app_name} cifrará su información con su contraseña. Esto significa que su información está protegida y solo usted puede acceder a ella o modificarla.' idv.messages.enter_password.phone_verified: Verificamos su número de teléfono diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 1c7fd9cb6e1..5dbd241726e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1107,12 +1107,13 @@ idv.gpo.request_another_letter.learn_more_link: En savoir plus sur la vérificat idv.gpo.request_another_letter.title: Vous voulez demander une autre lettre ? idv.gpo.return_to_profile: Retourner à votre profil idv.gpo.title: Saisir votre code de vérification -idv.gpo.will_send_to: 'Nous vous enverrons un courrier à :' +idv.gpo.will_send_to: 'Vous recevrez un courrier de %{app_name} d’ici 5 à 10 jours à l’adresse que nous avons confirmée qui est associée à votre nom :' idv.images.come_back_later: Lettre avec une coche idv.messages.activated_html: Votre identité a été vérifiée. Si vous souhaitez modifier les informations qui ont été vérifiées, veuillez %{link_html}. idv.messages.activated_link: nous contacter -idv.messages.come_back_later_html: 'Les lettres mettent de 5 à 10 jours pour arriver. Veuillez vous reconnecter après avoir reçu le courrier pour saisir votre code de vérification.' +idv.messages.come_back_later: Une fois ce courrier reçu, reconnectez-vous pour saisir le code de vérification qui y figure. idv.messages.confirm: Nous avons sécurisé vos informations vérifiées +idv.messages.contact_sp_html: Contactez %{sp_link_html} si vous avez besoin d’accéder à ses services avant l’arrivée de votre lettre. idv.messages.enter_password.by_mail_password_reminder_html: 'Mémorisez votre mot de passe. Le code de vérification contenu dans votre lettre ne fonctionnera pas si vous réinitialisez votre mot de passe par la suite.' idv.messages.enter_password.message: '%{app_name} chiffre vos informations avec votre mot de passe. Cela signifie que vos informations sont sécurisées et que vous seul pourrez y accéder ou les modifier.' idv.messages.enter_password.phone_verified: Nous avons vérifié votre numéro de téléphone diff --git a/config/locales/zh.yml b/config/locales/zh.yml index d7e911f2dc5..7f2e53a6730 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1120,12 +1120,13 @@ idv.gpo.request_another_letter.learn_more_link: 对通过邮件验证你地址 idv.gpo.request_another_letter.title: 要求再发一封信? idv.gpo.return_to_profile: 返回你的用户资料 idv.gpo.title: 输入你的验证代码 -idv.gpo.will_send_to: 我们会把信寄发到: +idv.gpo.will_send_to: 你会在5到10天内在我们验证过的与你相关的地址收到来自%{app_name}寄给你的一封信: idv.images.come_back_later: 带有打勾符的信件 idv.messages.activated_html: 你的身份已经验证。如要更改已验证过的你的信息,请 %{link_html}。 idv.messages.activated_link: 联系我们 -idv.messages.come_back_later_html: '信件需要 5 - 10 日送到。收到信后请重新登录来输入你的验证代码。' +idv.messages.come_back_later: 收到信件后请再登录并输入其中的验证码。 idv.messages.confirm: 我们对你验证过的信息做了安全处理 +idv.messages.contact_sp_html: 如果收到信件之前你需要访问我们合作伙伴机构的服务,请联系%{sp_link_html}。 idv.messages.enter_password.by_mail_password_reminder_html: '记住你的密码。 如果你随后重设密码的话,信中的验证码就不会奏效。' idv.messages.enter_password.message: '%{app_name}会用你的密码对你的账户加密。加密意味着你的信息很安全,而且只有你能够访问或变更你的信息。' idv.messages.enter_password.phone_verified: 我们验证了你的电话号码 diff --git a/config/routes.rb b/config/routes.rb index 0a903eab6c9..41308b87004 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -329,6 +329,7 @@ get '/redirect/help_center' => 'redirect/help_center#show', as: :help_center_redirect get '/redirect/contact/' => 'redirect/contact#show', as: :contact_redirect get '/redirect/policy/' => 'redirect/policy#show', as: :policy_redirect + get '/redirect/marketing' => 'redirect/marketing_site#show', as: :marketing_site_redirect get '/sign_up/completed/cancel/' => 'completions_cancellation#show' match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] diff --git a/spec/controllers/redirect/marketing_site_controller_spec.rb b/spec/controllers/redirect/marketing_site_controller_spec.rb new file mode 100644 index 00000000000..35b32bb8b9f --- /dev/null +++ b/spec/controllers/redirect/marketing_site_controller_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe Redirect::MarketingSiteController do + subject(:response) { get :show } + + before { stub_analytics } + + describe '#show' do + it 'redirects to the marketing site' do + expect(response).to redirect_to MarketingSite.base_url + end + + it 'logs an event' do + response + expect(@analytics).to have_logged_event( + 'External Redirect', + hash_including(redirect_url: MarketingSite.base_url), + ) + end + end +end diff --git a/spec/presenters/idv/by_mail/letter_enqueued_presenter_spec.rb b/spec/presenters/idv/by_mail/letter_enqueued_presenter_spec.rb index 5b370fa4e3f..a2491638987 100644 --- a/spec/presenters/idv/by_mail/letter_enqueued_presenter_spec.rb +++ b/spec/presenters/idv/by_mail/letter_enqueued_presenter_spec.rb @@ -114,26 +114,10 @@ def add_to_gpo_pending_profile(pii:) end end - describe '#button_text' do - context 'when there is no SP' do - it 'is a plain Continue button' do - expect(presenter.button_text).to eq(t('idv.buttons.continue_plain')) - end - end - - context 'when there is an SP' do - let(:service_provider) { double('service provider') } - - it 'is an Exit button' do - expect(presenter.button_text).to eq(t('idv.cancel.actions.exit', app_name: APP_NAME)) - end - end - end - describe '#button_destination' do context 'when there is no SP' do - it 'is the account page' do - expect(presenter.button_destination).to eq(account_path) + it 'is a redirect to the marketing page' do + expect(presenter.button_destination).to eq(marketing_site_redirect_path) end end diff --git a/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb b/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb index 206f436857b..71fa3703066 100644 --- a/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb +++ b/spec/views/idv/by_mail/letter_enqueued/show.html.erb_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' RSpec.describe 'idv/by_mail/letter_enqueued/show.html.erb' do - let(:service_provider) { '🔒🌐💻' } + let(:service_provider) { create(:service_provider) } let(:step_indicator_steps) { Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS_GPO } let(:applicant_pii) { Idp::Constants::MOCK_IDV_APPLICANT } @@ -34,7 +34,7 @@ it 'renders the come back later message' do expect(rendered).to have_content( strip_tags( - t('idv.messages.come_back_later_html'), + t('idv.messages.come_back_later'), ), ) end @@ -51,10 +51,10 @@ context 'without an SP' do let(:service_provider) { nil } - it 'renders a return to account button' do + it 'renders a return to Login.gov button' do expect(rendered).to have_link( - t('idv.buttons.continue_plain'), - href: account_path, + t('idv.cancel.actions.exit', app_name: APP_NAME), + href: marketing_site_redirect_path, ) end end From d7777bf3606fba626721d78f538fd10e2e2193b8 Mon Sep 17 00:00:00 2001 From: KeithNava <134446588+KeithNava@users.noreply.github.com> Date: Fri, 25 Oct 2024 12:50:04 -0400 Subject: [PATCH 09/16] LG-13006: remove instances of skip_doc_auth - step 4/6 (#11338) * feat: remove instances of skip_doc_auth * changelog: Internal, In-person proofing, remove old skip_doc_auth variable * feat: remove skip_doc_auth from hybrid handoff * feat: fix linting * feat: update selected_remote helper method * feat: add proper variables from session * feat: update new variable on the frontend * feat: lintfix * feat: more lintfixes * feat: remove skip_doc_auth_from_handoff in controller * feat: update checks for hybrid_handoff controller * feat: revert removal of skip_doc_auth from how to verify controller * feat: more updates to how_to_verify controller * feat: lintfix * feat: remove skipDocAuth from comment * feat: add back old var for 50/50 testing * feat: lintfix * feat: revert removal of skip_doc_auth * feat: updates to utilize skipDocAuth in flow along side skipDocAuthFromHowToVerify * feat: lint fix * feat: pull out space and line updates * fix: another whitespace fix * space fix * fix: another whitespace line fix * feat: sync up with git * lintfix * feat: lintfix --- app/controllers/idv/hybrid_handoff_controller.rb | 6 ++++-- .../components/document-capture.tsx | 15 ++++++++++----- .../components/in-person-prepare-step.tsx | 3 ++- .../document-capture/context/in-person.ts | 7 +++++++ app/javascript/packs/document-capture.tsx | 3 +++ 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index f5144913502..0702bf9b829 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -47,9 +47,11 @@ def self.selected_remote(idv_session:) if IdentityConfig.store.in_person_proofing_opt_in_enabled && IdentityConfig.store.in_person_proofing_enabled && idv_session.service_provider&.in_person_proofing_enabled - idv_session.skip_doc_auth == false + idv_session.skip_doc_auth_from_how_to_verify == false || + idv_session.skip_doc_auth == false else - idv_session.skip_doc_auth.nil? || + idv_session.skip_doc_auth_from_how_to_verify.nil? || + idv_session.skip_doc_auth_from_how_to_verify == false || idv_session.skip_doc_auth.nil? || idv_session.skip_doc_auth == false end end diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index 6ffda351cab..201aa85d315 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -39,8 +39,13 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { const { flowPath } = useContext(UploadContext); const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext); const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext); - const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth, skipDocAuthFromHandoff } = - useContext(InPersonContext); + const { + inPersonFullAddressEntryEnabled, + inPersonURL, + skipDocAuth, + skipDocAuthFromHandoff, + skipDocAuthFromHowToVerify, + } = useContext(InPersonContext); useDidUpdateEffect(onStepChange, [stepName]); useEffect(() => { if (stepName) { @@ -135,9 +140,9 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { if (submissionError && formValues) { initialValues = formValues; } - // If the user got here by opting-in to in-person proofing, when skipDocAuth === true, + // If the user got here by opting-in to in-person proofing, when skipDocAuthFromHowToVerify === true || skipDocAuth === true, // then set steps to inPersonSteps - const isInPersonStepEnabled = skipDocAuth || skipDocAuthFromHandoff; + const isInPersonStepEnabled = skipDocAuthFromHowToVerify || skipDocAuthFromHandoff || skipDocAuth; const inPersonSteps: FormStep[] = inPersonURL === undefined ? [] @@ -151,7 +156,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { } else if (submissionError) { steps = [reviewFormStep, ...inPersonSteps]; } - // If the user got here by opting-in to in-person proofing, when skipDocAuth === true; + // If the user got here by opting-in to in-person proofing, when skipDocAuthFromHowToVerify === true || skipDocAuth === true; // or opting-in ipp from handoff page, and selfie is required, when skipDocAuthFromHandoff === true // then set stepIndicatorPath to VerifyFlowPath.IN_PERSON const stepIndicatorPath = diff --git a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx index a8de5bbe374..5e5573d137b 100644 --- a/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-prepare-step.tsx @@ -20,6 +20,7 @@ function InPersonPrepareStep({ toPreviousStep }) { inPersonOutageMessageEnabled, inPersonOutageExpectedUpdateDate, skipDocAuth, + skipDocAuthFromHowToVerify, skipDocAuthFromHandoff, howToVerifyURL, previousStepURL, @@ -29,7 +30,7 @@ function InPersonPrepareStep({ toPreviousStep }) { if (skipDocAuthFromHandoff && previousStepURL) { // directly from handoff page forceRedirect(previousStepURL); - } else if (skipDocAuth && howToVerifyURL) { + } else if ((skipDocAuthFromHowToVerify || skipDocAuth) && howToVerifyURL) { forceRedirect(howToVerifyURL); } else { toPreviousStep(); diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts index 5de622d6ccb..09289efda74 100644 --- a/app/javascript/packages/document-capture/context/in-person.ts +++ b/app/javascript/packages/document-capture/context/in-person.ts @@ -49,6 +49,13 @@ export interface InPersonContextProps { */ skipDocAuth?: boolean; + /** + * When skipDocAuthFromHowToVerify is true and in_person_proofing_opt_in_enabled is true, + * users are directed to the beginning of the IPP flow. This is set to true when + * they choose Opt-in IPP on the new How To Verify page + */ + skipDocAuthFromHowToVerify?: boolean; + /** * Flag set when user select IPP from handoff page when IPP is available * and selfie is required diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index d21223f415b..a0c7a5a22d0 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -34,6 +34,7 @@ interface AppRootData { optedInToInPersonProofing: string; securityAndPrivacyHowItWorksUrl: string; skipDocAuth: string; + skipDocAuthFromHowToVerify: string; skipDocAuthFromHandoff: string; howToVerifyURL: string; previousStepUrl: string; @@ -106,6 +107,7 @@ const { optedInToInPersonProofing, usStatesTerritories = '', skipDocAuth, + skipDocAuthFromHowToVerify, skipDocAuthFromHandoff, howToVerifyUrl, previousStepUrl, @@ -137,6 +139,7 @@ render( optedInToInPersonProofing: optedInToInPersonProofing === 'true', usStatesTerritories: parsedUsStatesTerritories, skipDocAuth: skipDocAuth === 'true', + skipDocAuthFromHowToVerify: skipDocAuthFromHowToVerify === 'true', skipDocAuthFromHandoff: skipDocAuthFromHandoff === 'true', howToVerifyURL: howToVerifyUrl, previousStepURL: previousStepUrl, From ad1e9f03bbef7f7ddf2d1bef7e7d7019ceb99ba7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:04:07 -0400 Subject: [PATCH 10/16] Refactor 2FA setup controller specs (#11399) * Refactor 2FA setup controller specs changelog: Internal, Automated Testing, Refactor 2FA setup controller specs * Switch from "should" to "expect" syntax for RSpec Consistency See: https://github.com/18F/identity-idp/pull/11399#issuecomment-2438149819 --- ...or_authentication_setup_controller_spec.rb | 124 ++++-------------- 1 file changed, 25 insertions(+), 99 deletions(-) diff --git a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb index 578cdb6e2dc..0a89f10fbd7 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -103,146 +103,72 @@ end end - describe 'PATCH create' do - it 'submits the TwoFactorOptionsForm' do - user = build(:user) - stub_sign_in_before_2fa(user) - stub_analytics - - voice_params = { - two_factor_options_form: { - selection: ['voice'], - }, - } + describe '#create' do + let(:params) { { two_factor_options_form: { selection: ['voice'] } } } - expect(controller.two_factor_options_form).to receive(:submit). - with(hash_including(voice_params[:two_factor_options_form])).and_call_original + subject(:response) { patch :create, params: params } - patch :create, params: voice_params - - expect(@analytics).to have_logged_event( - 'User Registration: 2FA Setup', - success: true, - errors: {}, - enabled_mfa_methods_count: 0, - selected_mfa_count: 1, - selection: ['voice'], - ) + before do + stub_sign_in_before_2fa end it 'tracks analytics event' do - stub_sign_in_before_2fa stub_analytics - patch :create, params: { - two_factor_options_form: { - selection: ['voice', 'auth_app'], - }, - } + response expect(@analytics).to have_logged_event( 'User Registration: 2FA Setup', enabled_mfa_methods_count: 0, - selection: ['voice', 'auth_app'], + selection: ['voice'], success: true, - selected_mfa_count: 2, + selected_mfa_count: 1, errors: {}, ) end context 'when multi selection with phone first' do - it 'redirects properly' do - stub_sign_in_before_2fa - patch :create, params: { - two_factor_options_form: { - selection: ['phone', 'auth_app'], - }, - } - - expect(response).to redirect_to phone_setup_url - end + let(:params) { { two_factor_options_form: { selection: ['phone', 'auth_app'] } } } + + it { is_expected.to redirect_to phone_setup_url } end context 'when multi selection with auth app first' do - it 'redirects properly' do - stub_sign_in_before_2fa - patch :create, params: { - two_factor_options_form: { - selection: ['auth_app', 'phone', 'webauthn'], - }, - } - - expect(response).to redirect_to authenticator_setup_url - end + let(:params) { { two_factor_options_form: { selection: ['auth_app', 'phone', 'webauthn'] } } } + + it { is_expected.to redirect_to authenticator_setup_url } end context 'when the selection is auth_app' do - it 'redirects to authentication app setup page' do - stub_sign_in_before_2fa - - patch :create, params: { - two_factor_options_form: { - selection: ['auth_app'], - }, - } + let(:params) { { two_factor_options_form: { selection: ['auth_app'] } } } - expect(response).to redirect_to authenticator_setup_url - end + it { is_expected.to redirect_to authenticator_setup_url } end context 'when the selection is webauthn' do - it 'redirects to webauthn setup page' do - stub_sign_in_before_2fa + let(:params) { { two_factor_options_form: { selection: ['webauthn'] } } } - patch :create, params: { - two_factor_options_form: { - selection: ['webauthn'], - }, - } - - expect(response).to redirect_to webauthn_setup_url - end + it { is_expected.to redirect_to webauthn_setup_url } end context 'when the selection is webauthn platform authenticator' do - it 'redirects to webauthn setup page with the platform param' do - stub_sign_in_before_2fa + let(:params) { { two_factor_options_form: { selection: ['webauthn_platform'] } } } - patch :create, params: { - two_factor_options_form: { - selection: ['webauthn_platform'], - }, - } - - expect(response).to redirect_to webauthn_setup_url(platform: true) - end + it { is_expected.to redirect_to webauthn_setup_url(platform: true) } end context 'when the selection is piv_cac' do - it 'redirects to piv/cac setup page' do - stub_sign_in_before_2fa + let(:params) { { two_factor_options_form: { selection: ['piv_cac'] } } } - patch :create, params: { - two_factor_options_form: { - selection: ['piv_cac'], - }, - } - - expect(response).to redirect_to setup_piv_cac_url - end + it { is_expected.to redirect_to setup_piv_cac_url } end context 'when the selection is not valid' do - it 'renders index page' do - stub_sign_in_before_2fa - - patch :create, params: { - two_factor_options_form: { - selection: ['foo'], - }, - } + let(:params) { { two_factor_options_form: { selection: ['foo'] } } } + it 'renders setup page with error message' do expect(response).to render_template(:index) + expect(flash[:error]).to eq(t('errors.messages.inclusion')) end end end From 5ebb98dfdf71887cd67f18dc086a6d9a24c8b0f7 Mon Sep 17 00:00:00 2001 From: "Davida (she/they)" Date: Mon, 28 Oct 2024 09:03:27 -0400 Subject: [PATCH 11/16] Add id_token_hint usage tracking (#11404) * changelog: Internal, Analytics, Add id_token_hint usage tracking --- lib/reporting/protocols_report.rb | 85 +++++++++++++-------- spec/lib/reporting/protocols_report_spec.rb | 15 ++++ 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/lib/reporting/protocols_report.rb b/lib/reporting/protocols_report.rb index c7ee1e2a382..741177eecc6 100644 --- a/lib/reporting/protocols_report.rb +++ b/lib/reporting/protocols_report.rb @@ -148,6 +148,11 @@ def deprecated_parameters_table aal3_issuers_data.length, aal3_issuers_data.join(', '), ], + [ + 'id_token_hint', + id_token_hint_data.length, + id_token_hint_data.join(', '), + ], ] end @@ -183,33 +188,27 @@ def oidc_issuer_count end def loa_issuers_data - @loa_issuers_data ||= cloudwatch_client.fetch( + @loa_issuers_data ||= fetch_uniq_issuers( query: loa_issuers_query, - from: time_range.begin, - to: time_range.end, - ). - map { |slice| slice['issuer'] }. - uniq + ) end def aal3_issuers_data - @aal3_issuers_data ||= cloudwatch_client.fetch( + @aal3_issuers_data ||= fetch_uniq_issuers( query: aal3_issuers_query, - from: time_range.begin, - to: time_range.end, - ). - map { |slice| slice['issuer'] }. - uniq + ) end def facial_match_data - @facial_match_data ||= cloudwatch_client.fetch( + @facial_match_data ||= fetch_uniq_issuers( query: facial_match_issuers_query, - from: time_range.begin, - to: time_range.end, - ). - map { |slice| slice['issuer'] }. - uniq + ) + end + + def id_token_hint_data + @id_token_hint_data ||= fetch_uniq_issuers( + query: id_token_hint_query, + ) end def protocol_data @@ -225,22 +224,19 @@ def protocol_data select { |slice| slice['protocol'] == SAML_AUTH_EVENT }. map { |slice| slice['request_count'].to_i }. sum, - issuer_count: results. - select { |slice| slice['protocol'] == SAML_AUTH_EVENT }. - map { |slice| slice['issuer'] }. - uniq. - count, + issuer_count: by_uniq_issuers( + results. + select { |slice| slice['protocol'] == SAML_AUTH_EVENT }, + ).count, }, oidc: { request_count: results. select { |slice| slice['protocol'] == OIDC_AUTH_EVENT }. map { |slice| slice['request_count'].to_i }. sum, - issuer_count: results. - select { |slice| slice['protocol'] == OIDC_AUTH_EVENT }. - map { |slice| slice['issuer'] }. - uniq. - count, + issuer_count: by_uniq_issuers( + results.select { |slice| slice['protocol'] == OIDC_AUTH_EVENT }, + ).count, }, } end @@ -254,10 +250,9 @@ def saml_signature_data to: time_range.end, ) { - unsigned: results. - select { |slice| slice['unsigned_count'].to_i > 0 }. - map { |slice| slice['issuer'] }. - uniq, + unsigned: by_uniq_issuers( + results.select { |slice| slice['unsigned_count'].to_i > 0 }, + ), invalid_signature: results. select { |slice| slice['invalid_signature_count'].to_i > 0 }. map { |slice| slice['issuer'] }. @@ -299,6 +294,18 @@ def facial_match_issuers_query QUERY end + def id_token_hint_query + format(<<~QUERY) + fields @timestamp, + coalesce(properties.event_properties.id_token_hint_parameter_present, 0) as id_token_hint, + coalesce(properties.event_properties.client_id, properties.service_provider) as issuer + | filter ispresent(id_token_hint) and id_token_hint > 0 and name = 'OIDC Logout Requested' + | display issuer + | sort issuer + | dedup issuer + QUERY + end + def loa_issuers_query params = { event: quote([SAML_AUTH_EVENT, OIDC_AUTH_EVENT]), @@ -359,6 +366,20 @@ def saml_signature_query QUERY end + def fetch_uniq_issuers(query) + by_uniq_issuers( + cloudwatch_client.fetch( + query:, + from: time_range.begin, + to: time_range.end, + ), + ) + end + + def by_uniq_issuers(data) + data.map { |slice| slice['issuer'] }.uniq + end + def cloudwatch_client @cloudwatch_client ||= Reporting::CloudwatchClient.new( num_threads: @threads, diff --git a/spec/lib/reporting/protocols_report_spec.rb b/spec/lib/reporting/protocols_report_spec.rb index 2f62ac693de..6d39565bed9 100644 --- a/spec/lib/reporting/protocols_report_spec.rb +++ b/spec/lib/reporting/protocols_report_spec.rb @@ -90,11 +90,21 @@ }, ] + id_token_hint_query_response = [ + { + 'issuer' => 'Issuer3', + }, + { + 'issuer' => 'Issuer4', + }, + ] + stub_multiple_cloudwatch_logs( protocol_query_response, saml_signature_query_response, loa_issuers_query_response, aal3_issuers_query_response, + id_token_hint_query_response, facial_match_issuers_query_response, ) end @@ -236,6 +246,11 @@ def expected_tables(strings: false) string_or_num(strings, 2), 'Issuer1, Issuer3', ], + [ + 'id_token_hint', + string_or_num(strings, 2), + 'Issuer3, Issuer4', + ], ], [ [ From 085a3f1f84b023431eb8880b3276140a206bc0d0 Mon Sep 17 00:00:00 2001 From: Lauren George Date: Mon, 28 Oct 2024 10:12:22 -0400 Subject: [PATCH 12/16] LG-14642: Update design and copy of GPO 'letter on the way' email (#11351) LG-14642: Update design and copy of GPO 'letter on the way' email **Why** - Align the 'letter requested' design and language with our current design requirements **How** - Rewrote the email template to utilize the new language and iconography. - The template handles more complexity -- if the ServiceProvider is absent or has incomplete configuration details, we account for that and either hide or modify certain instructions and the call-to-action. This business logic is stored in a new presenter (`Idv::ByMail::LetterRequestedPresenter`). - Replaced `user_mailer.letter_reminder.*` with new message properties under `user_mailer.verify_by_mail_letter_requested.*` - Updated the Rails mailer previewer to include a dummy ServiceProvider and User with a pending profile to reflect the new content. changelog: User-Facing Improvements, IdV notifications, Redesign verify-by-mail 'letter requested' email notification --------- Co-authored-by: Jonathan Hooper --- app/assets/images/email/letter-success.png | Bin 0 -> 27873 bytes app/helpers/locale_helper.rb | 12 ++- app/mailers/user_mailer.rb | 12 ++- .../idv/account_verified_email_presenter.rb | 9 +- .../letter_requested_email_presenter.rb | 47 +++++++++ .../verify_by_mail_letter_requested.html.erb | 73 ++++++++++---- config/locales/en.yml | 7 +- config/locales/es.yml | 7 +- config/locales/fr.yml | 7 +- config/locales/zh.yml | 7 +- .../idv/verify_info_controller_spec.rb | 1 + .../idv/steps/enter_password_step_spec.rb | 2 +- spec/helpers/locale_helper_spec.rb | 1 + spec/i18n_spec.rb | 2 +- spec/mailers/previews/user_mailer_preview.rb | 9 ++ spec/mailers/user_mailer_spec.rb | 95 +++++++++++++++++- spec/policies/pending_profile_policy_spec.rb | 2 + .../presenters/account_show_presenter_spec.rb | 2 + .../account_verified_email_presenter_spec.rb | 2 +- .../letter_requested_email_presenter_spec.rb | 93 +++++++++++++++++ 20 files changed, 348 insertions(+), 42 deletions(-) create mode 100644 app/assets/images/email/letter-success.png create mode 100644 app/presenters/idv/by_mail/letter_requested_email_presenter.rb create mode 100644 spec/presenters/idv/by_mail/letter_requested_email_presenter_spec.rb diff --git a/app/assets/images/email/letter-success.png b/app/assets/images/email/letter-success.png new file mode 100644 index 0000000000000000000000000000000000000000..de287b7c8c52c8d9770b73d415d6fa90fffde3db GIT binary patch literal 27873 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 - <%= t('user_mailer.letter_reminder.info_html', link_html: link_to(APP_NAME, root_url)) %> +<%= image_tag( + 'email/letter-success.png', + width: 140, + height: 177, + alt: '', + class: 'float-center padding-bottom-4', + ) %> +

<%= message.subject %>

+

+ <%= t('user_mailer.verify_by_mail_letter_requested.intro_html', app_name: APP_NAME) %>

+ +<% if @presenter.show_sp_contact_instructions? %> +
    +
  • <%= t('user_mailer.verify_by_mail_letter_requested.instructions.sign_in') %>
  • +
  • + <%= t( + 'user_mailer.verify_by_mail_letter_requested.instructions.contact_sp', + friendly_name: @presenter.sp_name, + ) %> +
  • +
+<% else %> +

<%= t('user_mailer.verify_by_mail_letter_requested.instructions.sign_in') %>

+<% end %> - - - - - - -
-   -
- - - - -
-   -
+<% if @presenter.show_cta? %> +
+ + + + + + + +
+ + + + + + +
+ <%= link_to t('user_mailer.verify_by_mail_letter_requested.cta.sign_in'), + @presenter.sign_in_url, + target: '_blank', + class: 'float-center', + rel: 'noopener' %> +
+
+
+ +

+ <%= link_to @presenter.sign_in_url, @presenter.sign_in_url, target: '_blank', rel: 'noopener' %> +

+<% end %> + diff --git a/config/locales/en.yml b/config/locales/en.yml index 1b9f639c3e8..a20477621b3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1891,8 +1891,6 @@ user_mailer.letter_reminder_14_days.did_not_get_a_letter_html: If you didn’t g user_mailer.letter_reminder_14_days.finish: Finish verifying your identity user_mailer.letter_reminder_14_days.sign_in_and_request_another_letter: sign in to request another letter user_mailer.letter_reminder_14_days.subject: Finish verifying your identity -user_mailer.letter_reminder.info_html: The letter you are about to receive will contain a verification code that helps us verify your address. You can complete the identity verification process by signing into %{link_html} and entering the verification code. -user_mailer.letter_reminder.subject: We mailed a letter to the address you have on file user_mailer.new_device_sign_in_after_2fa.authentication_methods: authentication methods user_mailer.new_device_sign_in_after_2fa.info_p1: Your %{app_name} email and password were used to sign-in and authenticate on a new device. user_mailer.new_device_sign_in_after_2fa.info_p2: If you recognize this activity, you don’t need to do anything. @@ -1948,6 +1946,11 @@ user_mailer.suspended_reset_password.subject: We couldn’t reset your password user_mailer.suspension_confirmed.contact_agency: Please contact the agency whose service you are trying to access. user_mailer.suspension_confirmed.remain_locked: We have completed our review of your %{app_name} account and your account will remain locked. user_mailer.suspension_confirmed.subject: Your account is locked +user_mailer.verify_by_mail_letter_requested.cta.sign_in: Sign in +user_mailer.verify_by_mail_letter_requested.instructions.contact_sp: Contact %{friendly_name} if you need to access their services before your letter arrives. +user_mailer.verify_by_mail_letter_requested.instructions.sign_in: Sign back in and enter the verification code when your letter arrives. +user_mailer.verify_by_mail_letter_requested.intro_html: You’ll get a letter in the mail from %{app_name} in 5 to 10 days at the address we verified and associated with you. +user_mailer.verify_by_mail_letter_requested.subject: Your letter is on the way users.delete.actions.cancel: Back to profile users.delete.actions.delete: Delete account users.delete.bullet_1: You won’t have a %{app_name} account diff --git a/config/locales/es.yml b/config/locales/es.yml index 6c642f86249..fd71719a366 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1903,8 +1903,6 @@ user_mailer.letter_reminder_14_days.did_not_get_a_letter_html: Si no recibió es user_mailer.letter_reminder_14_days.finish: Termine de verificar su identidad user_mailer.letter_reminder_14_days.sign_in_and_request_another_letter: inicie sesión para solicitar otra carta user_mailer.letter_reminder_14_days.subject: Termine de verificar su identidad -user_mailer.letter_reminder.info_html: La carta que recibirá próximamente contiene un código de verificación que nos ayudará a verificar su dirección. Para completar el proceso de verificación de identidad, inicie sesión en %{link_html} e ingrese el código de verificación. -user_mailer.letter_reminder.subject: Enviamos una carta a la dirección de su expediente user_mailer.new_device_sign_in_after_2fa.authentication_methods: métodos de autenticación user_mailer.new_device_sign_in_after_2fa.info_p1: Su correo electrónico y contraseña de %{app_name} se usaron para iniciar sesión y hacer la autenticación en un dispositivo nuevo. user_mailer.new_device_sign_in_after_2fa.info_p2: Si reconoce esta actividad, no tiene que hacer nada. @@ -1960,6 +1958,11 @@ user_mailer.suspended_reset_password.subject: No pudimos restablecer su contrase user_mailer.suspension_confirmed.contact_agency: Contacte con la agencia a cuyo servicio está intentando acceder. user_mailer.suspension_confirmed.remain_locked: Terminamos la revisión de su cuenta de %{app_name} y la cuenta permanecerá bloqueada. user_mailer.suspension_confirmed.subject: Su cuenta está bloqueada +user_mailer.verify_by_mail_letter_requested.cta.sign_in: Iniciar sesión +user_mailer.verify_by_mail_letter_requested.instructions.contact_sp: Contacte con %{friendly_name} si necesita acceder a los servicios de esa agencia antes de que llegue su carta. +user_mailer.verify_by_mail_letter_requested.instructions.sign_in: Cuando reciba su carta, vuelva a iniciar sesión e ingrese el código de verificación. +user_mailer.verify_by_mail_letter_requested.intro_html: 'En un plazo de 5 a 10 días, recibirá por correo una carta de %{app_name} en la dirección que verificamos y asociamos con usted:' +user_mailer.verify_by_mail_letter_requested.subject: Su carta está en camino users.delete.actions.cancel: Volver al perfil users.delete.actions.delete: Eliminar cuenta users.delete.bullet_1: No tendrá una cuenta de %{app_name}. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5dbd241726e..3004ffa5261 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1891,8 +1891,6 @@ user_mailer.letter_reminder_14_days.did_not_get_a_letter_html: Si vous n’avez user_mailer.letter_reminder_14_days.finish: Terminer la vérification de votre identité user_mailer.letter_reminder_14_days.sign_in_and_request_another_letter: connectez-vous pour en demander une autre user_mailer.letter_reminder_14_days.subject: Terminer la vérification de votre identité -user_mailer.letter_reminder.info_html: La lettre que vous êtes sur le point de recevoir contiendra un code de vérification nous permettant de vérifier votre adresse. Vous pouvez terminer le processus de vérification d’identité en vous connectant à %{link_html} et en saisissant le code de vérification. -user_mailer.letter_reminder.subject: Nous avons posté une lettre à l’adresse que vous avez enregistrée user_mailer.new_device_sign_in_after_2fa.authentication_methods: méthodes d’authentification user_mailer.new_device_sign_in_after_2fa.info_p1: Votre e-mail et mot de passe %{app_name} ont été utilisés pour se connecter et s’authentifier sur un nouvel appareil. user_mailer.new_device_sign_in_after_2fa.info_p2: Si vous reconnaissez cette activité, vous n’avez rien à faire. @@ -1948,6 +1946,11 @@ user_mailer.suspended_reset_password.subject: Nous n’avons pas pu réinitialis user_mailer.suspension_confirmed.contact_agency: Veuillez contacter l’organisme dont vous essayez d’accéder au service. user_mailer.suspension_confirmed.remain_locked: Nous avons terminé l’examen de votre compte %{app_name} et votre compte restera verrouillé. user_mailer.suspension_confirmed.subject: Votre compte est verrouillé +user_mailer.verify_by_mail_letter_requested.cta.sign_in: Connexion +user_mailer.verify_by_mail_letter_requested.instructions.contact_sp: Contactez %{friendly_name} si vous avez besoin d’accéder à ses services avant l’arrivée de votre lettre. +user_mailer.verify_by_mail_letter_requested.instructions.sign_in: Une fois ce courrier reçu, reconnectez-vous pour saisir le code de vérification qui y figure. +user_mailer.verify_by_mail_letter_requested.intro_html: 'Vous recevrez un courrier de %{app_name} d’ici 5 à 10 jours à l’adresse que nous avons confirmée qui est associée à votre nom :' +user_mailer.verify_by_mail_letter_requested.subject: Votre lettre est en route users.delete.actions.cancel: Retour au profil users.delete.actions.delete: Supprimer le compte users.delete.bullet_1: Vous n’aurez pas de compte %{app_name}. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 7f2e53a6730..de4cbbc27c1 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1904,8 +1904,6 @@ user_mailer.letter_reminder_14_days.did_not_get_a_letter_html: 如果你没有 user_mailer.letter_reminder_14_days.finish: 完成验证你的身份 user_mailer.letter_reminder_14_days.sign_in_and_request_another_letter: 登录要求再发一封信 user_mailer.letter_reminder_14_days.subject: 完成验证你的身份 -user_mailer.letter_reminder.info_html: 你将收到的信件会含有帮助我们验证你地址的一次性代码。你可以登入 %{link_html} 并输入该一次性代码来完成身份验证流i程。 -user_mailer.letter_reminder.subject: 我们已向你存档地址发送了一封信。 user_mailer.new_device_sign_in_after_2fa.authentication_methods: 身份证实方法 user_mailer.new_device_sign_in_after_2fa.info_p1: 你的 %{app_name} 电邮和密码在一个新设备上被用来登录和进行身份验证。 user_mailer.new_device_sign_in_after_2fa.info_p2: 如果你知道该活动,则无需做任何事情。 @@ -1961,6 +1959,11 @@ user_mailer.suspended_reset_password.subject: 我们无法重设你的密码。 user_mailer.suspension_confirmed.contact_agency: 请联系你试图访问其服务的那个机构。 user_mailer.suspension_confirmed.remain_locked: 我们已完成了对你%{app_name}账户的审查,你的账户将继续被锁。 user_mailer.suspension_confirmed.subject: 你的账户被锁 +user_mailer.verify_by_mail_letter_requested.cta.sign_in: 登录 +user_mailer.verify_by_mail_letter_requested.instructions.contact_sp: 如果收到信件之前你需要访问我们合作伙伴机构的服务,请联系%{friendly_name}。 +user_mailer.verify_by_mail_letter_requested.instructions.sign_in: 收到信件后请再登录并输入其中的验证码。 +user_mailer.verify_by_mail_letter_requested.intro_html: '你会在5到10天内在我们验证过的与你相关的地址收到来自%{app_name}寄给你的一封信:' +user_mailer.verify_by_mail_letter_requested.subject: 你的信件已寄出。 users.delete.actions.cancel: 返回用户资料 users.delete.actions.delete: 删除账户 users.delete.bullet_1: 你不会有 %{app_name} 账户 diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 9f453c60677..50351d84b11 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -200,6 +200,7 @@ before do controller.idv_session.ssn = nil end + it 'does not log an idv_verify_info_missing_threatmetrix_session_id event' do get :show expect(@analytics).not_to have_logged_event( diff --git a/spec/features/idv/steps/enter_password_step_spec.rb b/spec/features/idv/steps/enter_password_step_spec.rb index b53a3e3c126..d7fa1e5a9dd 100644 --- a/spec/features/idv/steps/enter_password_step_spec.rb +++ b/spec/features/idv/steps/enter_password_step_spec.rb @@ -73,7 +73,7 @@ def sends_letter_creates_unverified_profile_sends_email to change { GpoConfirmation.count }.by(1) expect_delivered_email_count(email_count_before_continue + 1) - expect(last_email.subject).to eq(t('user_mailer.letter_reminder.subject')) + expect(last_email.subject).to eq(t('user_mailer.verify_by_mail_letter_requested.subject')) expect(user.events.account_verified.size).to be(0) expect(user.profiles.count).to eq 1 diff --git a/spec/helpers/locale_helper_spec.rb b/spec/helpers/locale_helper_spec.rb index dbffafd9230..356aa61e928 100644 --- a/spec/helpers/locale_helper_spec.rb +++ b/spec/helpers/locale_helper_spec.rb @@ -52,6 +52,7 @@ context 'when the user has an email_language' do let(:email_language) { 'es' } + let(:url_options) { {} } it 'sets the language inside the block and yields' do subject diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index ebcabaded71..e745a01ab1e 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -281,7 +281,7 @@ def allowed_untranslated_key?(locale, key) ) end - it 'has matching HTML tags' do + it 'has matching HTML tags across all locales' do i18n.data[i18n.base_locale].select_keys do |key, _node| if key.start_with?('i18n.transliterate.rule.') || i18n.t(key).is_a?(Array) || i18n.t(key).nil? next diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index e5c2f96a542..ac6dc41e16e 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -124,6 +124,14 @@ def please_reset_password end def verify_by_mail_letter_requested + service_provider = unsaveable( + ServiceProvider.new( + friendly_name: 'Sample App SP', + return_to_sp_url: 'https://example.com', + ), + ) + profile = Profile.new(initiating_service_provider: service_provider) + user.instance_variable_set(:@pending_profile, profile) UserMailer.with(user: user, email_address: email_address_record).verify_by_mail_letter_requested end @@ -286,6 +294,7 @@ def user ), ), ], + email_language: params[:locale], ), ) end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 59f25d32c69..7f4b80dd228 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -166,6 +166,7 @@ describe '#new_device_sign_in_before_2fa' do let(:event) { create(:event, event_type: :sign_in_before_2fa, user:, device: create(:device)) } + subject(:mail) do UserMailer.with(user:, email_address:).new_device_sign_in_before_2fa( events: user.events.where(event_type: 'sign_in_before_2fa').includes(:device).to_a, @@ -179,6 +180,7 @@ describe '#new_device_sign_in_after_2fa' do let(:event) { create(:event, event_type: :sign_in_after_2fa, user:, device: create(:device)) } + subject(:mail) do UserMailer.with(user:, email_address:).new_device_sign_in_after_2fa( events: user.events.where(event_type: 'sign_in_after_2fa').includes(:device).to_a, @@ -501,6 +503,24 @@ def expect_email_body_to_have_help_and_contact_links end describe '#verify_by_mail_letter_requested' do + let(:service_provider) do + create( + :service_provider, + return_to_sp_url: 'https://www.example.com', + friendly_name: 'My Awesome SP', + ) + end + + let(:profile) do + create( + :profile, + :verify_by_mail_pending, + initiating_service_provider: service_provider, + ) + end + + let(:user) { profile.user } + let(:mail) do UserMailer.with(user: user, email_address: email_address).verify_by_mail_letter_requested end @@ -513,12 +533,76 @@ def expect_email_body_to_have_help_and_contact_links end it 'renders the subject' do - expect(mail.subject).to eq t('user_mailer.letter_reminder.subject') + expect(mail.subject).to eq t('user_mailer.verify_by_mail_letter_requested.subject') end - it 'renders the body' do - expect(mail.html_part.body). - to have_content(strip_tags(t('user_mailer.letter_reminder.info_html', link_html: APP_NAME))) + context 'ServiceProvider has a homepage URL' do + it 'renders the contact SP instructions' do + expect(mail.html_part.body).to have_content( + t( + 'user_mailer.verify_by_mail_letter_requested.instructions.contact_sp', + friendly_name: 'My Awesome SP', + ), + ) + end + + it 'renders the sign in CTA' do + expect(mail.html_part.body).to have_link( + t( + 'user_mailer.verify_by_mail_letter_requested.cta.sign_in', + ), + href: 'https://www.example.com', + ) + end + end + + context 'ServiceProvider does not have a homepage URL' do + let(:service_provider) do + create( + :service_provider, + friendly_name: 'My Awesome SP', + return_to_sp_url: nil, + ) + end + + it 'renders the contact SP instructions' do + expect(mail.html_part.body).to have_content( + t( + 'user_mailer.verify_by_mail_letter_requested.instructions.contact_sp', + friendly_name: 'My Awesome SP', + ), + ) + end + + it 'does not render the sign in CTA' do + expect(mail.html_part.body).to_not have_link( + t( + 'user_mailer.verify_by_mail_letter_requested.cta.sign_in', + ), + ) + end + end + + context 'No Service Provider present' do + let(:service_provider) { nil } + + it 'it does not render the contact SP instructions' do + expect(mail.html_part.body).to_not have_content( + t( + 'user_mailer.verify_by_mail_letter_requested.instructions.contact_sp', + friendly_name: APP_NAME, + ), + ) + end + + it 'renders the sign in CTA with root URL' do + expect(mail.html_part.body).to have_link( + t( + 'user_mailer.verify_by_mail_letter_requested.cta.sign_in', + ), + href: root_url, + ) + end end end @@ -847,6 +931,7 @@ def expect_email_body_to_have_help_and_contact_links enrollment: enrollment, ) end + context 'For Informed Delivery IPP (ID-IPP)' do it_behaves_like 'a system email' it_behaves_like 'an email that respects user email locale preference' @@ -867,6 +952,7 @@ def expect_email_body_to_have_help_and_contact_links context 'For Enhanced In-Person Proofing (Enhanced IPP)' do let(:enrollment) { enhanced_ipp_enrollment } + it 'renders content that is applicable to Enhanced In-Person Proofing (Enhanced IPP)' do aggregate_failures do [ @@ -1064,6 +1150,7 @@ def expect_email_body_to_have_help_and_contact_links user.email_language = 'fr' user.save! end + it 'renders the pre opt-in in person completion survey url' do expect(mail.html_part.body). to have_selector( diff --git a/spec/policies/pending_profile_policy_spec.rb b/spec/policies/pending_profile_policy_spec.rb index d5676a57e9c..ec59c3eaee8 100644 --- a/spec/policies/pending_profile_policy_spec.rb +++ b/spec/policies/pending_profile_policy_spec.rb @@ -23,6 +23,7 @@ describe '#user_has_pending_profile?' do context 'has an active non-facial match profile and facial match comparison is requested' do let(:idv_level) { :unsupervised_with_selfie } + before do create(:profile, :active, :verified, idv_level: :legacy_unsupervised, user: user) create(:profile, :verify_by_mail_pending, idv_level: idv_level, user: user) @@ -62,6 +63,7 @@ context 'no facial match comparison is requested' do let(:idv_level) { :legacy_unsupervised } let(:vtr) { ['C2'] } + context 'user has pending profile' do before do create(:profile, :verify_by_mail_pending, idv_level: idv_level, user: user) diff --git a/spec/presenters/account_show_presenter_spec.rb b/spec/presenters/account_show_presenter_spec.rb index 546ae1f92af..3688bde40c1 100644 --- a/spec/presenters/account_show_presenter_spec.rb +++ b/spec/presenters/account_show_presenter_spec.rb @@ -16,6 +16,7 @@ let(:sp_name) { nil } let(:user) { build(:user) } let(:locked_for_session) { false } + subject(:presenter) do AccountShowPresenter.new( decrypted_pii:, @@ -480,6 +481,7 @@ describe '#connected_apps' do let(:user) { create(:user, identities: [create(:service_provider_identity)]) } + subject(:connected_apps) { presenter.connected_apps } it 'delegates to user, eager-loading view-specific relations' do diff --git a/spec/presenters/idv/account_verified_email_presenter_spec.rb b/spec/presenters/idv/account_verified_email_presenter_spec.rb index 760b22b2fd0..64b620fae57 100644 --- a/spec/presenters/idv/account_verified_email_presenter_spec.rb +++ b/spec/presenters/idv/account_verified_email_presenter_spec.rb @@ -12,7 +12,7 @@ ) end - subject(:presenter) { described_class.new(profile:) } + subject(:presenter) { described_class.new(profile:, url_options: {}) } context 'when there is no associated service provider' do let(:service_provider) { nil } diff --git a/spec/presenters/idv/by_mail/letter_requested_email_presenter_spec.rb b/spec/presenters/idv/by_mail/letter_requested_email_presenter_spec.rb new file mode 100644 index 00000000000..16adf3a2d07 --- /dev/null +++ b/spec/presenters/idv/by_mail/letter_requested_email_presenter_spec.rb @@ -0,0 +1,93 @@ +require 'rails_helper' + +RSpec.describe Idv::ByMail::LetterRequestedEmailPresenter do + include Rails.application.routes.url_helpers + + let(:service_provider) { create(:service_provider) } + + let(:profile) do + create( + :profile, + :verify_by_mail_pending, + initiating_service_provider: service_provider, + ) + end + + let(:user) { profile.user } + + subject(:presenter) { described_class.new(current_user: user, url_options: {}) } + + context 'when there is no associated service provider' do + let(:service_provider) { nil } + + describe '#sp_name' do + it { expect(presenter.sp_name).to be_nil } + end + + describe '#show_sp_contact_instructions?' do + it { expect(presenter.show_sp_contact_instructions?).to eq(false) } + end + + describe '#show_cta?' do + it { expect(presenter.show_cta?).to eq(true) } + end + + describe '#sign_in_url' do + it { expect(presenter.sign_in_url).to eq(root_url) } + end + end + + context 'where there is a service provider' do + context 'when the service provider has no return URL' do + let(:service_provider) do + create( + :service_provider, + return_to_sp_url: nil, + friendly_name: 'My Awesome SP', + ) + end + + describe '#sp_name' do + it { expect(presenter.sp_name).to eq('My Awesome SP') } + end + + describe '#show_sp_contact_instructions?' do + it { expect(presenter.show_sp_contact_instructions?).to eq(true) } + end + + describe '#show_cta?' do + it { expect(presenter.show_cta?).to eq(false) } + end + + describe '#sign_in_url' do + it { expect(presenter.sign_in_url).to eq(nil) } + end + end + + context 'when the service provider does have a return URL' do + let(:service_provider) do + create( + :service_provider, + return_to_sp_url: 'https://www.example.com', + friendly_name: 'My Awesome SP', + ) + end + + describe '#sp_name' do + it { expect(presenter.sp_name).to eq('My Awesome SP') } + end + + describe '#show_sp_contact_instructions?' do + it { expect(presenter.show_sp_contact_instructions?).to eq(true) } + end + + describe '#show_cta?' do + it { expect(presenter.show_cta?).to eq(true) } + end + + describe '#sign_in_url' do + it { expect(presenter.sign_in_url).to eq('https://www.example.com') } + end + end + end +end From 47960fe69141524e6b460e8e5eaed2e59001f2d5 Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:37:06 -0400 Subject: [PATCH 13/16] Delete deprecated code (#11403) * remove deprecated usps_unique_id method that is no longer needed for backward compatibility * Changelog: Internal, In-person proofing, remove deprecated method that is no longer needed for backwards compatibility * remove obsolete test --- app/jobs/get_usps_proofing_results_job.rb | 3 --- app/models/in_person_enrollment.rb | 5 ----- .../usps_in_person_proofing/enrollment_helper.rb | 6 +----- spec/jobs/get_usps_proofing_results_job_spec.rb | 12 ------------ .../enrollment_helper_spec.rb | 14 -------------- 5 files changed, 1 insertion(+), 39 deletions(-) diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index dfae07ef11f..e529c76b334 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -101,9 +101,6 @@ def check_enrollments(enrollments) end def check_enrollment(enrollment) - # Add a unique ID for enrollments that don't have one - enrollment.update(unique_id: enrollment.usps_unique_id) if enrollment.unique_id.blank? - status_check_attempted_at = Time.zone.now enrollment_outcomes[:enrollments_checked] += 1 diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 27aa547860e..2b719acf9ea 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -146,11 +146,6 @@ def eligible_for_notification? notification_phone_configuration.present? && (passed? || failed?) end - # (deprecated) Returns the value to use for the USPS enrollment ID - def usps_unique_id - user.uuid.delete('-').slice(0, 18) - end - def enhanced_ipp? IdentityConfig.store.usps_eipp_sponsor_id == sponsor_id end diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb index 0ac3955ec3d..564f9a7e96b 100644 --- a/app/services/usps_in_person_proofing/enrollment_helper.rb +++ b/app/services/usps_in_person_proofing/enrollment_helper.rb @@ -62,13 +62,9 @@ def send_ready_to_verify_email(user, enrollment, is_enhanced_ipp:) # @return [String] The enrollment code # @raise [Exception::RequestEnrollException] Raised with a problem creating the enrollment def create_usps_enrollment(enrollment, pii, is_enhanced_ipp) - # Use the enrollment's unique_id value if it exists, otherwise use the deprecated - # #usps_unique_id value in order to remain backwards-compatible. LG-7024 will remove this - unique_id = enrollment.unique_id || enrollment.usps_unique_id - applicant = UspsInPersonProofing::Applicant.new( { - unique_id: unique_id, + unique_id: enrollment.unique_id, first_name: transliterate(pii[:first_name]), last_name: transliterate(pii[:last_name]), address: transliterate(pii[:address1]), diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 9b4b2a4716e..48282d9c7ad 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -2728,18 +2728,6 @@ end end - context 'when the enrollment does not have a unique_id' do - before do - enrollment.update(unique_id: nil) - allow(analytics).to receive(:idv_in_person_usps_proofing_results_job_exception) - subject.perform(current_time) - end - - it 'updates the enrollment to have a unique_id' do - expect(enrollment.reload.unique_id).to be_present - end - end - context 'when multiple pending InPersonEnrollments exist' do let(:enrollments) do [ diff --git a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb index bfd985a06bb..e0381815dd5 100644 --- a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb +++ b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb @@ -180,20 +180,6 @@ end end - context 'when the enrollment does not have a unique ID' do - it 'generates a usps_unique_id value to create the enrollment' do - enrollment.update(unique_id: nil) - expect(proofer).to receive(:request_enroll) do |applicant| - # The InPersonEnrollment#usps_unique_id method is deprecated - expect(applicant.unique_id).to eq(enrollment.usps_unique_id) - - UspsInPersonProofing::Mock::Proofer.new.request_enroll(applicant, is_enhanced_ipp) - end - - subject.schedule_in_person_enrollment(user:, pii:, is_enhanced_ipp:) - end - end - it <<~STR.squish do sets enrollment status to pending, sponsor_id to usps_ipp_sponsor_id, and sets established at date and unique id From cab363b1d2d253a69bba445ec4e3e78b2170ec19 Mon Sep 17 00:00:00 2001 From: eileen-nava <80347702+eileen-nava@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:20:32 -0400 Subject: [PATCH 14/16] LG-11799: Remove same_address_as_id from analytics (#11400) * begin removing same_address_as_id from analytics * remove same_address_as_id from extra_analytics created in verify_info_concern * update analytics feature specs to match new expectations * Changelog: Internal, In-person Proofing, remove same_address_as_id from analytics * modify idv_result_to_form_response to avoid mutating result --- .../concerns/idv/verify_info_concern.rb | 10 +++++-- app/controllers/concerns/idv_step_concern.rb | 9 +----- app/services/analytics_events.rb | 30 ------------------- app/services/idv/flows/in_person_flow.rb | 8 +---- .../idv/in_person/address_controller_spec.rb | 3 -- .../idv/in_person/ssn_controller_spec.rb | 4 --- .../idv/in_person/state_id_controller_spec.rb | 1 - .../in_person/verify_info_controller_spec.rb | 2 -- spec/features/idv/analytics_spec.rb | 18 +++++------ 9 files changed, 17 insertions(+), 68 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 1108da94480..efceb72bc06 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -195,11 +195,10 @@ def async_state_done(current_async_state) [:proofing_results, :context, :stages, :resolution, :errors, :ssn], [:proofing_results, :context, :stages, :residential_address, :errors, :ssn], [:proofing_results, :context, :stages, :threatmetrix, :response_body, :first_name], - [:same_address_as_id], [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], [:proofing_results, :biographical_info, :identity_doc_address_state], [:proofing_results, :biographical_info, :state_id_jurisdiction], - [:proofing_results, :biographical_info, :same_address_as_id], + [:proofing_results, :biographical_info], ], }, ) @@ -291,7 +290,12 @@ def idv_result_to_form_response( FormResponse.new( success: result[:success], errors: result[:errors], - extra: extra.merge(proofing_results: result.except(:errors, :success)), + extra: extra.merge( + proofing_results: { + **result.except(:errors, :success), + biographical_info: result[:biographical_info]&.except(:same_address_as_id), + }, + ), ) end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index ed417070ca7..36aef0fbe0b 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -78,18 +78,11 @@ def confirm_hybrid_handoff_needed private def extra_analytics_properties - extra = { + { pii_like_keypaths: [ - [:same_address_as_id], [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], ], } - - unless flow_session.dig(:pii_from_user, :same_address_as_id).nil? - extra[:same_address_as_id] = - flow_session[:pii_from_user][:same_address_as_id].to_s == 'true' - end - extra end def letter_recently_enqueued? diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 25ed4fb4c79..7f4ea4e62f6 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1481,7 +1481,6 @@ def idv_doc_auth_link_sent_visited( # @param [String] step Current IdV step # @param [String] analytics_id Current IdV flow identifier # @param ["hybrid","standard"] flow_path Document capture user flow - # @param [Boolean] same_address_as_id # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing # @param [Number] previous_ssn_edit_distance The edit distance to the previous submitted SSN @@ -1491,7 +1490,6 @@ def idv_doc_auth_redo_ssn_submitted( flow_path:, opted_in_to_in_person_proofing: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, previous_ssn_edit_distance: nil, **extra ) @@ -1502,7 +1500,6 @@ def idv_doc_auth_redo_ssn_submitted( flow_path:, opted_in_to_in_person_proofing:, skip_hybrid_handoff:, - same_address_as_id:, previous_ssn_edit_distance:, **extra, ) @@ -1542,7 +1539,6 @@ def idv_doc_auth_socure_webhook_received( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] acuant_sdk_upgrade_ab_test_bucket A/B test bucket for Acuant document capture # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing # @param [Number] previous_ssn_edit_distance The edit distance to the previous submitted SSN def idv_doc_auth_ssn_submitted( @@ -1555,7 +1551,6 @@ def idv_doc_auth_ssn_submitted( error_details: nil, acuant_sdk_upgrade_ab_test_bucket: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, previous_ssn_edit_distance: nil, **extra ) @@ -1570,7 +1565,6 @@ def idv_doc_auth_ssn_submitted( acuant_sdk_upgrade_ab_test_bucket:, flow_path:, opted_in_to_in_person_proofing:, - same_address_as_id:, previous_ssn_edit_distance:, **extra, ) @@ -1583,7 +1577,6 @@ def idv_doc_auth_ssn_submitted( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] acuant_sdk_upgrade_ab_test_bucket A/B test bucket for Acuant document capture # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing # @param [Number] previous_ssn_edit_distance The edit distance to the previous submitted SSN def idv_doc_auth_ssn_visited( @@ -1593,7 +1586,6 @@ def idv_doc_auth_ssn_visited( opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, previous_ssn_edit_distance: nil, **extra ) @@ -1605,7 +1597,6 @@ def idv_doc_auth_ssn_visited( acuant_sdk_upgrade_ab_test_bucket:, flow_path:, opted_in_to_in_person_proofing:, - same_address_as_id:, previous_ssn_edit_distance:, **extra, ) @@ -1892,7 +1883,6 @@ def idv_doc_auth_verify_polling_wait_visited(**extra) # @param flow_path [String] "hybrid" for hybrid handoff, "standard" otherwise # @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 [Boolean] same_address_as_id # @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 @@ -1964,7 +1954,6 @@ def idv_doc_auth_verify_proofing_results( ssn_is_unique: nil, step: nil, success: nil, - same_address_as_id: nil, previous_ssn_edit_distance: nil, **extra ) @@ -1984,7 +1973,6 @@ def idv_doc_auth_verify_proofing_results( ssn_is_unique:, step:, success:, - same_address_as_id:, previous_ssn_edit_distance:, **extra, ) @@ -1998,7 +1986,6 @@ def idv_doc_auth_verify_proofing_results( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] acuant_sdk_upgrade_ab_test_bucket A/B test bucket for Acuant document capture # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing def idv_doc_auth_verify_submitted( step:, @@ -2007,7 +1994,6 @@ def idv_doc_auth_verify_submitted( opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, **extra ) track_event( @@ -2018,7 +2004,6 @@ def idv_doc_auth_verify_submitted( acuant_sdk_upgrade_ab_test_bucket:, flow_path:, opted_in_to_in_person_proofing:, - same_address_as_id:, **extra, ) end @@ -2030,7 +2015,6 @@ def idv_doc_auth_verify_submitted( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] acuant_sdk_upgrade_ab_test_bucket A/B test bucket for Acuant document capture # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing def idv_doc_auth_verify_visited( step:, @@ -2039,7 +2023,6 @@ def idv_doc_auth_verify_visited( opted_in_to_in_person_proofing: nil, acuant_sdk_upgrade_ab_test_bucket: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, **extra ) track_event( @@ -2050,7 +2033,6 @@ def idv_doc_auth_verify_visited( acuant_sdk_upgrade_ab_test_bucket:, flow_path:, opted_in_to_in_person_proofing:, - same_address_as_id:, **extra, ) end @@ -2761,7 +2743,6 @@ def idv_in_person_prepare_visited(flow_path:, opted_in_to_in_person_proofing:, * # @param [String] step # @param [String] analytics_id # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing # address page visited def idv_in_person_proofing_address_visited( @@ -2770,7 +2751,6 @@ def idv_in_person_proofing_address_visited( analytics_id:, opted_in_to_in_person_proofing: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, **extra ) track_event( @@ -2780,7 +2760,6 @@ def idv_in_person_proofing_address_visited( analytics_id:, opted_in_to_in_person_proofing:, skip_hybrid_handoff:, - same_address_as_id:, **extra, ) end @@ -2861,7 +2840,6 @@ def idv_in_person_proofing_nontransliterable_characters_submitted( # @param [String] step Current IdV step # @param [String] analytics_id Current IdV flow identifier # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active - # @param [Boolean, nil] same_address_as_id # @param [String] current_address_zip_code ZIP code of given address # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing def idv_in_person_proofing_residential_address_submitted( @@ -2874,7 +2852,6 @@ def idv_in_person_proofing_residential_address_submitted( opted_in_to_in_person_proofing: nil, error_details: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, **extra ) track_event( @@ -2888,7 +2865,6 @@ def idv_in_person_proofing_residential_address_submitted( opted_in_to_in_person_proofing:, error_details:, skip_hybrid_handoff:, - same_address_as_id:, **extra, ) end @@ -2899,7 +2875,6 @@ def idv_in_person_proofing_residential_address_submitted( # @param [Boolean] success Whether form validation was successful # @param [Hash] errors Errors resulting from form validation # @param [Hash] error_details Details for errors that occurred in unsuccessful submission - # @param [Boolean, nil] same_address_as_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing # @param [String] birth_year Birth year from document # @param [String] document_zip_code ZIP code from document @@ -2915,7 +2890,6 @@ def idv_in_person_proofing_state_id_submitted( document_zip_code:, skip_hybrid_handoff: nil, error_details: nil, - same_address_as_id: nil, opted_in_to_in_person_proofing: nil, **extra ) @@ -2930,7 +2904,6 @@ def idv_in_person_proofing_state_id_submitted( birth_year:, document_zip_code:, skip_hybrid_handoff:, - same_address_as_id:, opted_in_to_in_person_proofing:, **extra, ) @@ -2940,7 +2913,6 @@ def idv_in_person_proofing_state_id_submitted( # @param [String] step # @param [String] analytics_id # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing - # @param [Boolean] same_address_as_id # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active # State id page visited def idv_in_person_proofing_state_id_visited( @@ -2949,7 +2921,6 @@ def idv_in_person_proofing_state_id_visited( analytics_id: nil, opted_in_to_in_person_proofing: nil, skip_hybrid_handoff: nil, - same_address_as_id: nil, **extra ) track_event( @@ -2959,7 +2930,6 @@ def idv_in_person_proofing_state_id_visited( analytics_id:, opted_in_to_in_person_proofing:, skip_hybrid_handoff:, - same_address_as_id:, **extra, ) end diff --git a/app/services/idv/flows/in_person_flow.rb b/app/services/idv/flows/in_person_flow.rb index 1c393f4465f..3343481770d 100644 --- a/app/services/idv/flows/in_person_flow.rb +++ b/app/services/idv/flows/in_person_flow.rb @@ -36,17 +36,11 @@ def self.session_idv(session) end def extra_analytics_properties - extra = { + { pii_like_keypaths: [ - [:same_address_as_id], [:proofing_results, :context, :stages, :state_id, :state_id_jurisdiction], ], } - unless @flow_session[:pii_from_user]&.[](:same_address_as_id).nil? - extra[:same_address_as_id] = - @flow_session[:pii_from_user][:same_address_as_id].to_s == 'true' - end - extra end end end diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb index 1701eb459d9..5341ce95ec6 100644 --- a/spec/controllers/idv/in_person/address_controller_spec.rb +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -59,7 +59,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'address', - same_address_as_id: false, } end @@ -124,7 +123,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'address', - same_address_as_id: false, current_address_zip_code: '59010', } end @@ -206,7 +204,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'address', - same_address_as_id: false, current_address_zip_code: '59010', } end diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index 45eba00e5e2..4e363c1972c 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -42,7 +42,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'ssn', - same_address_as_id: true, } end @@ -112,7 +111,6 @@ step: 'ssn', success: true, errors: {}, - same_address_as_id: true, } end @@ -150,7 +148,6 @@ step: 'ssn', success: true, previous_ssn_edit_distance: 6, - same_address_as_id: true, errors: {}, } end @@ -178,7 +175,6 @@ ssn: ['Enter a nine-digit Social Security number'], }, error_details: { ssn: { invalid: true } }, - same_address_as_id: true, } end diff --git a/spec/controllers/idv/in_person/state_id_controller_spec.rb b/spec/controllers/idv/in_person/state_id_controller_spec.rb index 5fa3770ebcb..731f64212f9 100644 --- a/spec/controllers/idv/in_person/state_id_controller_spec.rb +++ b/spec/controllers/idv/in_person/state_id_controller_spec.rb @@ -146,7 +146,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'state_id', - same_address_as_id: true, birth_year: dob[:year], document_zip_code: identity_doc_zipcode&.slice(0, 5), } 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 c422007bce7..4a6dbda956b 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -73,7 +73,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'verify', - same_address_as_id: true, }, ) end @@ -136,7 +135,6 @@ analytics_id: 'In Person Proofing', flow_path: 'standard', step: 'verify', - same_address_as_id: true, }, ), ) diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index bc72841df9b..7505f8b7717 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -117,7 +117,6 @@ }, biographical_info: { identity_doc_address_state: nil, - same_address_as_id: nil, state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############', @@ -160,7 +159,6 @@ }, biographical_info: { identity_doc_address_state: 'ND', - same_address_as_id: 'false', state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############', @@ -561,25 +559,25 @@ step: 'state_id', flow_path: 'standard', analytics_id: 'In Person Proofing' }, 'IdV: in person proofing state_id submitted' => { - success: true, flow_path: 'standard', step: 'state_id', analytics_id: 'In Person Proofing', errors: {}, same_address_as_id: false, birth_year: '1938', document_zip_code: '12345' + success: true, flow_path: 'standard', step: 'state_id', analytics_id: 'In Person Proofing', errors: {}, birth_year: '1938', document_zip_code: '12345' }, 'IdV: in person proofing address visited' => { - step: 'address', flow_path: 'standard', analytics_id: 'In Person Proofing', same_address_as_id: false + step: 'address', flow_path: 'standard', analytics_id: 'In Person Proofing' }, 'IdV: in person proofing residential address submitted' => { - success: true, step: 'address', flow_path: 'standard', analytics_id: 'In Person Proofing', errors: {}, same_address_as_id: false, current_address_zip_code: '59010' + success: true, step: 'address', flow_path: 'standard', analytics_id: 'In Person Proofing', errors: {}, current_address_zip_code: '59010' }, 'IdV: doc auth ssn visited' => { - analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard', same_address_as_id: false + analytics_id: 'In Person Proofing', step: 'ssn', flow_path: 'standard' }, 'IdV: doc auth ssn submitted' => { - analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', errors: {}, same_address_as_id: false + analytics_id: 'In Person Proofing', success: true, step: 'ssn', flow_path: 'standard', errors: {} }, 'IdV: doc auth verify visited' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', same_address_as_id: false + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard' }, 'IdV: doc auth verify submitted' => { - analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard', same_address_as_id: false + analytics_id: 'In Person Proofing', step: 'verify', flow_path: 'standard' }, idv_threatmetrix_response_body: ( if threatmetrix_response_body.present? @@ -587,7 +585,7 @@ end ), 'IdV: doc auth verify proofing results' => { - success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', step: 'verify', same_address_as_id: false, + success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'In Person Proofing', step: 'verify', proofing_results: in_person_path_proofing_results }, 'IdV: phone confirmation form' => { From 763c845a78e1f4833392c93093e6d1911d0781f2 Mon Sep 17 00:00:00 2001 From: Jenny Verdeyen Date: Mon, 28 Oct 2024 11:32:36 -0400 Subject: [PATCH 15/16] Rename state id controller route after FSM step removal (#11379) * Rename route changelog: Internal, In-person proofing, rename state id controller routes --- app/controllers/idv/in_person/address_controller.rb | 2 +- app/controllers/idv/in_person_controller.rb | 2 +- app/services/flow/flow_state_machine.rb | 4 ++-- app/views/idv/in_person/verify_info/show.html.erb | 2 +- config/routes.rb | 5 ++++- .../idv/in_person/address_controller_spec.rb | 2 +- spec/controllers/idv/in_person_controller_spec.rb | 6 +++--- .../idv/steps/in_person/state_id_controller_spec.rb | 4 ++-- .../features/idv/steps/in_person/state_id_step_spec.rb | 4 ++-- spec/support/features/in_person_helper.rb | 10 +++++----- 10 files changed, 22 insertions(+), 19 deletions(-) diff --git a/app/controllers/idv/in_person/address_controller.rb b/app/controllers/idv/in_person/address_controller.rb index a1ca3afd542..b289c440ed7 100644 --- a/app/controllers/idv/in_person/address_controller.rb +++ b/app/controllers/idv/in_person/address_controller.rb @@ -109,7 +109,7 @@ def redirect_to_next_page def confirm_in_person_state_id_step_complete return if pii_from_user&.has_key?(:identity_doc_address1) - redirect_to idv_in_person_proofing_state_id_url + redirect_to idv_in_person_state_id_url end def confirm_in_person_address_step_needed diff --git a/app/controllers/idv/in_person_controller.rb b/app/controllers/idv/in_person_controller.rb index 0da218e48f7..3ee3e59b932 100644 --- a/app/controllers/idv/in_person_controller.rb +++ b/app/controllers/idv/in_person_controller.rb @@ -20,7 +20,7 @@ class InPersonController < ApplicationController FLOW_STATE_MACHINE_SETTINGS = { step_url: :idv_in_person_step_url, - final_url: :idv_in_person_proofing_state_id_url, + final_url: :idv_in_person_state_id_url, flow: Idv::Flows::InPersonFlow, analytics_id: 'In Person Proofing', }.freeze diff --git a/app/services/flow/flow_state_machine.rb b/app/services/flow/flow_state_machine.rb index 338e86de78e..a8d35f496e3 100644 --- a/app/services/flow/flow_state_machine.rb +++ b/app/services/flow/flow_state_machine.rb @@ -13,7 +13,7 @@ module FlowStateMachine attr_accessor :flow def index - redirect_to idv_in_person_proofing_state_id_url + redirect_to idv_in_person_state_id_url end def show @@ -175,7 +175,7 @@ def redirect_to_step(_step) end def redirect_url - redirect_to idv_in_person_proofing_state_id_url + redirect_to idv_in_person_state_id_url end def analytics_properties diff --git a/app/views/idv/in_person/verify_info/show.html.erb b/app/views/idv/in_person/verify_info/show.html.erb index baf8157e7d9..0f5f79aa2c0 100644 --- a/app/views/idv/in_person/verify_info/show.html.erb +++ b/app/views/idv/in_person/verify_info/show.html.erb @@ -73,7 +73,7 @@ locals:
<%= link_to( - idv_in_person_proofing_state_id_url, + idv_in_person_state_id_url, class: 'usa-button usa-button--unstyled padding-y-1', 'aria-label': t('idv.buttons.change_state_id_label'), ) { t('idv.buttons.change_label') } %> diff --git a/config/routes.rb b/config/routes.rb index 41308b87004..ad6b34f4acb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -405,7 +405,8 @@ # sometimes underscores get messed up when linked to via SMS as: :capture_doc_dashes - get '/in_person_proofing/state_id' => 'in_person/state_id#show' + # Deprecated route - temporary redirect while state id changes are rolled out + get '/in_person_proofing/state_id' => redirect('verify/in_person/state_id', status: 307) put '/in_person_proofing/state_id' => 'in_person/state_id#update' get '/in_person' => 'in_person#index' @@ -413,6 +414,8 @@ as: :in_person_ready_to_verify post '/in_person/usps_locations' => 'in_person/usps_locations#index' put '/in_person/usps_locations' => 'in_person/usps_locations#update' + get '/in_person/state_id' => 'in_person/state_id#show' + put '/in_person/state_id' => 'in_person/state_id#update' get '/in_person/address' => 'in_person/address#show' put '/in_person/address' => 'in_person/address#update' get '/in_person/ssn' => 'in_person/ssn#show' diff --git a/spec/controllers/idv/in_person/address_controller_spec.rb b/spec/controllers/idv/in_person/address_controller_spec.rb index 5341ce95ec6..ed079de766a 100644 --- a/spec/controllers/idv/in_person/address_controller_spec.rb +++ b/spec/controllers/idv/in_person/address_controller_spec.rb @@ -37,7 +37,7 @@ subject.user_session['idv/in_person'][:pii_from_user].delete(:identity_doc_address1) get :show - expect(response).to redirect_to idv_in_person_proofing_state_id_url + expect(response).to redirect_to idv_in_person_state_id_url end end diff --git a/spec/controllers/idv/in_person_controller_spec.rb b/spec/controllers/idv/in_person_controller_spec.rb index e29158a1562..9a6a3f73271 100644 --- a/spec/controllers/idv/in_person_controller_spec.rb +++ b/spec/controllers/idv/in_person_controller_spec.rb @@ -56,7 +56,7 @@ it 'redirects to the first step' do get :index - expect(response).to redirect_to idv_in_person_proofing_state_id_path + expect(response).to redirect_to idv_in_person_state_id_path end it 'has non-nil presenter' do @@ -82,7 +82,7 @@ it 'redirects to the first step' do get :index - expect(response).to redirect_to idv_in_person_proofing_state_id_path + expect(response).to redirect_to idv_in_person_state_id_path end end end @@ -101,7 +101,7 @@ it 'finishes the flow' do put :update, params: { step: 'state_id' } - expect(response).to redirect_to idv_in_person_proofing_state_id_path + expect(response).to redirect_to idv_in_person_state_id_path end end end diff --git a/spec/features/idv/steps/in_person/state_id_controller_spec.rb b/spec/features/idv/steps/in_person/state_id_controller_spec.rb index c889cbebdc6..1bf4c26f9a7 100644 --- a/spec/features/idv/steps/in_person/state_id_controller_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_controller_spec.rb @@ -39,7 +39,7 @@ click_link t('links.cancel') click_on t('idv.cancel.actions.keep_going') - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) end it 'allows user to submit valid inputs on form', allow_browser_log: true do @@ -75,7 +75,7 @@ click_link t('idv.buttons.change_state_id_label') # state id page has fields that are pre-populated - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) expect(page).to have_field( t('in_person_proofing.form.state_id.first_name'), diff --git a/spec/features/idv/steps/in_person/state_id_step_spec.rb b/spec/features/idv/steps/in_person/state_id_step_spec.rb index 0eb2a4ed9ec..a4496cf623a 100644 --- a/spec/features/idv/steps/in_person/state_id_step_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_step_spec.rb @@ -39,7 +39,7 @@ click_link t('links.cancel') click_on t('idv.cancel.actions.keep_going') - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) end it 'allows user to submit valid inputs on form', allow_browser_log: true do @@ -75,7 +75,7 @@ click_link t('idv.buttons.change_state_id_label') # state id page has fields that are pre-populated - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) expect(page).to have_content(t('in_person_proofing.headings.update_state_id')) expect(page).to have_field( t('in_person_proofing.form.state_id.first_name'), diff --git a/spec/support/features/in_person_helper.rb b/spec/support/features/in_person_helper.rb index 9c48b2b2b74..36730c23cc3 100644 --- a/spec/support/features/in_person_helper.rb +++ b/spec/support/features/in_person_helper.rb @@ -122,7 +122,7 @@ def complete_prepare_step(_user = nil) def complete_state_id_step(_user = nil, same_address_as_id: true, first_name: GOOD_FIRST_NAME) # Wait for page to load before attempting to fill out form - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) fill_out_state_id_form_ok(same_address_as_id: same_address_as_id, first_name:) click_idv_continue unless same_address_as_id @@ -134,7 +134,7 @@ def complete_state_id_step(_user = nil, same_address_as_id: true, first_name: GO def complete_state_id_controller(_user = nil, same_address_as_id: true, first_name: GOOD_FIRST_NAME) # Wait for page to load before attempting to fill out form - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) fill_out_state_id_form_ok(same_address_as_id: same_address_as_id, first_name:) click_idv_continue unless same_address_as_id @@ -163,7 +163,7 @@ def complete_steps_before_state_id_step begin_in_person_proofing complete_prepare_step complete_location_step - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) end def complete_steps_before_state_id_controller @@ -171,7 +171,7 @@ def complete_steps_before_state_id_controller begin_in_person_proofing complete_prepare_step complete_location_step - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) end def complete_all_in_person_proofing_steps(user = user_with_2fa, tmx_status = nil, @@ -254,7 +254,7 @@ def perform_mobile_hybrid_steps def perform_desktop_hybrid_steps(user = user_with_2fa, same_address_as_id: true) perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_in_person_proofing_state_id_path, wait: 10) + expect(page).to have_current_path(idv_in_person_state_id_path, wait: 10) complete_state_id_controller(user, same_address_as_id: same_address_as_id) complete_address_step(user, same_address_as_id: same_address_as_id) unless same_address_as_id From 1e55a4b1967367c0d4a0c7a2722e22e9a15d8e0f Mon Sep 17 00:00:00 2001 From: Andrew Duthie <1779930+aduth@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:44:58 -0400 Subject: [PATCH 16/16] LG-14887: Log action name with reCAPTCHA assessment result received (#11407) * Log action name with reCAPTCHA assessment result received` changelog: Internal, Analytics, Log action name with reCAPTCHA assessment result received` * Add missing recaptcha_action spec assertions --- app/forms/recaptcha_form.rb | 1 + app/services/analytics_events.rb | 5 +++- spec/features/phone/add_phone_spec.rb | 1 + spec/forms/recaptcha_enterprise_form_spec.rb | 31 ++++++++++++-------- spec/forms/recaptcha_form_spec.rb | 10 ++++++- spec/forms/recaptcha_mock_form_spec.rb | 5 +++- spec/support/shared_examples/sign_in.rb | 1 + 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/forms/recaptcha_form.rb b/app/forms/recaptcha_form.rb index 603b4f8f16e..83ff73ab01f 100644 --- a/app/forms/recaptcha_form.rb +++ b/app/forms/recaptcha_form.rb @@ -120,6 +120,7 @@ def log_analytics(result: nil, error: nil) evaluated_as_valid: recaptcha_result_valid?(result), exception_class: error&.class&.name, form_class: self.class.name, + recaptcha_action:, **extra_analytics_properties, ) end diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 7f4ea4e62f6..58c71942680 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -6108,13 +6108,15 @@ def reactivate_account_visit # @param [Boolean] evaluated_as_valid Whether result was considered valid # @param [String] form_class Class name of form # @param [String, nil] exception_class Class name of exception, if error occurred - # @param [String, nil] phone_country_code Country code associated with reCAPTCHA phone result + # @param [String] recaptcha_action reCAPTCHA action name, for distinct user flow + # @param [String, nil] phone_country_code Country code associated with reCAPTCHA phone results def recaptcha_verify_result_received( recaptcha_result:, score_threshold:, evaluated_as_valid:, form_class:, exception_class:, + recaptcha_action:, phone_country_code: nil, **extra ) @@ -6125,6 +6127,7 @@ def recaptcha_verify_result_received( evaluated_as_valid:, form_class:, exception_class:, + recaptcha_action:, phone_country_code:, **extra, ) diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index 38b3badf26a..a924e341fe9 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -232,6 +232,7 @@ evaluated_as_valid: false, score_threshold: 0.6, phone_country_code: 'AU', + recaptcha_action: 'phone_setup', form_class: 'RecaptchaMockForm', ) diff --git a/spec/forms/recaptcha_enterprise_form_spec.rb b/spec/forms/recaptcha_enterprise_form_spec.rb index 1d9858d8b2e..6a133b14570 100644 --- a/spec/forms/recaptcha_enterprise_form_spec.rb +++ b/spec/forms/recaptcha_enterprise_form_spec.rb @@ -4,7 +4,7 @@ let(:score_threshold) { 0.2 } let(:analytics) { FakeAnalytics.new } let(:extra_analytics_properties) { {} } - let(:action) { 'example_action' } + let(:recaptcha_action) { 'example_action' } let(:recaptcha_enterprise_api_key) { 'recaptcha_enterprise_api_key' } let(:recaptcha_enterprise_project_id) { 'project_id' } let(:recaptcha_site_key) { 'recaptcha_site_key' } @@ -19,7 +19,7 @@ subject(:form) do described_class.new( - recaptcha_action: action, + recaptcha_action:, score_threshold:, analytics:, extra_analytics_properties:, @@ -121,11 +121,11 @@ before do stub_recaptcha_response( body: { - tokenProperties: { valid: false, action:, invalidReason: 'EXPIRED' }, + tokenProperties: { valid: false, action: recaptcha_action, invalidReason: 'EXPIRED' }, event: {}, name:, }, - action:, + action: recaptcha_action, token:, ) end @@ -155,6 +155,7 @@ evaluated_as_valid: false, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, ) end end @@ -167,7 +168,7 @@ body: { error: { code: 400, status: 'INVALID_ARGUMENT' }, }, - action:, + action: recaptcha_action, token:, ) end @@ -194,6 +195,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, ) end end @@ -221,6 +223,7 @@ score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', exception_class: 'Faraday::ConnectionFailed', + recaptcha_action:, ) end end @@ -233,12 +236,12 @@ before do stub_recaptcha_response( body: { - tokenProperties: { valid: true, action: }, + tokenProperties: { valid: true, action: recaptcha_action }, riskAnalysis: { score:, reasons: ['AUTOMATION'] }, event: {}, name:, }, - action:, + action: recaptcha_action, token:, ) end @@ -268,6 +271,7 @@ evaluated_as_valid: false, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, ) end @@ -275,12 +279,12 @@ before do stub_recaptcha_response( body: { - tokenProperties: { valid: true, action: }, + tokenProperties: { valid: true, action: recaptcha_action }, riskAnalysis: { score:, reasons: ['LOW_CONFIDENCE_SCORE'] }, event: {}, name:, }, - action:, + action: recaptcha_action, token:, ) end @@ -307,6 +311,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, ) end end @@ -320,12 +325,12 @@ around do |example| stubbed_request = stub_recaptcha_response( body: { - tokenProperties: { valid: true, action: }, + tokenProperties: { valid: true, action: recaptcha_action }, riskAnalysis: { score:, reasons: ['LOW_CONFIDENCE'] }, event: {}, name:, }, - action:, + action: recaptcha_action, token:, ) example.run @@ -354,6 +359,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, ) end @@ -368,7 +374,7 @@ event: {}, name:, }, - action:, + action: recaptcha_action, token:, ) end @@ -402,6 +408,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaEnterpriseForm', + recaptcha_action:, extra: true, ) end diff --git a/spec/forms/recaptcha_form_spec.rb b/spec/forms/recaptcha_form_spec.rb index db30028205f..63bf1096e7d 100644 --- a/spec/forms/recaptcha_form_spec.rb +++ b/spec/forms/recaptcha_form_spec.rb @@ -5,9 +5,10 @@ let(:analytics) { FakeAnalytics.new } let(:extra_analytics_properties) { {} } let(:recaptcha_secret_key) { 'recaptcha_secret_key' } + let(:recaptcha_action) { 'example_action' } subject(:form) do - RecaptchaForm.new(score_threshold:, analytics:, extra_analytics_properties:) + RecaptchaForm.new(score_threshold:, recaptcha_action:, analytics:, extra_analytics_properties:) end before do @@ -129,6 +130,7 @@ evaluated_as_valid: false, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, ) end @@ -163,6 +165,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, ) end end @@ -197,6 +200,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, ) end end @@ -226,6 +230,7 @@ score_threshold: score_threshold, form_class: 'RecaptchaForm', exception_class: 'Faraday::ConnectionFailed', + recaptcha_action:, ) end end @@ -263,6 +268,7 @@ evaluated_as_valid: false, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, ) end end @@ -299,6 +305,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, ) end @@ -320,6 +327,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaForm', + recaptcha_action:, extra: true, ) end diff --git a/spec/forms/recaptcha_mock_form_spec.rb b/spec/forms/recaptcha_mock_form_spec.rb index 3ee562f5ad2..ea39bf7c056 100644 --- a/spec/forms/recaptcha_mock_form_spec.rb +++ b/spec/forms/recaptcha_mock_form_spec.rb @@ -4,8 +4,9 @@ let(:score_threshold) { 0.2 } let(:analytics) { FakeAnalytics.new } let(:score) { nil } + let(:recaptcha_action) { 'example_action' } subject(:form) do - RecaptchaMockForm.new(score_threshold:, analytics:, score:) + RecaptchaMockForm.new(score_threshold:, analytics:, recaptcha_action:, score:) end around do |example| @@ -44,6 +45,7 @@ evaluated_as_valid: false, score_threshold: score_threshold, form_class: 'RecaptchaMockForm', + recaptcha_action:, ) end end @@ -74,6 +76,7 @@ evaluated_as_valid: true, score_threshold: score_threshold, form_class: 'RecaptchaMockForm', + recaptcha_action:, ) end diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index 042952d7b70..bbe09be0a3e 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -348,6 +348,7 @@ def user_with_broken_personal_key(scenario) }, evaluated_as_valid: false, score_threshold: 0.2, + recaptcha_action: 'sign_in', form_class: 'RecaptchaMockForm', ) expect(fake_analytics).to have_logged_event(