diff --git a/Gemfile b/Gemfile index 7b54722971a..19d013e3bb5 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,7 @@ gem 'dotiw', '>= 4.0.1' gem 'faraday', '~> 2' gem 'faker' gem 'faraday-retry' +gem 'fugit' gem 'foundation_emails' gem 'good_job', '~> 3.0' gem 'http_accept_language' diff --git a/Gemfile.lock b/Gemfile.lock index 36ec53aae08..3173e2b63f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -788,6 +788,7 @@ DEPENDENCIES faraday (~> 2) faraday-retry foundation_emails + fugit good_job (~> 3.0) http_accept_language i18n-tasks (~> 1.0) diff --git a/app/components/icon_list_item_component.html.erb b/app/components/icon_list_item_component.html.erb index 128a16fccb8..baa4d53d8c7 100644 --- a/app/components/icon_list_item_component.html.erb +++ b/app/components/icon_list_item_component.html.erb @@ -2,5 +2,5 @@ <%= content_tag(:div, class: icon_css_class) do %> <%= render IconComponent.new(icon: icon) %> <% end %> -
<%= content %>
+
<%= content %>
<% end %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 047cd2de76b..bf92f6468a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -261,7 +261,7 @@ def user_needs_to_reactivate_account? end def user_recommended_for_piv_cac? - current_user.piv_cac_recommended_dismissed_at.nil? && current_user.has_gov_or_mil_email? && + current_user.piv_cac_recommended_dismissed_at.nil? && current_user.has_fed_or_mil_email? && !user_already_has_piv? end diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index e999c4c1376..ac111fae564 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -82,7 +82,7 @@ def show_skip_additional_mfa_link? end def check_if_possible_piv_user - if current_user.has_gov_or_mil_email? && current_user.piv_cac_recommended_dismissed_at.nil? + if current_user.has_fed_or_mil_email? && current_user.piv_cac_recommended_dismissed_at.nil? redirect_to login_piv_cac_recommended_path end end diff --git a/app/controllers/concerns/saml_idp_auth_concern.rb b/app/controllers/concerns/saml_idp_auth_concern.rb index 1a055c5cac2..41b01f10f04 100644 --- a/app/controllers/concerns/saml_idp_auth_concern.rb +++ b/app/controllers/concerns/saml_idp_auth_concern.rb @@ -140,9 +140,18 @@ def link_identity_from_session_data link_identity( ial: resolved_authn_context_int_ial, rails_session_id: session.id, + email_address_id: email_address_id, ) end + 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? + 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 + end + def identity_needs_verification? resolved_authn_context_result.identity_proofing? && current_user.identity_not_verified? end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index faa27028cba..f792dd55ac6 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -84,9 +84,17 @@ def link_identity_to_service_provider current_user: current_user, ial: resolved_authn_context_int_ial, rails_session_id: session.id, + email_address_id: email_address_id, ) end + 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? + identity = current_user.identities.find_by(service_provider: sp_session['issuer']) + identity&.email_address_id + end + def ial_context IalContext.new( ial: resolved_authn_context_int_ial, diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index 1107fa56309..007e1609c95 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -21,6 +21,10 @@ 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). + last_sign_in_email_address.id + end if decider.go_back_to_mobile_app? sign_user_out_and_instruct_to_go_back_to_mobile_app else @@ -49,6 +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], ) end diff --git a/app/controllers/sign_up/select_email_controller.rb b/app/controllers/sign_up/select_email_controller.rb new file mode 100644 index 00000000000..2c6b52a2382 --- /dev/null +++ b/app/controllers/sign_up/select_email_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module SignUp + class SelectEmailController < ApplicationController + before_action :confirm_two_factor_authenticated + before_action :verify_needs_completions_screen + + def show + @sp_name = current_sp.friendly_name || sp.agency&.name + @user_emails = user_emails + @last_sign_in_email_address = last_email + @select_email_form = build_select_email_form + end + + def create + @select_email_form = build_select_email_form + + result = @select_email_form.submit(form_params) + if result.success? + user_session[:selected_email_id] = form_params[:selected_email_id] + redirect_to sign_up_completed_path + else + flash[:error] = result.first_error_message + redirect_to sign_up_select_email_path + end + end + + def user_emails + @user_emails = current_user.confirmed_email_addresses + end + + private + + def build_select_email_form + SelectEmailForm.new(current_user) + end + + def form_params + params.fetch(:select_email_form, {}).permit(:selected_email_id) + end + + def last_email + if user_session[:selected_email_id] + user_emails.find(user_session[:selected_email_id]).email + else + EmailContext.new(current_user).last_sign_in_email_address.email + end + end + + def verify_needs_completions_screen + redirect_to account_url unless needs_completion_screen_reason + end + end +end diff --git a/app/controllers/socure_webhook_controller.rb b/app/controllers/socure_webhook_controller.rb index 69234ac992b..14bdfd494b4 100644 --- a/app/controllers/socure_webhook_controller.rb +++ b/app/controllers/socure_webhook_controller.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class SocureWebhookController < ApplicationController + include RenderConditionConcern + skip_before_action :verify_authenticity_token + check_or_render_not_found -> { IdentityConfig.store.socure_webhook_enabled } + def create if token_valid? render json: { message: 'Secret token is valid.' } diff --git a/app/controllers/users/piv_cac_recommended_controller.rb b/app/controllers/users/piv_cac_recommended_controller.rb index 4335917d6df..2b504239ac3 100644 --- a/app/controllers/users/piv_cac_recommended_controller.rb +++ b/app/controllers/users/piv_cac_recommended_controller.rb @@ -8,7 +8,7 @@ class PivCacRecommendedController < ApplicationController before_action :confirm_user_authenticated_for_2fa_setup before_action :apply_secure_headers_override - before_action :redirect_unless_user_email_is_gov_or_mil + before_action :redirect_unless_user_email_is_fed_or_mil def show @recommended_presenter = PivCacRecommendedPresenter.new(current_user) @@ -30,8 +30,8 @@ def skip private - def redirect_unless_user_email_is_gov_or_mil - redirect_to after_sign_in_path_for(current_user) unless current_user.has_gov_or_mil_email? + def redirect_unless_user_email_is_fed_or_mil + redirect_to after_sign_in_path_for(current_user) unless current_user.has_fed_or_mil_email? end end end diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index 7804b188d15..0ef866928b7 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -16,7 +16,7 @@ def index @presenter = two_factor_options_presenter analytics.user_registration_2fa_setup_visit( enabled_mfa_methods_count:, - gov_or_mil_email: has_gov_or_mil_email?, + gov_or_mil_email: fed_or_mil_email?, ) end @@ -44,8 +44,8 @@ def two_factor_options_form private - def has_gov_or_mil_email? - current_user.confirmed_email_addresses.any?(&:gov_or_mil?) + def fed_or_mil_email? + current_user.confirmed_email_addresses.any?(&:fed_or_mil_email?) end def mfa_context diff --git a/app/forms/openid_connect_authorize_form.rb b/app/forms/openid_connect_authorize_form.rb index a70dcf8d09d..bbe816167a7 100644 --- a/app/forms/openid_connect_authorize_form.rb +++ b/app/forms/openid_connect_authorize_form.rb @@ -94,7 +94,8 @@ def service_provider def link_identity_to_service_provider( current_user:, ial:, - rails_session_id: + rails_session_id:, + email_address_id: ) identity_linker = IdentityLinker.new(current_user, service_provider) @identity = identity_linker.link_identity( @@ -106,6 +107,7 @@ def link_identity_to_service_provider( requested_aal_value: requested_aal_value, scope: scope.join(' '), code_challenge: code_challenge, + email_address_id: email_address_id, ) end diff --git a/app/forms/select_email_form.rb b/app/forms/select_email_form.rb new file mode 100644 index 00000000000..165d5e0f331 --- /dev/null +++ b/app/forms/select_email_form.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class SelectEmailForm + include ActiveModel::Model + include ActionView::Helpers::TranslationHelper + + attr_reader :user, :selected_email_id + + validate :validate_owns_selected_email + + def initialize(user) + @user = user + end + + def submit(params) + @selected_email_id = params[:selected_email_id] + + success = valid? + FormResponse.new(success:, errors:) + end + + private + + def validate_owns_selected_email + return if user.confirmed_email_addresses.exists?(id: selected_email_id) + + errors.add :email, I18n.t( + 'email_address.not_found', + ), type: :selected_email_id + end +end diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 058faadb939..ed290f1ecee 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -236,7 +236,7 @@ def handle_unsupported_id_type(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) - + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) @@ -271,7 +271,7 @@ def handle_expired_status_update(enrollment, response, response_message) status: :expired, status_check_completed_at: Time.zone.now, ) - enrollment.profile.deactivate_due_to_ipp_expiration + enrollment.profile.deactivate_due_to_in_person_verification_cancelled if fraud_result_pending?(enrollment) analytics(user: enrollment.user).idv_ipp_deactivated_for_never_visiting_post_office( @@ -325,8 +325,10 @@ def handle_fraud_review_pending(enrollment) end def handle_unexpected_response(enrollment, response_message, reason:, cancel: true) - enrollment.cancelled! if cancel - + if cancel + enrollment.cancelled! + enrollment.profile.deactivate_due_to_in_person_verification_cancelled + end analytics(user: enrollment.user). idv_in_person_usps_proofing_results_job_unexpected_response( **enrollment_analytics_attributes(enrollment, complete: cancel), @@ -352,7 +354,7 @@ def handle_failed_status(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) - + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) if response['fraudSuspected'] @@ -442,6 +444,7 @@ def handle_unsupported_secondary_id(enrollment, response) proofed_at: proofed_at, status_check_completed_at: Time.zone.now, ) + enrollment.profile.deactivate_due_to_in_person_verification_cancelled # send SMS and email send_enrollment_status_sms_notification(enrollment: enrollment) send_failed_email(enrollment.user, enrollment) diff --git a/app/models/email_address.rb b/app/models/email_address.rb index 0f4ca204a6a..1692826343d 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -11,6 +11,7 @@ class EmailAddress < ApplicationRecord # rubocop:disable Rails/HasManyOrHasOneDependent has_one :suspended_email # rubocop:enable Rails/HasManyOrHasOneDependent + has_many :identities, class_name: 'ServiceProviderIdentity', dependent: :nullify scope :confirmed, -> { where('confirmed_at IS NOT NULL') } @@ -29,8 +30,25 @@ def confirmation_period_expired? Time.zone.now > expiration_time end - def gov_or_mil? - email.end_with?('.gov', '.mil') + def domain + Mail::Address.new(email).domain + end + + def fed_or_mil_email? + fed_email? || mil_email? + end + + def fed_email? + if IdentityConfig.store.use_fed_domain_class + return false unless domain + FederalEmailDomain.fed_domain?(domain) + else + email.end_with?('.gov') + end + end + + def mil_email? + email.end_with?('.mil') end class << self diff --git a/app/models/federal_email_domain.rb b/app/models/federal_email_domain.rb new file mode 100644 index 00000000000..1ba01ea0801 --- /dev/null +++ b/app/models/federal_email_domain.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FederalEmailDomain < ApplicationRecord + def self.fed_domain?(domain) + exists?(name: domain) + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 0ddb3b925bd..e26ab003fa3 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -196,7 +196,7 @@ def deactivate_due_to_gpo_expiration ) end - def deactivate_due_to_ipp_expiration + def deactivate_due_to_in_person_verification_cancelled update!( active: false, deactivation_reason: :verification_cancelled, diff --git a/app/models/service_provider_identity.rb b/app/models/service_provider_identity.rb index dd17e94a0f3..5030aea024c 100644 --- a/app/models/service_provider_identity.rb +++ b/app/models/service_provider_identity.rb @@ -19,6 +19,8 @@ class ServiceProviderIdentity < ApplicationRecord # rubocop:enable Rails/InverseOf has_one :agency, through: :service_provider_record + belongs_to :email_address + scope :not_deleted, -> { where(deleted_at: nil) } CONSENT_EXPIRATION = 1.year.freeze diff --git a/app/models/user.rb b/app/models/user.rb index 0da6ccc47e2..faeaa7ddfb6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,8 +79,8 @@ def confirmed? email_addresses.where.not(confirmed_at: nil).any? end - def has_gov_or_mil_email? - confirmed_email_addresses.any?(&:gov_or_mil?) + def has_fed_or_mil_email? + confirmed_email_addresses.any?(&:fed_or_mil_email?) end def accepted_rules_of_use_still_valid? diff --git a/app/presenters/completions_presenter.rb b/app/presenters/completions_presenter.rb index e83a3bb37f3..55f8cadb77a 100644 --- a/app/presenters/completions_presenter.rb +++ b/app/presenters/completions_presenter.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true class CompletionsPresenter - attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, :completion_context + include ActionView::Helpers::TranslationHelper + include ActionView::Helpers::TagHelper + + attr_reader :current_user, :current_sp, :decrypted_pii, :requested_attributes, + :completion_context, :selected_email_id SORTED_IAL2_ATTRIBUTE_MAPPING = [ [[:email], :email], @@ -30,7 +34,8 @@ def initialize( decrypted_pii:, requested_attributes:, ial2_requested:, - completion_context: + completion_context:, + selected_email_id: ) @current_user = current_user @current_sp = current_sp @@ -38,6 +43,7 @@ def initialize( @requested_attributes = requested_attributes @ial2_requested = ial2_requested @completion_context = completion_context + @selected_email_id = selected_email_id end def ial2_requested? @@ -72,33 +78,24 @@ def heading end def intro - if ial2_requested? - if consent_has_expired? - I18n.t( - 'help_text.requested_attributes.ial2_consent_reminder_html', - sp: sp_name, - ) - elsif reverified_after_consent? - I18n.t( - 'help_text.requested_attributes.ial2_reverified_consent_info', - sp: sp_name, - ) - else - I18n.t( - 'help_text.requested_attributes.ial2_intro_html', - sp: sp_name, - ) - end - elsif consent_has_expired? - I18n.t( - 'help_text.requested_attributes.ial1_consent_reminder_html', - sp: sp_name, + if consent_has_expired? + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, sp_name), + ), + t('help_text.requested_attributes.intro_html', sp_html: content_tag(:strong, sp_name)), + ], + ' ', ) - else - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: sp_name, + elsif ial2_requested? && reverified_after_consent? + t( + 'help_text.requested_attributes.ial2_reverified_consent_info_html', + sp_html: content_tag(:strong, sp_name), ) + else + t('help_text.requested_attributes.intro_html', sp_html: content_tag(:strong, sp_name)) end end @@ -108,6 +105,10 @@ def pii end end + def multiple_emails? + current_user.confirmed_email_addresses.many? + end + private def first_time_signing_in? @@ -118,6 +119,7 @@ def displayable_pii @displayable_pii ||= DisplayablePiiFormatter.new( current_user: current_user, pii: decrypted_pii, + selected_email_id: @selected_email_id, ).format end diff --git a/app/presenters/openid_connect_user_info_presenter.rb b/app/presenters/openid_connect_user_info_presenter.rb index 1fd4f925e22..bc87e20231b 100644 --- a/app/presenters/openid_connect_user_info_presenter.rb +++ b/app/presenters/openid_connect_user_info_presenter.rb @@ -54,7 +54,7 @@ def uuid_from_sp_identity(identity) end def email_from_sp_identity - email_context.last_sign_in_email_address.email + identity.email_address&.email || email_context.last_sign_in_email_address.email end def all_emails_from_sp_identity(identity) diff --git a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb index 79b9dc5b540..a6b599df37d 100644 --- a/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb +++ b/app/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter.rb @@ -19,7 +19,7 @@ def phishing_resistant? end def recommended? - user.confirmed_email_addresses.any?(&:gov_or_mil?) + user.confirmed_email_addresses.any?(&:fed_or_mil_email?) end def desktop_only? diff --git a/app/presenters/two_factor_options_presenter.rb b/app/presenters/two_factor_options_presenter.rb index 6c97445e661..bee7ac40787 100644 --- a/app/presenters/two_factor_options_presenter.rb +++ b/app/presenters/two_factor_options_presenter.rb @@ -11,7 +11,6 @@ class TwoFactorOptionsPresenter :user_agent delegate :two_factor_enabled?, to: :mfa_policy - def initialize( user_agent:, user: nil, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index edad5db7413..d1a2b002959 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1329,14 +1329,6 @@ def idv_doc_auth_link_sent_visited(**extra) track_event('IdV: doc auth link_sent visited', **extra) end - def idv_doc_auth_randomizer_defaulted(**extra) - track_event( - 'IdV: doc_auth random vendor error', - error: 'document_capture_session_uuid_key missing', - **extra, - ) - end - def idv_doc_auth_redo_ssn_submitted(**extra) track_event('IdV: doc auth redo_ssn submitted', **extra) end diff --git a/app/services/attribute_asserter.rb b/app/services/attribute_asserter.rb index 125a720d3be..d2320b77b10 100644 --- a/app/services/attribute_asserter.rb +++ b/app/services/attribute_asserter.rb @@ -200,7 +200,10 @@ def attribute_getter_function_ascii(attr) def add_email(attrs) attrs[:email] = { - getter: ->(principal) { EmailContext.new(principal).last_sign_in_email_address.email }, + getter: ->(principal) { + last_email_from_sp(principal) || + EmailContext.new(principal).last_sign_in_email_address.email + }, name_format: 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', name_id_format: Saml::XML::Namespaces::Formats::NameId::EMAIL_ADDRESS, } @@ -214,6 +217,13 @@ def add_all_emails(attrs) } end + def last_email_from_sp(principal) + return nil unless IdentityConfig.store.feature_select_email_to_share_enabled + identity = principal.active_identity_for(service_provider) + email_id = identity&.email_address_id + principal.confirmed_email_addresses.find_by(id: email_id)&.email if email_id + end + def bundle @bundle ||= ( authn_request_bundle || service_provider.metadata[:attribute_bundle] || [] diff --git a/app/services/displayable_pii_formatter.rb b/app/services/displayable_pii_formatter.rb index a9c61ad1580..1bc6399fd0b 100644 --- a/app/services/displayable_pii_formatter.rb +++ b/app/services/displayable_pii_formatter.rb @@ -8,10 +8,12 @@ class DisplayablePiiFormatter attr_reader :current_user attr_reader :pii + attr_reader :selected_email_id - def initialize(current_user:, pii:) + def initialize(current_user:, pii:, selected_email_id:) @current_user = current_user @pii = pii + @selected_email_id = selected_email_id end # @return [FormattedPii] @@ -36,7 +38,11 @@ def format private def email - EmailContext.new(current_user).last_sign_in_email_address.email + if @selected_email_id + current_user.confirmed_email_addresses.find(@selected_email_id).email + else + EmailContext.new(current_user).last_sign_in_email_address.email + end end def all_emails diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index 650c403b95c..a4c43f1d75f 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -196,9 +196,14 @@ def self.client(vendor:, warn_notifier: nil) # rubocop:enable Layout/LineLength def self.doc_auth_vendor_for_bucket(bucket) - bucket == :alternate_vendor ? - IdentityConfig.store.doc_auth_vendor_randomize_alternate_vendor : - IdentityConfig.store.doc_auth_vendor + case bucket + when :socure + Idp::Constants::Vendors::SOCURE + when :lexis_nexis + Idp::Constants::Vendors::LEXIS_NEXIS + else # e.g., nil + IdentityConfig.store.doc_auth_vendor_default + end end def self.doc_auth_vendor( diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index b34f76077f7..8f69f422560 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -25,7 +25,8 @@ def link_identity( scope: nil, verified_attributes: nil, last_consented_at: nil, - clear_deleted_at: nil + clear_deleted_at: nil, + email_address_id: nil ) return unless user && service_provider.present? @@ -43,6 +44,7 @@ def link_identity( rails_session_id: rails_session_id, scope: scope, verified_attributes: combined_verified_attributes(verified_attributes), + email_address_id: email_address_id, ).tap do |hash| hash[:last_consented_at] = last_consented_at if last_consented_at hash[:deleted_at] = nil if clear_deleted_at diff --git a/app/services/idv/aamva_state_maintenance_window.rb b/app/services/idv/aamva_state_maintenance_window.rb new file mode 100644 index 00000000000..422e45d122c --- /dev/null +++ b/app/services/idv/aamva_state_maintenance_window.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module Idv + class AamvaStateMaintenanceWindow + # _All_ AAMVA maintenance windows are expressed in 'ET' (LG-14028) + TZ = 'America/New_York' + + MAINTENANCE_WINDOWS = { + 'CA' => [ + # Daily, 4:00 - 5:30 am. ET. + { cron: '0 4 * * *', duration_minutes: 90 }, + # Monday, 1:00 - 1:45 am. ET + { cron: '0 1 * * Mon', duration_minutes: 45 }, + # Monday, 1:00 - 4:30 am. ET on 1st and 3rd Monday of month. + { cron: '0 1 * * Mon#1', duration_minutes: 3.5 * 60 }, + { cron: '0 1 * * Mon#3', duration_minutes: 3.5 * 60 }, + ], + 'CT' => [ + # Daily, 4:00 am. to 6:30 am. ET. + { cron: '0 4 * * *', duration_minutes: 90 }, + # Sunday 6:00 am. to 9:30 am. ET + { cron: '0 6 * * Mon', duration_minutes: 3.5 * 60 }, + ], + 'DC' => [ + # Daily, Midnight to 6 am. ET. + { cron: '0 0 * * *', duration_minutes: 6 * 60 }, + ], + 'DE' => [ + # Daily, Midnight to 5 am. ET. + { cron: '0 0 * * *', duration_minutes: 5 * 60 }, + ], + 'FL' => [ + # Sunday 7:00 am. to 12:00 pm. ET + { cron: '0 7 * * Sun', duration_minutes: 5 * 60 }, + ], + 'IA' => [ + # "Daily system resets, normally at 4:45 am. to 5:15 am ET." + { cron: '45 4 * * *', duration_minutes: 30 }, + ], + 'IN' => [ + # Sunday morning maintenance from 6 am. to 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'IL' => [ + { cron: '30 2 * * *', duration_minutes: 2.5 * 60 }, # Daily, 2:30 am. to 5 am. ET. + ], + 'KY' => [ + # Daily maintenance from 2:50 am. to 6:40 am. ET + { cron: '50 2 * * *', duration_minutes: 230 }, + ], + 'MA' => [ + # Daily maintenance from 6 am. to 6:15 am. ET. + { cron: '0 6 * * *', duration_minutes: 15 }, + # Wednesday 7 am. to 7:30 am. ET. + { cron: '0 7 * * Wed', duration_minutes: 30 }, + # Saturday 10:00 pm. to Sunday 10:00 am + { cron: '0 22 * * Sat', duration_minutes: 12 * 60 }, + # First Friday of each month: 12 to 6 am. ET. + { cron: '0 0 * * Fri#1', duration_minutes: 6 * 60 }, + ], + 'MD' => [ + # Daily maintenance from 3 am. to 3:15 am. ET. + { cron: '0 3 * * *', duration_minutes: 15 }, + # Sunday maintenance may occur from 6 am. to 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'MI' => [ + # Daily maintenance from 9 pm. to 9:15 pm. ET. + { cron: '0 21 * * *', duration_minutes: 15 }, + ], + 'MO' => [ + # Daily maintenance from 2 am. to 4:30 am. ... + { cron: '0 2 * * *', duration_minutes: 2.5 * 60 }, + # ... from 6:30 am to 6:45 am ... + { cron: '30 6 * * *', duration_minutes: 15 }, + # ... and 8:30 am. to 8:35 am ET. + { cron: '30 8 * * *', duration_minutes: 5 }, + # Sundays from 9 am. to 10:30 am. ET... + { cron: '0 9 * * Sun', duration_minutes: 90 }, + # ...and 5 am to 5:45 am ET on 2nd Sunday of month. + { cron: '0 5 * * Sun#2', duration_minutes: 45 }, + ], + 'NC' => [ + # Daily, Midnight to 7:00 am. ET. + { cron: '0 0 * * *', duration_minutes: 7 * 60 }, + # Sundays from 5am. till Noon + { cron: '0 5 * * Sun', duration_minutes: 7 * 60 }, + ], + # NM: "Sunday mornings." (not modeling; too vague) + 'NY' => [ + # Sunday maintenance 8 pm. to 9 pm. ET. + { cron: '0 20 * * Sun', duration_minutes: 60 }, + ], + 'PA' => [ + # Sunday maintenance may occur, often between 5:30 am. & 7:00 am. ET + { cron: '30 5 * * Sun', duration_minutes: 90 }, + ], + 'SC' => [ + # Sunday maintenance from 7:00 pm. to 10:00 pm. ET. + { cron: '0 19 * * Sun', duration_minutes: 3 * 60 }, + ], + 'TX' => [ + # Downtime on weekends between 9 pm ET to 7 am ET. + { cron: '0 21 * * Sat,Sun', duration_minutes: 10 * 60 }, + ], + 'VA' => [ + # Sunday morning maintenance 3:00 am. to 5 am. ET. + { cron: '0 3 * * Sun', duration_minutes: 120 }, + # Daily maintenance from 5 am. to 5:30 am. + { cron: '0 5 * * *', duration_minutes: 30 }, + # "Might not respond for short spells, daily between 7 pm and 8:30 pm." (not modeling this) + ], + 'VT' => [ + # Daily maintenance from midnight to 5 am. ET. + { cron: '0 0 * * *', duration_minutes: 5 * 60 }, + ], + 'WA' => [ + # Maintenance from Saturday 9:45 pm. to Sunday 8:15 am. ET. + { cron: '45 21 * * Sat', duration_minutes: 10.5 * 60 }, + ], + 'WI' => [ + # Downtime on Tuesday – Saturday typically between 3 – 4 am ET. + { cron: '0 3 * * Tue-Sat', duration_minutes: 60 }, + # Downtime on Sunday from 6 – 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, + ], + 'WV' => [ + # Occasional Sunday maintenance from 6:00 am. to noon ET. + { cron: '0 6 * * Sun', duration_minutes: 6 * 60 }, + ], + 'WY' => [ + # Daily, 2 am. to 5 am. ET. + { cron: '0 2 * * *', duration_minutes: 3 * 60 }, + ], + }.freeze + + PARSED_MAINTENANCE_WINDOWS = MAINTENANCE_WINDOWS.transform_values do |windows| + Time.use_zone(TZ) do + windows.map do |window| + cron = Fugit.parse_cron(window[:cron]) + { cron: cron, duration_minutes: window[:duration_minutes] } + end + end + end.freeze + + class << self + def in_maintenance_window?(state) + Time.use_zone(TZ) do + windows_for_state(state).any? { |window| window.cover?(Time.zone.now) } + end + end + + def windows_for_state(state) + Time.use_zone(TZ) do + PARSED_MAINTENANCE_WINDOWS.fetch(state, []).map do |window| + previous = window[:cron].previous_time.to_t + (previous..(previous + window[:duration_minutes].minutes)) + end + end + end + end + end +end diff --git a/app/services/idv/analytics_events_enhancer.rb b/app/services/idv/analytics_events_enhancer.rb index ab470910044..11163c5f185 100644 --- a/app/services/idv/analytics_events_enhancer.rb +++ b/app/services/idv/analytics_events_enhancer.rb @@ -25,7 +25,6 @@ module AnalyticsEventsEnhancer idv_doc_auth_hybrid_handoff_visited idv_doc_auth_link_sent_submitted idv_doc_auth_link_sent_visited - idv_doc_auth_randomizer_defaulted idv_doc_auth_redo_ssn_submitted idv_doc_auth_ssn_submitted idv_doc_auth_ssn_visited diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index 48616af80d0..374a83572f1 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -49,7 +49,7 @@ def proof(applicant) ).send_verification_request( applicant: aamva_applicant, ) - build_result_from_response(response) + build_result_from_response(response, applicant[:state]) rescue => exception failed_result = Proofing::StateIdResult.new( success: false, errors: {}, exception: exception, vendor_name: 'aamva:state_id', @@ -61,7 +61,7 @@ def proof(applicant) private - def build_result_from_response(verification_response) + def build_result_from_response(verification_response, jurisdiction) Proofing::StateIdResult.new( success: verification_response.success?, errors: parse_verification_errors(verification_response), @@ -70,11 +70,12 @@ def build_result_from_response(verification_response) transaction_id: verification_response.transaction_locator_id, requested_attributes: requested_attributes(verification_response).index_with(1), verified_attributes: verified_attributes(verification_response), + jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?(jurisdiction), ) end def parse_verification_errors(verification_response) - errors = errors = Hash.new { |h, k| h[k] = [] } + errors = Hash.new { |h, k| h[k] = [] } return errors if verification_response.success? @@ -119,6 +120,10 @@ def send_to_new_relic(result) end NewRelic::Agent.notice_error(result.exception) end + + def jurisdiction_in_maintenance_window?(state) + Idv::AamvaStateMaintenanceWindow.in_maintenance_window?(state) + end end end end diff --git a/app/services/proofing/state_id_result.rb b/app/services/proofing/state_id_result.rb index 02b2b0416ba..a2e5f1d6c10 100644 --- a/app/services/proofing/state_id_result.rb +++ b/app/services/proofing/state_id_result.rb @@ -8,7 +8,6 @@ class StateIdResult attr_reader :errors, :exception, - :success, :vendor_name, :transaction_id, :requested_attributes, @@ -21,7 +20,8 @@ def initialize( vendor_name: nil, transaction_id: '', requested_attributes: {}, - verified_attributes: [] + verified_attributes: [], + jurisdiction_in_maintenance_window: false ) @success = success @errors = errors @@ -30,10 +30,11 @@ def initialize( @transaction_id = transaction_id @requested_attributes = requested_attributes @verified_attributes = verified_attributes + @jurisdiction_in_maintenance_window = jurisdiction_in_maintenance_window end def success? - success + !!@success end def timed_out? @@ -56,6 +57,10 @@ def mva_exception? mva_unavailable? || mva_system_error? || mva_timeout? end + def jurisdiction_in_maintenance_window? + !!@jurisdiction_in_maintenance_window + end + def to_h { success: success?, @@ -67,6 +72,7 @@ def to_h transaction_id: transaction_id, vendor_name: vendor_name, verified_attributes: verified_attributes, + jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?, } end end diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index 6dbe1572268..e019e17a33d 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -42,6 +42,17 @@ last_number: attribute_value[-1], ), ) %> + <% elsif attribute_key == :email && IdentityConfig.store.feature_select_email_to_share_enabled %> +
+ <%= attribute_value.to_s %> +

+ <% if @presenter.multiple_emails? %> + <%= link_to t('help_text.requested_attributes.change_email_link'), sign_up_select_email_path %> + <% else %> + <%= link_to t('account.index.email_add'), add_email_path %> + <% end %> +

+
<% else %> <%= attribute_value.to_s %> <% end %> diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb new file mode 100644 index 00000000000..c89ce9a7676 --- /dev/null +++ b/app/views/sign_up/select_email/show.html.erb @@ -0,0 +1,50 @@ +<% self.title = t('titles.select_email') %> + +<%= render StatusPageComponent.new(status: :info, icon: :question) do |c| %> + <% c.with_header { t('titles.select_email') } %> +

+ <%= I18n.t('help_text.select_preferred_email', sp: @sp_name, app_name: APP_NAME) %> +

+ + <%= simple_form_for('', url: sign_up_select_email_path) do |f| %> +
+
+
+
+ <% @user_emails.each do |email, index| %> +
+ <%= radio_button_tag( + 'select_email_form[selected_email_id]', + email.id, + email.email == @last_sign_in_email_address, + class: 'usa-radio__input usa-radio__input--bordered', + ) %> + <%= label_tag( + "select_email_form_selected_email_id_#{email.id}", + class: 'usa-radio__label width-full', + ) do %> + <%= email.email %> + <% end %> +
+ <% end %> +
+
+
+
+
+ <%= f.submit t('help_text.requested_attributes.change_email_link'), class: 'margin-top-4' %> + <% end %> + + <%= render ButtonComponent.new( + url: add_email_path, + outline: true, + big: true, + wide: true, + class: 'margin-top-2', + ).with_content(t('account.index.email_add')) %> + + <%= render PageFooterComponent.new do %> + <%= link_to t('forms.buttons.back'), sign_up_completed_path %> + <% end %> + +<% end %> \ No newline at end of file diff --git a/config/application.yml.default b/config/application.yml.default index 017ef81a2d7..72726ab5de9 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -97,9 +97,9 @@ doc_auth_max_submission_attempts_before_native_camera: 3 doc_auth_selfie_desktop_test_mode: false doc_auth_separate_pages_enabled: false doc_auth_supported_country_codes: '["US", "GU", "VI", "AS", "MP", "PR", "USA" ,"GUM", "VIR", "ASM", "MNP", "PRI"]' -doc_auth_vendor_randomize: false -doc_auth_vendor_randomize_alternate_vendor: '' -doc_auth_vendor_randomize_percent: 0 +doc_auth_vendor_lexis_nexis_percent: 100 # note, LN is currently the default vendor +doc_auth_vendor_socure_percent: 0 +doc_auth_vendor_switching_enabled: false doc_capture_polling_enabled: true doc_capture_request_valid_for_minutes: 15 drop_off_report_config: '[{"emails":["ursula@example.com"],"issuers": ["urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name"]}]' @@ -116,6 +116,7 @@ enable_usps_verification: true event_disavowal_expiration_hours: 240 feature_idv_force_gpo_verification_enabled: false feature_idv_hybrid_flow_enabled: true +feature_select_email_to_share_enabled: true geo_data_file_path: 'geo_data/GeoLite2-City.mmdb' get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 @@ -335,6 +336,7 @@ sign_in_user_id_per_ip_attempt_window_exponential_factor: 1.1 sign_in_user_id_per_ip_attempt_window_in_minutes: 720 sign_in_user_id_per_ip_attempt_window_max_minutes: 43_200 sign_in_user_id_per_ip_max_attempts: 50 +socure_webhook_enabled: false socure_webhook_secret_key: '' socure_webhook_secret_key_queue: '[]' sp_handoff_bounce_max_seconds: 2 @@ -349,6 +351,7 @@ test_ssn_allowed_list: '' totp_code_interval: 30 unauthorized_scope_enabled: false use_dashboard_service_providers: false +use_fed_domain_class: false use_kms: false use_vot_in_sp_requests: true usps_auth_token_refresh_job_enabled: false @@ -398,6 +401,7 @@ development: dashboard_url: http://localhost:3001/api/service_providers doc_auth_selfie_desktop_test_mode: true doc_auth_vendor: 'mock' + doc_auth_vendor_default: 'mock' domain_name: localhost:3000 enable_rate_limiting: false hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c @@ -432,6 +436,7 @@ development: state_tracking_enabled: true telephony_adapter: test use_dashboard_service_providers: true + use_fed_domain_class: true usps_eipp_sponsor_id: '222222222222222' usps_ipp_sponsor_id: '111111111111111' usps_ipp_transliteration_enabled: true @@ -456,11 +461,13 @@ production: dashboard_url: https://dashboard.demo.login.gov disable_email_sending: false disable_logout_get_request: false - doc_auth_vendor: 'acuant' + doc_auth_vendor: 'lexisnexis' + doc_auth_vendor_default: 'lexisnexis' domain_name: login.gov email_registrations_per_ip_track_only_mode: true enable_test_routes: false enable_usps_verification: false + feature_select_email_to_share_enabled: false hmac_fingerprinter_key: hmac_fingerprinter_key_queue: '[]' idv_sp_required: true @@ -511,6 +518,7 @@ test: doc_auth_max_attempts: 4 doc_auth_selfie_desktop_test_mode: true doc_auth_vendor: 'mock' + doc_auth_vendor_default: 'mock' doc_capture_polling_enabled: false domain_name: www.example.com email_registrations_per_ip_limit: 3 @@ -563,6 +571,7 @@ test: telephony_adapter: test test_ssn_allowed_list: '999999999' totp_code_interval: 3 + use_fed_domain_class: true usps_eipp_sponsor_id: '222222222222222' usps_ipp_root_url: 'http://localhost:1000' usps_ipp_sponsor_id: '111111111111111' diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 38b69641eed..70fdddd95c2 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -35,13 +35,17 @@ def self.all constants.index_with { |test_name| const_get(test_name) } end + # This "test" will permanently be in place to allow a graceful transition from TrueID being the + # sole vendor to a multi-vendor configuration. DOC_AUTH_VENDOR = AbTest.new( experiment_name: 'Doc Auth Vendor', should_log: /^idv/i, + default_bucket: :lexis_nexis, buckets: { - alternate_vendor: IdentityConfig.store.doc_auth_vendor_randomize ? - IdentityConfig.store.doc_auth_vendor_randomize_percent : - 0, + socure: IdentityConfig.store.doc_auth_vendor_switching_enabled ? + IdentityConfig.store.doc_auth_vendor_socure_percent : 0, + lexis_nexis: IdentityConfig.store.doc_auth_vendor_switching_enabled ? + IdentityConfig.store.doc_auth_vendor_lexis_nexis_percent : 0, }.compact, ) do |service_provider:, session:, user:, user_session:, **| document_capture_session_uuid_discriminator(service_provider:, session:, user:, user_session:) diff --git a/config/locales/en.yml b/config/locales/en.yml index f1a3b37a92f..9dca9c10a6c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -692,6 +692,7 @@ doc_auth.tips.review_issues_id_text1: Did you use a dark background? doc_auth.tips.review_issues_id_text2: Did you take the photo on a flat surface? doc_auth.tips.review_issues_id_text3: Is the flash on your camera off? doc_auth.tips.review_issues_id_text4: Are all details sharp and clearly visible? +email_address.not_found: 'Email not found' email_addresses.add.duplicate: This email address is already registered to your account. email_addresses.add.limit: You’ve added the maximum number of email addresses. email_addresses.delete.bullet1: You won’t be able to sign in to %{app_name} (or any of the government applications linked to your account) using this email address @@ -958,18 +959,18 @@ headings.webauthn_setup.new: Insert your security key help_text.requested_attributes.address: Address help_text.requested_attributes.all_emails: Email addresses on your account help_text.requested_attributes.birthdate: Date of birth +help_text.requested_attributes.change_email_link: Change +help_text.requested_attributes.consent_reminder_html: You must consent each year to share your information with %{sp_html}. help_text.requested_attributes.email: Email address help_text.requested_attributes.full_name: Full name -help_text.requested_attributes.ial1_consent_reminder_html: You must consent each year to share your information with %{sp}. We’ll share your information with %{sp} to connect your account. -help_text.requested_attributes.ial1_intro_html: We’ll share your information with %{sp} to connect your account. -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} needs to know who you are to connect to your account. You must consent each year to share your verified information with %{sp}. We’ll share this information:' -help_text.requested_attributes.ial2_intro_html: '%{sp} needs to know who you are to connect your account. We’ll share this information with %{sp}:' -help_text.requested_attributes.ial2_reverified_consent_info: 'Because you verified your identity again, we need your permission to share this information with %{sp}:' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Because you verified your identity again, we need your permission to share this information with %{sp_html}:' +help_text.requested_attributes.intro_html: 'We’ll share this information with %{sp_html}:' help_text.requested_attributes.phone: Phone number help_text.requested_attributes.social_security_number: Social Security number help_text.requested_attributes.verified_at: Updated on help_text.requested_attributes.x509_issuer: PIV/CAC Issuer help_text.requested_attributes.x509_subject: PIV/CAC Identity +help_text.select_preferred_email: You may change which email you share with %{sp} since you have multiple emails associated with your %{app_name} account. i18n.language: Language i18n.locale.en: English i18n.locale.es: Español @@ -1606,6 +1607,7 @@ titles.reactivate_account: Reactivate your account titles.registrations.new: Create your account titles.revoke_consent: Revoke Consent titles.rules_of_use: Rules of Use +titles.select_email: Select your preferred email titles.sign_up.completion_consent_expired_ial1: It’s been a year since you gave us consent to share your information titles.sign_up.completion_consent_expired_ial2: It’s been a year since you gave us consent to share your verified identity titles.sign_up.completion_first_sign_in: Continue to %{sp} diff --git a/config/locales/es.yml b/config/locales/es.yml index 2d5bcae878a..de9a55b97fb 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -703,6 +703,7 @@ doc_auth.tips.review_issues_id_text1: '¿Usó un fondo de color oscuro?' doc_auth.tips.review_issues_id_text2: '¿Tomó la foto en una superficie plana?' doc_auth.tips.review_issues_id_text3: '¿Está apagado el flash de su cámara?' doc_auth.tips.review_issues_id_text4: '¿Todos los detalles se ven con precisión y claridad?' +email_address.not_found: El correo electrónico no encontrado email_addresses.add.duplicate: Esta dirección de correo electrónico ya está registrada en su cuenta. email_addresses.add.limit: Agregó el número máximo de direcciones de correo electrónico. email_addresses.delete.bullet1: Si usa esta dirección de correo electrónico, no podrá iniciar sesión en %{app_name} (ni en ninguna de las aplicaciones gubernamentales vinculadas a su cuenta). @@ -969,18 +970,18 @@ headings.webauthn_setup.new: Inserte su clave de seguridad help_text.requested_attributes.address: Dirección help_text.requested_attributes.all_emails: Direcciones de correo electrónico en su cuenta help_text.requested_attributes.birthdate: Fecha de nacimiento +help_text.requested_attributes.change_email_link: Cambiar +help_text.requested_attributes.consent_reminder_html: Debe dar su consentimiento cada año para divulgar su información a %{sp_html}. help_text.requested_attributes.email: Dirección de correo electrónico help_text.requested_attributes.full_name: Nombre completo -help_text.requested_attributes.ial1_consent_reminder_html: Debe dar su consentimiento cada año para divulgar su información a %{sp}. Divulgaremos su información a %{sp} para conectar su cuenta. -help_text.requested_attributes.ial1_intro_html: Divulgaremos su información a %{sp} para conectar su cuenta. -help_text.requested_attributes.ial2_consent_reminder_html: 'Para conectar su cuenta, %{sp} necesita saber quién es usted. Debe dar su consentimiento cada año para divulgar su información verificada a %{sp}. Divulgaremos esta información:' -help_text.requested_attributes.ial2_intro_html: 'Para conectar su cuenta, %{sp} necesita saber quién es usted. Divulgaremos esta información a %{sp}:' -help_text.requested_attributes.ial2_reverified_consent_info: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp}:' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Como volvió a verificar su identidad, necesitamos su permiso para divulgar esta información a %{sp_html}:' +help_text.requested_attributes.intro_html: 'Divulgaremos esta información a %{sp_html}:' help_text.requested_attributes.phone: Número de teléfono help_text.requested_attributes.social_security_number: Número de Seguro Social help_text.requested_attributes.verified_at: Actualizado en help_text.requested_attributes.x509_issuer: Emisor de la tarjeta PIV o CAC help_text.requested_attributes.x509_subject: Identidad de la tarjeta PIV o CAC +help_text.select_preferred_email: Puede cambiar el correo electrónico que comparte con %{sp} ya que tiene varios correos electrónicos asociados a su cuenta de %{app_name}. i18n.language: Idioma i18n.locale.en: English i18n.locale.es: Español @@ -1618,6 +1619,7 @@ titles.reactivate_account: Reactive su cuenta titles.registrations.new: Cree su cuenta titles.revoke_consent: Revocar consentimiento titles.rules_of_use: Reglas de uso +titles.select_email: Seleccione el correo electrónico que prefiera titles.sign_up.completion_consent_expired_ial1: Hace un año que nos dio su consentimiento para divulgar su información titles.sign_up.completion_consent_expired_ial2: Hace un año que nos dio su consentimiento para divulgar su identidad verificada titles.sign_up.completion_first_sign_in: Continuar con %{sp} diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a1489bc0f48..2cf64287c9b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -692,6 +692,7 @@ doc_auth.tips.review_issues_id_text1: Avez-vous utilisé un arrière-plan de cou doc_auth.tips.review_issues_id_text2: Avez-vous pris la photo sur une surface plane ? doc_auth.tips.review_issues_id_text3: Le flash de votre appareil photo est-il éteint ? doc_auth.tips.review_issues_id_text4: Tous les détails sont-ils nets et clairement visibles ? +email_address.not_found: Email non trouvé email_addresses.add.duplicate: Cette adresse e-mail est déjà enregistrée sur votre compte. email_addresses.add.limit: Vous avez ajouté le nombre maximum d’adresses e-mail. email_addresses.delete.bullet1: Vous ne pourrez pas vous connecter à %{app_name} (ni à aucune des applications de l’administration associées à votre compte) à l’aide de cette adresse e-mail @@ -958,18 +959,18 @@ headings.webauthn_setup.new: Insérer votre clé de sécurité help_text.requested_attributes.address: Adresse help_text.requested_attributes.all_emails: Adresses e-mail sur votre compte help_text.requested_attributes.birthdate: Date de naissance +help_text.requested_attributes.change_email_link: Modifier +help_text.requested_attributes.consent_reminder_html: Vous devez consentir chaque année au partage de vos informations avec %{sp_html}. help_text.requested_attributes.email: Adresse e-mail help_text.requested_attributes.full_name: Nom complet -help_text.requested_attributes.ial1_consent_reminder_html: Vous devez consentir chaque année au partage de vos informations avec %{sp}. Nous partagerons vos informations avec %{sp} pour connecter votre compte. -help_text.requested_attributes.ial1_intro_html: Nous partagerons vos informations avec %{sp} pour connecter votre compte. -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} a besoin de savoir qui vous êtes pour se connecter à votre compte. Vous devez consentir chaque année à partager vos informations vérifiées avec %{sp}. Nous partagerons ces informations :' -help_text.requested_attributes.ial2_intro_html: '%{sp} a besoin de savoir qui vous êtes pour connecter votre compte. Nous partagerons ces informations avec %{sp} :' -help_text.requested_attributes.ial2_reverified_consent_info: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp} :' +help_text.requested_attributes.ial2_reverified_consent_info_html: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp_html} :' +help_text.requested_attributes.intro_html: 'Nous partagerons ces informations avec %{sp_html}:' help_text.requested_attributes.phone: Numéro de téléphone help_text.requested_attributes.social_security_number: Numéro de sécurité sociale help_text.requested_attributes.verified_at: Mis à jour le help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC help_text.requested_attributes.x509_subject: Identité PIV/CAC +help_text.select_preferred_email: Vous pouvez modifier l’adresse e-mail que vous partagez avec %{sp} car vous possédez plusieurs adresses e-mail associées à votre compte %{app_name}. i18n.language: Langue i18n.locale.en: English i18n.locale.es: Español @@ -1606,6 +1607,7 @@ titles.reactivate_account: Réactiver votre compte titles.registrations.new: Créer votre compte titles.revoke_consent: Révoquer le consentement titles.rules_of_use: Règles d’utilisation +titles.select_email: Sélectionner l’adresse e-mail de votre choix titles.sign_up.completion_consent_expired_ial1: Cela fait un an que vous nous avez donné votre consentement pour partager vos informations titles.sign_up.completion_consent_expired_ial2: Cela fait un an que vous nous avez donné votre consentement pour partager votre identité vérifiée titles.sign_up.completion_first_sign_in: Continuer vers %{sp} diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 316555349c5..d2c92c4f34b 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -703,6 +703,7 @@ doc_auth.tips.review_issues_id_text1: 你是否使用了暗色背景? doc_auth.tips.review_issues_id_text2: 是否在平坦平面上拍的照? doc_auth.tips.review_issues_id_text3: 你相机的闪光灯是否关闭? doc_auth.tips.review_issues_id_text4: 是否所有细节都清晰可见? +email_address.not_found: 未找到电子邮件 email_addresses.add.duplicate: 该电邮地址已注册到你的账户。 email_addresses.add.limit: 你添加的电邮地址数目已达最多。 email_addresses.delete.bullet1: 使用该电邮地址你无法登录进入 %{app_name} (或任何其他与你账户关联的政府应用程序)。 @@ -971,18 +972,18 @@ headings.webauthn_setup.new: 插入您的安全密钥 help_text.requested_attributes.address: 地址 help_text.requested_attributes.all_emails: 你账户上的电邮地址 help_text.requested_attributes.birthdate: 生日 +help_text.requested_attributes.change_email_link: 更改 +help_text.requested_attributes.consent_reminder_html: 你每年都必须授权同意与 %{sp_html} 分享信息。 help_text.requested_attributes.email: 电邮地址 help_text.requested_attributes.full_name: 姓名 -help_text.requested_attributes.ial1_consent_reminder_html: 你每年都必须授权同意与 %{sp} 分享信息。我们将与 %{sp} 分享你的信息来连接你账户。 -help_text.requested_attributes.ial1_intro_html: 我们将与 %{sp} 分享你的信息来连接你账户。 -help_text.requested_attributes.ial2_consent_reminder_html: '%{sp} 需要知道你是谁才能连接你的账户。你每年都必须授权同意与 %{sp} 分享已验证过的你的信息。我们会分享这些信息:' -help_text.requested_attributes.ial2_intro_html: '%{sp} 需要知道你是谁才能连接你的账户。我们会与 %{sp} 分享这些信息:' -help_text.requested_attributes.ial2_reverified_consent_info: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp} 分享该信息。' +help_text.requested_attributes.ial2_reverified_consent_info_html: '因为你重新验证了身份,我们需要得到你的许可才能与 %{sp_html} 分享该信息。' +help_text.requested_attributes.intro_html: 我们会与 %{sp_html} 分享这些信息: help_text.requested_attributes.phone: 电话号码 help_text.requested_attributes.social_security_number: 社会保障号码 help_text.requested_attributes.verified_at: 更新是在 help_text.requested_attributes.x509_issuer: PIV/CAC 发放方 help_text.requested_attributes.x509_subject: PIV/CAC 身份 +help_text.select_preferred_email: 因为你有多个电邮与 %{app_name} 账户相关,你可以更改与我们 %{sp} 构分享哪个。 i18n.language: 语言 i18n.locale.en: English i18n.locale.es: Español @@ -1619,6 +1620,7 @@ titles.reactivate_account: 重新激活你账户 titles.registrations.new: 设立账户 titles.revoke_consent: 撤销同意 titles.rules_of_use: 使用规则 +titles.select_email: 选择你比较愿意分享的电邮 titles.sign_up.completion_consent_expired_ial1: 从你上次授权我们分享你的信息已经一年了。 titles.sign_up.completion_consent_expired_ial2: 从你上次授权我们分享你验证过的身份已经一年了。 titles.sign_up.completion_first_sign_in: 继续到 %{sp} diff --git a/config/routes.rb b/config/routes.rb index 30863b3e145..eb35a338ee7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -304,6 +304,8 @@ get '/sign_up/enter_email' => 'sign_up/registrations#new', as: :sign_up_email post '/sign_up/enter_email' => 'sign_up/registrations#create', as: :sign_up_register get '/sign_up/enter_password' => 'sign_up/passwords#new' + get '/sign_up/select_email' => 'sign_up/select_email#show' + post '/sign_up/select_email' => 'sign_up/select_email#create' get '/sign_up/verify_email' => 'sign_up/emails#show', as: :sign_up_verify_email get '/sign_up/completed' => 'sign_up/completions#show', as: :sign_up_completed post '/sign_up/completed' => 'sign_up/completions#update' diff --git a/db/primary_migrate/20240809152808_create_federal_email_domain.rb b/db/primary_migrate/20240809152808_create_federal_email_domain.rb new file mode 100644 index 00000000000..ef255ae82cc --- /dev/null +++ b/db/primary_migrate/20240809152808_create_federal_email_domain.rb @@ -0,0 +1,9 @@ +class CreateFederalEmailDomain < ActiveRecord::Migration[7.1] + def change + create_table :federal_email_domains do |t| + t.citext :name, null: false + end + + add_index :federal_email_domains, :name, unique: true + end +end diff --git a/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb b/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb new file mode 100644 index 00000000000..bed1e8ca51c --- /dev/null +++ b/db/primary_migrate/20240828182041_add_aaguid_to_webauthn_configuration.rb @@ -0,0 +1,5 @@ +class AddAaguidToWebauthnConfiguration < ActiveRecord::Migration[7.1] + def change + add_column :webauthn_configurations, :aaguid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index ef682f62526..385018287e3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_08_22_122355) do +ActiveRecord::Schema[7.1].define(version: 2024_08_28_182041) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -226,6 +226,11 @@ t.index ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at" end + create_table "federal_email_domains", force: :cascade do |t| + t.citext "name", null: false + t.index ["name"], name: "index_federal_email_domains_on_name", unique: true + end + create_table "fraud_review_requests", force: :cascade do |t| t.integer "user_id" t.string "uuid" @@ -651,6 +656,7 @@ t.boolean "platform_authenticator" t.string "transports", array: true t.jsonb "authenticator_data_flags" + t.string "aaguid" t.index ["user_id"], name: "index_webauthn_configurations_on_user_id" end diff --git a/lib/ab_test.rb b/lib/ab_test.rb index ef090aabe43..840cd1cc828 100644 --- a/lib/ab_test.rb +++ b/lib/ab_test.rb @@ -5,7 +5,7 @@ class AbTest MAX_SHA = (16 ** 64) - 1 - # @param [Proc,RegExp,string,Boolean,nil] should_log Controls whether bucket data for this + # @param [Proc,Regexp,string,Boolean,nil] should_log Controls whether bucket data for this # A/B test is logged with specific # events. # @yieldparam [ActionDispatch::Request] request diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 51d032a7b1e..246d4210f73 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -117,9 +117,10 @@ def self.store config.add(:doc_auth_separate_pages_enabled, type: :boolean) config.add(:doc_auth_supported_country_codes, type: :json) config.add(:doc_auth_vendor, type: :string) - config.add(:doc_auth_vendor_randomize, type: :boolean) - config.add(:doc_auth_vendor_randomize_alternate_vendor, type: :string) - config.add(:doc_auth_vendor_randomize_percent, type: :integer) + config.add(:doc_auth_vendor_default, type: :string) + config.add(:doc_auth_vendor_lexis_nexis_percent, type: :integer) + config.add(:doc_auth_vendor_socure_percent, type: :integer) + config.add(:doc_auth_vendor_switching_enabled, type: :boolean) config.add(:doc_capture_polling_enabled, type: :boolean) config.add(:doc_capture_request_valid_for_minutes, type: :integer) config.add(:drop_off_report_config, type: :json) @@ -137,6 +138,7 @@ def self.store config.add(:event_disavowal_expiration_hours, type: :integer) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) config.add(:feature_idv_hybrid_flow_enabled, type: :boolean) + config.add(:feature_select_email_to_share_enabled, type: :boolean) config.add(:geo_data_file_path, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) @@ -383,6 +385,7 @@ def self.store config.add(:sign_in_user_id_per_ip_max_attempts, type: :integer) config.add(:sign_in_recaptcha_score_threshold, type: :float) config.add(:skip_encryption_allowed_list, type: :json) + config.add(:socure_webhook_enabled, type: :boolean) config.add(:socure_webhook_secret_key, type: :string) config.add(:socure_webhook_secret_key_queue, type: :json) config.add(:sp_handoff_bounce_max_seconds, type: :integer) @@ -404,6 +407,7 @@ def self.store config.add(:usps_auth_token_refresh_job_enabled, type: :boolean) config.add(:usps_confirmation_max_days, type: :integer) config.add(:usps_eipp_sponsor_id, type: :string) + config.add(:use_fed_domain_class, type: :boolean) config.add(:usps_ipp_client_id, type: :string) config.add(:usps_ipp_password, type: :string) config.add(:usps_ipp_request_timeout, type: :integer) diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb index 5a3b32bce42..d7ba925e42d 100644 --- a/lib/idp/constants.rb +++ b/lib/idp/constants.rb @@ -12,6 +12,7 @@ module Constants module Vendors ACUANT = 'acuant' LEXIS_NEXIS = 'lexis_nexis' + SOCURE = 'socure' MOCK = 'mock' USPS = 'usps' AAMVA = 'aamva' diff --git a/lib/tasks/federal_email_domains.rake b/lib/tasks/federal_email_domains.rake new file mode 100644 index 00000000000..95ac234d520 --- /dev/null +++ b/lib/tasks/federal_email_domains.rake @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'faraday' +require 'csv' + +DOT_GOV_DOWNLOAD_PATH = 'https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-federal.csv' +namespace :federal_email_domains do + task load_to_db: :environment do |_task, _args| + response = Faraday.get(DOT_GOV_DOWNLOAD_PATH) + + csv = CSV.parse(response.body, col_sep: ',', headers: true) + csv.each do |row| + FederalEmailDomain.find_or_create_by(name: row['Domain name']) + end + end +end +# rake "federal_email_domains:load_to_db" diff --git a/package.json b/package.json index 65f2d0b3321..521c35d6c8d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "source-map-loader": "^4.0.0", - "webpack": "^5.91.0", + "webpack": "^5.94.0", "webpack-assets-manifest": "^5.2.1", "webpack-cli": "^5.1.4" }, diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index b6313e51bd6..f858344346d 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -3,13 +3,7 @@ RSpec.describe AbTests do describe '#all' do it 'returns all registered A/B tests' do - expect(AbTests.all).to match( - { - ACUANT_SDK: an_instance_of(AbTest), - DOC_AUTH_VENDOR: an_instance_of(AbTest), - - }, - ) + expect(AbTests.all.values).to all(be_kind_of(AbTest)) end end @@ -119,20 +113,20 @@ let(:enable_ab_test) do -> { - allow(IdentityConfig.store).to receive(:doc_auth_vendor). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default). and_return('vendor_a') - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). and_return(true) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_alternate_vendor). - and_return('vendor_b') - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_socure_percent). and_return(50) + allow(IdentityConfig.store).to receive(:doc_auth_vendor_lexis_nexis_percent). + and_return(30) } end let(:disable_ab_test) do -> { - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). + allow(IdentityConfig.store).to receive(:doc_auth_vendor_switching_enabled). and_return(false) } end diff --git a/spec/controllers/sign_up/select_email_controller_spec.rb b/spec/controllers/sign_up/select_email_controller_spec.rb new file mode 100644 index 00000000000..898c402091d --- /dev/null +++ b/spec/controllers/sign_up/select_email_controller_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +RSpec.describe SignUp::SelectEmailController do + describe 'before_actions' do + it 'requires the user be logged in and authenticated' do + expect(subject).to have_actions( + :before, + :confirm_two_factor_authenticated, + ) + end + + it 'requires the user be in the completions flow' do + expect(subject).to have_actions( + :before, + :verify_needs_completions_screen, + ) + end + end + + describe '#create' do + let(:email) { 'michael.motorist@email.com' } + let(:email2) { 'michael.motorist2@email.com' } + let(:email3) { 'david.motorist@email.com' } + let(:user) { create(:user) } + + before do + user.email_addresses = [] + create(:email_address, user:, email: email) + create(:email_address, user:, email: email2) + end + + it 'updates selected email address' do + post :create, params: { selected_email_id: email2 } + + expect(user.email_addresses.last.email). + to include('michael.motorist2@email.com') + end + + context 'with a corrupted email selected_email_id form' do + render_views + it 'rejects email not belonging to the user' do + stub_sign_in(user) + allow(controller).to receive(:needs_completion_screen_reason).and_return(true) + post :create, params: { selected_email_id: email3 } + + expect(user.email_addresses.last.email). + to include('michael.motorist2@email.com') + + expect(response).to redirect_to(sign_up_select_email_path) + end + end + end +end diff --git a/spec/controllers/socure_webhook_controller_spec.rb b/spec/controllers/socure_webhook_controller_spec.rb index 04a06d36d03..23d88f59fb7 100644 --- a/spec/controllers/socure_webhook_controller_spec.rb +++ b/spec/controllers/socure_webhook_controller_spec.rb @@ -6,12 +6,15 @@ describe 'POST /api/webhooks/socure/event' do let(:socure_secret_key) { 'this-is-a-secret' } let(:socure_secret_key_queue) { ['this-is-an-old-secret', 'this-is-an-older-secret'] } + let(:socure_webhook_enabled) { true } before do allow(IdentityConfig.store).to receive(:socure_webhook_secret_key). and_return(socure_secret_key) allow(IdentityConfig.store).to receive(:socure_webhook_secret_key_queue). and_return(socure_secret_key_queue) + allow(IdentityConfig.store).to receive(:socure_webhook_enabled). + and_return(socure_webhook_enabled) end it 'returns OK with a correct secret key' do @@ -40,5 +43,15 @@ expect(response).to have_http_status(:unauthorized) end + + context 'when socure webhook disabled' do + let(:socure_webhook_enabled) { false } + it 'the webhook route does not exist' do + request.headers['Authorization'] = socure_secret_key + post :create + + expect(response).to be_not_found + end + end end end diff --git a/spec/controllers/users/piv_cac_recommended_controller_spec.rb b/spec/controllers/users/piv_cac_recommended_controller_spec.rb index a9bd996aa9f..69d179b4506 100644 --- a/spec/controllers/users/piv_cac_recommended_controller_spec.rb +++ b/spec/controllers/users/piv_cac_recommended_controller_spec.rb @@ -2,7 +2,8 @@ RSpec.describe Users::PivCacRecommendedController do describe 'New user' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics @@ -28,10 +29,10 @@ end describe 'Sign in flow' do - let(:user) { create(:user, :with_phone, { email: 'example@example.gov' }) } + let(:user) { create(:user, :with_phone, { email: 'example@gsa.gov' }) } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_analytics - stub_sign_in(user) user.reload end @@ -49,7 +50,8 @@ end context '#confirm' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics @@ -77,7 +79,8 @@ end context '#skip' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } before do stub_sign_in_before_2fa(user) stub_analytics 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 e22cfc60ffd..5672ea37eb6 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -19,10 +19,16 @@ ) end - context 'with user having gov or mil email' do + context 'with user having gov or mil email and use_fed_domain_class set to false' do let(:user) do create(:user, email: 'example@example.gov', piv_cac_recommended_dismissed_at: Time.zone.now) end + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + context 'having already visited the PIV interstitial page' do it 'tracks the visit in analytics' do get :index @@ -48,6 +54,40 @@ end end + context 'with user having gov or mil email and use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } + let(:user) do + create(:user, email: 'example@gsa.gov', piv_cac_recommended_dismissed_at: Time.zone.now) + end + context 'having already visited the PIV interstitial page' do + it 'tracks the visit in analytics' do + get :index + + expect(@analytics).to have_logged_event( + 'User Registration: 2FA Setup visited', + enabled_mfa_methods_count: 0, + gov_or_mil_email: true, + ) + end + end + + context 'directed to page without having visited PIV interstitial page' do + let(:user) do + create(:user, email: 'example@gsa.gov') + end + + it 'redirects user to piv_recommended_path' do + get :index + + expect(response).to redirect_to(login_piv_cac_recommended_url) + end + end + end + context 'when signed out' do let(:user) { nil } diff --git a/spec/factories/federal_email_domain.rb b/spec/factories/federal_email_domain.rb new file mode 100644 index 00000000000..6903d7eb11a --- /dev/null +++ b/spec/factories/federal_email_domain.rb @@ -0,0 +1,4 @@ +FactoryBot.define do + factory :federal_email_domain do + end +end diff --git a/spec/factories/in_person_enrollments.rb b/spec/factories/in_person_enrollments.rb index 70238b182c2..bfb36296ea4 100644 --- a/spec/factories/in_person_enrollments.rb +++ b/spec/factories/in_person_enrollments.rb @@ -16,6 +16,9 @@ enrollment_established_at { Time.zone.now } status { :pending } status_updated_at { Time.zone.now } + profile do + association(:profile, :in_person_verification_pending, user: user) + end end trait :expired do diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index b321848299e..2e722d7f654 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -57,7 +57,8 @@ verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', - state_id_number: '#############' } + state_id_number: '#############', + jurisdiction_in_maintenance_window: false } end let(:resolution_block) do diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb index dbae13eb376..be6d75b55b6 100644 --- a/spec/features/multiple_emails/sp_sign_in_spec.rb +++ b/spec/features/multiple_emails/sp_sign_in_spec.rb @@ -17,15 +17,62 @@ fill_in_code_with_last_phone_otp click_submit_default click_agree_and_continue if current_path == sign_up_completed_path - decoded_id_token = fetch_oidc_id_token_info - expect(decoded_id_token[:email]).to eq(email) + expect(decoded_id_token[:email]).to eq(emails.first) expect(decoded_id_token[:all_emails]).to be_nil Capybara.reset_session! end end + scenario 'signing in with OIDC and selecting an alternative email address at first sign in' do + user = create(:user, :fully_registered, :with_multiple_emails) + emails = user.reload.email_addresses.map(&:email) + + visit_idp_from_oidc_sp(scope: 'openid email') + signin(emails.first, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_link(t('help_text.requested_attributes.change_email_link')) + + choose emails.second + + click_button(t('help_text.requested_attributes.change_email_link')) + + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + decoded_id_token = fetch_oidc_id_token_info + expect(decoded_id_token[:email]).to eq(emails.second) + end + + scenario 'signing in with OIDC after deleting email linked to identity' do + user = create(:user, :fully_registered) + email1 = create(:email_address, user:, email: 'email1@example.com') + email2 = create(:email_address, user:, email: 'email2@example.com') + + # Link identity with email + visit_idp_from_oidc_sp(scope: 'openid email') + signin(email1.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + click_link(t('help_text.requested_attributes.change_email_link')) + choose email2.email + click_button(t('help_text.requested_attributes.change_email_link')) + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + # Delete email from account + visit manage_email_confirm_delete_url(id: email2.id) + click_button t('forms.email.buttons.delete') + + # Sign in again to partner application + visit_idp_from_oidc_sp(scope: 'openid email') + + decoded_id_token = fetch_oidc_id_token_info + expect(decoded_id_token[:email]).to eq(email1.email) + end + scenario 'signing in with SAML sends the email address used to sign in' do user = create(:user, :fully_registered, :with_multiple_emails) emails = user.reload.email_addresses.map(&:email) @@ -42,12 +89,63 @@ xmldoc = SamlResponseDoc.new('feature', 'response_assertion') email_from_saml_response = xmldoc.attribute_value_for('email') - - expect(email_from_saml_response).to eq(email) + expect(email_from_saml_response).to eq(emails.first) Capybara.reset_session! end end + + scenario 'signing in with SAML and selecting an alternative email address at first sign in' do + user = create(:user, :fully_registered, :with_multiple_emails) + emails = user.reload.email_addresses.map(&:email) + + visit authn_request + signin(emails.first, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + + click_link(t('help_text.requested_attributes.change_email_link')) + choose emails.second + click_button(t('help_text.requested_attributes.change_email_link')) + + expect(current_path).to eq(sign_up_completed_path) + + click_agree_and_continue + click_submit_default + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + email_from_saml_response = xmldoc.attribute_value_for('email') + expect(email_from_saml_response).to eq(emails.second) + end + + scenario 'signing in with SAML after deleting email linked to identity' do + user = create(:user, :fully_registered) + email1 = create(:email_address, user:, email: 'email1@example.com') + email2 = create(:email_address, user:, email: 'email2@example.com') + + # Link identity with email + visit authn_request + signin(email1.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default_twice + click_link(t('help_text.requested_attributes.change_email_link')) + choose email2.email + click_button(t('help_text.requested_attributes.change_email_link')) + expect(current_path).to eq(sign_up_completed_path) + click_agree_and_continue + click_submit_default + + # Delete email from account + visit manage_email_confirm_delete_url(id: email2.id) + click_button t('forms.email.buttons.delete') + + # Sign in again to partner application + visit authn_request + + xmldoc = SamlResponseDoc.new('feature', 'response_assertion') + email_from_saml_response = xmldoc.attribute_value_for('email') + expect(email_from_saml_response).to eq(email1.email) + end end context 'with the all_emails scope' do diff --git a/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb b/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb new file mode 100644 index 00000000000..2906a36a34e --- /dev/null +++ b/spec/features/sign_in/piv_recommended_after_sign_in_spec.rb @@ -0,0 +1,132 @@ +require 'rails_helper' + +RSpec.feature 'Piv recommended after Sign in' do + context 'use_fed_domain_class set to true' do + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + scenario 'User with valid fed email directed to recommend page and get to setup piv' do + user = create(:user, :with_phone, { email: 'example@gsa.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with mil email directed to recommended PIV page and goes to add piv page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with fed email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@gsa.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with mil email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with invalid .gov email directed to account page' do + user = create(:user, :with_phone, { email: 'example@bad.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(account_path) + end + end + + context 'use_fed_domain_class set to false' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + scenario 'User with .gov email directed to recommend page and get to setup piv' do + user = create(:user, :with_phone, { email: 'example@good.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with .mil email directed to recommended PIV page and goes to add piv page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) + expect(page).to have_current_path(setup_piv_cac_path) + end + + scenario 'User with fed email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@example.gov' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with mil email and skips recommendation page' do + user = create(:user, :with_phone, { email: 'example@army.mil' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(login_piv_cac_recommended_path) + click_button(t('two_factor_authentication.piv_cac_upsell.skip')) + expect(page).to have_current_path(account_path) + end + + scenario 'User with invalid no .gov or .mil email directed to account page' do + user = create(:user, :with_phone, { email: 'example@bad.com' }) + + visit new_user_session_path + fill_in_credentials_and_submit(user.email, user.password) + fill_in_code_with_last_phone_otp + click_submit_default + expect(page).to have_current_path(account_path) + end + end +end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index b05147aaea5..215c19fe083 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -102,30 +102,6 @@ expect(oidc_redirect_url).to start_with service_provider.redirect_uris.first end - scenario 'User with gov/mil email directed to recommended PIV page' do - user = create(:user, :with_phone, { email: 'example@example.gov' }) - - visit new_user_session_path - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - expect(page).to have_current_path(login_piv_cac_recommended_path) - click_button(t('two_factor_authentication.piv_cac_upsell.add_piv')) - expect(page).to have_current_path(setup_piv_cac_path) - end - - scenario 'User with gov/mil email and skips recommendation page' do - user = create(:user, :with_phone, { email: 'example@example.gov' }) - - visit new_user_session_path - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - expect(page).to have_current_path(login_piv_cac_recommended_path) - click_button(t('two_factor_authentication.piv_cac_upsell.skip')) - expect(page).to have_current_path(account_path) - end - scenario 'user attempts sign in with piv/cac with no account then creates account' do visit_idp_from_sp_with_ial1(:oidc) click_on t('account.login.piv_cac') diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb index 6d1d2f0fa95..b71f4ce0a78 100644 --- a/spec/features/users/sign_up_spec.rb +++ b/spec/features/users/sign_up_spec.rb @@ -511,30 +511,117 @@ def clipboard_text end end - describe 'mil or gov email account' do - before do - confirm_email('test@test.gov') - submit_form_with_valid_password - end - it 'should land user on piv cac suggestion page' do - expect(current_path).to eq login_piv_cac_recommended_path - end + describe 'User Directed to Piv Cac recommended' do + context 'set config use_fed_domain_class to false' do + let(:email) { 'test@test.gov' } + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end - context 'user can skip piv cac prompt' do - it 'should skip piv cac prompt and land on mfa screen' do + it 'should land user on piv cac suggestion page' do + confirm_email(email) + submit_form_with_valid_password expect(current_path).to eq login_piv_cac_recommended_path - click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + end - expect(current_path).to eq authentication_methods_setup_path + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end end end - context 'user who selects to add piv is directed to piv screen' do - it 'should be directed straight to piv add screen' do - expect(current_path).to eq login_piv_cac_recommended_path - click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + context 'set config use_fed_domain_class to true' do + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + let(:email) { 'test@gsa.gov' } + + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + context 'valid fed email' do + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + end + + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end + end + end + + context 'any mil email' do + let(:email) { 'test@example.mil' } + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + end + + context 'user can skip piv cac prompt' do + it 'should skip piv cac prompt and land on mfa screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.choose_other_method') + + expect(current_path).to eq authentication_methods_setup_path + end + end + + context 'user who selects to add piv is directed to piv screen' do + it 'should be directed straight to piv add screen' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq login_piv_cac_recommended_path + click_button t('two_factor_authentication.piv_cac_upsell.add_piv') + + expect(current_path).to eq setup_piv_cac_path + end + end + end - expect(current_path).to eq setup_piv_cac_path + context 'invalid fed email' do + let(:email) { 'test@example.gov' } + it 'should land user on piv cac suggestion page when fed government' do + confirm_email(email) + submit_form_with_valid_password + expect(current_path).to eq authentication_methods_setup_path + end end end end diff --git a/spec/forms/delete_user_email_form_spec.rb b/spec/forms/delete_user_email_form_spec.rb index 8bc089fa4b6..68e9fbd88b6 100644 --- a/spec/forms/delete_user_email_form_spec.rb +++ b/spec/forms/delete_user_email_form_spec.rb @@ -60,6 +60,18 @@ submit end + + it 'removes associated identity email address id' do + user.identities << ServiceProviderIdentity.create( + service_provider: 'http://localhost:3000', + last_authenticated_at: Time.zone.now, + ) + user.identities.last.email_address_id = email_address.id + + submit + + expect(user.identities.last.email_address_id).to be(nil) + end end context 'with a email of a different user' do diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 568c2553535..580983f27d7 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -661,6 +661,7 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, + email_address_id: 4, ) identity = user.identities.where(service_provider: client_id).first @@ -684,6 +685,7 @@ current_user: user, ial: 1, rails_session_id: rails_session_id, + email_address_id: 4, ) end diff --git a/spec/forms/select_email_form_spec.rb b/spec/forms/select_email_form_spec.rb new file mode 100644 index 00000000000..23a17bdbba5 --- /dev/null +++ b/spec/forms/select_email_form_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe SelectEmailForm do + let(:user) { create(:user, :fully_registered, :with_multiple_emails) } + describe '#submit' do + it 'returns the email successfully' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: user.email_addresses.last.id) + + expect(response.success?).to eq(true) + end + + it 'returns an error when submitting an invalid email' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: nil) + + expect(response.success?).to eq(false) + end + + context 'with an unconfirmed email address added' do + before do + create( + :email_address, + email: 'michael.business@business.com', + user: user, + confirmed_at: nil, + confirmation_sent_at: 1.month.ago, + ) + end + + it 'returns an error' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: user.email_addresses.last.id) + + expect(response.success?).to eq(false) + end + end + + context 'with another user\'s email' do + let(:user2) { create(:user, :fully_registered, :with_multiple_emails) } + before do + create( + :email_address, + email: 'michael.business@business.com', + user: user2, + confirmed_at: nil, + confirmation_sent_at: 1.month.ago, + ) + @email2 = user2.email_addresses.last.id + end + + it 'returns an error' do + form = SelectEmailForm.new(user) + response = form.submit(selected_email_id: @email2) + + expect(response.success?).to eq(false) + end + end + end +end diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 77433303c03..454651054c6 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -259,12 +259,6 @@ before do enrollment_records = InPersonEnrollment.where(id: pending_enrollments.map(&:id)) - # Below sets in_person_verification_pending_at - # on the profile associated with each pending enrollment - enrollment_records.each do |enrollment| - profile = enrollment.profile - profile.update(in_person_verification_pending_at: enrollment.created_at) - end allow(InPersonEnrollment).to receive(:needs_usps_status_check). and_return(enrollment_records) allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) @@ -824,6 +818,16 @@ request_passed_proofing_unsupported_id_results_response, ) + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform Time.zone.now + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + it 'logs a message about the unsupported ID' do expected_wait_until = nil freeze_time do @@ -884,10 +888,11 @@ end it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil job.perform(Time.zone.now) pending_enrollment.reload - expect(pending_enrollment.profile).not_to be_active + expect(pending_enrollment.profile.active).to be false expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') end @@ -989,6 +994,17 @@ ), ) end + + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason). + to eq('verification_cancelled') + end end context 'when a unique id is invalid' do @@ -1348,57 +1364,84 @@ ), ) end + end - context 'when the enrollment has failed' do - before do - stub_request_failed_proofing_results - end + context 'when the enrollment has failed' do + before do + stub_request_failed_proofing_results + end + + it 'sends proofing failed email on response with failed status' do + user = pending_enrollment.user - it 'sends proofing failed email on response with failed status' do - user = pending_enrollment.user - - freeze_time do - expect do - job.perform(Time.zone.now) - end.to have_enqueued_mail(UserMailer, :in_person_failed).with( - params: { user: user, email_address: user.email_addresses.first }, - args: [{ enrollment: pending_enrollment }], - ) - expect(job_analytics).to have_logged_event( - 'GetUspsProofingResultsJob: Success or failure email initiated', - hash_including( - email_type: 'Failed', - job_name: 'GetUspsProofingResultsJob', - ), - ) - end + freeze_time do + expect do + job.perform(Time.zone.now) + end.to have_enqueued_mail(UserMailer, :in_person_failed).with( + params: { user: user, email_address: user.email_addresses.first }, + args: [{ enrollment: pending_enrollment }], + ) + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Success or failure email initiated', + hash_including( + email_type: 'Failed', + job_name: 'GetUspsProofingResultsJob', + ), + ) end end - end - it 'deactivates and sets fraud related fields of an expired enrollment' do - stub_request_expired_id_ipp_proofing_results + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil - job.perform(Time.zone.now) + job.perform(Time.zone.now) + pending_enrollment.reload + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason). + to eq('verification_cancelled') + end - profile = pending_enrollment.reload.profile - expect(profile).not_to be_active - expect(profile.fraud_review_pending_at).to be_nil - expect(profile.fraud_rejection_at).not_to be_nil - expect(job_analytics).to have_logged_event( - :idv_ipp_deactivated_for_never_visiting_post_office, - ) + it 'deactivates and sets fraud related fields of an expired enrollment' do + stub_request_expired_id_ipp_proofing_results + + job.perform(Time.zone.now) + + profile = pending_enrollment.reload.profile + expect(profile).not_to be_active + expect(profile.fraud_review_pending_at).to be_nil + expect(profile.fraud_rejection_at).not_to be_nil + expect(job_analytics).to have_logged_event( + :idv_ipp_deactivated_for_never_visiting_post_office, + ) + end end end end end describe 'Proofed with secondary id' do - let(:pending_enrollment) do - create( - :in_person_enrollment, :pending - ) + let!(:pending_enrollments) do + ['BALTIMORE', 'FRIENDSHIP', 'WASHINGTON', 'ARLINGTON', 'DEANWOOD'].map do |name| + create( + :in_person_enrollment, + :pending, + :with_notification_phone_configuration, + issuer: 'http://localhost:3000', + selected_location_details: { name: name }, + sponsor_id: usps_ipp_sponsor_id, + ) + end end + let(:pending_enrollment) { pending_enrollments.first } + + before do + enrollment_records = InPersonEnrollment.where(id: pending_enrollments.map(&:id)) + allow(InPersonEnrollment).to receive(:needs_usps_status_check). + and_return(enrollment_records) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + end + before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) end @@ -1417,6 +1460,16 @@ request_passed_proofing_secondary_id_type_results_response, ) + it 'deactivates the associated profile' do + expect(pending_enrollment.profile.in_person_verification_pending_at).not_to be_nil + job.perform(Time.zone.now) + pending_enrollment.reload + + expect(pending_enrollment.profile.in_person_verification_pending_at).to be_nil + expect(pending_enrollment.profile.active).to be false + expect(pending_enrollment.profile.deactivation_reason).to eq('verification_cancelled') + end + it 'logs a message about enrollment with secondary ID' do allow(IdentityConfig.store).to receive( :in_person_send_proofing_notifications_enabled, @@ -1428,6 +1481,7 @@ end.to have_enqueued_job(InPerson::SendProofingNotificationJob). with(pending_enrollment.id).at(1.hour.from_now).on_queue(:intentionally_delayed) end + expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time) expect(pending_enrollment.profile.active).to eq(false) expect(job_analytics).to have_logged_event( diff --git a/spec/models/email_address_spec.rb b/spec/models/email_address_spec.rb index eefbe9832e3..50adfa6256f 100644 --- a/spec/models/email_address_spec.rb +++ b/spec/models/email_address_spec.rb @@ -89,25 +89,80 @@ end end - describe '#gov_or_mil?' do - subject(:result) { email_address.gov_or_mil? } + describe '#fed_or_mil_email?' do + subject(:result) { email_address.fed_or_mil_email? } - context 'with an email domain ending in anything other than .gov or .mil' do - let(:email) { 'example@example.com' } + context 'with an email domain that is a fed email' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + let(:email) { 'example@example.gov' } + + it { expect(result).to eq(true) } + end + + context 'with an email that is a mil email' do + let(:email) { 'example@example.mil' } + + it { expect(result).to eq(true) } + end + + context 'with an email that is not a mil or fed email' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + let(:email) { 'example@bad.gov' } it { expect(result).to eq(false) } end - context 'with an email domain ending in .gov' do + context 'with a non fed email while use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + let(:email) { 'example@good.gov' } + + it { expect(result).to eq(false) } + end + end + + describe '#mil_email?' do + subject(:result) { email_address.mil_email? } + + context 'with an email domain not a mil email' do let(:email) { 'example@example.gov' } - it { expect(result).to eq(true) } + it { expect(result).to eq(false) } end - context 'with an email domain ending in .mil' do + context 'with an email domain ending in a mil domain email' do let(:email) { 'example@example.mil' } it { expect(result).to eq(true) } end end + + describe '#fed_email?' do + subject(:result) { email_address.fed_email? } + let!(:federal_email_domain) { create(:federal_email_domain, name: 'gsa.gov') } + + context 'use_fed_domain_class set to true' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(true) + end + + context 'with an email domain not a fed email' do + let(:email) { 'example@bad.gov' } + + it { expect(result).to eq(false) } + end + + context 'with an email domain ending in a fed domain email' do + let(:email) { 'example@gsa.gov' } + + it { expect(result).to eq(true) } + end + end + end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index b90c1bd4dbb..0f2058980aa 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -1001,10 +1001,10 @@ end end - describe '#deactivate_due_to_ipp_expiration' do + describe '#deactivate_due_to_in_person_verification_cancelled' do let(:profile) { create(:profile, :in_person_verification_pending) } it 'updates the profile' do - profile.deactivate_due_to_ipp_expiration + profile.deactivate_due_to_in_person_verification_cancelled expect(profile.active).to be false expect(profile.deactivation_reason).to eq('verification_cancelled') diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5958a803ed2..046e855586b 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1693,4 +1693,41 @@ def it_should_not_send_survey expect(user.second_last_signed_in_at).to eq(event2.reload.created_at) end end + + describe '#has_fed_or_mil_email?' do + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + + context 'with a valid fed email in domain file' do + let(:user) { create(:user, email: 'example@example.gov') } + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with use_fed_domain_class set to false and random .gov email' do + let(:user) { create(:user, email: 'example@example.gov') } + before do + allow(IdentityConfig.store).to receive(:use_fed_domain_class).and_return(false) + end + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with a valid mil email' do + let(:user) { create(:user, email: 'example@example.mil') } + it 'should return true' do + expect(user.has_fed_or_mil_email?).to eq(true) + end + end + + context 'with an invalid fed or mil email' do + let(:user) { create(:user, email: 'example@example.com') } + it 'should return false' do + expect(user.has_fed_or_mil_email?).to eq(false) + end + end + end end diff --git a/spec/presenters/completions_presenter_spec.rb b/spec/presenters/completions_presenter_spec.rb index 102087eaa70..132dd2fe5e8 100644 --- a/spec/presenters/completions_presenter_spec.rb +++ b/spec/presenters/completions_presenter_spec.rb @@ -1,6 +1,9 @@ require 'rails_helper' RSpec.describe CompletionsPresenter do + include ActionView::Helpers::OutputSafetyHelper + include ActionView::Helpers::TagHelper + let(:identities) do [ build( @@ -12,6 +15,7 @@ end let(:current_user) { create(:user, :fully_registered, identities: identities) } let(:current_sp) { create(:service_provider, friendly_name: 'Friendly service provider') } + let(:selected_email_id) { current_user.email_addresses.first.id } let(:decrypted_pii) do Pii::Attributes.new( first_name: 'Testy', @@ -38,12 +42,13 @@ subject(:presenter) do described_class.new( - current_user: current_user, - current_sp: current_sp, - decrypted_pii: decrypted_pii, - requested_attributes: requested_attributes, - ial2_requested: ial2_requested, - completion_context: completion_context, + current_user:, + current_sp:, + decrypted_pii:, + requested_attributes:, + ial2_requested:, + completion_context:, + selected_email_id:, ) end @@ -147,43 +152,49 @@ end describe '#intro' do - describe 'ial1' do - context 'consent has expired since the last sign in' do - let(:identities) do - [ - build( - :service_provider_identity, - service_provider: current_sp.issuer, - last_consented_at: 2.years.ago, - ), - ] - end - let(:completion_context) { :consent_expired } + it 'renders the standard intro message' do + expect(presenter.intro).to eq( + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ) + end - it 'renders the expired IAL1 consent intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial1_consent_reminder_html', - sp: current_sp.friendly_name, - ), - ) - end + context 'consent has expired since the last sign in' do + let(:identities) do + [ + build( + :service_provider_identity, + service_provider: current_sp.issuer, + last_consented_at: 2.years.ago, + ), + ] end + let(:completion_context) { :consent_expired } - context 'when consent has not expired' do - it 'renders the standard intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: current_sp.friendly_name, - ), - ) - end + it 'renders the expired consent intro message' do + expect(presenter.intro).to eq( + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ], + ' ', + ), + ) end end describe 'ial2' do let(:ial2_requested) { true } + context 'consent has expired since the last sign in' do let(:identities) do [ @@ -196,11 +207,20 @@ end let(:completion_context) { :consent_expired } - it 'renders the expired IAL2 consent intro message' do + it 'renders the expired consent intro message' do expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_consent_reminder_html', - sp: current_sp.friendly_name, + safe_join( + [ + t( + 'help_text.requested_attributes.consent_reminder_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, current_sp.friendly_name), + ), + ], + ' ', ), ) end @@ -217,22 +237,12 @@ ] end let(:completion_context) { :reverified_after_consent } - it 'renders the reverified IAL2 consent intro message' do - expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_reverified_consent_info', - sp: current_sp.friendly_name, - ), - ) - end - end - context 'when consent has not expired' do - it 'renders the standard intro message' do + it 'renders the reverified IAL2 consent intro message' do expect(presenter.intro).to eq( - I18n.t( - 'help_text.requested_attributes.ial2_intro_html', - sp: current_sp.friendly_name, + t( + 'help_text.requested_attributes.ial2_reverified_consent_info_html', + sp_html: content_tag(:strong, current_sp.friendly_name), ), ) end diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb index e22176e0329..8aa2bfecce4 100644 --- a/spec/presenters/openid_connect_user_info_presenter_spec.rb +++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb @@ -368,5 +368,47 @@ end end end + + context 'with a deleted email' do + let(:identity) do + build( + :service_provider_identity, + rails_session_id: rails_session_id, + user: create(:user, :fully_registered, :with_multiple_emails), + scope: scope, + ) + end + + before do + identity.email_address_id = identity.user.email_addresses.first.id + identity.user.email_addresses.first.delete + end + + it 'defers to user alternate email' do + expect(identity.user.reload.email_addresses.first.id). + to_not eq(identity.email_address_id) + expect(identity.user.reload.email_addresses.count).to be 1 + expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) + end + end + + context 'with nil email id' do + let(:identity) do + build( + :service_provider_identity, + rails_session_id: rails_session_id, + user: create(:user, :fully_registered), + scope: scope, + ) + end + + before do + identity.email_address_id = nil + end + + it 'adds the signed in email id to the identity' do + expect(user_info[:email]).to eq(identity.user.email_addresses.last.email) + end + end end end diff --git a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb index 3a46e5e2010..8499e9c53fb 100644 --- a/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb +++ b/spec/presenters/two_factor_authentication/set_up_piv_cac_selection_presenter_spec.rb @@ -2,6 +2,7 @@ RSpec.describe TwoFactorAuthentication::SetUpPivCacSelectionPresenter do let(:user) { create(:user) } + let!(:federal_domain) { create(:federal_email_domain, name: 'gsa.gov') } subject(:presenter) { described_class.new(user:) } describe '#type' do @@ -42,7 +43,7 @@ end context 'with a confirmed email address ending in .gov or .mil' do - let(:user) { create(:user, email: 'example@example.gov') } + let(:user) { create(:user, email: 'example@gsa.gov') } it { expect(recommended).to eq(true) } end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index 5ad52b0a9c0..f8e656f3e75 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -717,6 +717,136 @@ it_behaves_like 'unverified user' end + + context 'with a deleted email' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + create(:email_address, user:, email: 'email@example.com') + + ident = user.identities.last + ident.email_address_id = user.email_addresses.first.id + ident.save + subject.build + + user.email_addresses.first.delete + + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq 'email@example.com' + end + end + + context 'with a nil email id' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + + ident = user.identities.last + ident.email_address_id = nil + ident.save + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq user.email_addresses.last.email + end + end + + context 'select email to send to partner feature is disabled' do + before do + allow(IdentityConfig.store).to receive( + :feature_select_email_to_share_enabled, + ).and_return(false) + end + + context 'with a deleted email' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + create(:email_address, user:, email: 'email@example.com') + + ident = user.identities.last + ident.email_address_id = user.email_addresses.first.id + ident.save + subject.build + + user.email_addresses.first.delete + + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq 'email@example.com' + end + end + + context 'with a nil email id' do + let(:subject) do + described_class.new( + user: user, + name_id_format: name_id_format, + service_provider: service_provider, + authn_request: authn_request, + decrypted_pii: decrypted_pii, + user_session: user_session, + ) + end + before do + user.identities << identity + allow(service_provider.metadata).to receive(:[]).with(:attribute_bundle). + and_return(%w[email phone first_name]) + + ident = user.identities.last + ident.email_address_id = nil + ident.save + subject.build + end + + it 'defers to user alternate email' do + expect(get_asserted_attribute(user, :email)). + to eq user.email_addresses.last.email + end + end + end end describe 'aal attributes handling' do diff --git a/spec/services/displayable_pii_formatter_spec.rb b/spec/services/displayable_pii_formatter_spec.rb index b62885eaeb9..e2f6aa407bf 100644 --- a/spec/services/displayable_pii_formatter_spec.rb +++ b/spec/services/displayable_pii_formatter_spec.rb @@ -48,6 +48,8 @@ ) end + let(:selected_email_id) { current_user.email_addresses.first.id } + let(:pii) do { first_name: first_name, @@ -63,7 +65,13 @@ } end - subject(:formatter) { described_class.new(current_user: current_user, pii: pii) } + subject(:formatter) do + described_class.new( + current_user:, + pii:, + selected_email_id:, + ) + end describe '#format' do context 'ial1' do diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb index 76ecdf41e40..6e4cc22a49f 100644 --- a/spec/services/doc_auth_router_spec.rb +++ b/spec/services/doc_auth_router_spec.rb @@ -19,44 +19,6 @@ end end - describe '.doc_auth_vendor' do - def reload_ab_test_initializer! - # undefine the AB tests instances so we can re-initialize them with different config values - AbTests.constants.each do |const_name| - AbTests.class_eval { remove_const(const_name) } - end - load Rails.root.join('config', 'initializers', 'ab_tests.rb').to_s - end - - let(:doc_auth_vendor) { 'test1' } - let(:doc_auth_vendor_randomize_alternate_vendor) { 'test2' } - let(:analytics) { FakeAnalytics.new } - let(:doc_auth_vendor_randomize_percent) { 57 } - let(:doc_auth_vendor_randomize) { true } - - before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return(doc_auth_vendor) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_alternate_vendor). - and_return(doc_auth_vendor_randomize_alternate_vendor) - - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). - and_return(doc_auth_vendor_randomize_percent) - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). - and_return(doc_auth_vendor_randomize) - - reload_ab_test_initializer! - end - - after do - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize_percent). - and_call_original - allow(IdentityConfig.store).to receive(:doc_auth_vendor_randomize). - and_call_original - - reload_ab_test_initializer! - end - end - describe DocAuthRouter::DocAuthErrorTranslatorProxy do subject(:proxy) do DocAuthRouter::DocAuthErrorTranslatorProxy.new(DocAuth::Mock::DocAuthMockClient.new) diff --git a/spec/services/idv/aamva_state_maintenance_window_spec.rb b/spec/services/idv/aamva_state_maintenance_window_spec.rb new file mode 100644 index 00000000000..19560ba16d5 --- /dev/null +++ b/spec/services/idv/aamva_state_maintenance_window_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe Idv::AamvaStateMaintenanceWindow do + let(:tz) { 'America/New_York' } + let(:eastern_time) { ActiveSupport::TimeZone[tz] } + + before do + travel_to eastern_time.parse('2024-06-02T00:00:00') + end + + describe '#in_maintenance_window?' do + let(:state) { 'DC' } + + subject { described_class.in_maintenance_window?(state) } + + context 'for a state with a defined outage window' do + it 'is true during the maintenance window' do + travel_to(eastern_time.parse('June 2, 2024 at 1am')) do + expect(subject).to eq(true) + end + end + + it 'is false outside of the maintenance window' do + travel_to(eastern_time.parse('June 2, 2024 at 8am')) do + expect(subject).to eq(false) + end + end + end + + context 'for a state without a defined outage window' do + let(:state) { 'LG' } + + it 'returns false without an exception' do + expect(subject).to eq(false) + end + end + end + + describe '.windows_for_state' do + subject { described_class.windows_for_state(state) } + + context 'for a state with no entries' do + let(:state) { 'LG' } + + it 'returns an empty array for a state with no entries' do + expect(subject).to eq([]) + end + end + + context 'for a state with multiple overlapping windows' do + let(:state) { 'CA' } + let(:expected_windows) do + [ + eastern_time.parse('2024-06-01 04:00:00')..eastern_time.parse('2024-06-01 05:30:00'), + eastern_time.parse('2024-05-27 01:00:00')..eastern_time.parse('2024-05-27 01:45:00'), + eastern_time.parse('2024-05-06 01:00:00')..eastern_time.parse('2024-05-06 04:30:00'), + eastern_time.parse('2024-05-20 01:00:00')..eastern_time.parse('2024-05-20 04:30:00'), + ] + end + + it 'returns all of them as ranges' do + Time.use_zone(tz) do + expect(subject).to eq(expected_windows) + end + end + end + end +end diff --git a/spec/services/idv/proofing_components_spec.rb b/spec/services/idv/proofing_components_spec.rb index d78fb48b1fc..97e78cc5ab6 100644 --- a/spec/services/idv/proofing_components_spec.rb +++ b/spec/services/idv/proofing_components_spec.rb @@ -32,7 +32,7 @@ let(:pii_from_doc) { Idp::Constants::MOCK_IDV_APPLICANT } before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return('test_vendor') + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return('test_vendor') idv_session.mark_verify_info_step_complete! idv_session.address_verification_mechanism = 'gpo' allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?). @@ -78,7 +78,7 @@ context 'doc auth' do before do - allow(IdentityConfig.store).to receive(:doc_auth_vendor).and_return('test_vendor') + allow(IdentityConfig.store).to receive(:doc_auth_vendor_default).and_return('test_vendor') end context 'before doc auth complete' do it 'returns nil' do diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index 6afb19420a8..1de70664a09 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -293,5 +293,29 @@ end end end + + context 'when the DMV is in a defined maintenance window' do + before do + expect(Idv::AamvaStateMaintenanceWindow).to receive(:in_maintenance_window?). + and_return(true) + end + + it 'sets jurisdiction_in_maintenance_window to true' do + result = subject.proof(state_id_data) + expect(result.jurisdiction_in_maintenance_window?).to eq(true) + end + end + + context 'when the DMV is not in a defined maintenance window' do + before do + expect(Idv::AamvaStateMaintenanceWindow).to receive(:in_maintenance_window?). + and_return(false) + end + + it 'sets jurisdiction_in_maintenance_window to false' do + result = subject.proof(state_id_data) + expect(result.jurisdiction_in_maintenance_window?).to eq(false) + end + end end end diff --git a/spec/services/proofing/mock/state_id_mock_client_spec.rb b/spec/services/proofing/mock/state_id_mock_client_spec.rb index a342cff43c9..6615ac2ec5b 100644 --- a/spec/services/proofing/mock/state_id_mock_client_spec.rb +++ b/spec/services/proofing/mock/state_id_mock_client_spec.rb @@ -24,6 +24,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end @@ -49,6 +50,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end @@ -72,6 +74,7 @@ transaction_id: transaction_id, vendor_name: 'StateIdMock', verified_attributes: [], + jurisdiction_in_maintenance_window: false, ) end end diff --git a/spec/services/proofing/state_id_result_spec.rb b/spec/services/proofing/state_id_result_spec.rb index eb40e013c3f..d40e44d6568 100644 --- a/spec/services/proofing/state_id_result_spec.rb +++ b/spec/services/proofing/state_id_result_spec.rb @@ -8,6 +8,7 @@ let(:transaction_id) { 'ABCD1234' } let(:requested_attributes) { { dob: 1, first_name: 1 } } let(:verified_attributes) { [:dob, :first_name] } + let(:jurisdiction_in_maintenance_window) { false } subject do described_class.new( @@ -18,6 +19,7 @@ transaction_id:, requested_attributes:, verified_attributes:, + jurisdiction_in_maintenance_window:, ) end @@ -34,6 +36,7 @@ transaction_id: 'ABCD1234', requested_attributes: { dob: 1, first_name: 1 }, verified_attributes: [:dob, :first_name], + jurisdiction_in_maintenance_window: false, }, ) end diff --git a/spec/views/accounts/show.html.erb_spec.rb b/spec/views/accounts/show.html.erb_spec.rb index d24efb61757..178e15b4240 100644 --- a/spec/views/accounts/show.html.erb_spec.rb +++ b/spec/views/accounts/show.html.erb_spec.rb @@ -121,7 +121,7 @@ in_person_enrollment = user.in_person_enrollments.first in_person_enrollment.update!(status: :expired, status_check_completed_at: Time.zone.now) profile = user.profiles.first - profile.deactivate_due_to_ipp_expiration + profile.deactivate_due_to_in_person_verification_cancelled end it 'renders the idv partial' do diff --git a/spec/views/sign_up/completions/show.html.erb_spec.rb b/spec/views/sign_up/completions/show.html.erb_spec.rb index 41938c7ff20..352e3e293e1 100644 --- a/spec/views/sign_up/completions/show.html.erb_spec.rb +++ b/spec/views/sign_up/completions/show.html.erb_spec.rb @@ -3,6 +3,7 @@ RSpec.describe 'sign_up/completions/show.html.erb' do let(:user) { create(:user, :fully_registered) } let(:service_provider) { create(:service_provider) } + let(:selected_email_id) { user.email_addresses.first.id } let(:decrypted_pii) { {} } let(:requested_attributes) { [:email] } let(:ial2_requested) { false } @@ -22,10 +23,11 @@ CompletionsPresenter.new( current_user: user, current_sp: service_provider, - decrypted_pii: decrypted_pii, - requested_attributes: requested_attributes, - ial2_requested: ial2_requested, - completion_context: completion_context, + decrypted_pii:, + requested_attributes:, + ial2_requested:, + completion_context:, + selected_email_id:, ) end @@ -43,9 +45,9 @@ expect(text).to_not include(service_provider.agency.name) expect(text).to include( view_context.strip_tags( - I18n.t( - 'help_text.requested_attributes.ial1_intro_html', - sp: service_provider.friendly_name, + t( + 'help_text.requested_attributes.intro_html', + sp_html: content_tag(:strong, service_provider.friendly_name), ), ), ) @@ -59,6 +61,48 @@ ) end + context 'select email to send to partner and select email feature is disabled' do + before do + allow(IdentityConfig.store).to receive( + :feature_select_email_to_share_enabled, + ).and_return(false) + end + + it 'does not show a link to select different email' do + create(:email_address, user: user) + user.reload + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + + it 'does not show a link to add another email' do + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + end + + context 'select email to send to partner' do + it 'does not show a link to select different email' do + create(:email_address, user: user) + user.reload + render + + expect(rendered).to include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to_not include(t('account.index.email_add')) + end + + it 'does not show a link to add another email' do + render + + expect(rendered).to_not include(t('help_text.requested_attributes.change_email_link')) + expect(rendered).to include(t('account.index.email_add')) + end + end + context 'the all_emails scope is requested' do let(:requested_attributes) { [:email, :all_emails] } diff --git a/spec/views/sign_up/select_email/show.html.erb_spec.rb b/spec/views/sign_up/select_email/show.html.erb_spec.rb new file mode 100644 index 00000000000..a699da041ae --- /dev/null +++ b/spec/views/sign_up/select_email/show.html.erb_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe 'sign_up/select_email/show.html.erb' do + let(:email) { 'michael.motorist@email.com' } + let(:email2) { 'michael.motorist2@email.com' } + let(:user) { create(:user) } + + before do + user.email_addresses.create(email: email, confirmed_at: Time.zone.now) + user.email_addresses.create(email: email2, confirmed_at: Time.zone.now) + user.reload + @user_emails = user.email_addresses + @select_email_form = SelectEmailForm.new(user) + end + + it 'shows all of the user\'s emails' do + render + + expect(rendered).to include('michael.motorist@email.com') + expect(rendered).to include('michael.motorist2@email.com') + end +end diff --git a/yarn.lock b/yarn.lock index 1bacc1cdecc..1a803270cc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1475,23 +1475,7 @@ "@types/chai" "*" "@types/chai-as-promised" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.4.5" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.5.tgz#acdfb7dd36b91cc5d812d7c093811a8f3d9b31e4" - integrity sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^1.0.5": +"@types/estree@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== @@ -1566,7 +1550,7 @@ dependencies: "@types/sizzle" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2069,10 +2053,10 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -3165,10 +3149,10 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -enhanced-resolve@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -7100,21 +7084,20 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.91.0: - version "5.91.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" - integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw== +webpack@^5.94.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== dependencies: - "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" "@webassemblyjs/wasm-edit" "^1.12.1" "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" + acorn-import-attributes "^1.9.5" browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.16.0" + enhanced-resolve "^5.17.1" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0"