diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4dcf6078669..fa9120acf94 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -293,7 +293,7 @@ specs: - cp -a keys.example keys - cp -a certs.example certs - cp pwned_passwords/pwned_passwords.txt.sample pwned_passwords/pwned_passwords.txt - - "echo -e \"test:\n redis_url: 'redis://db-redis:6379/0'\n redis_throttle_url: 'redis://db-redis:6379/1'\" > config/application.yml" + - "echo -e \"test:\n redis_url: 'redis://db-redis:6379/0'\n redis_throttle_url: 'redis://db-redis:6379/1'\n redis_attempts_api_url: 'redis://db-redis:6379/2'\" > config/application.yml" - bundle exec rake db:create db:migrate --trace - bundle exec rake db:seed - bundle exec rake knapsack:rspec["--format documentation --format RspecJunitFormatter --out rspec.xml --format json --out rspec_json/${CI_NODE_INDEX}.json"] diff --git a/Gemfile.lock b/Gemfile.lock index e152e8df347..fa23eee29b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -458,7 +458,7 @@ GEM net-smtp (0.5.0) net-protocol net-ssh (6.1.0) - newrelic_rpm (9.7.0) + newrelic_rpm (9.16.1) nio4r (2.7.4) nokogiri (1.16.8) mini_portile2 (~> 2.8.2) diff --git a/app/controllers/concerns/threat_metrix_concern.rb b/app/controllers/concerns/threat_metrix_concern.rb index 570b8c69fb3..401d56587c4 100644 --- a/app/controllers/concerns/threat_metrix_concern.rb +++ b/app/controllers/concerns/threat_metrix_concern.rb @@ -5,12 +5,6 @@ module ThreatMetrixConcern THREAT_METRIX_WILDCARD_DOMAIN = '*.online-metrix.net' def override_csp_for_threat_metrix - return unless FeatureManagement.proofing_device_profiling_collecting_enabled? - - threat_metrix_csp_overrides - end - - def threat_metrix_csp_overrides policy = current_content_security_policy # ThreatMetrix requires additional Content Security Policy (CSP) diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index 6701a85a985..f485511808a 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -106,9 +106,14 @@ def sign_in_notification_timeframe_expired_event return @sign_in_notification_timeframe_expired_event if defined?( @sign_in_notification_timeframe_expired_event ) - @sign_in_notification_timeframe_expired_event = current_user.events.where( - event_type: 'sign_in_notification_timeframe_expired', - ).order(created_at: :desc).limit(1).take + @sign_in_notification_timeframe_expired_event = current_user.events + .where( + event_type: 'sign_in_notification_timeframe_expired', + created_at: (IdentityConfig.store.session_total_duration_timeout_in_minutes.minutes.ago..), + ) + .order(created_at: :desc) + .limit(1) + .take end def handle_remember_device_preference(remember_device_preference) diff --git a/app/controllers/idv/forgot_password_controller.rb b/app/controllers/idv/forgot_password_controller.rb index aa3041b13ff..af5fce773fa 100644 --- a/app/controllers/idv/forgot_password_controller.rb +++ b/app/controllers/idv/forgot_password_controller.rb @@ -15,7 +15,7 @@ def new def update analytics.idv_forgot_password_confirmed request_id = sp_session[:request_id] - email = current_user.confirmed_email_addresses.first.email + email = current_user.last_sign_in_email_address.email reset_password(email, request_id) end diff --git a/app/controllers/idv/in_person/ready_to_verify_controller.rb b/app/controllers/idv/in_person/ready_to_verify_controller.rb index fbd0aeeee2c..79e7172745d 100644 --- a/app/controllers/idv/in_person/ready_to_verify_controller.rb +++ b/app/controllers/idv/in_person/ready_to_verify_controller.rb @@ -18,6 +18,8 @@ class ReadyToVerifyController < ApplicationController def show @is_enhanced_ipp = resolved_authn_context_result.enhanced_ipp? + @show_closed_post_office_banner = + IdentityConfig.store.in_person_proofing_post_office_closed_alert_enabled analytics.idv_in_person_ready_to_verify_visit(**opt_in_analytics_properties) @presenter = ReadyToVerifyPresenter.new( enrollment: enrollment, diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb index d1ca08e83de..62f8c7f02e3 100644 --- a/app/controllers/idv/in_person/ssn_controller.rb +++ b/app/controllers/idv/in_person/ssn_controller.rb @@ -12,7 +12,8 @@ class SsnController < ApplicationController before_action :confirm_not_rate_limited_after_doc_auth before_action :confirm_in_person_address_step_complete before_action :confirm_repeat_ssn, only: :show - before_action :override_csp_for_threat_metrix + before_action :override_csp_for_threat_metrix, + if: -> { FeatureManagement.proofing_device_profiling_collecting_enabled? } attr_reader :ssn_presenter diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index eb20938b909..04c7dcb8e9c 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -10,7 +10,8 @@ class SsnController < ApplicationController before_action :confirm_not_rate_limited_after_doc_auth before_action :confirm_step_allowed - before_action :override_csp_for_threat_metrix + before_action :override_csp_for_threat_metrix, + if: -> { FeatureManagement.proofing_device_profiling_collecting_enabled? } attr_reader :ssn_presenter diff --git a/app/controllers/two_factor_authentication/otp_verification_controller.rb b/app/controllers/two_factor_authentication/otp_verification_controller.rb index 73f5572b3c7..625341c4516 100644 --- a/app/controllers/two_factor_authentication/otp_verification_controller.rb +++ b/app/controllers/two_factor_authentication/otp_verification_controller.rb @@ -24,7 +24,10 @@ def create increment_mfa_selection_attempt_count(otp_auth_method) end result = otp_verification_form.submit - post_analytics(result) + + if UserSessionContext.confirmation_context?(context) + log_confirmation_analytics(result) + end if UserSessionContext.authentication_or_reauthentication_context?(context) handle_verification_for_authentication_context( @@ -149,9 +152,9 @@ def form_params params.permit(:code) end - def post_analytics(result) + def log_confirmation_analytics(result) properties = result.to_h.merge(analytics_properties) - analytics.multi_factor_auth_setup(**properties) if context == 'confirmation' + analytics.multi_factor_auth_setup(**properties) end def analytics_properties diff --git a/app/controllers/users/two_factor_authentication_setup_controller.rb b/app/controllers/users/two_factor_authentication_setup_controller.rb index e60a2057a67..5defc432652 100644 --- a/app/controllers/users/two_factor_authentication_setup_controller.rb +++ b/app/controllers/users/two_factor_authentication_setup_controller.rb @@ -12,7 +12,8 @@ class TwoFactorAuthenticationSetupController < ApplicationController before_action :authenticate_user before_action :confirm_user_authenticated_for_2fa_setup before_action :check_if_possible_piv_user - before_action :override_csp_for_threat_metrix + before_action :override_csp_for_threat_metrix, + if: -> { FeatureManagement.account_creation_device_profiling_collecting_enabled? } delegate :enabled_mfa_methods_count, to: :mfa_context diff --git a/app/controllers/users/webauthn_setup_controller.rb b/app/controllers/users/webauthn_setup_controller.rb index b7ae7912f5a..43fbc570331 100644 --- a/app/controllers/users/webauthn_setup_controller.rb +++ b/app/controllers/users/webauthn_setup_controller.rb @@ -46,6 +46,7 @@ def new increment_mfa_selection_attempt_count(webauthn_auth_method) analytics.webauthn_setup_submitted( platform_authenticator: form.platform_authenticator?, + in_account_creation_flow: user_session[:in_account_creation_flow] || false, errors: result.errors, success: false, ) @@ -129,6 +130,7 @@ def process_valid_webauthn(form) create_user_event(:webauthn_key_added) analytics.webauthn_setup_submitted( platform_authenticator: form.platform_authenticator?, + in_account_creation_flow: user_session[:in_account_creation_flow] || false, success: true, ) handle_remember_device_preference(params[:remember_device]) diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index 14afc208ce3..e0c96cf5cd4 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -77,6 +77,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) { submissionError instanceof UploadFormEntriesError ? withProps({ remainingSubmitAttempts: submissionError.remainingSubmitAttempts, + submitAttempts: submissionError.submitAttempts, isResultCodeInvalid: submissionError.isResultCodeInvalid, isFailedResult: submissionError.isFailedResult, isFailedDocType: submissionError.isFailedDocType, diff --git a/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx b/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx index c6ef919ae05..512423fc522 100644 --- a/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx +++ b/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx @@ -3,10 +3,12 @@ import { Button } from '@18f/identity-components'; import { useInstanceId } from '@18f/identity-react-hooks'; import { t } from '@18f/identity-i18n'; import AnalyticsContext from '../context/analytics'; +import UploadContext from '../context/upload'; function InPersonCallToAction() { const instanceId = useInstanceId(); const { trackEvent } = useContext(AnalyticsContext); + const { submitAttempts } = useContext(UploadContext); return (
{ - trackEvent('IdV: verify in person troubleshooting option clicked'); + trackEvent('IdV: verify in person troubleshooting option clicked', { + submit_attempts: submitAttempts, + }); }} > {t('in_person_proofing.body.cta.button')} diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx index 9875d59c146..e7844cc40fc 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.tsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx @@ -38,6 +38,7 @@ export interface ReviewIssuesStepValue { interface ReviewIssuesStepProps extends FormStepComponentProps { remainingSubmitAttempts?: number; + submitAttempts?: number; isResultCodeInvalid?: boolean; isFailedResult?: boolean; isFailedSelfie?: boolean; @@ -57,6 +58,7 @@ function ReviewIssuesStep({ registerField = () => undefined, toPreviousStep = () => undefined, remainingSubmitAttempts = Infinity, + submitAttempts, isResultCodeInvalid = false, isFailedResult = false, isFailedDocType = false, @@ -106,6 +108,7 @@ function ReviewIssuesStep({ function onWarningPageDismissed() { trackEvent('IdV: Capture troubleshooting dismissed', { liveness_checking_required: isSelfieCaptureEnabled, + submit_attempts: submitAttempts, }); setHasDismissed(true); diff --git a/app/javascript/packages/document-capture/context/upload.tsx b/app/javascript/packages/document-capture/context/upload.tsx index fbfc4a514cd..6671d590753 100644 --- a/app/javascript/packages/document-capture/context/upload.tsx +++ b/app/javascript/packages/document-capture/context/upload.tsx @@ -1,7 +1,7 @@ -import { createContext } from 'react'; +import { createContext, useState } from 'react'; import { useObjectMemo } from '@18f/identity-react-hooks'; import type { ReactNode } from 'react'; -import defaultUpload from '../services/upload'; +import defaultUpload, { UploadFormEntriesError } from '../services/upload'; import type { PII } from '../services/upload'; const UploadContext = createContext({ @@ -11,6 +11,7 @@ const UploadContext = createContext({ isMockClient: false, flowPath: 'standard' as FlowPath, formData: {} as Record, + submitAttempts: 0, }); UploadContext.displayName = 'UploadContext'; @@ -80,6 +81,11 @@ export interface UploadErrorResponse { */ remaining_submit_attempts?: number; + /** + * Number of submitted doc capture attempts for user + */ + submit_attempts?: number; + /** * Boolean to decide if capture hints should be shown with error. */ @@ -189,7 +195,19 @@ function UploadContextProvider({ flowPath, children, }: UploadContextProviderProps) { - const uploadWithFormData = (payload) => upload({ ...payload, ...formData }, { endpoint }); + const [submitAttempts, setSubmitAttempts] = useState(0); + + const uploadWithFormData = async (payload) => { + try { + const result = await upload({ ...payload, ...formData }, { endpoint }); + return result; + } catch (error) { + if (error instanceof UploadFormEntriesError && error.submitAttempts !== undefined) { + setSubmitAttempts(error.submitAttempts); + } + throw error; + } + }; const getStatus = () => statusEndpoint @@ -203,6 +221,7 @@ function UploadContextProvider({ isMockClient, flowPath, formData, + submitAttempts, }); return {children}; diff --git a/app/javascript/packages/document-capture/services/upload.ts b/app/javascript/packages/document-capture/services/upload.ts index 34b1b710a9e..a8722f4e956 100644 --- a/app/javascript/packages/document-capture/services/upload.ts +++ b/app/javascript/packages/document-capture/services/upload.ts @@ -38,6 +38,8 @@ export class UploadFormEntriesError extends FormError { remainingSubmitAttempts = Infinity; + submitAttempts = 0; + isResultCodeInvalid = false; isFailedResult = false; @@ -120,6 +122,10 @@ const upload: UploadImplementation = async function (payload, { method = 'POST', error.remainingSubmitAttempts = result.remaining_submit_attempts; } + if (result.submit_attempts) { + error.submitAttempts = result.submit_attempts; + } + if (result.ocr_pii) { error.pii = result.ocr_pii; } diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index cf5b3b9cbe3..23e63b75e6e 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -25,6 +25,7 @@ def perform( service_provider_issuer: nil, threatmetrix_session_id: nil, request_ip: nil, + proofing_components: nil, # rubocop:disable Lint/UnusedMethodArgument # DEPRECATED ARGUMENTS should_proof_state_id: false # rubocop:disable Lint/UnusedMethodArgument ) @@ -130,7 +131,7 @@ def make_vendor_proofing_requests( end def user_email_for_proofing(user) - user.confirmed_email_addresses.first.email + user.last_sign_in_email_address.email end def log_threatmetrix_info(threatmetrix_result, user) diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index eb623771eed..caed7d1e823 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -263,8 +263,6 @@ def idv_please_call(**) end end - alias_method :in_person_please_call, :idv_please_call - def in_person_completion_survey with_user_locale(user) do @header = t('user_mailer.in_person_completion_survey.header') @@ -314,6 +312,9 @@ def in_person_ready_to_verify(enrollment:, is_enhanced_ipp:) is_enhanced_ipp: is_enhanced_ipp, ) @is_enhanced_ipp = is_enhanced_ipp + @show_closed_post_office_banner = + IdentityConfig.store.in_person_proofing_post_office_closed_alert_enabled + mail( to: email_address.email, subject: t('user_mailer.in_person_ready_to_verify.subject', app_name: APP_NAME), @@ -327,6 +328,9 @@ def in_person_ready_to_verify_reminder(enrollment:) ).image_data @is_enhanced_ipp = enrollment.enhanced_ipp? + @show_closed_post_office_banner = + IdentityConfig.store.in_person_proofing_post_office_closed_alert_enabled + with_user_locale(user) do @presenter = Idv::InPerson::ReadyToVerifyPresenter.new( enrollment: enrollment, @@ -435,6 +439,16 @@ def account_reinstated end end + def in_person_post_office_closed + with_user_locale(user) do + @hide_title = true + mail( + to: email_address.email, + subject: t('in_person_proofing.post_office_closed.email.subject'), + ) + end + end + private attr_reader :user, :email_address diff --git a/app/models/user.rb b/app/models/user.rb index 44270a01e15..2eca557512a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,7 +65,7 @@ class User < ApplicationRecord attr_accessor :asserted_attributes, :email def confirmed_email_addresses - email_addresses.confirmed.order('last_sign_in_at DESC NULLS LAST') + email_addresses.confirmed end def fully_registered? diff --git a/app/presenters/image_upload_response_presenter.rb b/app/presenters/image_upload_response_presenter.rb index 5a33c7f4288..131b315a6b3 100644 --- a/app/presenters/image_upload_response_presenter.rb +++ b/app/presenters/image_upload_response_presenter.rb @@ -24,6 +24,10 @@ def remaining_submit_attempts @form_response.to_h[:remaining_submit_attempts] end + def submit_attempts + @form_response.to_h[:submit_attempts] + end + def status if success? :ok @@ -41,6 +45,7 @@ def as_json(*) json = { success: false, errors: errors, remaining_submit_attempts: remaining_submit_attempts, + submit_attempts: submit_attempts, doc_type_supported: doc_type_supported? } if remaining_submit_attempts&.zero? if @form_response.extra[:flow_path] == 'standard' diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 3bfbe0a1357..17ee037292a 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1198,12 +1198,14 @@ def idv_cancellation_visited( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] use_alternate_sdk # @param [Boolean] liveness_checking_required + # @param [Integer] submit_attempts Times that user has tried submitting document capture def idv_capture_troubleshooting_dismissed( acuant_sdk_upgrade_a_b_testing_enabled:, acuant_version:, flow_path:, use_alternate_sdk:, liveness_checking_required:, + submit_attempts:, **extra ) track_event( @@ -1213,6 +1215,7 @@ def idv_capture_troubleshooting_dismissed( flow_path: flow_path, use_alternate_sdk: use_alternate_sdk, liveness_checking_required: liveness_checking_required, + submit_attempts: submit_attempts, **extra, ) end @@ -5309,16 +5312,19 @@ def idv_verify_by_mail_enter_code_visited( # @param ["hybrid","standard"] flow_path Document capture user flow # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing + # @param [Integer] submit_attempts Times that user has tried submitting document capture # The user clicked the troubleshooting option to start in-person proofing def idv_verify_in_person_troubleshooting_option_clicked( flow_path:, opted_in_to_in_person_proofing:, + submit_attempts:, **extra ) track_event( 'IdV: verify in person troubleshooting option clicked', flow_path: flow_path, opted_in_to_in_person_proofing: opted_in_to_in_person_proofing, + submit_attempts: submit_attempts, **extra, ) end @@ -7692,12 +7698,20 @@ def webauthn_platform_recommended_visited # @param [Hash] platform_authenticator # @param [Boolean] success # @param [Hash, nil] errors + # @param [Boolean] in_account_creation_flow Whether user is going through account creation flow # Tracks whether or not Webauthn setup was successful - def webauthn_setup_submitted(platform_authenticator:, success:, errors: nil, **extra) + def webauthn_setup_submitted( + platform_authenticator:, + success:, + in_account_creation_flow: nil, + errors: nil, + **extra + ) track_event( :webauthn_setup_submitted, platform_authenticator: platform_authenticator, success: success, + in_account_creation_flow:, errors: errors, **extra, ) diff --git a/app/services/attempts_api/attempt_event.rb b/app/services/attempts_api/attempt_event.rb new file mode 100644 index 00000000000..bd5821d5721 --- /dev/null +++ b/app/services/attempts_api/attempt_event.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module AttemptsApi + class AttemptEvent + attr_reader :jti, :iat, :event_type, :session_id, :occurred_at, :event_metadata + + def initialize( + event_type:, + session_id:, + occurred_at:, + event_metadata:, + jti: SecureRandom.uuid, + iat: Time.zone.now.to_i + ) + @jti = jti + @iat = iat + @event_type = event_type + @session_id = session_id + @occurred_at = occurred_at + @event_metadata = event_metadata + end + + def to_jwe(public_key:, issuer:) + jwk = JWT::JWK.new(public_key) + + JWE.encrypt( + payload_json(issuer: issuer), + public_key, + typ: 'secevent+jwe', + zip: 'DEF', + alg: 'RSA-OAEP', + enc: 'A256GCM', + kid: jwk.kid, + ) + end + + def self.from_jwe(jwe, private_key) + decrypted_event = JWE.decrypt(jwe, private_key) + parsed_event = JSON.parse(decrypted_event) + event_type = parsed_event['events'].keys.first.split('/').last + event_data = parsed_event['events'].values.first + jti = parsed_event['jti'].split(':').last + AttemptEvent.new( + jti: jti, + iat: parsed_event['iat'], + event_type: event_type, + session_id: event_data['subject']['session_id'], + occurred_at: Time.zone.at(event_data['occurred_at']), + event_metadata: event_data.symbolize_keys.except(:subject, :occurred_at), + ) + end + + def payload(issuer:) + { + jti: jti, + iat: iat, + iss: Rails.application.routes.url_helpers.root_url, + aud: issuer, + events: { + long_event_type => event_data, + }, + } + end + + def payload_json(issuer:) + @payload_json ||= payload(issuer:).to_json + end + + private + + def event_data + { + 'subject' => { + 'subject_type' => 'session', + 'session_id' => session_id, + }, + 'occurred_at' => occurred_at.to_f, + }.merge(event_metadata || {}) + end + + def long_event_type + dasherized_name = event_type.to_s.dasherize + "https://schemas.login.gov/secevent/attempts-api/event-type/#{dasherized_name}" + end + end +end diff --git a/app/services/attempts_api/redis_client.rb b/app/services/attempts_api/redis_client.rb new file mode 100644 index 00000000000..2f9fd809382 --- /dev/null +++ b/app/services/attempts_api/redis_client.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module AttemptsApi + class RedisClient + def write_event(event_key:, jwe:, timestamp:, issuer:) + key = key(timestamp, issuer) + REDIS_ATTEMPTS_API_POOL.with do |client| + client.hset(key, event_key, jwe) + client.expire(key, IdentityConfig.store.attempts_api_event_ttl_seconds) + end + end + + def read_events(timestamp:, issuer:, batch_size: 5000) + key = key(timestamp, issuer) + events = {} + REDIS_ATTEMPTS_API_POOL.with do |client| + client.hscan_each(key, count: batch_size) do |k, v| + events[k] = v + end + end + events + end + + def key(timestamp, issuer) + formatted_time = timestamp.in_time_zone('UTC').change(min: 0, sec: 0).iso8601 + "attempts-api-events:#{issuer}:#{formatted_time}" + end + end +end diff --git a/app/services/proofing/aamva/proofer.rb b/app/services/proofing/aamva/proofer.rb index d5481d63cb6..c246c9219f3 100644 --- a/app/services/proofing/aamva/proofer.rb +++ b/app/services/proofing/aamva/proofer.rb @@ -58,11 +58,14 @@ def proof(applicant) applicant: aamva_applicant, ) - build_result_from_response(response, applicant[:state]) + build_result_from_response(response, applicant[:state_id_jurisdiction]) rescue => exception failed_result = Proofing::StateIdResult.new( success: false, errors: {}, exception: exception, vendor_name: 'aamva:state_id', - transaction_id: nil, verified_attributes: [] + transaction_id: nil, verified_attributes: [], + jurisdiction_in_maintenance_window: jurisdiction_in_maintenance_window?( + applicant[:state_id_jurisdiction], + ) ) send_to_new_relic(failed_result) failed_result diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb index f268492a311..55f01e51174 100644 --- a/app/views/idv/in_person/ready_to_verify/show.html.erb +++ b/app/views/idv/in_person/ready_to_verify/show.html.erb @@ -228,6 +228,13 @@
<% end %> +<%# Alert %> +<% if @show_closed_post_office_banner %> + <%= render AlertComponent.new(type: :warning, class: 'margin-y-4', text_tag: :div) do %> +

<%= t('in_person_proofing.post_office_closed.heading') %>

+

<%= t('in_person_proofing.post_office_closed.body') %>

+ <% end %> +<% end %> <% if !@is_enhanced_ipp %>

<%= t('in_person_proofing.body.location.change_location_heading') %>

diff --git a/app/views/user_mailer/in_person_post_office_closed.html.erb b/app/views/user_mailer/in_person_post_office_closed.html.erb new file mode 100644 index 00000000000..14abf7ed812 --- /dev/null +++ b/app/views/user_mailer/in_person_post_office_closed.html.erb @@ -0,0 +1,2 @@ +

<%= t('in_person_proofing.post_office_closed.email.heading') %>

+

<%= t('in_person_proofing.post_office_closed.email.body_html') %>

diff --git a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb index d61de5bf003..46e0a0090e6 100644 --- a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb +++ b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb @@ -275,6 +275,21 @@ <% end %> +<%# alert %> +<% if @show_closed_post_office_banner %> + + + + + +
+ <%= image_tag('email/warning.png', width: 16, height: 16, alt: '', style: 'margin-top: 4px;') %> + +

<%= t('in_person_proofing.post_office_closed.heading') %>

+

<%= t('in_person_proofing.post_office_closed.body') %>

+
+<% end %> + <% if !@is_enhanced_ipp %>

<%= t('in_person_proofing.body.location.change_location_heading') %>

diff --git a/app/views/users/webauthn_setup/new.html.erb b/app/views/users/webauthn_setup/new.html.erb index 1323e2fe05d..a0ad8a34ce6 100644 --- a/app/views/users/webauthn_setup/new.html.erb +++ b/app/views/users/webauthn_setup/new.html.erb @@ -27,7 +27,7 @@ }, ) do |f| %> <%= hidden_field_tag :user_id, current_user.id, id: 'user_id' %> - <%= hidden_field_tag :user_email, current_user.confirmed_email_addresses.first.email, id: 'user_email' %> + <%= hidden_field_tag :user_email, current_user.last_sign_in_email_address.email, id: 'user_email' %> <%= hidden_field_tag :user_challenge, user_session[:webauthn_challenge].to_json, id: 'user_challenge' %> <%= hidden_field_tag :exclude_credentials, @exclude_credentials&.join(','), id: 'exclude_credentials' %> <%= hidden_field_tag :webauthn_id, '', id: 'webauthn_id' %> diff --git a/config/application.yml.default b/config/application.yml.default index dcd7ef6c452..0c72eaaba56 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -21,7 +21,7 @@ aamva_private_key: '' aamva_public_key: '' aamva_send_id_type: true aamva_send_middle_name: true -aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' +aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NH","NJ","NM","NV","OH","OK","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url account_creation_device_profiling: disabled @@ -40,6 +40,7 @@ allowed_verified_within_providers: '[]' asset_host: '' async_stale_job_timeout_seconds: 300 async_wait_timeout_seconds: 60 +attempts_api_event_ttl_seconds: 3_600 attribute_encryption_key: attribute_encryption_key_queue: '[]' available_locales: 'en,es,fr,zh' @@ -192,6 +193,7 @@ in_person_outage_message_enabled: false in_person_proofing_enabled: false in_person_proofing_enforce_tmx: false in_person_proofing_opt_in_enabled: false +in_person_proofing_post_office_closed_alert_enabled: true in_person_results_delay_in_hours: 1 in_person_send_proofing_notifications_enabled: false in_person_stop_expiring_enrollments: false @@ -321,6 +323,8 @@ recaptcha_site_key: '' recommend_webauthn_platform_for_sms_ab_test_account_creation_percent: 0 recommend_webauthn_platform_for_sms_ab_test_authentication_percent: 0 recovery_code_length: 4 +redis_attempts_api_pool_size: 1 +redis_attempts_api_url: redis://localhost:6379/2 redis_pool_size: 10 redis_throttle_pool_size: 5 redis_throttle_url: redis://localhost:6379/1 @@ -557,6 +561,7 @@ test: hmac_fingerprinter_key: a2c813d4dca919340866ba58063e4072adc459b767a74cf2666d5c1eef3861db26708e7437abde1755eb24f4034386b0fea1850a1cb7e56bff8fae3cc6ade96c hmac_fingerprinter_key_queue: '["old-key-one", "old-key-two"]' identity_pki_disabled: true + in_person_proofing_post_office_closed_alert_enabled: false lexisnexis_trueid_account_id: 'test_account' lockout_period_in_minutes: 5 logins_per_email_and_ip_limit: 2 diff --git a/config/initializers/01_redis.rb b/config/initializers/01_redis.rb index 39851d84299..3a88b1c43f9 100644 --- a/config/initializers/01_redis.rb +++ b/config/initializers/01_redis.rb @@ -9,3 +9,8 @@ REDIS_THROTTLE_POOL = ConnectionPool.new(size: IdentityConfig.store.redis_throttle_pool_size) do Redis.new(url: IdentityConfig.store.redis_throttle_url) end.freeze + +REDIS_ATTEMPTS_API_POOL = + ConnectionPool.new(size: IdentityConfig.store.redis_attempts_api_pool_size) do + Redis.new(url: IdentityConfig.store.redis_attempts_api_url) + end.freeze diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index c4a4cce8924..aff0162a210 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -109,6 +109,7 @@ def self.all experiment_name: 'Desktop F/T unlock setup', should_log: [ 'User Registration: 2FA Setup visited', + 'WebAuthn Setup Visited', :webauthn_setup_submitted, 'Multi-Factor Authentication Setup', ].to_set, diff --git a/config/locales/en.yml b/config/locales/en.yml index 09965179ec3..3a2e615c72f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1327,6 +1327,11 @@ in_person_proofing.headings.state_id_milestone_2: Enter the information on your in_person_proofing.headings.switch_back: Switch back to your computer to prepare to verify your identity in person in_person_proofing.headings.update_address: Update your current address in_person_proofing.headings.update_state_id: Update the information on your ID +in_person_proofing.post_office_closed.body: Post Office locations will resume regular hours on Friday, January 10, 2025. +in_person_proofing.post_office_closed.email.body_html: You will not be able to visit a Post Office on Thursday, January 9, 2025 to finish verifying your identity. Post Office locations will resume regular hours on Friday, January 10, 2025. +in_person_proofing.post_office_closed.email.heading: All Post Offices will be closed on January 9, 2025 to honor former President Jimmy Carter +in_person_proofing.post_office_closed.email.subject: All Post Offices will be closed on Thursday, January 9, 2025 +in_person_proofing.post_office_closed.heading: All Post Offices will be closed on Thursday, January 9, 2025 to honor former President Jimmy Carter. in_person_proofing.process.barcode.caption_label: Enrollment code in_person_proofing.process.barcode.heading: Show your %{app_name} barcode in_person_proofing.process.barcode.info: The retail associate needs to scan your barcode at the top of this page. You can print this page or show it on your mobile device. diff --git a/config/locales/es.yml b/config/locales/es.yml index cd034e65a22..a8b5b98d7b1 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1338,6 +1338,11 @@ in_person_proofing.headings.state_id_milestone_2: Ingrese la información de su in_person_proofing.headings.switch_back: Vuelva a su computadora para preparar la verificación de su identidad en persona in_person_proofing.headings.update_address: Actualice su dirección actual in_person_proofing.headings.update_state_id: Actualice la información de su identificación +in_person_proofing.post_office_closed.body: Las oficinas de correos reanudarán su horario habitual el viernes 10 de enero de 2025. +in_person_proofing.post_office_closed.email.body_html: No podrás visitar una Oficina de Correos el jueves 9 de enero de 2025 para terminar de verificar tu identidad. Las oficinas de correos reanudarán su horario habitual el viernes 10 de enero de 2025. +in_person_proofing.post_office_closed.email.heading: Todas las oficinas de correos estarán cerradas el 9 de enero de 2025 en honor al expresidente Jimmy Carter +in_person_proofing.post_office_closed.email.subject: Todas las oficinas de correos estarán cerradas el jueves 9 de enero de 2025 +in_person_proofing.post_office_closed.heading: Todas las oficinas de correos estarán cerradas el jueves 9 de enero de 2025 en honor al ex presidente Jimmy Carter. in_person_proofing.process.barcode.caption_label: Código de registro in_person_proofing.process.barcode.heading: Muestre su código de barras de %{app_name} in_person_proofing.process.barcode.info: El empleado debe escanear el código de barras que aparece en la parte superior de esta página. Puede imprimir esta página o mostrarla en su dispositivo móvil. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d5925e2e0f8..cdbd12ab40c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1327,6 +1327,11 @@ in_person_proofing.headings.state_id_milestone_2: Saisissez les informations fig in_person_proofing.headings.switch_back: Revenez à votre ordinateur pour vous préparer à confirmer votre identité en personne in_person_proofing.headings.update_address: Mettez à jour votre adresse actuelle in_person_proofing.headings.update_state_id: Mettez à jour les informations figurant sur votre document d’identité +in_person_proofing.post_office_closed.body: Les bureaux de poste reprendront leurs horaires habituels le vendredi 10 janvier 2025. +in_person_proofing.post_office_closed.email.body_html: Vous ne pourrez pas vous rendre dans un bureau de Poste le jeudi 9 janvier 2025 pour terminer la vérification de votre identité. Les bureaux de poste reprendront leurs horaires habituels le vendredi 10 janvier 2025 +in_person_proofing.post_office_closed.email.heading: Tous les bureaux de poste seront fermés le 9 janvier 2025 en l’honneur de l’ancien président Jimmy Carter +in_person_proofing.post_office_closed.email.subject: Tous les bureaux de poste seront fermés le jeudi 9 janvier 2025 +in_person_proofing.post_office_closed.heading: Tous les bureaux de poste seront fermés le jeudi 9 janvier 2025 en l’honneur de l’ancien président Jimmy Carter. in_person_proofing.process.barcode.caption_label: Code d’inscription in_person_proofing.process.barcode.heading: Montrez votre code-barres %{app_name} in_person_proofing.process.barcode.info: Le préposé doit scanner votre code-barres en haut de cette page. Vous pouvez imprimer cette page ou la montrer sur votre appareil mobile. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index ae1903db5b6..ea0185944b9 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1340,6 +1340,11 @@ in_person_proofing.headings.state_id_milestone_2: 输入你州政府颁发身份 in_person_proofing.headings.switch_back: 切换回你的电脑,来准备亲身去验证身份。 in_person_proofing.headings.update_address: 更新你当前地址 in_person_proofing.headings.update_state_id: 更新你身份证件上的信息 +in_person_proofing.post_office_closed.body: 邮局地点将于 2025 年 1 月 10 日星期五恢复正常工作时间。 +in_person_proofing.post_office_closed.email.body_html: 您将无法在 2025 年 1 月 9 日星期四前往邮局完成身份验证。 邮局地点将于 2025 年 1 月 10 日星期五恢复正常工作时间 +in_person_proofing.post_office_closed.email.heading: 所有邮局将于 2025 年 1 月 9 日关闭,以纪念前总统吉米·卡特 +in_person_proofing.post_office_closed.email.subject: 所有邮局将于 2025 年 1 月 9 日星期四关闭 +in_person_proofing.post_office_closed.heading: 所有邮局将于 2025 年 1 月 9 日关闭,以纪念前总统吉米·卡特。 in_person_proofing.process.barcode.caption_label: 注册代码 in_person_proofing.process.barcode.heading: 出示你的 %{app_name} 条形码 in_person_proofing.process.barcode.info: 邮局工作人员需要扫描该页顶部的条形码你可以把该页打印出来,或在你的移动设备上显示。 diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml index 2a69dd342e1..c52f49f86a0 100644 --- a/config/service_providers.localdev.yml +++ b/config/service_providers.localdev.yml @@ -375,7 +375,7 @@ development: 'urn:gov:gsa:SAML:2.0.profiles:sp:sso:localhost': agency_id: 1 - friendly_name: 'Test SAML SP' + friendly_name: 'Example SAML App' acs_url: 'http://localhost:4567/consume' sp_initiated_login_url: 'http://localhost:4567/test/saml' assertion_consumer_logout_service_url: 'http://localhost:4567/slo_logout' @@ -448,7 +448,7 @@ development: - 'http://localhost:9292/logout' certs: - 'sp_sinatra_demo' - friendly_name: 'Example Sinatra App' + friendly_name: 'Example OIDC App' in_person_proofing_enabled: true 'urn:gov:gsa:openidconnect:sp:sinatra_pkce': @@ -462,7 +462,7 @@ development: - 'http://localhost:9292/logout' certs: - 'sp_sinatra_demo' - friendly_name: 'Example Sinatra App with PKCE' + friendly_name: 'Example OIDC App with PKCE' in_person_proofing_enabled: true pkce: true diff --git a/lib/identity_config.rb b/lib/identity_config.rb index c8178d27958..9ea782ad0d8 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -58,6 +58,7 @@ def self.store config.add(:asset_host, type: :string) config.add(:async_stale_job_timeout_seconds, type: :integer) config.add(:async_wait_timeout_seconds, type: :integer) + config.add(:attempts_api_event_ttl_seconds, type: :integer) config.add(:attribute_encryption_key, type: :string) config.add(:attribute_encryption_key_queue, type: :json) config.add(:available_locales, type: :comma_separated_string_list) @@ -216,6 +217,7 @@ def self.store config.add(:in_person_proofing_enabled, type: :boolean) config.add(:in_person_proofing_enforce_tmx, type: :boolean) config.add(:in_person_proofing_opt_in_enabled, type: :boolean) + config.add(:in_person_proofing_post_office_closed_alert_enabled, type: :boolean) config.add(:in_person_results_delay_in_hours, type: :integer) config.add(:in_person_send_proofing_notifications_enabled, type: :boolean) config.add(:in_person_stop_expiring_enrollments, type: :boolean) @@ -355,6 +357,8 @@ def self.store config.add(:recaptcha_secret_key, type: :string) config.add(:recaptcha_site_key, type: :string) config.add(:recovery_code_length, type: :integer) + config.add(:redis_attempts_api_pool_size, type: :integer) + config.add(:redis_attempts_api_url, type: :string) config.add(:redis_pool_size, type: :integer) config.add(:redis_throttle_pool_size, type: :integer) config.add(:redis_throttle_url, type: :string) diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index 2865e06cec3..b6b47801ade 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -1,12 +1,6 @@ require 'rails_helper' RSpec.describe AbTests do - include AbTestsHelper - - after :suite do - reload_ab_tests - end - describe '#all' do it 'returns all registered A/B tests' do expect(AbTests.all.values).to all(be_kind_of(AbTest)) diff --git a/spec/controllers/concerns/threat_metrix_concern_spec.rb b/spec/controllers/concerns/threat_metrix_concern_spec.rb index 9d540624520..2afe4f26f7a 100644 --- a/spec/controllers/concerns/threat_metrix_concern_spec.rb +++ b/spec/controllers/concerns/threat_metrix_concern_spec.rb @@ -12,60 +12,42 @@ def index; end end describe '#override_csp_for_threat_metrix' do - let(:ff_enabled) { true } + it 'modifies CSP headers' do + get :index - before do - allow(IdentityConfig.store).to receive(:proofing_device_profiling) - .and_return(ff_enabled ? :enabled : :disabled) - end - - context 'ff is set' do - it 'modifies CSP headers' do - get :index + csp = response.request.content_security_policy - csp = response.request.content_security_policy + aggregate_failures do + expect(csp.directives['script-src']).to include('h.online-metrix.net') + expect(csp.directives['script-src']).to include("'unsafe-eval'") - aggregate_failures do - expect(csp.directives['script-src']).to include('h.online-metrix.net') - expect(csp.directives['script-src']).to include("'unsafe-eval'") + expect(csp.directives['style-src']).to include("'unsafe-inline'") - expect(csp.directives['style-src']).to include("'unsafe-inline'") + expect(csp.directives['child-src']).to include('h.online-metrix.net') - expect(csp.directives['child-src']).to include('h.online-metrix.net') + expect(csp.directives['connect-src']).to include('h.online-metrix.net') - expect(csp.directives['connect-src']).to include('h.online-metrix.net') - - expect(csp.directives['img-src']).to include('*.online-metrix.net') - end + expect(csp.directives['img-src']).to include('*.online-metrix.net') end + end - context 'with content security policy directives for style-src' do - let(:csp_nonce_directives) { ['style-src'] } - - before do - request.content_security_policy_nonce_directives = csp_nonce_directives - end + context 'with content security policy directives for style-src' do + let(:csp_nonce_directives) { ['style-src'] } - it 'removes style-src nonce directive to allow all unsafe inline styles' do - get :index + before do + request.content_security_policy_nonce_directives = csp_nonce_directives + end - csp = parse_content_security_policy + it 'removes style-src nonce directive to allow all unsafe inline styles' do + get :index - expect(csp['style-src']).to_not include(/'nonce-.+'/) + csp = parse_content_security_policy - # Ensure that the default configuration is not mutated as a result of the request-specific - # revisions to the content security policy. - expect(csp_nonce_directives).to eq(['style-src']) - end - end - end + expect(csp['style-src']).to_not include(/'nonce-.+'/) - context 'ff is not set' do - let(:ff_enabled) { false } - it 'does not modify CSP headers' do - get :index - secure_header_config = response.request.headers.env['secure_headers_request_config'] - expect(secure_header_config).to be_nil + # Ensure that the default configuration is not mutated as a result of the request-specific + # revisions to the content security policy. + expect(csp_nonce_directives).to eq(['style-src']) end end end diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb index e9f5ef0f835..277187e96cc 100644 --- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb +++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb @@ -289,7 +289,7 @@ it 'does not send the "Please Call" email' do action expect_email_not_delivered( - to: user.confirmed_email_addresses.first.email, + to: user.last_sign_in_email_address.email, subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), ) end @@ -335,7 +335,7 @@ it 'sends the "Please Call" email' do action expect_delivered_email( - to: user.confirmed_email_addresses.first.email, + to: user.last_sign_in_email_address.email, subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), ) end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index a96a19998d4..7f88fcc31d0 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -416,7 +416,7 @@ def show it 'sends the idv_please_call email' do put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } expect_delivered_email( - to: user.confirmed_email_addresses.first.email, + to: user.last_sign_in_email_address.email, subject: t('user_mailer.idv_please_call.subject', app_name: APP_NAME), ) end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 2dae6836aa9..5b1efff6ded 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -165,6 +165,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { front: [], back: [], selfie: [] }, + submit_attempts: 2, }, ) end @@ -182,6 +183,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { front: [], back: [], selfie: [] }, + submit_attempts: IdentityConfig.store.doc_auth_max_attempts, } end diff --git a/spec/controllers/idv/in_person/ready_to_verify_controller_spec.rb b/spec/controllers/idv/in_person/ready_to_verify_controller_spec.rb index cc14296fd92..86e3f4664c5 100644 --- a/spec/controllers/idv/in_person/ready_to_verify_controller_spec.rb +++ b/spec/controllers/idv/in_person/ready_to_verify_controller_spec.rb @@ -32,6 +32,7 @@ context 'with in person proofing enabled' do let(:in_person_proofing_enabled) { true } + let(:ipp_post_office_closed_alert_enabled) { false } context 'authenticated' do before do @@ -127,6 +128,19 @@ expect(assigns(:is_enhanced_ipp)).to be true end end + + context 'with in_person_proofing_post_office_closed_alert_enabled' do + let(:ipp_post_office_closed_alert_enabled) { true } + before do + allow(IdentityConfig.store) + .to receive(:in_person_proofing_post_office_closed_alert_enabled) + .and_return(ipp_post_office_closed_alert_enabled) + end + + it 'renders the show template' do + expect(response).to render_template :show + end + end end end diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb index d8ab3d9cd68..9e56e2e319c 100644 --- a/spec/controllers/idv/in_person/ssn_controller_spec.rb +++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb @@ -13,9 +13,9 @@ before do stub_sign_in(user) - subject.user_session['idv/in_person'] = flow_session + controller.user_session['idv/in_person'] = flow_session stub_analytics - subject.idv_session.flow_path = 'standard' + controller.idv_session.flow_path = 'standard' end describe '#step_info' do @@ -36,6 +36,8 @@ end describe '#show' do + subject(:response) { get :show } + let(:analytics_name) { 'IdV: doc auth ssn visited' } let(:analytics_args) do { @@ -66,18 +68,18 @@ end it 'adds a threatmetrix session id to idv_session' do - expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil) + expect { get :show }.to change { controller.idv_session.threatmetrix_session_id }.from(nil) end it 'does not change threatmetrix_session_id when updating ssn' do - subject.idv_session.ssn = ssn - expect { get :show }.not_to change { subject.idv_session.threatmetrix_session_id } + controller.idv_session.ssn = ssn + expect { get :show }.not_to change { controller.idv_session.threatmetrix_session_id } end context 'with an ssn in idv_session' do let(:referer) { idv_in_person_address_url } before do - subject.idv_session.ssn = ssn + controller.idv_session.ssn = ssn request.env['HTTP_REFERER'] = referer end @@ -98,6 +100,32 @@ end end end + + context 'with ThreatMetrix profiling disabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?) + .and_return(false) + end + + it 'does not override CSPs for ThreatMetrix' do + expect(controller).not_to receive(:override_csp_for_threat_metrix) + + response + end + end + + context 'with ThreatMetrix profiling enabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?) + .and_return(true) + end + + it 'overrides CSPs for ThreatMetrix' do + expect(controller).to receive(:override_csp_for_threat_metrix) + + response + end + end end describe '#update' do diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 4afbe54f828..b330a154d4d 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -9,7 +9,7 @@ before do stub_sign_in(user) - stub_up_to(:document_capture, idv_session: subject.idv_session) + stub_up_to(:document_capture, idv_session: controller.idv_session) stub_analytics end @@ -33,16 +33,11 @@ :check_for_mail_only_outage, ) end - - it 'overrides CSPs for ThreatMetrix' do - expect(subject).to have_actions( - :before, - :override_csp_for_threat_metrix, - ) - end end describe '#show' do + subject(:response) { get :show } + let(:analytics_name) { 'IdV: doc auth ssn visited' } let(:analytics_args) do { @@ -73,25 +68,25 @@ end it 'adds a threatmetrix session id to idv_session' do - expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil) + expect { get :show }.to change { controller.idv_session.threatmetrix_session_id }.from(nil) end context 'when updating ssn' do let(:threatmetrix_session_id) { 'original-session-id' } before do - subject.idv_session.ssn = ssn - subject.idv_session.threatmetrix_session_id = threatmetrix_session_id + controller.idv_session.ssn = ssn + controller.idv_session.threatmetrix_session_id = threatmetrix_session_id end it 'does not change threatmetrix_session_id' do - expect { get :show }.not_to change { subject.idv_session.threatmetrix_session_id } + expect { get :show }.not_to change { controller.idv_session.threatmetrix_session_id } end context 'but there is no threatmetrix_session_id in the session' do let(:threatmetrix_session_id) { nil } it 'sets a threatmetrix_session_id' do - expect { get :show }.to change { subject.idv_session.threatmetrix_session_id } + expect { get :show }.to change { controller.idv_session.threatmetrix_session_id } end end end @@ -102,22 +97,22 @@ end it 'still add a threatmetrix session id to idv_session' do - expect { get :show }.to change { subject.idv_session.threatmetrix_session_id }.from(nil) + expect { get :show }.to change { controller.idv_session.threatmetrix_session_id }.from(nil) end context 'when idv_session has a threatmetrix_session_id' do before do - subject.idv_session.threatmetrix_session_id = 'fake-session-id' + controller.idv_session.threatmetrix_session_id = 'fake-session-id' end it 'changes the threatmetrix_session_id' do - expect { get :show }.to change { subject.idv_session.threatmetrix_session_id } + expect { get :show }.to change { controller.idv_session.threatmetrix_session_id } end end end context 'with an ssn in idv_session' do before do - subject.idv_session.ssn = ssn + controller.idv_session.ssn = ssn end it 'does not redirect and allows the back button' do @@ -127,24 +122,29 @@ end end - it 'overrides Content Security Policies for ThreatMetrix' do - allow(IdentityConfig.store).to receive(:proofing_device_profiling) - .and_return(:enabled) - get :show - - csp = response.request.content_security_policy + context 'with ThreatMetrix profiling disabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?) + .and_return(false) + end - aggregate_failures do - expect(csp.directives['script-src']).to include('h.online-metrix.net') - expect(csp.directives['script-src']).to include("'unsafe-eval'") + it 'does not override CSPs for ThreatMetrix' do + expect(controller).not_to receive(:override_csp_for_threat_metrix) - expect(csp.directives['style-src']).to include("'unsafe-inline'") + response + end + end - expect(csp.directives['child-src']).to include('h.online-metrix.net') + context 'with ThreatMetrix profiling enabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?) + .and_return(true) + end - expect(csp.directives['connect-src']).to include('h.online-metrix.net') + it 'overrides CSPs for ThreatMetrix' do + expect(controller).to receive(:override_csp_for_threat_metrix) - expect(csp.directives['img-src']).to include('*.online-metrix.net') + response end end end 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 3c378b69ec9..97810f3400d 100644 --- a/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb +++ b/spec/controllers/users/two_factor_authentication_setup_controller_spec.rb @@ -27,6 +27,19 @@ expect(assigns(:presenter).desktop_ft_ab_test).to be false end + context 'with threatmetrix disabled' do + before do + allow(FeatureManagement).to receive(:proofing_device_profiling_collecting_enabled?) + .and_return(false) + end + + it 'does not override CSPs for ThreatMetrix' do + expect(controller).not_to receive(:override_csp_for_threat_metrix) + + response + end + end + context 'with threatmetrix enabled' do let(:tmx_session_id) { '1234' } @@ -52,6 +65,12 @@ expect(response).to render_template(:index) end + + it 'overrides CSPs for ThreatMetrix' do + expect(controller).to receive(:override_csp_for_threat_metrix) + + response + end end context 'with user having gov or mil email' do diff --git a/spec/controllers/users/webauthn_setup_controller_spec.rb b/spec/controllers/users/webauthn_setup_controller_spec.rb index 8e0e47be037..e12bc5943ec 100644 --- a/spec/controllers/users/webauthn_setup_controller_spec.rb +++ b/spec/controllers/users/webauthn_setup_controller_spec.rb @@ -135,6 +135,7 @@ expect(@analytics).to have_logged_event( :webauthn_setup_submitted, platform_authenticator: false, + in_account_creation_flow: false, success: true, ) end @@ -288,6 +289,7 @@ expect(@analytics).to have_logged_event( :webauthn_setup_submitted, platform_authenticator: false, + in_account_creation_flow: true, success: true, ) end @@ -325,6 +327,7 @@ expect(@analytics).to have_logged_event( :webauthn_setup_submitted, platform_authenticator: true, + in_account_creation_flow: true, success: true, ) expect(@analytics).to have_logged_event( @@ -453,6 +456,7 @@ expect(@analytics).to have_logged_event( :webauthn_setup_submitted, platform_authenticator: false, + in_account_creation_flow: true, success: true, ) end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 8ae9a08b59a..fb4855ab711 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -569,7 +569,7 @@ }, 'IdV: doc auth image upload vendor submitted' => hash_including(success: true, flow_path: 'standard', attention_with_barcode: true, doc_auth_result: 'Attention', liveness_checking_required: boolean), 'IdV: verify in person troubleshooting option clicked' => { - flow_path: 'standard', opted_in_to_in_person_proofing: false + flow_path: 'standard', opted_in_to_in_person_proofing: false, submit_attempts: 1 }, 'IdV: in person proofing location visited' => { flow_path: 'standard', opted_in_to_in_person_proofing: false diff --git a/spec/features/idv/steps/in_person/state_id_spec.rb b/spec/features/idv/steps/in_person/state_id_spec.rb index 25a883e38fa..5ee6369f04a 100644 --- a/spec/features/idv/steps/in_person/state_id_spec.rb +++ b/spec/features/idv/steps/in_person/state_id_spec.rb @@ -286,9 +286,14 @@ it 'shows error for dob under minimum age', allow_browser_log: true do complete_steps_before_state_id_controller - fill_in t('components.memorable_date.month'), with: '1' - fill_in t('components.memorable_date.day'), with: '1' - fill_in t('components.memorable_date.year'), with: Time.zone.now.strftime('%Y') + buffer_to_avoid_test_flakiness = 2.days + + less_than_13_years_ago = Time.zone.now - (13.years - buffer_to_avoid_test_flakiness) + + fill_in t('components.memorable_date.month'), with: less_than_13_years_ago.month + fill_in t('components.memorable_date.day'), with: less_than_13_years_ago.day + fill_in t('components.memorable_date.year'), with: less_than_13_years_ago.year + click_idv_continue expect(page).to have_content( t( @@ -297,8 +302,11 @@ ), ) - year = (Time.zone.now - 13.years).strftime('%Y') - fill_in t('components.memorable_date.year'), with: year + thirteenish_years_ago = Time.zone.now - (13.years + buffer_to_avoid_test_flakiness) + fill_in t('components.memorable_date.month'), with: thirteenish_years_ago.month + fill_in t('components.memorable_date.day'), with: thirteenish_years_ago.day + fill_in t('components.memorable_date.year'), with: thirteenish_years_ago.year + click_idv_continue expect(page).not_to have_content( t( diff --git a/spec/features/multiple_emails/email_management_spec.rb b/spec/features/multiple_emails/email_management_spec.rb index 4018460e401..78deed13679 100644 --- a/spec/features/multiple_emails/email_management_spec.rb +++ b/spec/features/multiple_emails/email_management_spec.rb @@ -40,7 +40,7 @@ context 'allows deletion of email address' do it 'does not allow last confirmed email to be deleted' do user = create(:user, :fully_registered, email: 'test@example.com ') - confirmed_email = user.confirmed_email_addresses.first + confirmed_email = user.last_sign_in_email_address unconfirmed_email = create(:email_address, user: user, confirmed_at: nil) user.email_addresses.reload @@ -56,7 +56,7 @@ it 'Allows delete when more than one confirmed email exists' do user = create(:user, :fully_registered, email: 'test@example.com ') - confirmed_email1 = user.confirmed_email_addresses.first + confirmed_email1 = user.last_sign_in_email_address confirmed_email2 = create( :email_address, user: user, confirmed_at: Time.zone.now @@ -74,7 +74,7 @@ it 'sends notification to all confirmed emails when email address is deleted' do user = create(:user, :fully_registered, email: 'test@example.com ') - confirmed_email1 = user.confirmed_email_addresses.first + confirmed_email1 = user.last_sign_in_email_address confirmed_email2 = create(:email_address, user: user, confirmed_at: Time.zone.now) sign_in_and_2fa_user(user) diff --git a/spec/features/sp_cost_tracking_spec.rb b/spec/features/sp_cost_tracking_spec.rb index feb8f84eac2..2bf4a64a083 100644 --- a/spec/features/sp_cost_tracking_spec.rb +++ b/spec/features/sp_cost_tracking_spec.rb @@ -45,7 +45,7 @@ user.active_profile.update!(verified_at: 60.days.ago) visit_idp_from_sp_with_ial2(:oidc, verified_within: '45d') - fill_in_credentials_and_submit(user.confirmed_email_addresses.first.email, password) + fill_in_credentials_and_submit(user.last_sign_in_email_address.email, password) fill_in_code_with_last_totp(user) click_submit_default complete_all_doc_auth_steps_before_password_step diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 63ff2dc5a12..075dc608440 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -92,7 +92,7 @@ scenario 'allows a user to recreate their account after account reset' do sign_in_before_2fa(user) - email = user.confirmed_email_addresses.first.email + email = user.last_sign_in_email_address.email expect(page).to have_content(t('two_factor_authentication.opt_in.title')) diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 982fbc6a6f8..15d4e3abe6d 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -406,7 +406,7 @@ create(:user, :fully_registered, email: email, password: password) user = User.find_with_email(email) - encrypted_email = user.confirmed_email_addresses.first.encrypted_email + encrypted_email = user.last_sign_in_email_address.encrypted_email rotate_attribute_encryption_key_with_invalid_queue @@ -414,7 +414,7 @@ .to raise_error Encryption::EncryptionError, 'unable to decrypt attribute with any key' user = user.reload - expect(user.confirmed_email_addresses.first.encrypted_email).to eq encrypted_email + expect(user.last_sign_in_email_address.encrypted_email).to eq encrypted_email end end @@ -426,14 +426,14 @@ create(:user, :fully_registered, email: email, password: password) user = User.find_with_email(email) - encrypted_email = user.confirmed_email_addresses.first.encrypted_email + encrypted_email = user.last_sign_in_email_address.encrypted_email rotate_attribute_encryption_key_with_invalid_queue sign_in_user_with_piv(user) user = user.reload - expect(user.confirmed_email_addresses.first.encrypted_email).to eq encrypted_email + expect(user.last_sign_in_email_address.encrypted_email).to eq encrypted_email end end end diff --git a/spec/javascript/packages/document-capture/context/upload-spec.jsx b/spec/javascript/packages/document-capture/context/upload-spec.jsx index 68e3348d4cb..2cd549d58c7 100644 --- a/spec/javascript/packages/document-capture/context/upload-spec.jsx +++ b/spec/javascript/packages/document-capture/context/upload-spec.jsx @@ -19,6 +19,7 @@ describe('document-capture/context/upload', () => { 'getStatus', 'flowPath', 'formData', + 'submitAttempts', ]); expect(result.current.upload).to.equal(defaultUpload); @@ -26,6 +27,7 @@ describe('document-capture/context/upload', () => { expect(result.current.statusPollInterval).to.be.undefined(); expect(result.current.isMockClient).to.be.false(); expect(await result.current.getStatus()).to.deep.equal({}); + expect(result.current.submitAttempts).to.equal(0); }); it('can be overridden with custom upload behavior', async () => { diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index 5ea9d8b9131..b22d3ee27e5 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -130,7 +130,7 @@ ) allow(UserMailer).to receive(:with).with( user: enrollment.user, - email_address: enrollment.user.confirmed_email_addresses.first, + email_address: enrollment.user.last_sign_in_email_address, ).and_return(user_mailer) allow(mail_deliverer).to receive(:deliver_later) allow(InPerson::SendProofingNotificationJob).to receive(:set).and_return( @@ -366,7 +366,7 @@ ) allow(UserMailer).to receive(:with).with( user: enrollment.user, - email_address: enrollment.user.confirmed_email_addresses.first, + email_address: enrollment.user.last_sign_in_email_address, ).and_raise(StandardError) subject.perform(current_time) end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 66edf8f05ee..9f36238d62f 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -286,6 +286,10 @@ def account_reinstated ).account_reinstated end + def in_person_post_office_closed + UserMailer.with(user: user, email_address: email_address_record).in_person_post_office_closed + end + private def user diff --git a/spec/mailers/previews/user_mailer_preview_spec.rb b/spec/mailers/previews/user_mailer_preview_spec.rb index d533387f9e5..fcc2bda9e53 100644 --- a/spec/mailers/previews/user_mailer_preview_spec.rb +++ b/spec/mailers/previews/user_mailer_preview_spec.rb @@ -2,7 +2,7 @@ require_relative './user_mailer_preview' RSpec.describe UserMailerPreview do - it_behaves_like 'a mailer preview', preview_methods_that_can_be_missing: [:in_person_please_call] + it_behaves_like 'a mailer preview' it 'uses user and email records that cannot be saved' do expect(User.count).to eq(0) diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 9698100ac76..6c182992a74 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -934,6 +934,50 @@ def expect_email_body_to_have_help_and_contact_links end end + context 'post office closed alert' do + context 'when the post office closed alert flag is disabled' do + before do + allow(IdentityConfig.store) + .to receive(:in_person_proofing_post_office_closed_alert_enabled) + .and_return(false) + end + + it 'does not render the post office closed alert' do + aggregate_failures do + [ + t('in_person_proofing.post_office_closed.heading'), + t('in_person_proofing.post_office_closed.body'), + ].each do |copy| + Array(copy).each do |part| + expect(mail.html_part.body).to_not have_content(part) + end + end + end + end + end + + context 'when the post office closed alert flag is enabled' do + before do + allow(IdentityConfig.store) + .to receive(:in_person_proofing_post_office_closed_alert_enabled) + .and_return(true) + end + + it 'renders the post office closed alert' do + aggregate_failures do + [ + t('in_person_proofing.post_office_closed.heading'), + t('in_person_proofing.post_office_closed.body'), + ].each do |copy| + Array(copy).each do |part| + expect(mail.html_part.body).to have_content(part) + end + end + end + end + end + end + context 'Need to change location section' do context 'when Enhanced IPP is not enabled' do let(:is_enhanced_ipp) { false } @@ -1319,37 +1363,6 @@ def expect_email_body_to_have_help_and_contact_links end end - describe '#in_person_please_call' do - let(:mail) do - UserMailer.with(user: user, email_address: email_address).in_person_please_call( - enrollment: enrollment, - visited_location_name: visited_location_name, - ) - end - - it_behaves_like 'a system email' - it_behaves_like 'an email that respects user email locale preference' - - it 'renders the idv_please_call template' do - expect_any_instance_of(ActionMailer::Base).to receive(:mail) - .with(hash_including(template_name: 'idv_please_call')) - .and_call_original - - mail.deliver_later - end - - context 'when the keyword argument visited_location_name is missing' do - let(:mail) do - UserMailer.with(user: user, email_address: email_address).in_person_please_call( - enrollment: enrollment, - ) - end - it 'sends the email successfully' do - mail.deliver_later - end - end - end - describe '#in_person_completion_survey' do let(:mail) do UserMailer.with(user: user, email_address: email_address).in_person_completion_survey @@ -1610,4 +1623,23 @@ def expect_email_body_to_have_help_and_contact_links it_behaves_like 'a system email' it_behaves_like 'an email that respects user email locale preference' end + + describe '#in_person_post_office_closed' do + let(:mail) do + UserMailer.with(user: user, email_address: email_address).in_person_post_office_closed + end + + it_behaves_like 'a system email' + it_behaves_like 'an email that respects user email locale preference' + + it 'includes a translated header' do + expect(mail.html_part.body) + .to include(t('in_person_proofing.post_office_closed.email.heading', locale: :en)) + end + + it 'includes a translated body' do + expect(mail.html_part.body) + .to include(t('in_person_proofing.post_office_closed.email.body_html', locale: :en)) + end + end end diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb index 984f34fa017..a03eb1dcda4 100644 --- a/spec/presenters/image_upload_response_presenter_spec.rb +++ b/spec/presenters/image_upload_response_presenter_spec.rb @@ -4,7 +4,7 @@ include Rails.application.routes.url_helpers let(:extra_attributes) do - { remaining_submit_attempts: 3, flow_path: 'standard' } + { remaining_submit_attempts: 3, flow_path: 'standard', submit_attempts: 2 } end let(:form_response) do @@ -109,7 +109,8 @@ let(:extra_attributes) do { remaining_submit_attempts: 0, flow_path: 'standard', - failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] } } + failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] }, + submit_attempts: 5 } end let(:form_response) do FormResponse.new( @@ -132,6 +133,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] }, + submit_attempts: 5, } expect(presenter.as_json).to eq expected @@ -141,7 +143,8 @@ let(:extra_attributes) do { remaining_submit_attempts: 0, flow_path: 'hybrid', - failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] } } + failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] }, + submit_attempts: 5 } end it 'returns hash of properties redirecting to capture_complete' do @@ -155,6 +158,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { back: [], front: ['12345'], selfie: [] }, + submit_attempts: 5, } expect(presenter.as_json).to eq expected @@ -185,6 +189,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { back: [], front: [], selfie: [] }, + submit_attempts: 2, } expect(presenter.as_json).to eq expected @@ -198,7 +203,7 @@ front: t('doc_auth.errors.not_a_file'), hints: true, }, - extra: { doc_auth_result: 'Failed', remaining_submit_attempts: 3 }, + extra: { doc_auth_result: 'Failed', remaining_submit_attempts: 3, submit_attempts: 2 }, ) end @@ -213,6 +218,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { front: [], back: [], selfie: [] }, + submit_attempts: 2, } expect(presenter.as_json).to eq expected @@ -221,7 +227,7 @@ context 'no remaining attempts' do let(:extra_attributes) do - { remaining_submit_attempts: 0, flow_path: 'standard' } + { remaining_submit_attempts: 0, flow_path: 'standard', submit_attempts: 5 } end let(:form_response) do FormResponse.new( @@ -236,7 +242,7 @@ context 'hybrid flow' do let(:extra_attributes) do - { remaining_submit_attempts: 0, flow_path: 'hybrid' } + { remaining_submit_attempts: 0, flow_path: 'hybrid', submit_attempts: 5 } end it 'returns hash of properties' do @@ -251,6 +257,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { front: [], back: [], selfie: [] }, + submit_attempts: 5, } expect(presenter.as_json).to eq expected @@ -269,6 +276,7 @@ ocr_pii: nil, doc_type_supported: true, failed_image_fingerprints: { back: [], front: [], selfie: [] }, + submit_attempts: 5, } expect(presenter.as_json).to eq expected @@ -325,7 +333,7 @@ let(:form_response) do response = DocAuth::Response.new( success: true, - extra: { remaining_submit_attempts: 3 }, + extra: { remaining_submit_attempts: 3, submit_attempts: 2 }, pii_from_doc: Idp::Constants::MOCK_IDV_APPLICANT, ) allow(response).to receive(:attention_with_barcode?).and_return(true) @@ -343,6 +351,7 @@ result_code_invalid: false, doc_type_supported: true, failed_image_fingerprints: { back: [], front: [], selfie: [] }, + submit_attempts: 2, } expect(presenter.as_json).to eq expected @@ -352,7 +361,7 @@ let(:form_response) do response = DocAuth::Response.new( success: true, - extra: { remaining_submit_attempts: 3 }, + extra: { remaining_submit_attempts: 3, submit_attempts: 2 }, pii_from_doc: Idp::Constants::MOCK_IDV_APPLICANT, ) allow(response).to receive(:attention_with_barcode?).and_return(true) @@ -370,6 +379,7 @@ ocr_pii: Idp::Constants::MOCK_IDV_APPLICANT.slice(:first_name, :last_name, :dob), doc_type_supported: true, failed_image_fingerprints: { back: [], front: [], selfie: [] }, + submit_attempts: 2, } expect(presenter.as_json).to eq expected diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4dc85032e9f..6c27735fd6d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -32,6 +32,7 @@ config.infer_spec_type_from_file_location! config.include ActiveSupport::Testing::TimeHelpers + config.include AbTestsHelper config.include EmailSpec::Helpers config.include EmailSpec::Matchers config.include AbstractController::Translation @@ -111,6 +112,7 @@ class Analytics Telephony::Test::Call.clear_calls PushNotification::LocalEventQueue.clear! REDIS_THROTTLE_POOL.with { |client| client.flushdb } if Identity::Hostdata.config + REDIS_ATTEMPTS_API_POOL.with { |client| client.flushdb } if Identity::Hostdata.config end config.before(:each) do @@ -165,4 +167,8 @@ class Analytics ], ) end + + config.after(:context) do + reload_ab_tests + end end diff --git a/spec/services/attempts_api/attempt_event_spec.rb b/spec/services/attempts_api/attempt_event_spec.rb new file mode 100644 index 00000000000..3ed0446151e --- /dev/null +++ b/spec/services/attempts_api/attempt_event_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +RSpec.describe AttemptsApi::AttemptEvent do + let(:attempts_api_private_key) { OpenSSL::PKey::RSA.new(2048) } + let(:attempts_api_public_key) { attempts_api_private_key.public_key } + + let(:jti) { 'test-unique-id' } + let(:iat) { Time.zone.now.to_i } + let(:event_type) { 'test-event' } + let(:session_id) { 'test-session-id' } + let(:occurred_at) { Time.zone.now.round } + let(:event_metadata) { { 'foo' => 'bar' } } + let(:service_provider) { build(:service_provider) } + + subject do + described_class.new( + jti: jti, + iat: iat, + event_type: event_type, + session_id: session_id, + occurred_at: occurred_at, + event_metadata: event_metadata, + ) + end + + describe '#to_jwe' do + it 'returns a JWE for the event' do + jwe = subject.to_jwe(issuer: service_provider.issuer, public_key: attempts_api_public_key) + + header_str, *_rest = JWE::Serialization::Compact.decode(jwe) + headers = JSON.parse(header_str) + + expect(headers['alg']).to eq('RSA-OAEP') + expect(headers['kid']).to eq(JWT::JWK.new(attempts_api_public_key).kid) + + decrypted_jwe_payload = JWE.decrypt(jwe, attempts_api_private_key) + + token = JSON.parse(decrypted_jwe_payload) + + expect(token['iss']).to eq(Rails.application.routes.url_helpers.root_url) + expect(token['jti']).to eq(jti) + expect(token['iat']).to eq(iat) + expect(token['aud']).to eq(service_provider.issuer) + + event_key = 'https://schemas.login.gov/secevent/attempts-api/event-type/test-event' + event_data = token['events'][event_key] + + expect(event_data['subject']).to eq( + 'subject_type' => 'session', 'session_id' => 'test-session-id', + ) + expect(event_data['foo']).to eq('bar') + expect(event_data['occurred_at']).to eq(occurred_at.to_f) + end + end + + describe '.from_jwe' do + it 'returns an event decrypted from the JWE' do + jwe = subject.to_jwe(issuer: service_provider.issuer, public_key: attempts_api_public_key) + + decrypted_event = described_class.from_jwe(jwe, attempts_api_private_key) + + expect(decrypted_event.jti).to eq(subject.jti) + expect(decrypted_event.iat).to eq(subject.iat) + expect(decrypted_event.event_type).to eq(subject.event_type) + expect(decrypted_event.session_id).to eq(subject.session_id) + expect(decrypted_event.occurred_at).to eq(subject.occurred_at) + expect(decrypted_event.event_metadata).to eq(subject.event_metadata.symbolize_keys) + end + end +end diff --git a/spec/services/attempts_api/redis_client_spec.rb b/spec/services/attempts_api/redis_client_spec.rb new file mode 100644 index 00000000000..d768f01eeca --- /dev/null +++ b/spec/services/attempts_api/redis_client_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +RSpec.describe AttemptsApi::RedisClient do + let(:attempts_api_private_key) { OpenSSL::PKey::RSA.new(2048) } + let(:attempts_api_public_key) { attempts_api_private_key.public_key } + let(:issuer) { 'test' } + + describe '#write_event' do + it 'writes the attempt data to redis with the event key as the key' do + freeze_time do + now = Time.zone.now + event = AttemptsApi::AttemptEvent.new( + event_type: 'test_event', + session_id: 'test-session-id', + occurred_at: Time.zone.now, + event_metadata: { + first_name: Idp::Constants::MOCK_IDV_APPLICANT[:first_name], + }, + ) + event_key = event.jti + jwe = event.to_jwe(issuer: issuer, public_key: attempts_api_public_key) + + subject.write_event(event_key: event_key, jwe: jwe, timestamp: now, issuer: issuer) + + result = subject.read_events(timestamp: now, issuer: issuer) + expect(result[event_key]).to eq(jwe) + end + end + end + + describe '#read_events' do + it 'reads the event events from redis' do + freeze_time do + now = Time.zone.now + events = {} + 3.times do + event = AttemptsApi::AttemptEvent.new( + event_type: 'test_event', + session_id: 'test-session-id', + occurred_at: now, + event_metadata: { + first_name: Idp::Constants::MOCK_IDV_APPLICANT[:first_name], + }, + ) + event_key = event.jti + jwe = event.to_jwe(issuer: issuer, public_key: attempts_api_public_key) + events[event_key] = jwe + end + events.each do |event_key, jwe| + subject.write_event(event_key: event_key, jwe: jwe, timestamp: now, issuer: issuer) + end + + result = subject.read_events(timestamp: now, issuer: issuer) + + expect(result).to eq(events) + end + end + + it 'stores events in hourly buckets' do + time1 = Time.new(2022, 1, 1, 1, 0, 0, 'Z') + time2 = Time.new(2022, 1, 1, 2, 0, 0, 'Z') + event1 = AttemptsApi::AttemptEvent.new( + event_type: 'test_event', + session_id: 'test-session-id', + occurred_at: time1, + event_metadata: { + first_name: Idp::Constants::MOCK_IDV_APPLICANT[:first_name], + }, + ) + event2 = AttemptsApi::AttemptEvent.new( + event_type: 'test_event', + session_id: 'test-session-id', + occurred_at: time2, + event_metadata: { + first_name: Idp::Constants::MOCK_IDV_APPLICANT[:first_name], + }, + ) + jwe1 = event1.to_jwe(issuer: issuer, public_key: attempts_api_public_key) + jwe2 = event2.to_jwe(issuer: issuer, public_key: attempts_api_public_key) + + subject.write_event( + event_key: event1.jti, jwe: jwe1, timestamp: event1.occurred_at, issuer: issuer, + ) + subject.write_event( + event_key: event2.jti, jwe: jwe2, timestamp: event2.occurred_at, issuer: issuer, + ) + + expect(subject.read_events(timestamp: time1, issuer: issuer)).to eq({ event1.jti => jwe1 }) + expect(subject.read_events(timestamp: time2, issuer: issuer)).to eq({ event2.jti => jwe2 }) + end + end +end diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index d975953a270..f3ebb0f9619 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -4,10 +4,6 @@ RSpec.describe Idv::Agent do let(:user) { create(:user) } - let(:bad_phone) do - Proofing::Mock::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER - end - describe 'instance' do let(:trace_id) { SecureRandom.uuid } let(:request_ip) { Faker::Internet.ip_v4_address } @@ -15,6 +11,12 @@ let(:friendly_name) { 'fake-name' } let(:app_id) { 'fake-app-id' } let(:ipp_enrollment_in_progress) { false } + let(:applicant) do + Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN + end + let(:document_capture_session) { DocumentCaptureSession.new(result_id: SecureRandom.hex) } + + subject(:agent) { Idv::Agent.new(applicant) } before do ServiceProvider.create( @@ -25,118 +27,92 @@ end describe '#proof_resolution' do - let(:document_capture_session) { DocumentCaptureSession.new(result_id: SecureRandom.hex) } + subject(:proof_resolution) do + agent.proof_resolution( + document_capture_session, + trace_id: trace_id, + user_id: user.id, + threatmetrix_session_id: nil, + request_ip: request_ip, + ipp_enrollment_in_progress: ipp_enrollment_in_progress, + ) + end - context 'proofing in an AAMVA state' do - it 'does not proof state_id if resolution fails' do - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '444-55-6666'), - ) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) + subject(:result) do + proof_resolution + document_capture_session.load_proofing_result.result + end - result = document_capture_session.load_proofing_result.result - expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] - expect(result[:context][:stages][:state_id][:vendor_name]).to eq 'UnsupportedJurisdiction' + context 'proofing in an AAMVA state' do + context 'when resolution fails' do + let(:applicant) do + super().merge(ssn: '444-55-6666') + end + + it 'does not proof state_id' do + expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] + expect(result[:context][:stages][:state_id][:vendor_name]).to( + eq('UnsupportedJurisdiction'), + ) + end end - - it 'does proof state_id if resolution succeeds' do - agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) - result = document_capture_session.load_proofing_result.result - expect(result[:context][:stages][:state_id]).to include( - transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, - errors: {}, - exception: nil, - success: true, - timed_out: false, - vendor_name: 'StateIdMock', - ) + context 'when resolution succeeds' do + it 'proofs state_id' do + expect(result[:context][:stages][:state_id]).to include( + transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, + errors: {}, + exception: nil, + success: true, + timed_out: false, + vendor_name: 'StateIdMock', + ) + end end end - context 'proofing state_id disabled' do - it 'does not proof state_id if resolution fails' do - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge( - ssn: '444-55-6666', state_id_jurisdiction: 'NY', - ), - ) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) - result = document_capture_session.load_proofing_result.result - expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] - expect(result[:context][:stages][:state_id][:vendor_name]).to eq 'UnsupportedJurisdiction' + context 'non-AAMVA state' do + let(:applicant) do + super().merge(state_id_jurisdiction: 'NY') end - it 'does not proof state_id if resolution succeeds' do - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge( - state_id_jurisdiction: 'NY', - ), - ) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) + context 'when resolution fails' do + let(:applicant) do + super().merge(ssn: '444-55-6666') + end + + it 'does not proof state_id' do + expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] + expect(result[:context][:stages][:state_id][:vendor_name]).to( + eq('UnsupportedJurisdiction'), + ) + end + end - result = document_capture_session.load_proofing_result.result - expect(result[:context][:stages]).to_not include( - state_id: 'StateIdMock', - transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, - ) + context 'when resolution succeeds' do + it 'does not proof state_id' do + expect(result[:context][:stages]).to_not include( + state_id: 'StateIdMock', + transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, + ) + end end end - it 'returns a successful result if SSN does not start with 900 but is in SSN allowlist' do - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '999-99-9999'), - ) - - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) - result = document_capture_session.load_proofing_result.result + context 'when SSN does not start with 900 but is in SSN allowlist' do + let(:applicant) do + super().merge(ssn: '999-99-9999') + end - expect(result).to include( - success: true, - ) + it 'returns a successful result' do + expect(result).to include( + success: true, + ) + end end it 'passes the correct service provider to the ResolutionProofingJob' do issuer = 'https://rp1.serviceprovider.com/auth/saml/metadata' document_capture_session.update!(issuer: issuer) - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '999-99-9999'), - ) expect(ResolutionProofingJob).to receive(:perform_later).with( hash_including( @@ -144,52 +120,29 @@ ), ) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) + proof_resolution end - it 'returns an unsuccessful result and notifies exception trackers if an exception occurs' do - agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(first_name: 'Time Exception'), - ) - - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) - result = document_capture_session.load_proofing_result.result - - expect(result[:exception].to_s).to include('resolution mock timeout') - expect(result).to include( - success: false, - timed_out: true, - ) + context 'when a proofing timeout occurs' do + let(:applicant) do + super().merge(first_name: 'Time Exception') + end + it 'returns unsuccessful result and notifies exception trackers if an exception occurs' do + expect(result[:exception].to_s).to include('resolution mock timeout') + expect(result).to include( + success: false, + timed_out: true, + ) + end end context 'in-person proofing is enabled' do let(:ipp_enrollment_in_progress) { true } + let(:applicant) do + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS + end it 'returns a successful result if resolution passes' do - agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS) - agent.proof_resolution( - document_capture_session, - trace_id: trace_id, - user_id: user.id, - threatmetrix_session_id: nil, - request_ip: request_ip, - ipp_enrollment_in_progress: ipp_enrollment_in_progress, - ) - result = document_capture_session.load_proofing_result.result expect(result[:context][:stages][:state_id]).to include( transaction_id: Proofing::Mock::StateIdMockClient::TRANSACTION_ID, errors: {}, @@ -202,37 +155,42 @@ end describe '#proof_address' do - let(:document_capture_session) { DocumentCaptureSession.new(result_id: SecureRandom.hex) } - let(:user_id) { SecureRandom.random_number(1000) } - let(:issuer) { build(:service_provider).issuer } - - it 'proofs addresses successfully with valid information' do - agent = Idv::Agent.new( - first_name: 'Fakey', - last_name: 'Fakersgerald', - dob: 50.years.ago.to_date.to_s, - ssn: '666-12-1234', + let(:applicant) do + super().merge( phone: Faker::PhoneNumber.cell_phone, ) + end + + subject(:proof_address) do agent.proof_address( document_capture_session, trace_id: trace_id, - user_id: user_id, + user_id: user.id, issuer: issuer, ) - result = document_capture_session.load_proofing_result[:result] + end + + subject(:result) do + proof_address + document_capture_session.load_proofing_result[:result] + end + + it 'proofs addresses successfully with valid information' do expect(result[:vendor_name]).to eq('AddressMock') expect(result[:success]).to eq true end - it 'fails to proof addresses with invalid information' do - agent = Idv::Agent.new(phone: bad_phone) - agent.proof_address( - document_capture_session, trace_id: trace_id, user_id: user_id, issuer: issuer - ) - result = document_capture_session.load_proofing_result[:result] - expect(result[:vendor_name]).to eq('AddressMock') - expect(result[:success]).to eq false + context 'when address has invalid information' do + let(:applicant) do + super().merge( + phone: Proofing::Mock::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER, + ) + end + + it 'fails to proof address' do + expect(result[:vendor_name]).to eq('AddressMock') + expect(result[:success]).to eq false + end end end end diff --git a/spec/services/proofing/aamva/proofer_spec.rb b/spec/services/proofing/aamva/proofer_spec.rb index 0885a98d462..ad17e5c19e8 100644 --- a/spec/services/proofing/aamva/proofer_spec.rb +++ b/spec/services/proofing/aamva/proofer_spec.rb @@ -737,6 +737,18 @@ def self.test_not_successful expect(result.mva_timeout?).to eq(true) expect(result.mva_exception?).to eq(true) 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 end end diff --git a/spec/services/user_alerts/alert_user_about_account_verified_spec.rb b/spec/services/user_alerts/alert_user_about_account_verified_spec.rb index c956abd7ba1..be082872867 100644 --- a/spec/services/user_alerts/alert_user_about_account_verified_spec.rb +++ b/spec/services/user_alerts/alert_user_about_account_verified_spec.rb @@ -36,7 +36,7 @@ described_class.call(profile: profile) expect_delivered_email( - to: [user.confirmed_email_addresses.first.email], + to: [user.last_sign_in_email_address.email], subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), body: [ 'http://www.example.com/redirect/return_to_sp/account_verified_cta', @@ -71,7 +71,7 @@ described_class.call(profile: profile) expect_delivered_email( - to: [user.confirmed_email_addresses.first.email], + to: [user.last_sign_in_email_address.email], subject: t('user_mailer.account_verified.subject', app_name: APP_NAME), body: ['http://example.com'], ) diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 123748bd7b9..60b5ecdacfe 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -537,7 +537,7 @@ def skip_second_mfa_prompt end def sign_in_via_branded_page(user) - fill_in_credentials_and_submit(user.confirmed_email_addresses.first.email, user.password) + fill_in_credentials_and_submit(user.last_sign_in_email_address.email, user.password) fill_in_code_with_last_phone_otp click_submit_default end diff --git a/spec/support/idv_examples/sp_handoff.rb b/spec/support/idv_examples/sp_handoff.rb index 71786093558..ee43a76e175 100644 --- a/spec/support/idv_examples/sp_handoff.rb +++ b/spec/support/idv_examples/sp_handoff.rb @@ -145,7 +145,7 @@ def expect_successful_oidc_handoff expect(decoded_id_token[:aud]).to eq(@client_id) expect(decoded_id_token[:acr]).to eq(Saml::Idp::Constants::IAL_VERIFIED_ACR) expect(decoded_id_token[:iss]).to eq(root_url) - expect(decoded_id_token[:email]).to eq(user.confirmed_email_addresses.first.email) + expect(decoded_id_token[:email]).to eq(user.last_sign_in_email_address.email) expect(decoded_id_token[:given_name]).to eq('FAKEY') expect(decoded_id_token[:social_security_number]).to eq(DocAuthHelper::GOOD_SSN) @@ -159,7 +159,7 @@ def expect_successful_oidc_handoff userinfo_response = JSON.parse(page.body).with_indifferent_access expect(userinfo_response[:sub]).to eq(sub) expect(AgencyIdentity.where(user_id: user.id, agency_id: 2).first.uuid).to eq(sub) - expect(userinfo_response[:email]).to eq(user.confirmed_email_addresses.first.email) + expect(userinfo_response[:email]).to eq(user.last_sign_in_email_address.email) expect(userinfo_response[:given_name]).to eq('FAKEY') expect(userinfo_response[:social_security_number]).to eq(DocAuthHelper::GOOD_SSN) end diff --git a/spec/support/shared_examples/phone/rate_limiting.rb b/spec/support/shared_examples/phone/rate_limiting.rb index 0d6b2d0c2a4..f0154c9f94b 100644 --- a/spec/support/shared_examples/phone/rate_limiting.rb +++ b/spec/support/shared_examples/phone/rate_limiting.rb @@ -89,7 +89,7 @@ def expect_user_to_be_rate_limitted visit root_path signin( - user.confirmed_email_addresses.first.email, + user.last_sign_in_email_address.email, user.password || Features::SessionHelper::VALID_PASSWORD, ) @@ -101,7 +101,7 @@ def expect_rate_limiting_to_expire visit root_path signin( - user.confirmed_email_addresses.first.email, + user.last_sign_in_email_address.email, user.password || Features::SessionHelper::VALID_PASSWORD, ) diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index 013e9ad5172..07c1c258f91 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -130,12 +130,12 @@ old_personal_key = PersonalKeyGenerator.new(user).generate! visit_idp_from_sp_with_ial1(sp) - trigger_reset_password_and_click_email_link(user.confirmed_email_addresses.first.email) + trigger_reset_password_and_click_email_link(user.last_sign_in_email_address.email) fill_in t('forms.passwords.edit.labels.password'), with: new_password fill_in t('components.password_confirmation.confirm_label'), with: new_password click_button t('forms.passwords.edit.buttons.submit') - fill_in_credentials_and_submit(user.confirmed_email_addresses.first.email, new_password) + fill_in_credentials_and_submit(user.last_sign_in_email_address.email, new_password) choose_another_security_option('personal_key') enter_personal_key(personal_key: old_personal_key) click_submit_default @@ -392,7 +392,7 @@ def ial1_sign_in_with_personal_key_goes_to_sp(sp) Capybara.reset_sessions! visit_idp_from_sp_with_ial1(sp) - fill_in_credentials_and_submit(user.confirmed_email_addresses.first.email, 'Val!d Pass w0rd') + fill_in_credentials_and_submit(user.last_sign_in_email_address.email, 'Val!d Pass w0rd') choose_another_security_option('personal_key') enter_personal_key(personal_key: old_personal_key) click_submit_default diff --git a/spec/support/shared_examples/webauthn_setup.rb b/spec/support/shared_examples/webauthn_setup.rb index 9070dec2f81..b240051bc2e 100644 --- a/spec/support/shared_examples/webauthn_setup.rb +++ b/spec/support/shared_examples/webauthn_setup.rb @@ -70,6 +70,7 @@ ), ] }, platform_authenticator: false, + in_account_creation_flow: true, success: false, ) end @@ -83,6 +84,7 @@ expect(fake_analytics).to have_logged_event( :webauthn_setup_submitted, success: true, + in_account_creation_flow: true, platform_authenticator: false, ) end diff --git a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb index 4379ef66113..f5874ffb8a2 100644 --- a/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb +++ b/spec/views/idv/in_person/ready_to_verify/show.html.erb_spec.rb @@ -122,8 +122,6 @@ let(:in_person_outage_expected_update_date) { 'October 31, 2023' } before do - allow(IdentityConfig.store).to receive(:in_person_outage_message_enabled) - .and_return(true) allow(IdentityConfig.store).to receive(:in_person_outage_emailed_by_date) .and_return(in_person_outage_emailed_by_date) allow(IdentityConfig.store).to receive(:in_person_outage_expected_update_date) @@ -178,6 +176,50 @@ end end + context 'post office warning' do + context 'when the show closed post office banner is disabled' do + before do + @show_closed_post_office_banner = false + end + + it 'does not render the post office closed alert' do + render + + aggregate_failures do + [ + t('in_person_proofing.post_office_closed.heading'), + t('in_person_proofing.post_office_closed.body'), + ].each do |copy| + Array(copy).each do |part| + expect(rendered).to_not have_content(part) + end + end + end + end + end + + context 'when the show closed post office banner is enabled' do + before do + @show_closed_post_office_banner = true + end + + it 'renders the post office closed alert' do + render + + aggregate_failures do + [ + t('in_person_proofing.post_office_closed.heading'), + t('in_person_proofing.post_office_closed.body'), + ].each do |copy| + Array(copy).each do |part| + expect(rendered).to have_content(part) + end + end + end + end + end + end + context 'what to expect section' do context 'when Enhanced IPP is not enabled' do let(:is_enhanced_ipp) { false }