diff --git a/Makefile b/Makefile index ec1071a64f8..e521072f34d 100644 --- a/Makefile +++ b/Makefile @@ -115,8 +115,8 @@ lint_yarn_workspaces: ## Lints Yarn workspace packages scripts/validate-workspaces.js lint_asset_bundle_size: ## Lints JavaScript and CSS compiled bundle size - find app/assets/builds/application.css -size -350000c | grep . - find public/packs/js/application-*.digested.js -size -8000c | grep . + find app/assets/builds/application.css -size -270000c | grep . + find public/packs/js/application-*.digested.js -size -5000c | grep . lint_migrations: scripts/migration_check diff --git a/app/assets/stylesheets/_uswds-form-controls.scss b/app/assets/stylesheets/_uswds-form-controls.scss index bb2e588ff0d..d8d59ba6963 100644 --- a/app/assets/stylesheets/_uswds-form-controls.scss +++ b/app/assets/stylesheets/_uswds-form-controls.scss @@ -1,7 +1,6 @@ @forward 'usa-checkbox'; @forward 'usa-error-message'; @forward 'usa-fieldset'; -@forward 'usa-file-input'; @forward 'usa-form-group'; @forward 'usa-hint'; @forward 'usa-input'; diff --git a/app/assets/stylesheets/_uswds.scss b/app/assets/stylesheets/_uswds.scss index 5964271ffd8..0595c4695e1 100644 --- a/app/assets/stylesheets/_uswds.scss +++ b/app/assets/stylesheets/_uswds.scss @@ -18,7 +18,6 @@ @forward 'usa-modal'; @forward 'usa-nav'; @forward 'usa-process-list'; -@forward 'usa-prose'; @forward 'usa-sidenav'; @forward 'usa-skipnav'; @forward 'usa-step-indicator'; diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index 916ee1b6a7c..460d8b8d320 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -6,7 +6,6 @@ @forward 'btn'; @forward 'card'; @forward 'code'; -@forward 'file-input'; @forward 'form-steps'; @forward 'footer'; @forward 'form'; diff --git a/app/components/accordion_component.html.erb b/app/components/accordion_component.html.erb index ad55e802fff..faa3d4a2a71 100644 --- a/app/components/accordion_component.html.erb +++ b/app/components/accordion_component.html.erb @@ -10,7 +10,7 @@
-
+
<%= content %>
diff --git a/app/components/webauthn_verify_button_component.html.erb b/app/components/webauthn_verify_button_component.html.erb index 2aaa4e3da84..6f5940c6c98 100644 --- a/app/components/webauthn_verify_button_component.html.erb +++ b/app/components/webauthn_verify_button_component.html.erb @@ -18,9 +18,7 @@ <%= t('two_factor_authentication.webauthn_authenticating') %>

- <%= render ButtonComponent.new( - big: true, - wide: true, + <%= render SubmitButtonComponent.new( class: 'webauthn-verify-button__button display-block margin-y-3', ).with_content(content) %> <%= hidden_field_tag :credential_id, '' %> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a2b4bbd9cd3..69573ae365e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -221,7 +221,7 @@ def after_sign_in_path_for(_user) end def signed_in_url - return url_for_pending_profile_reason if user_has_pending_profile? + return idv_verify_by_mail_enter_code_url if current_user.gpo_verification_pending_profile? return backup_code_reminder_url if user_needs_backup_code_reminder? account_path end diff --git a/app/controllers/concerns/idv/step_indicator_concern.rb b/app/controllers/concerns/idv/step_indicator_concern.rb index 31a170597df..084b37032b6 100644 --- a/app/controllers/concerns/idv/step_indicator_concern.rb +++ b/app/controllers/concerns/idv/step_indicator_concern.rb @@ -43,11 +43,12 @@ def in_person_proofing? end def gpo_address_verification? - # Proofing component values are (currently) never reset between proofing attempts, hence why - # this refers to the session address verification mechanism and not the proofing component. - return true if current_user&.gpo_verification_pending_profile? + # This can be used in a context where user_session and idv_session are not available + # (hybrid flow), so check for current_user before accessing them. + return false unless current_user + return true if current_user.gpo_verification_pending_profile? - return idv_session&.address_verification_mechanism == 'gpo' if defined?(idv_session) + return idv_session.address_verification_mechanism == 'gpo' end end end diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 741b547444f..656a5a5c49e 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -21,7 +21,7 @@ def hybrid_session? def confirm_phone_or_address_confirmed return if idv_session.address_confirmed? || idv_session.phone_confirmed? - redirect_to idv_review_url + redirect_to idv_enter_password_url end def idv_session diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index 8ae8d16106f..b8ba20ca6f2 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -99,7 +99,7 @@ def confirm_verify_info_step_complete def confirm_verify_info_step_needed return unless idv_session.verify_info_step_complete? - redirect_to idv_review_url + redirect_to idv_enter_password_url end def confirm_address_step_complete diff --git a/app/controllers/concerns/mfa_setup_concern.rb b/app/controllers/concerns/mfa_setup_concern.rb index ea4ef91b010..6120cd979f9 100644 --- a/app/controllers/concerns/mfa_setup_concern.rb +++ b/app/controllers/concerns/mfa_setup_concern.rb @@ -6,24 +6,17 @@ def next_setup_path auth_method_confirmation_url elsif next_setup_choice confirmation_path - else - if user_session[:mfa_selections] - analytics.user_registration_mfa_setup_complete( - mfa_method_counts: mfa_context.enabled_two_factor_configuration_counts_hash, - in_account_creation_flow: user_session[:in_account_creation_flow] || false, - enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count, - pii_like_keypaths: [[:mfa_method_counts, :phone]], - second_mfa_reminder_conversion: user_session.delete(:second_mfa_reminder_conversion), - success: true, - ) - end + elsif user_session[:mfa_selections] + track_user_registration_mfa_setup_complete_event user_session.delete(:mfa_selections) - nil + + sign_up_completed_path end end def confirmation_path(next_mfa_selection_choice = nil) user_session[:next_mfa_selection_choice] = next_mfa_selection_choice || next_setup_choice + case user_session[:next_mfa_selection_choice] when 'voice', 'sms', 'phone' phone_setup_url @@ -80,6 +73,17 @@ def show_skip_additional_mfa_link? private + def track_user_registration_mfa_setup_complete_event + analytics.user_registration_mfa_setup_complete( + mfa_method_counts: mfa_context.enabled_two_factor_configuration_counts_hash, + in_account_creation_flow: user_session[:in_account_creation_flow] || false, + enabled_mfa_methods_count: mfa_context.enabled_mfa_methods_count, + pii_like_keypaths: [[:mfa_method_counts, :phone]], + second_mfa_reminder_conversion: user_session.delete(:second_mfa_reminder_conversion), + success: true, + ) + end + def determine_next_mfa return unless user_session[:mfa_selections] current_setup_step = user_session[:next_mfa_selection_choice] diff --git a/app/controllers/concerns/remember_device_concern.rb b/app/controllers/concerns/remember_device_concern.rb index e684903bb65..df20d56a90a 100644 --- a/app/controllers/concerns/remember_device_concern.rb +++ b/app/controllers/concerns/remember_device_concern.rb @@ -64,9 +64,8 @@ def expired_for_interval?(user, interval) end def has_remember_device_auth_event? - auth_methods_session.auth_events.any? do |auth_event| - auth_event[:auth_method] == TwoFactorAuthenticatable::AuthMethod::REMEMBER_DEVICE - end + auth_methods_session.last_auth_event&.fetch(:auth_method) == + TwoFactorAuthenticatable::AuthMethod::REMEMBER_DEVICE end def handle_valid_remember_device_cookie(remember_device_cookie:) diff --git a/app/controllers/idv/by_mail/enter_code_controller.rb b/app/controllers/idv/by_mail/enter_code_controller.rb index a15a1645d27..3bad1e7b7da 100644 --- a/app/controllers/idv/by_mail/enter_code_controller.rb +++ b/app/controllers/idv/by_mail/enter_code_controller.rb @@ -23,15 +23,14 @@ def index return end - gpo_mail = Idv::GpoMail.new(current_user) - @gpo_mail_spammed = gpo_mail.mail_spammed? @last_date_letter_was_sent = last_date_letter_was_sent @gpo_verify_form = GpoVerifyForm.new(user: current_user, pii: pii) @code = session[:last_gpo_confirmation_code] if FeatureManagement.reveal_gpo_code? - @should_prompt_user_to_request_another_letter = + gpo_mail = Idv::GpoMail.new(current_user) + @can_request_another_letter = FeatureManagement.gpo_verification_enabled? && - !@gpo_mail_spammed && + !gpo_mail.mail_spammed? && !gpo_mail.profile_too_old? if pii_locked? diff --git a/app/controllers/idv/by_mail/request_letter_controller.rb b/app/controllers/idv/by_mail/request_letter_controller.rb index 32c4f9a6585..87aaf28191c 100644 --- a/app/controllers/idv/by_mail/request_letter_controller.rb +++ b/app/controllers/idv/by_mail/request_letter_controller.rb @@ -34,7 +34,7 @@ def create flash[:success] = t('idv.messages.gpo.another_letter_on_the_way') redirect_to idv_letter_enqueued_url else - redirect_to idv_review_url + redirect_to idv_enter_password_url end end @@ -83,7 +83,7 @@ def first_letter_requested_at end def confirm_mail_not_spammed - redirect_to idv_review_url if gpo_mail_service.mail_spammed? + redirect_to idv_enter_password_url if gpo_mail_service.mail_spammed? end def confirm_user_completed_idv_profile_step diff --git a/app/controllers/idv/cancellations_controller.rb b/app/controllers/idv/cancellations_controller.rb index 1f48f3ce8e1..3798119cf0d 100644 --- a/app/controllers/idv/cancellations_controller.rb +++ b/app/controllers/idv/cancellations_controller.rb @@ -33,6 +33,16 @@ def destroy end end + def exit + analytics.idv_cancellation_confirmed(step: params[:step]) + cancel_session + if hybrid_session? + render :destroy + else + redirect_to cancelled_redirect_path + end + end + private def barcode_step? diff --git a/app/controllers/idv/confirm_start_over_controller.rb b/app/controllers/idv/confirm_start_over_controller.rb index 99075494f49..d8c7ea88c2f 100644 --- a/app/controllers/idv/confirm_start_over_controller.rb +++ b/app/controllers/idv/confirm_start_over_controller.rb @@ -10,22 +10,13 @@ class ConfirmStartOverController < ApplicationController def index @step_indicator_step = requested_letter_before? ? :get_a_letter : :verify_phone_or_address - # Temporarily check referer until request letter view is updated to link to - # the before_letter route - if request.referer == idv_request_letter_url - analytics.idv_gpo_confirm_start_over_before_letter_visited - render 'idv/confirm_start_over/before_letter' - else - analytics.idv_gpo_confirm_start_over_visited - render :index - end + analytics.idv_gpo_confirm_start_over_visited end def before_letter @step_indicator_step = requested_letter_before? ? :get_a_letter : :verify_phone_or_address - analytics.idv_gpo_confirm_start_over_before_letter_visited - render 'idv/confirm_start_over/before_letter' + analytics.idv_gpo_confirm_start_over_before_letter_visited end private diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index fe5c62b2537..cc11c6c4967 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -4,6 +4,7 @@ class DocumentCaptureController < ApplicationController include DocumentCaptureConcern include IdvStepConcern include StepIndicatorConcern + include PhoneQuestionAbTestConcern before_action :confirm_not_rate_limited, except: [:update] before_action :confirm_hybrid_handoff_complete @@ -47,6 +48,7 @@ def extra_view_variables failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'), }.merge( acuant_sdk_upgrade_a_b_testing_variables, + phone_question_ab_test_analytics_bucket, ) end diff --git a/app/controllers/idv/enter_password_controller.rb b/app/controllers/idv/enter_password_controller.rb index 8bcef1db319..ea97c3435e9 100644 --- a/app/controllers/idv/enter_password_controller.rb +++ b/app/controllers/idv/enter_password_controller.rb @@ -27,11 +27,6 @@ def new @title = title @heading = heading - flash_now = flash.now - if gpo_mail_service.mail_spammed? - flash_now[:error] = t('idv.errors.mail_limit_reached') - end - @verifying_by_mail = address_verification_method == 'gpo' end @@ -110,7 +105,7 @@ def confirm_current_password irs_attempts_api_tracker.idv_password_entered(success: false) flash[:error] = t('idv.errors.incorrect_password') - redirect_to idv_review_url + redirect_to idv_enter_password_url end def gpo_mail_service @@ -191,7 +186,7 @@ def handle_request_enroll_exception(err) reason: 'Request exception', ) flash[:error] = t('idv.failure.exceptions.internal_error') - redirect_to idv_review_url + redirect_to idv_enter_password_url end end end diff --git a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb index b6f9e2678d2..37d991ce058 100644 --- a/app/controllers/idv/hybrid_mobile/document_capture_controller.rb +++ b/app/controllers/idv/hybrid_mobile/document_capture_controller.rb @@ -3,6 +3,7 @@ module HybridMobile class DocumentCaptureController < ApplicationController include DocumentCaptureConcern include HybridMobileConcern + include PhoneQuestionAbTestConcern before_action :check_valid_document_capture_session before_action :override_csp_to_allow_acuant @@ -42,6 +43,7 @@ def extra_view_variables failure_to_proof_url: return_to_sp_failure_to_proof_url(step: 'document_capture'), }.merge( acuant_sdk_upgrade_a_b_testing_variables, + phone_question_ab_test_analytics_bucket, ) end diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index 72c97b97b50..3ec8f07177f 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -3,6 +3,7 @@ class LinkSentController < ApplicationController include DocumentCaptureConcern include IdvStepConcern include StepIndicatorConcern + include PhoneQuestionAbTestConcern before_action :confirm_not_rate_limited before_action :confirm_hybrid_handoff_complete @@ -32,7 +33,9 @@ def update end def extra_view_variables - { phone: idv_session.phone_for_mobile_flow } + { phone: idv_session.phone_for_mobile_flow }.merge( + phone_question_ab_test_analytics_bucket, + ) end private diff --git a/app/controllers/idv/otp_verification_controller.rb b/app/controllers/idv/otp_verification_controller.rb index fea46e50045..2e34cf10173 100644 --- a/app/controllers/idv/otp_verification_controller.rb +++ b/app/controllers/idv/otp_verification_controller.rb @@ -34,7 +34,7 @@ def update idv_session.user_phone_confirmation = true save_in_person_notification_phone flash[:success] = t('idv.messages.enter_password.phone_verified') - redirect_to idv_review_url + redirect_to idv_enter_password_url else handle_otp_confirmation_failure end @@ -44,7 +44,7 @@ def update def confirm_step_needed return unless idv_session.user_phone_confirmation - redirect_to idv_review_url + redirect_to idv_enter_password_url end def confirm_otp_sent diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index f54c706427d..2a41a4399cd 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -73,7 +73,7 @@ def redirect_to_next_step send_phone_confirmation_otp_and_handle_result end else - redirect_to idv_review_url + redirect_to idv_enter_password_url end end diff --git a/app/controllers/idv/phone_errors_controller.rb b/app/controllers/idv/phone_errors_controller.rb index 41cb400335f..3322a2d113a 100644 --- a/app/controllers/idv/phone_errors_controller.rb +++ b/app/controllers/idv/phone_errors_controller.rb @@ -46,7 +46,7 @@ def rate_limiter def confirm_idv_phone_step_needed return unless user_fully_authenticated? - redirect_to idv_review_url if idv_session.user_phone_confirmation == true + redirect_to idv_enter_password_url if idv_session.user_phone_confirmation == true end def confirm_idv_phone_step_submitted diff --git a/app/controllers/idv/resend_otp_controller.rb b/app/controllers/idv/resend_otp_controller.rb index febd4b4a1ef..f020b6fd49c 100644 --- a/app/controllers/idv/resend_otp_controller.rb +++ b/app/controllers/idv/resend_otp_controller.rb @@ -30,7 +30,7 @@ def handle_send_phone_confirmation_otp_failure(result) def confirm_user_phone_confirmation_needed return unless idv_session.user_phone_confirmation - redirect_to idv_review_url + redirect_to idv_enter_password_url end def confirm_user_phone_confirmation_session_started diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 0ceaf3434b1..2dfdcf52d94 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -18,6 +18,7 @@ def warning rate_limit_type: :idv_resolution, ) + @step_indicator_steps = step_indicator_steps @remaining_attempts = rate_limiter.remaining_count log_event(based_on_limiter: rate_limiter) end diff --git a/app/controllers/mfa_confirmation_controller.rb b/app/controllers/mfa_confirmation_controller.rb index 18ae426a3e8..eaa27c9de26 100644 --- a/app/controllers/mfa_confirmation_controller.rb +++ b/app/controllers/mfa_confirmation_controller.rb @@ -40,7 +40,7 @@ def after_skip_path if backup_code_confirmation_needed? confirm_backup_codes_path else - after_mfa_setup_path + sign_up_completed_path end end diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb index b5147ae23a3..8b510e33235 100644 --- a/app/controllers/sign_up/completions_controller.rb +++ b/app/controllers/sign_up/completions_controller.rb @@ -81,14 +81,16 @@ def sign_user_out_and_instruct_to_go_back_to_mobile_app end def analytics_attributes(page_occurence) - { ial2: sp_session[:ial2], + { + ial2: sp_session[:ial2], ialmax: sp_session[:ialmax], service_provider_name: decorated_sp_session.sp_name, sp_session_requested_attributes: sp_session[:requested_attributes], sp_request_requested_attributes: service_provider_request.requested_attributes, page_occurence: page_occurence, in_account_creation_flow: user_session[:in_account_creation_flow] || false, - needs_completion_screen_reason: needs_completion_screen_reason } + needs_completion_screen_reason: needs_completion_screen_reason, + } end def track_completion_event(last_page) diff --git a/app/javascript/packages/components/checkbox.spec.tsx b/app/javascript/packages/components/checkbox.spec.tsx new file mode 100644 index 00000000000..23b430ddfce --- /dev/null +++ b/app/javascript/packages/components/checkbox.spec.tsx @@ -0,0 +1,49 @@ +import { render } from '@testing-library/react'; +import Checkbox from './checkbox'; + +describe('Checkbox', () => { + it('renders given checkbox', () => { + const { getByRole, getByText } = render( + , + ); + + const checkbox = getByRole('checkbox'); + expect(checkbox.classList.contains('usa-checkbox__input')).to.be.true(); + expect(checkbox.classList.contains('usa-button__input-title')).to.be.false(); + expect(checkbox.id).to.eq('checkbox1'); + + const label = getByText('A checkbox'); + expect(label).to.be.ok(); + expect(label.classList.contains('usa-checkbox__label')).to.be.true(); + expect(label.getAttribute('for')).eq('checkbox1'); + + const labelDescription = getByText('A checkbox for testing'); + expect(labelDescription).to.be.ok(); + expect(labelDescription.classList.contains('usa-checkbox__label-description')).to.be.true(); + }); + + context('with isTitle', () => { + it('renders with correct style', () => { + const { getByRole } = render( + , + ); + const checkbox = getByRole('checkbox'); + expect(checkbox.classList.contains('usa-button__input-title')).to.be.true(); + }); + }); + + context('with hint', () => { + it('renders hint', () => { + const { getByText } = render( + , + ); + const hint = getByText('Please check this box'); + expect(hint).to.be.ok(); + }); + }); +}); diff --git a/app/javascript/packages/components/checkbox.tsx b/app/javascript/packages/components/checkbox.tsx new file mode 100644 index 00000000000..286087bb90f --- /dev/null +++ b/app/javascript/packages/components/checkbox.tsx @@ -0,0 +1,74 @@ +import type { InputHTMLAttributes } from 'react'; +import { useInstanceId } from '@18f/identity-react-hooks'; + +export interface CheckboxProps extends InputHTMLAttributes { + /** + * Whether checkbox is considered title, a box around it, with optional description for the label + */ + isTitle?: boolean; + /** + * Whether is focused, a focus box around the checkbox + */ + isFocus?: boolean; + /** + * Optional id for the element + */ + id?: string; + /** + * Optional additional class names. + */ + className?: string; + /** + * Label text for the checkbox + */ + label: string; + /** + * Optional description for the label, used with isTitle + */ + labelDescription?: string; + /** + * Muted explainer text sitting below the label. + */ + hint?: string; +} + +function Checkbox({ + id, + isTitle, + isFocus, + className, + label, + labelDescription, + hint, + ...inputProps +}: CheckboxProps) { + const instanceId = useInstanceId(); + const inputId = id ?? `check-input-${instanceId}`; + const hintId = id ?? `check-input-hint-${instanceId}`; + const classes = [ + 'usa-checkbox__input', + isTitle && 'usa-button__input-title', + isFocus && 'usa-focus', + className, + ] + .filter(Boolean) + .join(' '); + + return ( +
+ + + {hint && ( +
+ {hint} +
+ )} +
+ ); +} +export default Checkbox; diff --git a/app/javascript/packages/components/field-set-spec.tsx b/app/javascript/packages/components/field-set-spec.tsx new file mode 100644 index 00000000000..a57ef4dfee0 --- /dev/null +++ b/app/javascript/packages/components/field-set-spec.tsx @@ -0,0 +1,30 @@ +import { render } from '@testing-library/react'; +import FieldSet from './field-set'; + +describe('FieldSet', () => { + it('renders given fieldset', () => { + const { getByRole, getByText } = render( +
+

Inner text

+
, + ); + const fieldSet = getByRole('group'); + expect(fieldSet).to.be.ok(); + expect(fieldSet.classList.contains('usa-fieldset')).to.be.true(); + + const child = getByText('Inner text'); + expect(child).to.be.ok(); + }); + context('with legend', () => { + it('renders legend', () => { + const { getByText } = render( +
+

Inner text

+
, + ); + const legend = getByText('Legend text'); + expect(legend).to.be.ok(); + expect(legend.classList.contains('usa-legend')).to.be.true(); + }); + }); +}); diff --git a/app/javascript/packages/components/field-set.tsx b/app/javascript/packages/components/field-set.tsx new file mode 100644 index 00000000000..08e0a2b46d1 --- /dev/null +++ b/app/javascript/packages/components/field-set.tsx @@ -0,0 +1,21 @@ +import { FieldsetHTMLAttributes, ReactNode } from 'react'; + +export interface FieldSetProps extends FieldsetHTMLAttributes { + /** + * Footer contents. + */ + children: ReactNode; + + legend?: string; +} + +function FieldSet({ legend, children }: FieldSetProps) { + return ( +
+ {legend && {legend}} + {children} +
+ ); +} + +export default FieldSet; diff --git a/app/javascript/packages/components/index.ts b/app/javascript/packages/components/index.ts index c3b5dac43ee..5888c91f13f 100644 --- a/app/javascript/packages/components/index.ts +++ b/app/javascript/packages/components/index.ts @@ -21,8 +21,11 @@ export { default as StatusPage } from './status-page'; export { default as Tag } from './tag'; export { default as TextInput } from './text-input'; export { default as TroubleshootingOptions } from './troubleshooting-options'; +export { default as Checkbox } from './checkbox'; +export { default as FieldSet } from './field-set'; export type { ButtonProps } from './button'; export type { FullScreenRefHandle } from './full-screen'; export type { LinkProps } from './link'; export type { TextInputProps } from './text-input'; +export type { CheckboxProps } from './checkbox'; diff --git a/app/javascript/packages/document-capture-polling/index.ts b/app/javascript/packages/document-capture-polling/index.ts index f6164766390..87cbbe3ecee 100644 --- a/app/javascript/packages/document-capture-polling/index.ts +++ b/app/javascript/packages/document-capture-polling/index.ts @@ -18,6 +18,8 @@ interface DocumentCapturePollingOptions { elements: DocumentCapturePollingElements; + phoneQuestionAbTestBucket: string | undefined; + trackEvent?: typeof defaultTrackEvent; } @@ -47,19 +49,25 @@ export class DocumentCapturePolling { cleanUpPromptOnNavigate: (() => void) | undefined; + phoneQuestionAbTestBucket: string | undefined; + constructor({ elements, statusEndpoint, trackEvent = defaultTrackEvent, + phoneQuestionAbTestBucket, }: DocumentCapturePollingOptions) { this.elements = elements; this.statusEndpoint = statusEndpoint; this.trackEvent = trackEvent; + this.phoneQuestionAbTestBucket = phoneQuestionAbTestBucket; } bind() { this.toggleFormVisible(false); - this.trackEvent('IdV: Link sent capture doc polling started'); + this.trackEvent('IdV: Link sent capture doc polling started', { + phone_question_ab_test_bucket: this.phoneQuestionAbTestBucket, + }); this.schedulePoll(); this.bindPromptOnNavigate(true); this.elements.backLink.addEventListener('click', () => this.bindPromptOnNavigate(false)); @@ -92,6 +100,7 @@ export class DocumentCapturePolling { this.trackEvent('IdV: Link sent capture doc polling complete', { isCancelled: result === ResultType.CANCELLED, isRateLimited: result === ResultType.RATE_LIMITED, + phone_question_ab_test_bucket: this.phoneQuestionAbTestBucket, }); this.bindPromptOnNavigate(false); if (redirect) { diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/javascript/packages/document-capture/components/_file-input.scss similarity index 97% rename from app/assets/stylesheets/components/_file-input.scss rename to app/javascript/packages/document-capture/components/_file-input.scss index ade8a41a785..8967e4e1f9a 100644 --- a/app/assets/stylesheets/components/_file-input.scss +++ b/app/javascript/packages/document-capture/components/_file-input.scss @@ -1,5 +1,6 @@ @use 'uswds-core' as *; -@use '../utilities/typography' as *; +@use 'utilities/typography' as *; +@forward 'usa-file-input'; // =============================================== // Pending upstream Login Design System revisions: diff --git a/app/javascript/packages/document-capture/components/document-capture-abandon.tsx b/app/javascript/packages/document-capture/components/document-capture-abandon.tsx new file mode 100644 index 00000000000..e381fbddaf3 --- /dev/null +++ b/app/javascript/packages/document-capture/components/document-capture-abandon.tsx @@ -0,0 +1,135 @@ +import { Tag, Checkbox, FieldSet, Button, Link } from '@18f/identity-components'; +import { useI18n } from '@18f/identity-react-i18n'; +import { useContext, useState } from 'react'; +import FlowContext from '@18f/identity-verify-flow/context/flow-context'; +import formatHTML from '@18f/identity-react-i18n/format-html'; +import { addSearchParams, forceRedirect, Navigate } from '@18f/identity-url'; +import { getConfigValue } from '@18f/identity-config'; +import AnalyticsContext from '../context/analytics'; +import { ServiceProviderContext } from '../context'; + +function formatContentHtml({ msg, url }) { + return formatHTML(msg, { + a: ({ children }) => ( + + {children} + + ), + strong: ({ children }) => {children}, + }); +} + +export interface DocumentCaptureAbandonProps { + navigate?: Navigate; +} + +function DocumentCaptureAbandon({ navigate }: DocumentCaptureAbandonProps) { + const { t } = useI18n(); + const { trackEvent } = useContext(AnalyticsContext); + const { currentStep, exitURL, cancelURL } = useContext(FlowContext); + const { name: spName } = useContext(ServiceProviderContext); + const appName = getConfigValue('appName'); + const header =

{t('doc_auth.exit_survey.header')}

; + + const content = ( +

+ {formatContentHtml({ + msg: spName?.trim() + ? t('doc_auth.exit_survey.content_html', { + sp_name: spName, + app_name: appName, + }) + : t('doc_auth.exit_survey.content_nosp_html', { + app_name: appName, + }), + url: addSearchParams(spName?.trim() ? exitURL : cancelURL, { + step: currentStep, + location: 'optional_question', + }), + })} +

+ ); + const optionalTag = ( + + {t('doc_auth.exit_survey.optional.tag', { app_name: appName })} + + ); + const optionalText = ( +

+ {t('doc_auth.exit_survey.optional.content', { app_name: appName })} +

+ ); + + const idTypeLabels = [ + t('doc_auth.exit_survey.optional.id_types.us_passport'), + t('doc_auth.exit_survey.optional.id_types.resident_card'), + t('doc_auth.exit_survey.optional.id_types.military_id'), + t('doc_auth.exit_survey.optional.id_types.tribal_id'), + t('doc_auth.exit_survey.optional.id_types.voter_registration_card'), + t('doc_auth.exit_survey.optional.id_types.other'), + ]; + + const allIdTypeOptions = [ + { name: 'us_passport', checked: false }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: false }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: false }, + ]; + + const [idTypeOptions, setIdTypeOptions] = useState(allIdTypeOptions); + + const updateCheckStatus = (index: number) => { + setIdTypeOptions( + idTypeOptions.map((idOption, currentIndex) => + currentIndex === index ? { ...idOption, checked: !idOption.checked } : { ...idOption }, + ), + ); + }; + + const checkboxes = ( + <> + {idTypeOptions.map((idType, idx) => ( + updateCheckStatus(idx)} + /> + ))} + + ); + + const handleExit = () => { + trackEvent('IdV: exit optional questions', { ids: idTypeOptions }); + forceRedirect( + addSearchParams(spName ? exitURL : cancelURL, { + step: currentStep, + location: 'optional_question', + }), + navigate, + ); + }; + + return ( + <> + {header} + {content} +
+ {optionalTag} + {optionalText} +
{checkboxes}
+ +
+ {t('idv.legal_statement.information_collection')} +
+
+ + ); +} + +export default DocumentCaptureAbandon; diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index 44dc1a798eb..dc81220c050 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -10,6 +10,7 @@ import { useI18n } from '@18f/identity-react-i18n'; import UnknownError from './unknown-error'; import TipList from './tip-list'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; +import DocumentCaptureAbandon from './document-capture-abandon'; interface DocumentCaptureReviewIssuesProps { isFailedDocType: boolean; @@ -78,6 +79,7 @@ function DocumentCaptureReviewIssues({ /> ))} + ); diff --git a/app/javascript/packages/document-capture/components/documents-step.jsx b/app/javascript/packages/document-capture/components/documents-step.jsx index bc88c754cf7..bad438701a6 100644 --- a/app/javascript/packages/document-capture/components/documents-step.jsx +++ b/app/javascript/packages/document-capture/components/documents-step.jsx @@ -8,6 +8,7 @@ import DocumentSideAcuantCapture from './document-side-acuant-capture'; import DeviceContext from '../context/device'; import UploadContext from '../context/upload'; import TipList from './tip-list'; +import DocumentCaptureAbandon from './document-capture-abandon'; /** * @typedef {'front'|'back'} DocumentSide @@ -70,6 +71,8 @@ function DocumentsStep({ /> ))} {isLastStep ? : } + + ); diff --git a/app/javascript/packages/document-capture/components/optional-questions.scss b/app/javascript/packages/document-capture/components/optional-questions.scss new file mode 100644 index 00000000000..77cfd283403 --- /dev/null +++ b/app/javascript/packages/document-capture/components/optional-questions.scss @@ -0,0 +1,23 @@ +@use 'uswds-core' as *; + +.document-capture-optional-questions { + margin-top: 1.5em; + .usa-fieldset { + margin-top: 0; + .usa-legend { + text-transform: none; + margin-top: 0; + font-size: 1rem; + line-height: 1.4; + display: block; + border-bottom: none; + padding-bottom: 0; + padding-top: 0; + } + .usa-checkbox { + .usa-checkbox__label { + display: block; //collapsing margin + } + } + } +} diff --git a/app/javascript/packages/document-capture/styles.scss b/app/javascript/packages/document-capture/styles.scss index 7f5a3f89355..042b07e5189 100644 --- a/app/javascript/packages/document-capture/styles.scss +++ b/app/javascript/packages/document-capture/styles.scss @@ -1,4 +1,6 @@ -@import './components/acuant-capture'; -@import './components/acuant-capture-canvas'; -@import './components/location-collection-item'; -@import './components/select-error'; +@forward './components/acuant-capture'; +@forward './components/acuant-capture-canvas'; +@forward './components/file-input'; +@forward './components/location-collection-item'; +@forward './components/optional-questions'; +@forward './components/select-error'; diff --git a/app/javascript/packages/verify-flow/cancel.spec.tsx b/app/javascript/packages/verify-flow/cancel.spec.tsx index 33492d2883e..2529a0c5359 100644 --- a/app/javascript/packages/verify-flow/cancel.spec.tsx +++ b/app/javascript/packages/verify-flow/cancel.spec.tsx @@ -15,6 +15,7 @@ describe('Cancel', () => { diff --git a/app/javascript/packages/verify-flow/context/flow-context.tsx b/app/javascript/packages/verify-flow/context/flow-context.tsx index 1579c06e97d..fe465c47284 100644 --- a/app/javascript/packages/verify-flow/context/flow-context.tsx +++ b/app/javascript/packages/verify-flow/context/flow-context.tsx @@ -6,6 +6,11 @@ export interface FlowContextValue { */ cancelURL: string; + /** + * URL to exit session without confirmation + */ + exitURL: string; + /** * Current step name. */ @@ -14,6 +19,7 @@ export interface FlowContextValue { const FlowContext = createContext({ cancelURL: '', + exitURL: '', currentStep: '', }); diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts index 42f3be18738..c9e6affc43c 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts @@ -2,6 +2,7 @@ import sinon from 'sinon'; import quibble from 'quibble'; import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; +import '@18f/identity-submit-button/submit-button-element'; import type { WebauthnVerifyButtonDataset } from './webauthn-verify-button-element'; describe('WebauthnVerifyButtonElement', () => { @@ -30,9 +31,11 @@ describe('WebauthnVerifyButtonElement', () => { - + + + @@ -81,6 +84,16 @@ describe('WebauthnVerifyButtonElement', () => { }); }); + it('calls to verify at most one time', async () => { + createElement(); + + const button = screen.getByRole('button', { name: 'Authenticate' }); + await userEvent.click(button); + await userEvent.click(button); + + expect(verifyWebauthnDevice).to.have.been.calledOnce(); + }); + it('submits with error name as input on thrown expected error', async () => { const { form } = createElement(); diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts index b876f970358..21d8378df9c 100644 --- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts +++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts @@ -1,4 +1,5 @@ import { trackError } from '@18f/identity-analytics'; +import type SubmitButtonElement from '@18f/identity-submit-button/submit-button-element'; import verifyWebauthnDevice from './verify-webauthn-device'; import type { VerifyCredentialDescriptor } from './verify-webauthn-device'; import isExpectedWebauthnError from './is-expected-error'; @@ -20,6 +21,10 @@ class WebauthnVerifyButtonElement extends HTMLElement { return this.querySelector('.webauthn-verify-button__button')!; } + get submitButton(): SubmitButtonElement { + return this.querySelector('lg-submit-button')!; + } + get spinner(): HTMLElement { return this.querySelector('.webauthn-verify-button__spinner')!; } @@ -42,6 +47,7 @@ class WebauthnVerifyButtonElement extends HTMLElement { async verify() { this.spinner.hidden = false; + this.submitButton.activate(); const { userChallenge, credentials } = this; diff --git a/app/javascript/packs/doc-capture-polling.ts b/app/javascript/packs/doc-capture-polling.ts index b53647e05bd..e8df12c29ba 100644 --- a/app/javascript/packs/doc-capture-polling.ts +++ b/app/javascript/packs/doc-capture-polling.ts @@ -4,6 +4,9 @@ new DocumentCapturePolling({ statusEndpoint: document .querySelector('[data-status-endpoint]') ?.getAttribute('data-status-endpoint') as string, + phoneQuestionAbTestBucket: document + .querySelector('[data-phone-question-ab-test-bucket') + ?.getAttribute('data-phone-question-ab-test-bucket') as string, elements: { backLink: document.querySelector('.link-sent-back-link') as HTMLAnchorElement, form: document.querySelector('.link-sent-continue-button-form') as HTMLFormElement, diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 3448e037f5b..12111620d6c 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -30,6 +30,7 @@ interface AppRootData { acuantVersion: string; flowPath: FlowPath; cancelUrl: string; + exitUrl: string; idvInPersonUrl?: string; securityAndPrivacyHowItWorksUrl: string; } @@ -53,14 +54,20 @@ function getMetaContent(name): string | null { const device: DeviceContextValue = { isMobile: isCameraCapableMobile() }; const trackEvent: typeof baseTrackEvent = (event, payload) => { - const { flowPath, acuantSdkUpgradeABTestingEnabled, useAlternateSdk, acuantVersion } = - appRoot.dataset; + const { + flowPath, + acuantSdkUpgradeABTestingEnabled, + useAlternateSdk, + acuantVersion, + phoneQuestionAbTestBucket, + } = appRoot.dataset; return baseTrackEvent(event, { ...payload, flow_path: flowPath, acuant_sdk_upgrade_a_b_testing_enabled: acuantSdkUpgradeABTestingEnabled, use_alternate_sdk: useAlternateSdk, acuant_version: acuantVersion, + phone_question_ab_test_bucket: phoneQuestionAbTestBucket, }); }; @@ -76,6 +83,7 @@ const { acuantVersion, flowPath, cancelUrl: cancelURL, + exitUrl: exitURL, idvInPersonUrl: inPersonURL, securityAndPrivacyHowItWorksUrl: securityAndPrivacyHowItWorksURL, inPersonFullAddressEntryEnabled, @@ -134,6 +142,7 @@ const App = composeComponents( { value: { cancelURL, + exitURL, currentStep: 'document_capture', }, }, diff --git a/app/jobs/reports/base_report.rb b/app/jobs/reports/base_report.rb index e2148679d4d..bb60c335e98 100644 --- a/app/jobs/reports/base_report.rb +++ b/app/jobs/reports/base_report.rb @@ -26,10 +26,6 @@ def public_bucket_name end end - def fiscal_start_date(time = Time.zone.now.beginning_of_day) - time.change(year: time.month >= 10 ? time.year : time.year - 1, month: 10, day: 1) - end - def first_of_this_month Time.zone.now.beginning_of_month end diff --git a/app/jobs/reports/monthly_key_metrics_report.rb b/app/jobs/reports/monthly_key_metrics_report.rb index 4061352fb69..463273051c9 100644 --- a/app/jobs/reports/monthly_key_metrics_report.rb +++ b/app/jobs/reports/monthly_key_metrics_report.rb @@ -29,15 +29,28 @@ def perform(date = Time.zone.today) email: email_addresses, subject: "Monthly Key Metrics Report - #{date}", reports: reports, + message: preamble, attachment_format: :xlsx, ).deliver_now end + # Explanatory text to go before the report in the email + # @return [String] + def preamble + <<~HTML.html_safe # rubocop:disable Rails/OutputSafety +

+ For more information on how each of these metrics are calculated, take a look at our + + Monthly Key Metrics Report Explainer document. +

+ HTML + end + def reports @reports ||= [ # Number of verified users (total) - LG-11148 # Number of verified users (new) - LG-11164 - monthly_active_users_count_report.monthly_active_users_count_emailable_report, + active_users_count_report.active_users_count_emailable_report, # Total Annual Users - LG-11150 total_user_count_report.total_user_count_emailable_report, # Proofing rate(s) (tbd on this one pager) - LG-11152 @@ -47,7 +60,6 @@ def reports monthly_proofing_report.document_upload_proofing_emailable_report, # Number of applications using Login (separated by auth / IdV) - LG-11154 # Number of agencies using Login - LG-11155 - # Fiscal year active users, sum and split - LG-10816 # APG Reporting Annual Active Users by FY (w/ cumulative Active Users by quarter) - LG-11156 # APG Reporting of Active Federal Partner Agencies - LG-11157 # APG Reporting of Active Login.gov Serviced Applications - LG-11158 @@ -60,6 +72,7 @@ def emails emails = [IdentityConfig.store.team_agnes_email] if report_date.day == 1 emails << IdentityConfig.store.team_all_feds_email + emails << IdentityConfig.store.team_all_contractors_email end emails end @@ -85,8 +98,8 @@ def total_user_count_report @total_user_count_report ||= Reporting::TotalUserCountReport.new(report_date) end - def monthly_active_users_count_report - @monthly_active_users_count_report ||= Reporting::MonthlyActiveUsersCountReport.new( + def active_users_count_report + @active_users_count_report ||= Reporting::ActiveUsersCountReport.new( report_date, ) end diff --git a/app/jobs/reports/sp_active_users_report.rb b/app/jobs/reports/sp_active_users_report.rb index 4034e6d86b2..a597e9cbf38 100644 --- a/app/jobs/reports/sp_active_users_report.rb +++ b/app/jobs/reports/sp_active_users_report.rb @@ -41,8 +41,12 @@ def finish_time(time) end end + def fiscal_start_date(time = Time.zone.now.beginning_of_day) + CalendarService.fiscal_start_date(time) + end + def fiscal_end_date(time) - time.change(year: time.month >= 10 ? time.year + 1 : time.year, month: 9, day: 30).end_of_day + CalendarService.fiscal_end_date(time).end_of_day end def reporting_range(time) diff --git a/app/services/calendar_service.rb b/app/services/calendar_service.rb index be9beeac260..2c6cda0bd26 100644 --- a/app/services/calendar_service.rb +++ b/app/services/calendar_service.rb @@ -17,6 +17,14 @@ def weekend?(date) def weekend_or_holiday?(date) weekend?(date) || holiday?(date) end + + def fiscal_start_date(time) + time.change(year: time.month >= 10 ? time.year : time.year - 1, month: 10, day: 1) + end + + def fiscal_end_date(time) + time.change(year: time.month >= 10 ? time.year + 1 : time.year, month: 9, day: 30) + end end attr_reader :year diff --git a/app/services/doc_auth/acuant/request.rb b/app/services/doc_auth/acuant/request.rb index 0ca6f910f0d..852dd66c05c 100644 --- a/app/services/doc_auth/acuant/request.rb +++ b/app/services/doc_auth/acuant/request.rb @@ -127,16 +127,20 @@ def create_error_response(errors, exception) end def handle_expected_http_error(http_response) - error = case http_response.status - when 438 - Errors::IMAGE_LOAD_FAILURE - when 439 - Errors::PIXEL_DEPTH_FAILURE - when 440 - Errors::IMAGE_SIZE_FAILURE - end - - create_error_response({ general: [error] }, create_http_exception(http_response)) + errors = errors_from_http_status(http_response.status) + create_error_response(errors, create_http_exception(http_response)) + end + + def errors_from_http_status(status) + error = case status + when 438 + Errors::IMAGE_LOAD_FAILURE + when 439 + Errors::PIXEL_DEPTH_FAILURE + when 440 + Errors::IMAGE_SIZE_FAILURE + end + { general: [error] } end def handle_invalid_response(http_response) diff --git a/app/services/doc_auth/acuant/requests/get_results_request.rb b/app/services/doc_auth/acuant/requests/get_results_request.rb index df9419a14a9..a4d7bd7983a 100644 --- a/app/services/doc_auth/acuant/requests/get_results_request.rb +++ b/app/services/doc_auth/acuant/requests/get_results_request.rb @@ -32,6 +32,29 @@ def metric_name def timeout IdentityConfig.store.acuant_get_results_timeout end + + def errors_from_http_status(status) + case status + when 438 + { + general: [Errors::IMAGE_LOAD_FAILURE], + front: [Errors::IMAGE_LOAD_FAILURE_FIELD], + back: [Errors::IMAGE_LOAD_FAILURE_FIELD], + } + when 439 + { + general: [Errors::PIXEL_DEPTH_FAILURE], + front: [Errors::PIXEL_DEPTH_FAILURE_FIELD], + back: [Errors::PIXEL_DEPTH_FAILURE_FIELD], + } + when 440 + { + general: [Errors::IMAGE_SIZE_FAILURE], + front: [Errors::IMAGE_SIZE_FAILURE_FIELD], + back: [Errors::IMAGE_SIZE_FAILURE_FIELD], + } + end + end end end end diff --git a/app/services/doc_auth/acuant/requests/upload_image_request.rb b/app/services/doc_auth/acuant/requests/upload_image_request.rb index 83879cd5e40..cd597738e86 100644 --- a/app/services/doc_auth/acuant/requests/upload_image_request.rb +++ b/app/services/doc_auth/acuant/requests/upload_image_request.rb @@ -41,6 +41,26 @@ def metric_name def timeout IdentityConfig.store.acuant_upload_image_timeout end + + def errors_from_http_status(status) + case status + when 438 + { + general: [Errors::IMAGE_LOAD_FAILURE], + side.downcase.to_sym => [Errors::IMAGE_LOAD_FAILURE_FIELD], + } + when 439 + { + general: [Errors::PIXEL_DEPTH_FAILURE], + side.downcase.to_sym => [Errors::PIXEL_DEPTH_FAILURE_FIELD], + } + when 440 + { + general: [Errors::IMAGE_SIZE_FAILURE], + side.downcase.to_sym => [Errors::IMAGE_SIZE_FAILURE_FIELD], + } + end + end end end end diff --git a/app/services/doc_auth/errors.rb b/app/services/doc_auth/errors.rb index ba72fdc3e52..fbc15666cab 100644 --- a/app/services/doc_auth/errors.rb +++ b/app/services/doc_auth/errors.rb @@ -2,8 +2,11 @@ module DocAuth module Errors # HTTP Status Codes IMAGE_LOAD_FAILURE = 'image_load_failure' # 438 + IMAGE_LOAD_FAILURE_FIELD = 'image_load_failure_field' # 438 PIXEL_DEPTH_FAILURE = 'pixel_depth_failure' # 439 + PIXEL_DEPTH_FAILURE_FIELD = 'pixel_depth_failure_field' IMAGE_SIZE_FAILURE = 'image_size_failure' # 440 + IMAGE_SIZE_FAILURE_FIELD = 'image_size_failure_field' # 440 # Network NETWORK = 'network' # usually 500 or other unhandled error # Alerts @@ -80,6 +83,10 @@ module Errors # rubocop:disable Layout/LineLength USER_DISPLAY = { + # Http status + IMAGE_LOAD_FAILURE => { long_msg: IMAGE_LOAD_FAILURE, long_msg_plural: IMAGE_LOAD_FAILURE, field_msg: IMAGE_LOAD_FAILURE_FIELD }, + PIXEL_DEPTH_FAILURE => { long_msg: PIXEL_DEPTH_FAILURE, long_msg_plural: PIXEL_DEPTH_FAILURE, field_msg: PIXEL_DEPTH_FAILURE_FIELD }, + IMAGE_SIZE_FAILURE => { long_msg: IMAGE_SIZE_FAILURE, long_msg_plural: IMAGE_SIZE_FAILURE, field_msg: IMAGE_SIZE_FAILURE_FIELD }, # Image metrics DPI_LOW => { long_msg: DPI_LOW_ONE_SIDE, long_msg_plural: DPI_LOW_BOTH_SIDES, field_msg: DPI_LOW_FIELD }, SHARP_LOW => { long_msg: SHARP_LOW_ONE_SIDE, long_msg_plural: SHARP_LOW_BOTH_SIDES, field_msg: SHARP_LOW_FIELD }, diff --git a/app/services/doc_auth/mock/doc_auth_mock_client.rb b/app/services/doc_auth/mock/doc_auth_mock_client.rb index 449056998e6..141f8f986c7 100644 --- a/app/services/doc_auth/mock/doc_auth_mock_client.rb +++ b/app/services/doc_auth/mock/doc_auth_mock_client.rb @@ -105,25 +105,42 @@ def http_error_response(image, side) data = parse_yaml(image.to_s) status = data.dig('http_status', side) return nil unless [500, 440, 438, 439].include?(status) - error = case status - when 438 - Errors::IMAGE_LOAD_FAILURE - when 439 - Errors::PIXEL_DEPTH_FAILURE - when 440 - Errors::IMAGE_SIZE_FAILURE - when 500 - Errors::NETWORK - end - return nil unless error - errors = { general: [error] } + errors = case status + when 438 + { + general: [Errors::IMAGE_LOAD_FAILURE], + side.downcase.to_sym => [Errors::IMAGE_LOAD_FAILURE_FIELD], + } + when 439 + { + general: [Errors::PIXEL_DEPTH_FAILURE], + side.downcase.to_sym => [Errors::PIXEL_DEPTH_FAILURE_FIELD], + } + when 440 + { + general: [Errors::IMAGE_SIZE_FAILURE], + side.downcase.to_sym => [Errors::IMAGE_SIZE_FAILURE_FIELD], + } + when 500 + { + general: [Errors::NETWORK], + } + end + return nil unless errors + errors = errors.tap do |h| + if h.has_key?(:result) + h[:front] = h[:result] + h[:back] = h[:result] + h.delete(:result) + end + end message = [ self.class.name, 'Unexpected HTTP response', status, ].join(' ') exception = DocAuth::RequestError.new(message, status) - return DocAuth::Response.new( + DocAuth::Response.new( success: false, errors: errors, exception: exception, diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index d206c354b5d..92c734940e7 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -72,16 +72,24 @@ def attention_with_barcode? parsed_alerts == [ATTENTION_WITH_BARCODE_ALERT] end - def self.create_image_error_response(status) - error = case status + def self.create_image_error_response(status, side) + errors = case status when 438 - Errors::IMAGE_LOAD_FAILURE + { + general: [Errors::IMAGE_LOAD_FAILURE], + side.to_sym => [Errors::IMAGE_LOAD_FAILURE_FIELD], + } when 439 - Errors::PIXEL_DEPTH_FAILURE + { + general: [Errors::PIXEL_DEPTH_FAILURE], + side.to_sym => [Errors::IMAGE_LOAD_FAILURE_FIELD], + } when 440 - Errors::IMAGE_SIZE_FAILURE + { + general: [Errors::IMAGE_SIZE_FAILURE], + side.to_sym => [Errors::IMAGE_SIZE_FAILURE_FIELD], + } end - errors = { general: [error] } message = [ 'Unexpected HTTP response', status, diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index 5ca18db56f3..2545bbd859a 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -78,12 +78,18 @@ module DocAuthRouter DocAuth::Errors::GLARE_LOW_BOTH_SIDES => 'doc_auth.errors.glare.top_msg_plural', # i18n-tasks-use t('doc_auth.errors.glare.failed_short') DocAuth::Errors::GLARE_LOW_FIELD => 'doc_auth.errors.glare.failed_short', - # i18n-tasks-use t('doc_auth.errors.http.image_load') - DocAuth::Errors::IMAGE_LOAD_FAILURE => 'doc_auth.errors.http.image_load', - # i18n-tasks-use t('doc_auth.errors.http.pixel_depth') - DocAuth::Errors::PIXEL_DEPTH_FAILURE => 'doc_auth.errors.http.pixel_depth', - # i18n-tasks-use t('doc_auth.errors.http.image_size') - DocAuth::Errors::IMAGE_SIZE_FAILURE => 'doc_auth.errors.http.image_size', + # i18n-tasks-use t('doc_auth.errors.http.image_load.top_msg') + DocAuth::Errors::IMAGE_LOAD_FAILURE => 'doc_auth.errors.http.image_load.top_msg', + # i18n-tasks-use t('doc_auth.errors.http.image_load.failed_short') + DocAuth::Errors::IMAGE_LOAD_FAILURE_FIELD => 'doc_auth.errors.http.image_load.failed_short', + # i18n-tasks-use t('doc_auth.errors.http.pixel_depth.top_msg') + DocAuth::Errors::PIXEL_DEPTH_FAILURE => 'doc_auth.errors.http.pixel_depth.top_msg', + # i18n-tasks-use t('doc_auth.errors.http.pixel_depth.failed_short') + DocAuth::Errors::PIXEL_DEPTH_FAILURE_FIELD => 'doc_auth.errors.http.pixel_depth.failed_short', + # i18n-tasks-use t('doc_auth.errors.http.image_size.top_msg') + DocAuth::Errors::IMAGE_SIZE_FAILURE => 'doc_auth.errors.http.image_size.top_msg', + # i18n-tasks-use t('doc_auth.errors.http.image_size.failed_short') + DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD => 'doc_auth.errors.http.image_size.failed_short', # i18n-tasks-use t('doc_auth.errors.general.fallback_field_level') DocAuth::Errors::FALLBACK_FIELD_LEVEL => 'doc_auth.errors.general.fallback_field_level', }.freeze @@ -122,7 +128,9 @@ def translate_doc_auth_errors!(response) error_keys = DocAuth::ErrorGenerator::ERROR_KEYS.dup error_keys.each do |category| - response.errors[category]&.map! do |plain_error| + cat_errors = response.errors[category] + next unless cat_errors + translated_cat_errors = cat_errors.map do |plain_error| error_key = ERROR_TRANSLATIONS[plain_error] if error_key I18n.t(error_key) @@ -131,6 +139,7 @@ def translate_doc_auth_errors!(response) I18n.t('doc_auth.errors.general.no_liveness') end end + response.errors[category] = translated_cat_errors end end diff --git a/app/services/proofing/resolution/progressive_proofer.rb b/app/services/proofing/resolution/progressive_proofer.rb index ab1f7fc9bc6..27fff0f4a3d 100644 --- a/app/services/proofing/resolution/progressive_proofer.rb +++ b/app/services/proofing/resolution/progressive_proofer.rb @@ -124,13 +124,13 @@ def proof_residential_address_if_needed( end def residential_address_unnecessary_result - Proofing::AddressResult.new( + Proofing::Resolution::Result.new( success: true, errors: {}, exception: nil, vendor_name: 'ResidentialAddressNotRequired', ) end def resolution_cannot_pass - Proofing::AddressResult.new( + Proofing::Resolution::Result.new( success: false, errors: {}, exception: nil, vendor_name: 'ResolutionCannotPass', ) end @@ -155,8 +155,10 @@ def should_proof_state_id_with_aamva?(ipp_enrollment_in_progress:, same_address_ residential_instant_verify_result:, double_address_verification:) return false unless should_proof_state_id + # If the user is in double-address-verification and they have changed their address then + # they are not eligible for get-to-yes # rubocop:disable Layout/LineLength - if (ipp_enrollment_in_progress == false || double_address_verification == false) || same_address_as_id == 'true' + if !(ipp_enrollment_in_progress || double_address_verification) || same_address_as_id == 'true' # rubocop:enable Layout/LineLength user_can_pass_after_state_id_check?(instant_verify_result) else diff --git a/app/services/reporting/active_users_count_report.rb b/app/services/reporting/active_users_count_report.rb new file mode 100644 index 00000000000..241852c7079 --- /dev/null +++ b/app/services/reporting/active_users_count_report.rb @@ -0,0 +1,107 @@ +module Reporting + class ActiveUsersCountReport + attr_reader :report_date + + def initialize(report_date = Time.zone.today) + @report_date = report_date + end + + def active_users_count_emailable_report + EmailableReport.new( + title: 'Active Users', + table: generate_report, + filename: 'active_users_count', + ) + end + + def generate_report + [ + ['Active Users', 'IAL1', 'IDV', 'Total', 'Range start', 'Range end'], + current_month_row, + fiscal_year_row, + ] + end + + private + + def current_month_row + [ + "Monthly #{report_month_year}", + monthly_ial1, + monthly_ial2, + monthly_total, + monthly_range.begin, + monthly_range.end, + ] + end + + def fiscal_year_row + [ + "Fiscal Year #{fiscal_end_date.year}", + fiscal_year_ial1, + fiscal_year_ial2, + fiscal_total, + fiscal_start_date, + fiscal_end_date, + ] + end + + def monthly_ial1 + monthly_active_users['total_ial1_active'] + end + + def monthly_ial2 + monthly_active_users['total_ial2_active'] + end + + def monthly_total + monthly_ial1 + monthly_ial2 + end + + def fiscal_year_ial1 + fiscal_year_active_users['total_ial1_active'] + end + + def fiscal_year_ial2 + fiscal_year_active_users['total_ial2_active'] + end + + def fiscal_total + fiscal_year_ial1 + fiscal_year_ial2 + end + + def monthly_active_users + @monthly_active_users ||= Reports::BaseReport.transaction_with_timeout do + Db::Identity::SpActiveUserCounts.overall( + monthly_range.begin, + monthly_range.end, + ).first + end + end + + def fiscal_year_active_users + @fiscal_year_active_users ||= Reports::BaseReport.transaction_with_timeout do + Db::Identity::SpActiveUserCounts.overall( + fiscal_start_date.beginning_of_day, + fiscal_end_date.end_of_day, + ).first + end + end + + def monthly_range + report_date.day == 1 ? report_date.last_month.all_month : report_date.all_month + end + + def fiscal_start_date + CalendarService.fiscal_start_date(report_date) + end + + def fiscal_end_date + CalendarService.fiscal_end_date(report_date) + end + + def report_month_year + "#{monthly_range.begin.strftime("%B")} #{monthly_range.begin.year}" + end + end +end diff --git a/app/services/reporting/monthly_active_users_count_report.rb b/app/services/reporting/monthly_active_users_count_report.rb deleted file mode 100644 index 7d04c6dbccb..00000000000 --- a/app/services/reporting/monthly_active_users_count_report.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Reporting - class MonthlyActiveUsersCountReport - attr_reader :report_date - - def initialize(report_date = Time.zone.today) - @report_date = report_date - end - - def monthly_active_users_count_report - [ - ['Monthly Active Users', 'Value'], - ['IAL1', total_ial1_active], - ['IDV', total_ial2_active], - ['Total', total_ial1_active + total_ial2_active], - ] - end - - def monthly_active_users_count_emailable_report - EmailableReport.new( - title: "#{report_month_year} Active Users", - table: monthly_active_users_count_report, - filename: 'monthly_active_users_count', - ) - end - - private - - def active_users_count - @active_users_count ||= Reports::BaseReport.transaction_with_timeout do - Db::Identity::SpActiveUserCounts.overall(range.begin, range.end).first - end - end - - def total_ial1_active - active_users_count['total_ial1_active'] - end - - def total_ial2_active - active_users_count['total_ial2_active'] - end - - def range - @range ||= report_date.day == 1 ? report_date.last_month.all_month : report_date.all_month - end - - def report_month_year - "#{range.begin.strftime("%B")} #{range.begin.year}" - end - end -end diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 42232b9b29d..784af585ef8 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -7,7 +7,7 @@ <%= render TabNavigationComponent.new( label: t('account.login.tab_navigation'), routes: [ - { text: t('links.next'), path: new_user_session_url }, + { text: t('links.sign_in'), path: new_user_session_url }, { text: t('links.create_account'), path: sign_up_email_url }, ], class: 'margin-bottom-4', @@ -47,7 +47,7 @@ }, }, ) %> - <%= f.submit t('links.next'), full_width: true, wide: false %> + <%= f.submit t('links.sign_in'), full_width: true, wide: false %> <% end %> <% if @ial && desktop_device? %>
diff --git a/app/views/idv/by_mail/enter_code/index.html.erb b/app/views/idv/by_mail/enter_code/index.html.erb index 1eab6fe93e4..1c62bc8500f 100644 --- a/app/views/idv/by_mail/enter_code/index.html.erb +++ b/app/views/idv/by_mail/enter_code/index.html.erb @@ -13,7 +13,7 @@ <% title t('idv.gpo.title') %> <% end %> -<% if @gpo_mail_spammed %> +<% if !@can_request_another_letter %> <%= render AlertComponent.new(type: :warning, class: 'margin-bottom-4') do %> <%= t( 'idv.gpo.alert_spam_warning_html', @@ -50,7 +50,7 @@ ) %> <% if @user_did_not_receive_letter %> - <% if @should_prompt_user_to_request_another_letter %> + <% if @can_request_another_letter %> <%= t( 'idv.gpo.did_not_receive_letter.intro.request_new_letter_prompt_html', request_new_letter_link: link_to( @@ -97,7 +97,7 @@
<% end %> -<% if @should_prompt_user_to_request_another_letter %> +<% if @can_request_another_letter %> <% unless @user_did_not_receive_letter %> <%= link_to t('idv.messages.gpo.resend'), idv_request_letter_path, class: 'display-block margin-bottom-2' %> <% end %> diff --git a/app/views/idv/by_mail/request_letter/index.html.erb b/app/views/idv/by_mail/request_letter/index.html.erb index c9d5208bbed..c27b1513999 100644 --- a/app/views/idv/by_mail/request_letter/index.html.erb +++ b/app/views/idv/by_mail/request_letter/index.html.erb @@ -60,7 +60,7 @@

<%= start_over_link_html = link_to( t('idv.messages.gpo.start_over_link_text'), - idv_confirm_start_over_path, + idv_confirm_start_over_before_letter_path, ) t( 'idv.messages.gpo.start_over_html', diff --git a/app/views/idv/confirm_start_over/before_letter.html.erb b/app/views/idv/confirm_start_over/before_letter.html.erb index c48f8462b08..1c235438c4b 100644 --- a/app/views/idv/confirm_start_over/before_letter.html.erb +++ b/app/views/idv/confirm_start_over/before_letter.html.erb @@ -14,7 +14,7 @@

<%= render ButtonComponent.new( action: ->(**tag_options, &block) do - button_to(idv_session_path(step: :gpo_verify), **tag_options, &block) + button_to(idv_session_path(step: :request_letter), **tag_options, &block) end, method: :delete, big: true, @@ -22,4 +22,4 @@ ).with_content(t('idv.buttons.continue_plain')) %>
<% end %> -<%= render('idv/shared/back', step: 'gpo_verify', fallback_path: idv_verify_by_mail_enter_code_path) %> +<%= render('idv/shared/back', step: 'request_letter', fallback_path: idv_request_letter_path) %> diff --git a/app/views/idv/document_capture/show.html.erb b/app/views/idv/document_capture/show.html.erb index 817d3cc0c0e..324e041ae30 100644 --- a/app/views/idv/document_capture/show.html.erb +++ b/app/views/idv/document_capture/show.html.erb @@ -7,4 +7,5 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + phone_question_ab_test_bucket: phone_question_ab_test_bucket, ) %> \ No newline at end of file diff --git a/app/views/idv/enter_password/new.html.erb b/app/views/idv/enter_password/new.html.erb index bb6025f9b2d..134a0570184 100644 --- a/app/views/idv/enter_password/new.html.erb +++ b/app/views/idv/enter_password/new.html.erb @@ -17,7 +17,7 @@ <%= simple_form_for( current_user, - url: idv_review_path, + url: idv_enter_password_path, html: { autocomplete: 'off', method: :put, class: 'margin-top-4' }, ) do |f| %> <%= render PasswordToggleComponent.new( diff --git a/app/views/idv/forgot_password/new.html.erb b/app/views/idv/forgot_password/new.html.erb index 72a6227bf0f..0d0437d0de0 100644 --- a/app/views/idv/forgot_password/new.html.erb +++ b/app/views/idv/forgot_password/new.html.erb @@ -11,7 +11,7 @@ <% c.with_action_button( action: ->(**tag_options, &block) do - link_to(idv_review_path, **tag_options, &block) + link_to(idv_enter_password_path, **tag_options, &block) end, ) { t('idv.forgot_password.try_again') } %> diff --git a/app/views/idv/hybrid_mobile/document_capture/show.html.erb b/app/views/idv/hybrid_mobile/document_capture/show.html.erb index 962a29d216b..2d9a14bb71a 100644 --- a/app/views/idv/hybrid_mobile/document_capture/show.html.erb +++ b/app/views/idv/hybrid_mobile/document_capture/show.html.erb @@ -7,4 +7,5 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + phone_question_ab_test_bucket: phone_question_ab_test_bucket, ) %> \ No newline at end of file diff --git a/app/views/idv/link_sent/show.html.erb b/app/views/idv/link_sent/show.html.erb index fa4e321a6dc..8f622ec5555 100644 --- a/app/views/idv/link_sent/show.html.erb +++ b/app/views/idv/link_sent/show.html.erb @@ -41,7 +41,10 @@ <% if FeatureManagement.doc_capture_polling_enabled? %> - <%= content_tag 'script', '', data: { status_endpoint: idv_capture_doc_status_url } %> + <%= content_tag 'script', '', data: { + status_endpoint: idv_capture_doc_status_url, + phone_question_ab_test_bucket: phone_question_ab_test_bucket, + } %> <%= javascript_packs_tag_once 'doc-capture-polling' %> <% end %> diff --git a/app/views/idv/session_errors/failure.html.erb b/app/views/idv/session_errors/failure.html.erb index 214f7755b7c..45a3984f337 100644 --- a/app/views/idv/session_errors/failure.html.erb +++ b/app/views/idv/session_errors/failure.html.erb @@ -2,7 +2,6 @@ 'idv/shared/error', title: t('titles.failure.information_not_verified'), heading: t('idv.failure.sessions.heading'), - current_step: :verify_info, options: [ { url: MarketingSite.contact_url, diff --git a/app/views/idv/session_errors/warning.html.erb b/app/views/idv/session_errors/warning.html.erb index 723475492eb..8e9e87f23e2 100644 --- a/app/views/idv/session_errors/warning.html.erb +++ b/app/views/idv/session_errors/warning.html.erb @@ -1,5 +1,14 @@ <% title t('titles.failure.information_not_verified') %> +<% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: @step_indicator_steps, + current_step: :verify_info, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> +<% end %> + <%= render StatusPageComponent.new(status: :warning) do |c| %> <% c.with_header { t('idv.warning.sessions.heading') } %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index fb737b2b7de..36aaab64390 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -23,9 +23,11 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + phone_question_ab_test_bucket: phone_question_ab_test_bucket, sp_name: sp_name, flow_path: flow_path, cancel_url: idv_cancel_path(step: :document_capture), + exit_url: idv_exit_path, failure_to_proof_url: failure_to_proof_url, idv_in_person_url: (IdentityConfig.store.in_person_doc_auth_button_enabled && Idv::InPersonConfig.enabled_for_issuer?(decorated_sp_session.sp_issuer)) ? idv_in_person_url : nil, security_and_privacy_how_it_works_url: MarketingSite.security_and_privacy_how_it_works_url, diff --git a/app/views/sign_up/registrations/new.html.erb b/app/views/sign_up/registrations/new.html.erb index be7a81383ed..f638911f189 100644 --- a/app/views/sign_up/registrations/new.html.erb +++ b/app/views/sign_up/registrations/new.html.erb @@ -9,7 +9,7 @@ <%= render TabNavigationComponent.new( label: t('account.login.tab_navigation'), routes: [ - { text: t('links.next'), path: new_user_session_url }, + { text: t('links.sign_in'), path: new_user_session_url }, { text: t('links.create_account'), path: sign_up_email_path }, ], class: 'margin-bottom-4', diff --git a/app/views/users/webauthn_setup/new.html.erb b/app/views/users/webauthn_setup/new.html.erb index 16e6b9ed185..b5c179d7021 100644 --- a/app/views/users/webauthn_setup/new.html.erb +++ b/app/views/users/webauthn_setup/new.html.erb @@ -64,11 +64,7 @@ checked: @presenter.remember_device_box_checked?, }, ) %> - <%= submit_tag( - @presenter.button_text, - id: 'continue-button', - class: 'display-block usa-button usa-button--big usa-button--wide margin-y-5', - ) %> + <%= render SubmitButtonComponent.new(class: 'display-block margin-y-5').with_content(@presenter.button_text) %> <% end %> <%= render 'shared/cancel_or_back_to_options' %> diff --git a/bin/query-cloudwatch b/bin/query-cloudwatch index cd326622f91..dd015f42147 100755 --- a/bin/query-cloudwatch +++ b/bin/query-cloudwatch @@ -29,6 +29,7 @@ class QueryCloudwatch :format, :complete, :progress, + :num_threads, :wait_duration, :count_distinct, keyword_init: true, @@ -83,6 +84,7 @@ class QueryCloudwatch log_group_name: config.group, progress: config.progress, slice_interval: config.slice, + num_threads: config.num_threads, **config.to_h.slice(:wait_duration).compact, ) end @@ -98,6 +100,7 @@ class QueryCloudwatch time_slices: [], complete: false, progress: true, + num_threads: Reporting::CloudwatchClient::DEFAULT_NUM_THREADS, count_distinct: nil, ) @@ -199,6 +202,13 @@ class QueryCloudwatch end end + opts.on( + '--num-threads NUM', + "number of threads, defaults to #{Reporting::CloudwatchClient::DEFAULT_NUM_THREADS}", + ) do |str| + config.num_threads = Integer(str, 10) + end + opts.on( '--date DATE,DATE', '(optional) dates to query, can be disjoint, to provide time slices to query' diff --git a/config/application.yml.default b/config/application.yml.default index 48312397eac..c5b4ac666ca 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -311,6 +311,7 @@ otp_min_attempts_remaining_warning_count: 3 system_demand_report_email: 'foo@bar.com' sp_issuer_user_counts_report_configs: '[]' team_agnes_email: '' +team_all_contractors_email: '' team_all_feds_email: '' team_ursula_email: '' test_ssn_allowed_list: '' @@ -559,6 +560,9 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 skip_encryption_allowed_list: '[]' state_tracking_enabled: true + team_agnes_email: 'a@example.com' + team_all_contractors_email: 'c@example.com' + team_all_feds_email: 'f@example.com' telephony_adapter: test test_ssn_allowed_list: '999999999' totp_code_interval: 3 diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml index 5d46e774f0b..71b4bee6999 100644 --- a/config/locales/doc_auth/en.yml +++ b/config/locales/doc_auth/en.yml @@ -88,13 +88,19 @@ en: top_msg_plural: We couldn’t read your ID. Your photos may have glare. Make sure that the flash on your camera is off and try taking new pictures. http: - image_load: The image file that you added is not supported. Please take new - photos of your ID and try again. - image_size: Your image size is too large or too small. Please add images of your - ID that are about 2025 x 1275 pixels. - pixel_depth: The pixel depth of your image file is not supported. Please take - new photos of your ID and try again. Supported image pixel depth is - 24-bit RGB. + image_load: + failed_short: Image file is not supported, please try again. + top_msg: The image file that you added is not supported. Please take new photos + of your ID and try again. + image_size: + failed_short: Image file is not supported, please try again. + top_msg: Your image size is too large or too small. Please add images of your ID + that are about 2025 x 1275 pixels. + pixel_depth: + failed_short: Image file is not supported, please try again. + top_msg: The pixel depth of your image file is not supported. Please take new + photos of your ID and try again. Supported image pixel depth is + 24-bit RGB. not_a_file: The selection was not a valid file. pii: birth_date_min_age: Your birthday does not meet the minimum age requirement. @@ -105,6 +111,30 @@ en: top_msg_plural: We couldn’t read your ID. Your photos may be too blurry or dark. Try taking new pictures in a bright area. upload_error: Sorry, something went wrong on our end. + exit_survey: + content_html: If you do not have a driver’s license or state ID card, + you cannot continue with %{app_name}. Please exit + %{app_name} and contact %{sp_name} to find out what you can do. + content_nosp_html: If you do not have a driver’s license or state ID + card, you cannot continue with %{app_name}. Cancel verifying + your identity with %{app_name} and you can restart the process when + you’re ready. + header: Don’t have a driver’s license or state ID? + optional: + button: Submit and exit %{app_name} + content: Help us add more identity documents to %{app_name}. Which types of + identity documents do you have instead? + id_types: + military_id: Military ID card (this includes a Department of Defense + Identification Card, a Veteran Health Identification Card, or a + Veteran ID Card) + other: Something not listed + resident_card: U.S. Green Card (also referred to as a Permanent Resident Card) + tribal_id: Tribal ID card + us_passport: U.S. Passport + voter_registration_card: Voter registration card + legend: Optional. Select any of the documents you have. + tag: Optional forms: captured_image: Captured Image change_file: Change file diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml index 9eeffa9ac2e..6db3fec2b98 100644 --- a/config/locales/doc_auth/es.yml +++ b/config/locales/doc_auth/es.yml @@ -112,15 +112,20 @@ es: tengan reflejos. Asegúrese de que el flash de su cámara esté desactivado e intente tomar nuevas fotos. http: - image_load: El archivo de imagen que ha añadido no es compatible. Por favor, - tome nuevas fotos de su identificación y vuelva a intentarlo. - image_size: El tamaño de la imagen es demasiado grande o demasiado pequeño. - Añada imágenes de su documento de identidad de unos 2025 x 1275 - píxeles. - pixel_depth: No es compatible con la profundidad de píxeles de su archivo de - imagen. Tome nuevas fotos de su documento de identidad e inténtelo - nuevamente. La profundidad de píxeles de la imagen admitida es de 24 - bits RGB. + image_load: + failed_short: El archivo de la imagen no es compatible. Inténtalo de nuevo. + top_msg: El archivo de imagen que ha añadido no es compatible. Por favor, tome + nuevas fotos de su identificación y vuelva a intentarlo. + image_size: + failed_short: El archivo de la imagen no es compatible. Inténtalo de nuevo. + top_msg: El tamaño de la imagen es demasiado grande o demasiado pequeño. Añada + imágenes de su documento de identidad de unos 2025 x 1275 píxeles. + pixel_depth: + failed_short: El archivo de la imagen no es compatible. Inténtalo de nuevo. + top_msg: No es compatible con la profundidad de píxeles de su archivo de imagen. + Tome nuevas fotos de su documento de identidad e inténtelo + nuevamente. La profundidad de píxeles de la imagen admitida es de 24 + bits RGB. not_a_file: La selección no era un archivo válido. pii: birth_date_min_age: Tu cumpleaños no cumple con el requisito de edad mínima. @@ -133,6 +138,32 @@ es: estén demasiado borrosas u oscuras. Intente tomar nuevas fotos en un área iluminada. upload_error: Lo siento, algo salió mal por nuestra parte. + exit_survey: + content_html: Si no tiene una licencia de conducir o identificación + estatal, no puede continuar en %{app_name}. Por favor salga + de %{app_name} y contacte a %{sp_name} para averiguar qué puede + hacer. + content_nosp_html: Si no tiene una licencia de conducir o identificación + estatal, no puede continuar en %{app_name}. Cancele la + verificación de identidad con %{app_name} y podrá reiniciar el + proceso cuando esté listo. + header: No cuenta con una licencia de conducir o identificación estatal? + optional: + button: Enviar y salir de %{app_name} + content: Ayúdenos a agregar más identificaciones oficiales a %{app_name}. ¿Qué + identificaciones tiene como alternativa? + id_types: + military_id: Credencial militar (puede ser una credencial del Departamento de + Defensa, una credencial de Salud de los Veteranos o una credencial + de veterano) + other: Una que no aparece en la lista + resident_card: Tarjeta Verde de Estados Unidos (también conocida como Tarjeta de + Residente Permanente) + tribal_id: Credencial tribal + us_passport: Pasaporte estadounidense + voter_registration_card: Credencial para votar + legend: Opcional. Seleccione todas las identificaciones que tenga. + tag: Opcional forms: captured_image: Imagen capturada change_file: Cambiar archivo diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml index 767f8320c96..660d0e18d51 100644 --- a/config/locales/doc_auth/fr.yml +++ b/config/locales/doc_auth/fr.yml @@ -117,16 +117,22 @@ fr: peuvent avoir des reflets. Assurez-vous que le flash de votre appareil photo est désactivé puis essayez de prendre de nouvelles photos. http: - image_load: Le fichier image que vous avez ajouté n’est pas pris en charge. - Veuillez prendre de nouvelles photos de votre pièce d’identité et - réessayer. - image_size: La taille de votre image est trop grande ou trop petite. Veuillez - ajouter des images de votre pièce d’identité d’environ 2025 x 1275 - pixels. - pixel_depth: La profondeur de pixel de votre fichier image n’est pas supportée. - Veuillez prendre de nouvelles photos de votre pièce d’identité et - réessayer. La profondeur de pixel de l’image prise en charge est de 24 - bits RGB. + image_load: + failed_short: Le fichier image n’est pas pris en charge, veuillez réessayer. + top_msg: Le fichier image que vous avez ajouté n’est pas pris en charge. + Veuillez prendre de nouvelles photos de votre pièce d’identité et + réessayer. + image_size: + failed_short: Le fichier image n’est pas pris en charge, veuillez réessayer. + top_msg: La taille de votre image est trop grande ou trop petite. Veuillez + ajouter des images de votre pièce d’identité d’environ 2025 x 1275 + pixels. + pixel_depth: + failed_short: Le fichier image n’est pas pris en charge, veuillez réessayer. + top_msg: La profondeur de pixel de votre fichier image n’est pas supportée. + Veuillez prendre de nouvelles photos de votre pièce d’identité et + réessayer. La profondeur de pixel de l’image prise en charge est de + 24 bits RGB. not_a_file: La sélection n’était pas un fichier valide. pii: birth_date_min_age: Votre anniversaire ne correspond pas à l’âge minimum requis. @@ -139,6 +145,33 @@ fr: peut-être trop floues ou trop sombres. Essayez de prendre de nouvelles photos dans un endroit lumineux. upload_error: Désolé, quelque chose a mal tourné de notre côté. + exit_survey: + content_html: Si vous n’avez pas de permis de conduire ou de carte + d’identité de l’État, vous ne pouvez pas continuer à utiliser + %{app_name}. Veuillez quitter %{app_name} et contacter + %{sp_name} pour savoir ce que vous pouvez faire. + content_nosp_html: Si vous n’avez pas de permis de conduire ou de carte + d’identité de l’État, vous ne pouvez pas continuer à utiliser + %{app_name}. Annulez la vérification de votre identité avec + %{app_name} et vous pourrez redémarrer le processus lorsque vous + serez prêt. + header: N’avez-vous pas de permis de conduire ou de carte d’identité de l’État? + optional: + button: Soumettre et quitter %{app_name} + content: Aidez-nous à ajouter plus de documents d’identité à %{app_name}. Quels + types de documents d’identité avez-vous à la place? + id_types: + military_id: Carte d’identité militaire (y compris la carte d’identité du + ministère de la défense, la carte d’identité médicale des anciens + combattants ou la carte d’identité des anciens combattants) + other: Quelque chose qui ne figure pas dans la liste + resident_card: Carte Verte américaine (également appelée Carte de Résident + Permanent) + tribal_id: Carte d’identité tribale + us_passport: Passeport américain + voter_registration_card: Carte d’électeur + legend: Facultatif. Sélectionnez l’un des documents que vous possédez. + tag: Facultatif forms: captured_image: Image capturée change_file: Changer de fichier @@ -280,6 +313,7 @@ fr: lettre à votre adresse personnelle. Cela prend 5 à 10 jours.' welcome: 'Vous aurez besoin de votre:' + phone_question: do_not_have: Je n’ai pas de téléphone tips: diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 15a33364261..f897ed0c315 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -55,7 +55,6 @@ en: start_over: Start over verifying your identity errors: incorrect_password: The password you entered is not correct. - mail_limit_reached: You have requested too much mail in the last month. pattern_mismatch: ssn: 'Enter a nine-digit Social Security number' zipcode: Enter a 5 or 9 digit ZIP Code @@ -176,8 +175,8 @@ en: JavaScript to continue this process.' gpo: alert_info: 'We sent a letter with your verification code to:' - alert_spam_warning_html: 'We are unable to send more letters. The most recent - letter was sent on %{date_letter_was_sent}.' + alert_spam_warning_html: You can’t request more letters right now. Your previous + letter request was on %{date_letter_was_sent}. change_to_verification_code_html: 'The one-time code from your letter is now referred to as verification code.' clear_and_start_over: Clear your information and start over @@ -213,6 +212,19 @@ en: wrong_address: Not the right address? images: come_back_later: Letter with a check mark + legal_statement: + information_collection: >- + This information collection meets the requirements of 44 U.S.C. § 3507, + as amended by section 2 of the Paperwork Reduction Act of 1995. You do + not need to answer these questions unless we display a valid Office of + Management and Budget (OMB) control number. The OMB control number for + this collection is 3090-0325. We estimate that it will take 1 minute to + read the instructions, gather the facts, and answer the questions. Send + only comments relating to our time estimate, including suggestions for + reducing this burden, or any other aspects of this collection of + information to: General Services Administration, Regulatory Secretariat + Division (MVCB), ATTN: Lois Mandell/IC 3090-0325, 1800 F Street, NW, + Washington, DC 20405. messages: activated_html: Your identity has been verified. If you need to change your verified information, please %{link_html}. diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index c57b772ffff..984d2a801e0 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -57,7 +57,6 @@ es: start_over: Empezar de nuevo a verificar su identidad errors: incorrect_password: La contraseña que ingresó no es correcta. - mail_limit_reached: Usted ha solicitado demasiado correo en el último mes. pattern_mismatch: ssn: 'Ingrese un número de Seguro Social de nueve dígitos' zipcode: Ingresa un código postal de 5 o 9 dígitos @@ -185,8 +184,8 @@ es: habilitar JavaScript para continuar con este proceso.' gpo: alert_info: 'Enviamos una carta con su código de verificación a:' - alert_spam_warning_html: 'No podemos enviar más cartas. La última carta se envió - el %{date_letter_was_sent}.' + alert_spam_warning_html: No puede solicitar más cartas ahora mismo. Su solicitud + de carta anterior la hizo el %{date_letter_was_sent}. change_to_verification_code_html: 'El código único de su carta ahora se conoce como código de verificación.' clear_and_start_over: Borrar su información y empezar de nuevo @@ -223,6 +222,21 @@ es: wrong_address: ¿La dirección no es correcta? images: come_back_later: Carta con una marca de verificación + legal_statement: + information_collection: >- + Cette collecte d’informations répond aux exigences de l’article 3507 du + 44 U.S.C., tel que modifié par l’article 2 de la loi de 1995 sur la + réduction des tâches administratives. Vous n’avez pas besoin de répondre + à ces questions, sauf si nous affichons un numéro de contrôle valide de + l’Office of Management and Budget (OMB). Le numéro de contrôle de l’OMB + pour cette collecte est 3090-0325. Calculamos que tomará un minuto leer + las instrucciones, recopilar los datos y responder las preguntas. Envíe + solo comentarios relacionados con nuestro tiempo estimado, incluidas + sugerencias para reducir esta molestia, o cualquier otro aspecto + relacionado con esta recopilación de información a: Administración de + Servicios Generales, División de la Secretaría Reguladora (MVCB), a la + atención de Lois Mandell/IC 3090-0325, 1800 F Street, NW, Washington, D. + C. 20405. messages: activated_html: Su identidad ha sido verificada. Si necesita cambiar la información verificada, por favor, %{link_html}. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index f44b8ba03f0..7bdc2a1bb1b 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -59,7 +59,6 @@ fr: start_over: Recommencez la vérification de votre identité errors: incorrect_password: Le mot de passe que vous avez inscrit est incorrect. - mail_limit_reached: Vous avez demandé trop de lettres au cours du dernier mois. pattern_mismatch: ssn: 'Entrez un numéro de sécurité sociale à neuf chiffres' zipcode: Entrez un code postal à 5 ou 9 chiffres @@ -190,9 +189,9 @@ fr: devez activer JavaScript pour poursuivre ce processus.' gpo: alert_info: 'Nous avons envoyé une lettre avec votre code de vérification à:' - alert_spam_warning_html: 'Nous ne sommes pas en mesure d’envoyer d’autres - lettres. La dernière lettre a été envoyée - %{date_letter_was_sent}.' + alert_spam_warning_html: Vous ne pouvez pas demander d’autres lettres pour le + moment. Votre précédente demande de lettre a été effectuée le + %{date_letter_was_sent}. change_to_verification_code_html: 'Le code à usage unique figurant dans votre lettre est désormais appelé code de vérification.' @@ -233,6 +232,20 @@ fr: wrong_address: Pas la bonne adresse? images: come_back_later: Lettre avec un crochet + legal_statement: + information_collection: >- + Esta recopilación de información cumple con los requisitos del título 44 + del U.S.C., § 3507, modificado por la sección 2 de la Ley de Reducción + de Trámites de 1995. No es necesario que responda a estas preguntas, a + menos que le mostremos un número de control válido de la Oficina de + Administración y Presupuesto (OMB). El número de control de la OMB para + esta recopilación es 3090-0325. Nous estimons qu’il faut une minute pour + lire les instructions, rassembler les preuves et répondre aux questions. + N’envoyez que des commentaires relatifs à notre estimation du temps, y + compris des suggestions pour réduire cette charge, ou tout autre aspect + de cette collecte d’informations à : General Services Administration, + Regulatory Secretariat Division (MVCB), ATTN : Lois Mandell/IC + 3090-0325, 1800 F Street, NW, Washington, DC 20405. messages: activated_html: Votre identité a été vérifiée. Si vous souhaitez modifier votre information vérifiée, veuillez %{link_html}. diff --git a/config/locales/links/en.yml b/config/locales/links/en.yml index 661799f3143..d0d19d56cfe 100644 --- a/config/locales/links/en.yml +++ b/config/locales/links/en.yml @@ -16,12 +16,12 @@ en: go_back: Go back help: Help new_tab: '(opens new tab)' - next: Sign in passwords: forgot: Forgot your password? privacy_policy: Privacy & security resend: Resend reverify: Please verify your identity again. + sign_in: Sign in sign_out: Sign out two_factor_authentication: send_another_code: Send another code diff --git a/config/locales/links/es.yml b/config/locales/links/es.yml index 0c863e61542..abfa3d0af66 100644 --- a/config/locales/links/es.yml +++ b/config/locales/links/es.yml @@ -16,12 +16,12 @@ es: go_back: Regresa help: Ayuda new_tab: (abrir nueva pestaña) - next: Siguiente passwords: forgot: '¿Olvidó su contraseña?' privacy_policy: Privacidad y seguridad resend: Reenviar reverify: Verifique su identidad nuevamente. + sign_in: Iniciar sesión sign_out: Cerrar sesión two_factor_authentication: send_another_code: Enviar otro código diff --git a/config/locales/links/fr.yml b/config/locales/links/fr.yml index f16f349884e..4311519a413 100644 --- a/config/locales/links/fr.yml +++ b/config/locales/links/fr.yml @@ -16,12 +16,12 @@ fr: go_back: Retourner help: Aide new_tab: '(ouvre un nouvel onglet)' - next: Suivant passwords: forgot: Vous avez oublié votre mot de passe? privacy_policy: Confidentialité et sécurité resend: Renvoyer reverify: Veuillez vérifier votre identité de nouveau. + sign_in: Se connecter sign_out: Déconnexion two_factor_authentication: send_another_code: Envoyer un autre code diff --git a/config/routes.rb b/config/routes.rb index 8ca032ca35c..99bf4c658f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -369,6 +369,7 @@ get '/cancel/' => 'cancellations#new', as: :cancel put '/cancel' => 'cancellations#update' delete '/cancel' => 'cancellations#destroy' + get '/exit' => 'cancellations#exit', as: :exit get '/address' => 'address#new' post '/address' => 'address#update' get '/capture_doc' => 'hybrid_mobile/entry#show' diff --git a/docs/frontend.md b/docs/frontend.md index e9716c2494d..77483f66a3a 100644 --- a/docs/frontend.md +++ b/docs/frontend.md @@ -33,6 +33,18 @@ margins or borders. - Packages are managed with [Yarn](https://classic.yarnpkg.com/), organized using [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) - JavaScript is transpiled, bundled, and minified via [Webpack](https://webpack.js.org/) and [Babel](https://babeljs.io/) +### Naming Conventions + +- Files within `app/javascript` should be named as kebab-case, e.g. `./path-to/my-javascript.ts`. +- Variables and functions (excluding React components) should be named as camelCase, e.g. `const myFavoriteNumber = 1;`. + - Only the first letter of an abbreviation should be capitalized, e.g. `const userId = 10;`. + - All letters of an acronym should be capitalized, e.g. `const siteURL = 'https://example.com';`. +- Classes and React components should be named as PascalCase (upper camel case), e.g. `class MyCustomElement {}`. +- Constants should be named as SCREAMING_SNAKE_CASE, e.g. `const MEANING_OF_LIFE = 42;`. +- TypeScript enums should be named as PascalCase with SCREAMING_SNAKE_CASE members, e.g. `enum Color { RED = '#f00'; }`. + +Related: [Component Naming Conventions](#naming) + ### Prettier [Prettier](https://prettier.io/) is an opinionated code formatter which simplifies adherence to @@ -214,8 +226,8 @@ For example, consider a **Password Input** component: - A ViewComponent file would be named `app/components/password_input_component.rb` - A stylesheet file would be named `app/assets/stylesheets/componewnts/_password-input.scss` - A stylesheet selector would be named `.password-input`, with child elements prefixed as `.password-input__` -- A react component would be named `` -- A react component file would be named `app/javascript/packages/password-input/password-input.tsx` +- A React component would be named `` +- A React component file would be named `app/javascript/packages/password-input/password-input.tsx` - A web component would be named `PasswordInputElement` - A web components file would be named `app/javascript/packages/password-input/password-input-element.ts` diff --git a/lib/identity_config.rb b/lib/identity_config.rb index c7bdbbb4ec5..2b46412557a 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -440,6 +440,7 @@ def self.build_store(config_map) config.add(:state_tracking_enabled, type: :boolean) config.add(:system_demand_report_email, type: :string) config.add(:team_agnes_email, type: :string) + config.add(:team_all_contractors_email, type: :string) config.add(:team_all_feds_email, type: :string) config.add(:team_ursula_email, type: :string) config.add(:telephony_adapter, type: :string) diff --git a/lib/pinpoint_supported_countries.rb b/lib/pinpoint_supported_countries.rb index d70b52dd3d4..8f3205041d6 100644 --- a/lib/pinpoint_supported_countries.rb +++ b/lib/pinpoint_supported_countries.rb @@ -14,6 +14,7 @@ class PinpointSupportedCountries BY EG FR + GB JO PH TH diff --git a/lib/reporting/cloudwatch_client.rb b/lib/reporting/cloudwatch_client.rb index 41d91bce456..4140e8949a6 100644 --- a/lib/reporting/cloudwatch_client.rb +++ b/lib/reporting/cloudwatch_client.rb @@ -165,9 +165,9 @@ def fetch_one(query:, start_time:, end_time:) wait_for_query_result(query_id) rescue Aws::CloudWatchLogs::Errors::InvalidParameterException => err if err.message.match?(/End time should not be before the service was generally available/) - # rubocop:disable Layout/LineLength - log(:warn, "query end_time=#{end_time} (#{Time.zone.at(end_time)}) is before Cloudwatch Insights availability, skipping") - # rubocop:enable Layout/LineLength + # rubocop:disable Layout/LineLength, Rails/TimeZone + log(:warn, "query end_time=#{end_time} (#{Time.at(end_time)}) is before Cloudwatch Insights availability, skipping") + # rubocop:enable Layout/LineLength, Rails/TimeZone Aws::CloudWatchLogs::Types::GetQueryResultsResponse.new(results: []) else raise err diff --git a/lib/script_base.rb b/lib/script_base.rb index 88ca8975b95..6d6ea2a2eb2 100644 --- a/lib/script_base.rb +++ b/lib/script_base.rb @@ -76,6 +76,19 @@ def run else self.class.render_output(result.table, format: config.format, stdout: stdout) end + rescue => err + self.class.render_output( + [ + ['Error', 'Message'], + [err.class.name, err.message], + ], + format: config.format, + stdout: stdout, + ) + + stderr.puts "#{err.class.name}: #{err.message}" + + exit 1 # rubocop:disable Rails/Exit end # rubocop:disable Metrics/BlockLength diff --git a/spec/bin/query-cloudwatch_spec.rb b/spec/bin/query-cloudwatch_spec.rb index 0b2b1d8f9d1..1e44c3e69db 100644 --- a/spec/bin/query-cloudwatch_spec.rb +++ b/spec/bin/query-cloudwatch_spec.rb @@ -197,6 +197,24 @@ end end + context 'number of threads' do + let(:argv) { required_parameters } + + it 'defaults to Reporting::CloudwatchClient::DEFAULT_NUM_THREADS' do + config = parse! + expect(config.num_threads).to eq(Reporting::CloudwatchClient::DEFAULT_NUM_THREADS) + end + + context 'with --num-threads' do + let(:argv) { required_parameters + %w[--num-threads 15] } + + it 'overrides the number of threads' do + config = parse! + expect(config.num_threads).to eq(15) + end + end + end + def build_stdin_without_query StringIO.new.tap do |io| allow(io).to receive(:tty?).and_return(true) @@ -222,6 +240,7 @@ def build_stdin_with_query(query) query: 'fields @timestamp, @message', format: format, count_distinct: count_distinct, + num_threads: Reporting::CloudwatchClient::DEFAULT_NUM_THREADS, ) end let(:query_cloudwatch) { QueryCloudwatch.new(config) } diff --git a/spec/components/webauthn_verify_button_component_spec.rb b/spec/components/webauthn_verify_button_component_spec.rb index 24fb41a524c..2fa0cad0991 100644 --- a/spec/components/webauthn_verify_button_component_spec.rb +++ b/spec/components/webauthn_verify_button_component_spec.rb @@ -17,7 +17,7 @@ expect(element.attr('data-credentials')).to eq('[]') expect(element.attr('data-user-challenge')).to eq('[]') - expect(rendered).to have_button(content) + expect(rendered).to have_css('lg-submit-button', text: content) end it 'renders hidden fields' do 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 134f7659cb6..c4418de2eea 100644 --- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb +++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb @@ -66,9 +66,9 @@ expect(response).to render_template('idv/by_mail/enter_code/index') end - it 'sets @should_prompt_user_to_request_another_letter to true' do + it 'sets @can_request_another_letter to true' do action - expect(assigns(:should_prompt_user_to_request_another_letter)).to eql(true) + expect(assigns(:can_request_another_letter)).to eql(true) end it 'shows rate limited page if user is rate limited' do @@ -81,9 +81,9 @@ context 'but that profile is > 30 days old' do let(:profile_created_at) { 31.days.ago } - it 'sets @should_prompt_user_to_request_another_letter to false' do + it 'sets @can_request_another_letter to false' do action - expect(assigns(:should_prompt_user_to_request_another_letter)).to eql(false) + expect(assigns(:can_request_another_letter)).to eql(false) end end diff --git a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb index a9c7e8f2d7c..13c6f620675 100644 --- a/spec/controllers/idv/by_mail/request_letter_controller_spec.rb +++ b/spec/controllers/idv/by_mail/request_letter_controller_spec.rb @@ -59,7 +59,7 @@ and_return(true) get :index - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path end it 'allows a user to request another letter' do @@ -143,7 +143,7 @@ put :create - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path expect(subject.idv_session.address_verification_mechanism).to eq :gpo end diff --git a/spec/controllers/idv/enter_password_controller_spec.rb b/spec/controllers/idv/enter_password_controller_spec.rb index fa3d4333c5b..be92af67d22 100644 --- a/spec/controllers/idv/enter_password_controller_spec.rb +++ b/spec/controllers/idv/enter_password_controller_spec.rb @@ -108,7 +108,7 @@ def show post :show, params: { user: { password: '' } } expect(flash[:error]).to eq t('idv.errors.incorrect_password') - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path end end @@ -119,7 +119,7 @@ def show it 'redirects to new' do expect(flash[:error]).to eq t('idv.errors.incorrect_password') - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path end it 'tracks irs password entered event (idv_password_entered)' do @@ -222,36 +222,6 @@ def show end end - context 'user has not requested too much mail' do - before do - idv_session.address_verification_mechanism = 'gpo' - gpo_mail_service = instance_double(Idv::GpoMail) - allow(Idv::GpoMail).to receive(:new).with(user).and_return(gpo_mail_service) - allow(gpo_mail_service).to receive(:mail_spammed?).and_return(false) - end - - it 'displays a success message' do - get :new - - expect(flash.now[:error]).to be_nil - end - end - - context 'user has requested too much mail' do - before do - idv_session.address_verification_mechanism = 'gpo' - gpo_mail_service = instance_double(Idv::GpoMail) - allow(Idv::GpoMail).to receive(:new).with(user).and_return(gpo_mail_service) - allow(gpo_mail_service).to receive(:mail_spammed?).and_return(true) - end - - it 'displays a helpful error message' do - get :new - - expect(flash.now[:error]).to eq t('idv.errors.mail_limit_reached') - end - end - it 'redirects to the verify info controller if the user has not completed it' do controller.idv_session.resolution_successful = nil @@ -279,7 +249,7 @@ def show it 'redirects to original path' do put :create, params: { user: { password: 'wrong' } } - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path expect(@analytics).to have_logged_event( 'IdV: review complete', @@ -520,7 +490,7 @@ def show it 'allows the user to retry the request' do put :create, params: { user: { password: ControllerHelper::VALID_PASSWORD } } expect(flash[:error]).to eq t('idv.failure.exceptions.internal_error') - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path stub_request_enroll diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 506b9045c33..69252dda3ed 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -170,7 +170,7 @@ it 'redirects to review' do get :show, params: { redo: true } - expect(response).to redirect_to(idv_review_url) + expect(response).to redirect_to(idv_enter_password_url) end end end diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index fd5c8ea3ead..c19f70e1177 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -305,6 +305,47 @@ end end + context 'when image upload fails with 4xx status' do + before do + status = 440 + errors = { general: [DocAuth::Errors::IMAGE_SIZE_FAILURE], + front: [DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD] } + message = [ + self.class.name, + 'Unexpected HTTP response', + status, + ].join(' ') + exception = DocAuth::RequestError.new(message, status) + response = DocAuth::Response.new( + success: false, + errors: errors, + exception: exception, + extra: { vendor: 'Mock' }, + ) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: response, + ) + end + + it 'returns error response' do + action + expect(response.status).to eq(400) + expect(json[:success]).to eq(false) + expect(json[:remaining_attempts]).to be_a_kind_of(Numeric) + expect(json[:errors]).to eq [ + { + field: 'general', + message: I18n.t('doc_auth.errors.http.image_size.top_msg'), + }, + { + field: 'front', + message: I18n.t('doc_auth.errors.http.image_size.failed_short'), + }, + ] + end + end + context 'when image upload succeeds' do it 'returns a successful response and modifies the session' do action diff --git a/spec/controllers/idv/otp_verification_controller_spec.rb b/spec/controllers/idv/otp_verification_controller_spec.rb index ea9e9604155..12c31acbcf5 100644 --- a/spec/controllers/idv/otp_verification_controller_spec.rb +++ b/spec/controllers/idv/otp_verification_controller_spec.rb @@ -55,7 +55,7 @@ it 'redirects to the review step' do get :show - expect(response).to redirect_to(idv_review_path) + expect(response).to redirect_to(idv_enter_password_path) end end @@ -85,7 +85,7 @@ it 'redirects to the review step' do put :update, params: otp_code_param - expect(response).to redirect_to(idv_review_path) + expect(response).to redirect_to(idv_enter_password_path) end end diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index c844f4cc1a6..2992cebfeaa 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -175,7 +175,7 @@ def index it 'redirects to review url' do get :show - expect(response).to redirect_to idv_review_url + expect(response).to redirect_to idv_enter_password_url end end end @@ -236,7 +236,7 @@ def index it 'redirects to review url' do patch :update - expect(response).to redirect_to idv_review_url + expect(response).to redirect_to idv_enter_password_url end end diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb index 970b81a7e2a..bee6b83d0ec 100644 --- a/spec/controllers/idv/phone_controller_spec.rb +++ b/spec/controllers/idv/phone_controller_spec.rb @@ -64,7 +64,7 @@ subject.idv_session.vendor_phone_confirmation = true get :new - expect(response).to redirect_to idv_review_path + expect(response).to redirect_to idv_enter_password_path end end diff --git a/spec/controllers/idv/phone_errors_controller_spec.rb b/spec/controllers/idv/phone_errors_controller_spec.rb index 07b3242e80f..6f08b18147c 100644 --- a/spec/controllers/idv/phone_errors_controller_spec.rb +++ b/spec/controllers/idv/phone_errors_controller_spec.rb @@ -81,7 +81,7 @@ it 'redirects to the review url' do get action - expect(response).to redirect_to(idv_review_url) + expect(response).to redirect_to(idv_enter_password_url) end it 'does not log an event' do expect(@analytics).not_to receive(:track_event).with( diff --git a/spec/controllers/idv/resend_otp_controller_spec.rb b/spec/controllers/idv/resend_otp_controller_spec.rb index a930f626004..2162252da92 100644 --- a/spec/controllers/idv/resend_otp_controller_spec.rb +++ b/spec/controllers/idv/resend_otp_controller_spec.rb @@ -45,7 +45,7 @@ it 'redirects to the enter password step' do post :create - expect(response).to redirect_to(idv_review_path) + expect(response).to redirect_to(idv_enter_password_path) end end diff --git a/spec/controllers/idv/session_errors_controller_spec.rb b/spec/controllers/idv/session_errors_controller_spec.rb index 89161ea7635..b3e8d7f2044 100644 --- a/spec/controllers/idv/session_errors_controller_spec.rb +++ b/spec/controllers/idv/session_errors_controller_spec.rb @@ -103,6 +103,20 @@ get action end end + + context 'the user is in the hybrid flow' do + render_views + let(:effective_user) { create(:user) } + + before do + session[:doc_capture_user_id] = effective_user.id + end + + it 'renders the error template' do + get action + expect(response).to render_template(template) + end + end end let(:verify_info_step_complete) { false } @@ -134,20 +148,6 @@ it_behaves_like 'an idv session errors controller action' it_behaves_like 'non-authenticated idv session errors controller action' - - context 'the user is in the hybrid flow' do - render_views - let(:effective_user) { create(:user) } - - before do - session[:doc_capture_user_id] = effective_user.id - end - - it 'renders the error template' do - get action - expect(response).to render_template(template) - end - end end describe '#warning' do @@ -160,20 +160,6 @@ it_behaves_like 'an idv session errors controller action' it_behaves_like 'non-authenticated idv session errors controller action' - context 'the user is in the hybrid flow' do - render_views - let(:effective_user) { create(:user) } - - before do - session[:doc_capture_user_id] = effective_user.id - end - - it 'renders the error template' do - get action - expect(response).to render_template(template) - end - end - context 'with rate limit attempts' do let(:user) { create(:user) } @@ -334,20 +320,6 @@ it_behaves_like 'an idv session errors controller action' it_behaves_like 'non-authenticated idv session errors controller action' - context 'the user is in the hybrid flow' do - render_views - let(:effective_user) { create(:user) } - - before do - session[:doc_capture_user_id] = effective_user.id - end - - it 'renders the error template' do - get action - expect(response).to render_template(template) - end - end - context 'while rate limited' do let(:user) { create(:user) } diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 0d98c59a74f..a0225e75013 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -104,7 +104,7 @@ get :show - expect(response).to redirect_to(idv_review_url) + expect(response).to redirect_to(idv_enter_password_url) end end diff --git a/spec/features/accessibility/idv_pages_spec.rb b/spec/features/accessibility/idv_pages_spec.rb index 71b56e171fd..b75b8212df3 100644 --- a/spec/features/accessibility/idv_pages_spec.rb +++ b/spec/features/accessibility/idv_pages_spec.rb @@ -36,7 +36,7 @@ visit idv_path complete_all_doc_auth_steps_before_password_step - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) expect_page_to_have_no_accessibility_violations(page) end diff --git a/spec/features/accessibility/user_pages_spec.rb b/spec/features/accessibility/user_pages_spec.rb index 5b33372668a..de7fa0b1a95 100644 --- a/spec/features/accessibility/user_pages_spec.rb +++ b/spec/features/accessibility/user_pages_spec.rb @@ -108,6 +108,10 @@ visit account_path expect_page_to_have_no_accessibility_violations(page) + + activate_skip_link + page.active_element.send_keys(:tab) + expect(page.active_element).to match_css('a', text: t('account.index.email_add'), wait: 5) end scenario 'delete email page' do diff --git a/spec/features/accessibility/visitor_pages_spec.rb b/spec/features/accessibility/visitor_pages_spec.rb index 25fb54905b6..ad6c980d1d3 100644 --- a/spec/features/accessibility/visitor_pages_spec.rb +++ b/spec/features/accessibility/visitor_pages_spec.rb @@ -6,6 +6,10 @@ visit root_path expect_page_to_have_no_accessibility_violations(page) + + activate_skip_link + page.active_element.send_keys(:tab) + expect(page.active_element).to match_css('a', text: t('links.sign_in'), wait: 5) end scenario 'forgot password page' do diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index bee4ba0b2f1..8d81db55777 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -63,10 +63,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'Frontend: IdV: back image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question @@ -91,7 +91,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, irs_reproofing: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, @@ -168,10 +168,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'Frontend: IdV: back image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question @@ -196,7 +196,7 @@ }, 'IdV: doc auth verify proofing results' => { success: true, errors: {}, flow_path: 'standard', address_edited: false, address_line2_present: false, analytics_id: 'Doc Auth', ssn_is_unique: true, step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, irs_reproofing: false, skip_hybrid_handoff: nil, - proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired' }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } + proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', resolution_adjudication_reason: 'pass_resolution_and_state_id', should_proof_state_id: true, stages: { resolution: { success: true, errors: {}, exception: nil, timed_out: false, transaction_id: 'resolution-mock-transaction-id-123', reference: 'aaa-bbb-ccc', can_pass_with_additional_verification: false, attributes_requiring_additional_verification: [], vendor_name: 'ResolutionMock', vendor_workflow: nil }, residential_address: { attributes_requiring_additional_verification: [], can_pass_with_additional_verification: false, errors: {}, exception: nil, reference: '', success: true, timed_out: false, transaction_id: '', vendor_name: 'ResidentialAddressNotRequired', vendor_workflow: nil }, state_id: { success: true, errors: {}, exception: nil, mva_exception: nil, timed_out: false, transaction_id: 'state-id-mock-transaction-id-456', vendor_name: 'StateIdMock', verified_attributes: [], state: 'MT', state_id_jurisdiction: 'ND', state_id_number: '#############' }, threatmetrix: threatmetrix_response } } } }, 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, skip_hybrid_handoff: nil, @@ -255,10 +255,10 @@ flow_path: 'standard', step: 'document_capture', redo_document_capture: nil, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question, analytics_id: 'Doc Auth', skip_hybrid_handoff: nil, irs_reproofing: false }, 'Frontend: IdV: front image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'Frontend: IdV: back image added' => { - 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean + 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything, 'acuantCaptureMode' => 'AUTO', 'fingerprint' => anything, 'failedImageResubmission' => boolean, 'phone_question_ab_test_bucket' => 'bypass_phone_question' }, 'IdV: doc auth image upload form submitted' => { success: true, errors: {}, attempts: 1, remaining_attempts: 3, user_id: user.uuid, flow_path: 'standard', front_image_fingerprint: an_instance_of(String), back_image_fingerprint: an_instance_of(String), getting_started_ab_test_bucket: :welcome_default, phone_question_ab_test_bucket: :bypass_phone_question diff --git a/spec/features/idv/confirm_start_over_spec.rb b/spec/features/idv/confirm_start_over_spec.rb index 383f708f66a..5d04e342c87 100644 --- a/spec/features/idv/confirm_start_over_spec.rb +++ b/spec/features/idv/confirm_start_over_spec.rb @@ -34,23 +34,6 @@ complete_idv_steps_before_gpo_step click_on t('idv.messages.gpo.start_over_link_text') - expect(current_path).to eq idv_confirm_start_over_path - expect(page).to have_content(t('idv.cancel.description.gpo.start_over_new_address')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_phone_or_address')) - expect(fake_analytics).to have_logged_event(:idv_gpo_confirm_start_over_before_letter_visited) - click_idv_continue - - expect(current_path).to eq idv_welcome_path - end - end - - context 'user decides to start over from request letter page with new route' do - it 'allows user to start over' do - start_idv_from_sp - complete_idv_steps_before_gpo_step - click_on t('idv.messages.gpo.start_over_link_text') - visit idv_confirm_start_over_before_letter_path - expect(current_path).to eq idv_confirm_start_over_before_letter_path expect(page).to have_content(t('idv.cancel.description.gpo.start_over_new_address')) expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_phone_or_address')) diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index eedb3fef6bc..cad9cfa9349 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -134,6 +134,20 @@ expect(DocAuthLog.find_by(user_id: user.id).state).to be_nil end + + it 'return to sp when click on exit link', :js do + click_sp_exit_link(sp_name: sp_name) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end + + it 'logs event and return to sp when click on submit and exit button', :js do + click_submit_exit_button + expect(fake_analytics).to have_logged_event( + 'Frontend: IdV: exit optional questions', + hash_including('ids'), + ) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end end context 'standard mobile flow' do @@ -162,6 +176,16 @@ expect(page).to have_current_path(idv_phone_url) end end + + it 'return to sp when click on exit link', :js do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_document_capture_step + click_sp_exit_link(sp_name: sp_name) + expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied') + end + end end def expect_costing_for_document diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index 18431c01425..fabf4ce161c 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -189,6 +189,23 @@ end end + shared_examples_for 'inline error for 4xx status shown' do |status| + it "shows inline error for status #{status}" do + error = case status + when 438 + t('doc_auth.errors.http.image_load.failed_short') + when 439 + t('doc_auth.errors.http.pixel_depth.failed_short') + when 440 + t('doc_auth.errors.http.image_size.failed_short') + end + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: error, + ) + end + end + context 'error due to data issue with 2xx status code', allow_browser_log: true do before do sign_in_and_2fa_user @@ -232,6 +249,7 @@ attach_and_submit_images click_try_again end + it_behaves_like 'inline error for 4xx status shown', 440 it_behaves_like 'image re-upload not allowed' end diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index 43d01fbc8e1..ad9fc2fdb67 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -111,6 +111,7 @@ click_idv_continue expect(page).to have_current_path(idv_session_errors_warning_path) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_info')) click_on t('idv.failure.button.warning') expect(page).to have_current_path(idv_verify_info_path) @@ -173,6 +174,7 @@ click_idv_continue expect(page).to have_current_path(idv_session_errors_failure_path) + expect(page).not_to have_css('.step-indicator__step--current', text: text, wait: 5) expect(fake_analytics).to have_logged_event( 'Rate Limit Reached', limiter_type: :idv_resolution, diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb index cb78a0d9a9c..f4281428750 100644 --- a/spec/features/idv/end_to_end_idv_spec.rb +++ b/spec/features/idv/end_to_end_idv_spec.rb @@ -254,7 +254,7 @@ def complete_otp_verification_page(user) end def validate_enter_password_page - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) expect(page).to have_content(t('idv.messages.enter_password.message', app_name: APP_NAME)) expect(page).to have_content(t('idv.messages.enter_password.phone_verified')) @@ -262,7 +262,7 @@ def validate_enter_password_page fill_in 'Password', with: 'this is not the right password' click_idv_continue expect(page).to have_content(t('idv.errors.incorrect_password')) - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) end def validate_enter_password_submit(user) @@ -308,7 +308,7 @@ def validate_personal_key_page end def try_to_skip_ahead_before_signing_in - visit idv_review_path + visit idv_enter_password_path expect(current_path).to eq(root_path) end @@ -336,7 +336,7 @@ def try_to_skip_ahead_from_hybrid_handoff end def try_to_skip_ahead_from_phone - visit idv_review_path + visit idv_enter_password_path expect(page).to have_current_path(idv_phone_path) end diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 623a3f707f5..a1b8568f59e 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -106,6 +106,7 @@ # signing in again before completing in-person proofing at a post office Capybara.reset_session! sign_in_live_with_2fa(user) + visit_idp_from_sp_with_ial2(:oidc) expect(page).to have_current_path(idv_in_person_ready_to_verify_path) end end @@ -232,6 +233,7 @@ # signing in again before completing in-person proofing at a post office Capybara.reset_session! sign_in_live_with_2fa(user) + visit_idp_from_sp_with_ial2(:oidc) expect(page).to have_current_path(idv_in_person_ready_to_verify_path) # confirm that user cannot visit other IdV pages before completing in-person proofing @@ -241,6 +243,11 @@ expect(page).to have_current_path(idv_in_person_ready_to_verify_path) visit idv_verify_info_url expect(page).to have_current_path(idv_in_person_ready_to_verify_path) + + # Confirms that user can visit account page even if not completing in person proofing + Capybara.reset_session! + sign_in_and_2fa_user(user) + expect(page).to have_current_path(account_path) end it 'allows the user to cancel and start over from the beginning', allow_browser_log: true do @@ -900,6 +907,7 @@ # signing in again before completing in-person proofing at a post office Capybara.reset_session! sign_in_live_with_2fa(user) + visit_idp_from_sp_with_ial2(:oidc) expect(page).to have_current_path(idv_in_person_ready_to_verify_path) end end diff --git a/spec/features/idv/phone_otp_rate_limiting_spec.rb b/spec/features/idv/phone_otp_rate_limiting_spec.rb index 8829bbd1645..c5d3c14ba72 100644 --- a/spec/features/idv/phone_otp_rate_limiting_spec.rb +++ b/spec/features/idv/phone_otp_rate_limiting_spec.rb @@ -78,7 +78,7 @@ def expect_rate_limit_to_expire(user) click_submit_default expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(current_path).to eq(idv_review_path) + expect(current_path).to eq(idv_enter_password_path) end end end diff --git a/spec/features/idv/proof_address_rate_limit_spec.rb b/spec/features/idv/proof_address_rate_limit_spec.rb index d27a03714c5..4620892a4e9 100644 --- a/spec/features/idv/proof_address_rate_limit_spec.rb +++ b/spec/features/idv/proof_address_rate_limit_spec.rb @@ -18,7 +18,7 @@ click_on t('idv.buttons.mail.send') expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(current_path).to eq(idv_review_path) + expect(current_path).to eq(idv_enter_password_path) fill_in 'Password', with: user.password click_idv_continue expect(page).to have_current_path(idv_letter_enqueued_path) @@ -59,7 +59,7 @@ click_submit_default expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(current_path).to eq(idv_review_path) + expect(current_path).to eq(idv_enter_password_path) fill_in 'Password', with: user.password click_idv_continue expect(current_path).to eq(idv_personal_key_path) diff --git a/spec/features/idv/steps/enter_code_step_spec.rb b/spec/features/idv/steps/enter_code_step_spec.rb index 5f853329c49..32bd399f366 100644 --- a/spec/features/idv/steps/enter_code_step_spec.rb +++ b/spec/features/idv/steps/enter_code_step_spec.rb @@ -203,6 +203,30 @@ end end + context 'when the letter is too old' do + let(:code_sent_at) { (IdentityConfig.store.usps_confirmation_max_days + 1).days.ago } + + before do + user.gpo_verification_pending_profile.update( + created_at: code_sent_at, + updated_at: code_sent_at, + ) + + gpo_confirmation_code.update( + code_sent_at: code_sent_at, + created_at: code_sent_at, + updated_at: code_sent_at, + ) + + sign_in_live_with_2fa(user) + end + + it 'shows a warning message and does not allow the user to request another letter' do + verify_spam_warning_banner_present(code_sent_at) + expect(page).not_to have_content t('idv.messages.gpo.resend') + end + end + def verify_no_spam_warning_banner expect(page).not_to have_content( t( diff --git a/spec/features/idv/steps/forgot_password_step_spec.rb b/spec/features/idv/steps/forgot_password_step_spec.rb index 3c1ea63093e..91d8f18986e 100644 --- a/spec/features/idv/steps/forgot_password_step_spec.rb +++ b/spec/features/idv/steps/forgot_password_step_spec.rb @@ -19,7 +19,7 @@ click_link t('idv.forgot_password.link_text') click_link t('idv.forgot_password.try_again') - expect(page.current_path).to eq(idv_review_path) + expect(page.current_path).to eq(idv_enter_password_path) end it 'allows the user to reset their password' do diff --git a/spec/features/idv/steps/phone_otp_verification_step_spec.rb b/spec/features/idv/steps/phone_otp_verification_step_spec.rb index 2de1de92c14..5b397d0fcfd 100644 --- a/spec/features/idv/steps/phone_otp_verification_step_spec.rb +++ b/spec/features/idv/steps/phone_otp_verification_step_spec.rb @@ -10,7 +10,7 @@ complete_idv_steps_before_phone_otp_verification_step(user) # Attempt to bypass the step - visit idv_review_path + visit idv_enter_password_path expect(current_path).to eq(idv_otp_verification_path) # Enter an incorrect otp @@ -25,7 +25,7 @@ click_submit_default expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) end it 'rejects OTPs after they are expired' do @@ -58,7 +58,7 @@ click_submit_default expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) end it 'redirects back to the step with an error if Telephony raises an error on resend' do diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 435af6e96c2..940fca00f2b 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -48,7 +48,7 @@ visit idv_phone_path expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) fill_in 'Password', with: user_password click_continue diff --git a/spec/features/idv/steps/request_letter_step_spec.rb b/spec/features/idv/steps/request_letter_step_spec.rb index 12fe8fb6490..7d6585aaf1b 100644 --- a/spec/features/idv/steps/request_letter_step_spec.rb +++ b/spec/features/idv/steps/request_letter_step_spec.rb @@ -23,7 +23,7 @@ click_on t('idv.buttons.mail.send') expect(page).to have_content(t('idv.titles.session.enter_password', app_name: APP_NAME)) - expect(page).to have_current_path(idv_review_path) + expect(page).to have_current_path(idv_enter_password_path) complete_enter_password_step expect(page).to have_content(t('idv.messages.gpo.letter_on_the_way')) diff --git a/spec/features/remember_device/signed_in_sp_expiration.rb b/spec/features/remember_device/signed_in_sp_expiration.rb new file mode 100644 index 00000000000..034cde39285 --- /dev/null +++ b/spec/features/remember_device/signed_in_sp_expiration.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +RSpec.feature 'SP expiration while signed in' do + include SamlAuthHelper + + ## + # This test is a regression spec for a specific bug in `remember_device_expired_for_sp?` + # + # See https://github.com/18F/identity-idp/pull/9458 + # + scenario 'signed in user with expired remember device does not get stuck in MFA loop' do + user = sign_up_and_set_password + user.password = Features::SessionHelper::VALID_PASSWORD + + select_2fa_option('phone') + fill_in :new_phone_form_phone, with: '2025551212' + click_send_one_time_code + check t('forms.messages.remember_device') + fill_in_code_with_last_phone_otp + click_submit_default + skip_second_mfa_prompt + + first(:button, t('links.sign_out')).click + + sign_in_user(user) + + travel_to(5.seconds.from_now) do + visit_idp_from_sp_with_ial1_aal2(:oidc) + + expect(current_path).to eq(login_two_factor_path(otp_delivery_preference: :sms)) + expect(page).to have_content(t('two_factor_authentication.header_text')) + + fill_in_code_with_last_phone_otp + uncheck t('forms.messages.remember_device') + click_submit_default + + expect(page).to have_current_path(sign_up_completed_path) + end + end +end diff --git a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb index 76b117b2cef..c7e03919a7f 100644 --- a/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/backup_code_sign_up_spec.rb @@ -5,6 +5,12 @@ include SamlAuthHelper context 'with js', js: true do + let(:fake_analytics) { FakeAnalytics.new } + + before do + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + end + it 'allows backup code only MFA configurations' do user = sign_up_and_set_password expect(page).to_not \ @@ -25,6 +31,14 @@ expect(page).to have_content(t('notices.backup_codes_configured')) expect(current_path).to eq auth_method_confirmation_path expect(user.backup_code_configurations.count).to eq(10) + + click_on t('mfa.skip') + expect(current_path).to eq(confirm_backup_codes_path) + + click_on t('two_factor_authentication.backup_codes.saved_backup_codes') + + expect(fake_analytics).to have_logged_event('User registration: complete') + expect(page).to have_title(t('titles.account')) end end diff --git a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb index 3eeeec8e952..cef2807daec 100644 --- a/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb +++ b/spec/features/two_factor_authentication/multiple_mfa_sign_up_spec.rb @@ -4,6 +4,12 @@ include WebAuthnHelper describe 'When the user has not set up 2FA' do + let(:fake_analytics) { FakeAnalytics.new } + + before do + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) + end + scenario 'user can set up 2 MFA methods properly' do sign_in_before_2fa @@ -34,6 +40,7 @@ click_continue expect(page).to have_content(t('notices.backup_codes_configured')) + expect(fake_analytics).to have_logged_event('User registration: complete') expect(current_path).to eq account_path end @@ -74,6 +81,7 @@ check t('forms.messages.remember_device') click_submit_default + expect(fake_analytics).to have_logged_event('User registration: complete') expect(current_path).to eq account_path end diff --git a/spec/features/visitors/password_recovery_spec.rb b/spec/features/visitors/password_recovery_spec.rb index 5bb404bfc59..ccda0376641 100644 --- a/spec/features/visitors/password_recovery_spec.rb +++ b/spec/features/visitors/password_recovery_spec.rb @@ -198,7 +198,7 @@ fill_in t('account.index.email'), with: @user.email fill_in t('components.password_toggle.label'), with: 'NewVal!dPassw0rd' - click_button t('links.next') + click_button t('links.sign_in') fill_in_code_with_last_phone_otp click_submit_default click_agree_and_continue diff --git a/spec/javascript/packages/document-capture-polling/index-spec.js b/spec/javascript/packages/document-capture-polling/index-spec.js index 2a34a308a17..1356bed1ec5 100644 --- a/spec/javascript/packages/document-capture-polling/index-spec.js +++ b/spec/javascript/packages/document-capture-polling/index-spec.js @@ -40,6 +40,7 @@ describe('DocumentCapturePolling', () => { ), }, trackEvent, + phoneQuestionAbTestBucket: 'bypass_phone_question', }); subject.bind(); }); @@ -67,7 +68,10 @@ describe('DocumentCapturePolling', () => { sandbox.clock.tick(DOC_CAPTURE_POLL_INTERVAL); expect(global.fetch).to.have.been.calledTwice(); - expect(trackEvent).to.have.been.calledOnceWith('IdV: Link sent capture doc polling started'); + expect(trackEvent).to.have.been.calledOnceWithExactly( + 'IdV: Link sent capture doc polling started', + { phone_question_ab_test_bucket: 'bypass_phone_question' }, + ); }); it('submits when done', async () => { @@ -83,11 +87,18 @@ describe('DocumentCapturePolling', () => { await flushPromises(); // Flush `json` expect(subject.elements.form.submit).to.have.been.called(); - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started'); - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling complete', { - isCancelled: false, - isRateLimited: false, - }); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling started', + { phone_question_ab_test_bucket: 'bypass_phone_question' }, + ); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling complete', + { + isCancelled: false, + isRateLimited: false, + phone_question_ab_test_bucket: 'bypass_phone_question', + }, + ); }); it('redirects if given redirect URL on success', async () => { @@ -116,11 +127,18 @@ describe('DocumentCapturePolling', () => { await flushPromises(); // Flush `fetch` await flushPromises(); // Flush `json` - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started'); - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling complete', { - isCancelled: true, - isRateLimited: false, - }); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling started', + { phone_question_ab_test_bucket: 'bypass_phone_question' }, + ); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling complete', + { + isCancelled: true, + isRateLimited: false, + phone_question_ab_test_bucket: 'bypass_phone_question', + }, + ); expect(subject.elements.form.submit).to.have.been.called(); }); @@ -134,11 +152,18 @@ describe('DocumentCapturePolling', () => { await flushPromises(); // Flush `fetch` await flushPromises(); // Flush `json` - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling started'); - expect(trackEvent).to.have.been.calledWith('IdV: Link sent capture doc polling complete', { - isCancelled: false, - isRateLimited: true, - }); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling started', + { phone_question_ab_test_bucket: 'bypass_phone_question' }, + ); + expect(trackEvent).to.have.been.calledWithExactly( + 'IdV: Link sent capture doc polling complete', + { + isCancelled: false, + isRateLimited: true, + phone_question_ab_test_bucket: 'bypass_phone_question', + }, + ); expect(window.location.hash).to.equal('#rate_limited'); }); diff --git a/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx b/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx new file mode 100644 index 00000000000..1235587fefa --- /dev/null +++ b/spec/javascript/packages/document-capture/components/document-capture-abandon-spec.tsx @@ -0,0 +1,199 @@ +import sinon from 'sinon'; + +import { FlowContext } from '@18f/identity-verify-flow'; +import DocumentCaptureAbandon from '@18f/identity-document-capture/components/document-capture-abandon'; +import { I18nContext } from '@18f/identity-react-i18n'; +import { I18n } from '@18f/identity-i18n'; +import userEvent from '@testing-library/user-event'; +import type { Navigate } from '@18f/identity-url'; +import { + AnalyticsContextProvider, + ServiceProviderContextProvider, +} from '@18f/identity-document-capture/context'; +import { expect } from 'chai'; +import { render } from '../../../support/document-capture'; + +describe('DocumentCaptureAbandon', () => { + beforeEach(() => { + const config = document.createElement('script'); + config.id = 'test-config'; + config.type = 'application/json'; + config.setAttribute('data-config', ''); + config.textContent = JSON.stringify({ appName: 'Login.gov' }); + document.body.append(config); + }); + const trackEvent = sinon.spy(); + const navigateSpy: Navigate = sinon.spy(); + context('with service provider', () => { + const spName = 'testSP'; + it('renders, track event and redirect', async () => { + const { getByRole, getByText } = render( + + '', + }} + > + + exit %{app_name} and contact %{sp_name} to find out what you can do.', + 'doc_auth.exit_survey.optional.button': 'Submit and exit %{app_name}', + }, + }) + } + > + + + + + , + ); + // header + expect(getByRole('heading', { name: 'header text', level: 2 })).to.be.ok(); + + // content and exit link + const exitLink = getByRole('link', { name: 'exit Login.gov and contact testSP' }); + expect(exitLink).to.be.ok(); + expect(exitLink.getAttribute('href')).to.contain( + '/exit?step=document_capture&location=optional_question', + ); + + expect(getByText('doc_auth.exit_survey.optional.tag')).to.be.ok(); + // legend + expect(getByText('doc_auth.exit_survey.optional.legend')).to.be.ok(); + // checkboxes + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.us_passport' }), + ).to.be.ok(); + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.resident_card' }), + ).to.be.ok(); + const militaryId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.military_id', + }); + expect(militaryId).to.be.ok(); + expect( + getByRole('checkbox', { name: 'doc_auth.exit_survey.optional.id_types.tribal_id' }), + ).to.be.ok(); + expect( + getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.voter_registration_card', + }), + ).to.be.ok(); + const otherId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.other', + }); + expect(otherId).to.be.ok(); + + // legal statement + expect(getByText('idv.legal_statement.information_collection')).to.be.ok(); + + // exit button + const exitButton = getByRole('button', { name: 'Submit and exit Login.gov' }); + expect(exitButton).to.be.ok(); + expect(exitButton.classList.contains('usa-button--outline')).to.be.true(); + + await userEvent.click(otherId); + await userEvent.click(militaryId); + await userEvent.click(exitButton); + expect(navigateSpy).to.be.called.calledWithMatch( + /exit\?step=document_capture&location=optional_question/, + ); + expect(trackEvent).to.be.calledWithMatch(/IdV: exit optional questions/, { + ids: [ + { name: 'us_passport', checked: false }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: true }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: true }, + ], + }); + }); + }); + + context('without service provider', () => { + it('renders, track event and redirect', async () => { + const { getByRole, getByText } = render( + + '' }} + > + + Cancel verifying your identity with %{app_name} and you can restart the process when you’re ready.', + 'doc_auth.exit_survey.optional.button': 'Submit and exit %{app_name}', + }, + }) + } + > + + + + + , + ); + + expect( + getByRole('link', { name: 'Cancel verifying your identity with Login.gov' }).getAttribute( + 'href', + ), + ).to.contain('/cancel?step=document_capture&location=optional_question'); + + expect(getByText('doc_auth.exit_survey.optional.tag')).to.be.ok(); + + const usPassport = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.us_passport', + }); + expect(usPassport).to.be.ok(); + const otherId = getByRole('checkbox', { + name: 'doc_auth.exit_survey.optional.id_types.other', + }); + expect(otherId).to.be.ok(); + + const exitButton = getByRole('button', { name: 'Submit and exit Login.gov' }); + expect(exitButton).to.be.ok(); + + await userEvent.click(otherId); + await userEvent.click(usPassport); + await userEvent.click(exitButton); + expect(navigateSpy).to.be.calledWithMatch( + /cancel\?step=document_capture&location=optional_question/, + ); + expect(trackEvent).to.be.calledWithMatch(/IdV: exit optional questions/, { + ids: [ + { name: 'us_passport', checked: true }, + { name: 'resident_card', checked: false }, + { name: 'military_id', checked: false }, + { name: 'tribal_id', checked: false }, + { name: 'voter_registration_card', checked: false }, + { name: 'other', checked: true }, + ], + }); + }); + }); +}); diff --git a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx index 28244c1787c..d640c75a01c 100644 --- a/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx +++ b/spec/javascript/packages/document-capture/components/document-capture-review-issues-spec.jsx @@ -67,9 +67,8 @@ describe('DocumentCaptureReviewIssues', () => { const backCapture = getByLabelText('doc_auth.headings.document_capture_back'); expect(backCapture).to.be.ok(); expect(getByText('back side error')).to.be.ok(); - const submitButton = getByRole('button'); - expect(submitButton).to.be.ok(); - expect(within(submitButton).getByText('forms.buttons.submit.default')).to.be.ok(); + expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); + expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); it('renders for a doc type failure', () => { @@ -115,9 +114,8 @@ describe('DocumentCaptureReviewIssues', () => { const backCapture = getByLabelText('doc_auth.headings.document_capture_back'); expect(backCapture).to.be.ok(); expect(getByText('back side doc type error')).to.be.ok(); - const submitButton = getByRole('button'); - expect(submitButton).to.be.ok(); - expect(within(submitButton).getByText('forms.buttons.submit.default')).to.be.ok(); + expect(getByRole('button', { name: 'forms.buttons.submit.default' })).to.be.ok(); + expect(getByRole('button', { name: 'doc_auth.exit_survey.optional.button' })).to.be.ok(); }); }); }); diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx index d160d95c5b4..ecff722a4d4 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/documents-step-spec.jsx @@ -82,4 +82,16 @@ describe('document-capture/components/documents-step', () => { expect(queryByText(notExpectedText)).to.not.exist(); }); + + it('renders optional question part', () => { + const { getByRole, getByText } = render( + + + + + , + ); + expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); + expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); + }); }); diff --git a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx index fac01242337..d24b71cfd1a 100644 --- a/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx +++ b/spec/javascript/packages/document-capture/components/review-issues-step-spec.jsx @@ -343,6 +343,14 @@ describe('document-capture/components/review-issues-step', () => { expect(getByLabelText('doc_auth.headings.document_capture_back')).to.be.ok(); }); + it('renders optional questions', async () => { + const { getByText, getByRole } = render(); + + await userEvent.click(getByRole('button', { name: 'idv.failure.button.warning' })); + expect(getByRole('heading', { name: 'doc_auth.exit_survey.header', level: 2 })).to.be.ok(); + expect(getByText('doc_auth.exit_survey.optional.button')).to.be.ok(); + }); + context('service provider context', () => { context('ial2', () => { it('renders with front and back inputs', async () => { diff --git a/spec/jobs/reports/monthly_key_metrics_report_spec.rb b/spec/jobs/reports/monthly_key_metrics_report_spec.rb index 8102abe4654..12faf041904 100644 --- a/spec/jobs/reports/monthly_key_metrics_report_spec.rb +++ b/spec/jobs/reports/monthly_key_metrics_report_spec.rb @@ -5,8 +5,6 @@ subject(:report) { Reports::MonthlyKeyMetricsReport.new(report_date) } let(:name) { 'monthly-key-metrics-report' } - let(:agnes_email) { 'fake@agnes_email.com' } - let(:feds_email) { 'fake@feds_email.com' } let(:s3_report_bucket_prefix) { 'reports-bucket' } let(:report_folder) do 'int/monthly-key-metrics-report/2021/2021-03-02.monthly-key-metrics-report' @@ -16,7 +14,7 @@ let(:document_upload_proofing_s3_path) { "#{report_folder}/document_upload_proofing.csv" } let(:account_deletion_rate_s3_path) { "#{report_folder}/account_deletion_rate.csv" } let(:total_user_count_s3_path) { "#{report_folder}/total_user_count.csv" } - let(:monthly_active_users_count_s3_path) { "#{report_folder}/monthly_active_users_count.csv" } + let(:active_users_count_s3_path) { "#{report_folder}/active_users_count.csv" } let(:expected_s3_paths) do [ account_reuse_s3_path, @@ -24,7 +22,7 @@ account_deletion_rate_s3_path, total_user_count_s3_path, document_upload_proofing_s3_path, - monthly_active_users_count_s3_path, + active_users_count_s3_path, ] end let(:s3_metadata) do @@ -42,11 +40,6 @@ end before do - allow(IdentityConfig.store).to receive(:team_agnes_email). - and_return(agnes_email) - allow(IdentityConfig.store).to receive(:team_all_feds_email). - and_return(feds_email) - allow(Identity::Hostdata).to receive(:env).and_return('int') allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') @@ -59,32 +52,38 @@ }, } - allow(subject.monthly_proofing_report).to receive(:proofing_report). + allow(report.monthly_proofing_report).to receive(:proofing_report). and_return(mock_proofing_report_data) end it 'sends out a report to the email listed with one total user' do expect(ReportMailer).to receive(:tables_report).once.with( - email: [agnes_email], + email: [IdentityConfig.store.team_agnes_email], subject: 'Monthly Key Metrics Report - 2021-03-02', reports: anything, + message: report.preamble, attachment_format: :xlsx, ).and_call_original - subject.perform(report_date) + report.perform(report_date) end it 'sends out a report to the emails listed with two users' do first_of_month_date = report_date - 1 expect(ReportMailer).to receive(:tables_report).once.with( - email: [agnes_email, feds_email], + email: [ + IdentityConfig.store.team_agnes_email, + IdentityConfig.store.team_all_feds_email, + IdentityConfig.store.team_all_contractors_email, + ], subject: 'Monthly Key Metrics Report - 2021-03-01', reports: anything, + message: report.preamble, attachment_format: :xlsx, ).and_call_original - subject.perform(first_of_month_date) + report.perform(first_of_month_date) end it 'does not send out a report with no emails' do @@ -95,7 +94,7 @@ expect(ReportMailer).not_to receive(:tables_report) - subject.perform(report_date) + report.perform(report_date) end it 'uploads a file to S3 based on the report date' do @@ -106,6 +105,16 @@ ).exactly(1).time.and_call_original end - subject.perform(report_date) + report.perform(report_date) + end + + describe '#preamble' do + subject(:preamble) { report.preamble } + + it 'has a preamble that is valid HTML' do + expect(preamble).to be_html_safe + + expect { Nokogiri::XML(preamble) { |config| config.strict } }.to_not raise_error + end end end diff --git a/spec/lib/reporting/cloudwatch_client_spec.rb b/spec/lib/reporting/cloudwatch_client_spec.rb index dd8f8aa5832..2fcd16f3514 100644 --- a/spec/lib/reporting/cloudwatch_client_spec.rb +++ b/spec/lib/reporting/cloudwatch_client_spec.rb @@ -233,6 +233,9 @@ def stub_single_page ), }, } + + # override Zonebie + allow(Time).to receive(:zone).and_return(nil) end it 'logs a warning and returns an empty array for that range' do diff --git a/spec/lib/script_base_spec.rb b/spec/lib/script_base_spec.rb index d3bb75e22c5..d121f02b49a 100644 --- a/spec/lib/script_base_spec.rb +++ b/spec/lib/script_base_spec.rb @@ -44,5 +44,33 @@ def run(args:, config:) # rubocop:disable Lint/UnusedMethodArgument expect(JSON.parse(Zlib::Inflate.inflate(Base64.decode64(stdout.string)))).to eq(table) end end + + context 'throwing an error inside the task' do + let(:subtask_class) do + Class.new do + def run(args:, config:) # rubocop:disable Lint/UnusedMethodArgument + raise 'some dangerous error' + end + end + end + + before do + base.config.format = :csv + end + + it 'logs the error message to stderr but not the backtrace' do + expect(base).to receive(:exit).with(1) + + expect { base.run }.to_not raise_error + + expect(stderr.string.chomp).to eq('RuntimeError: some dangerous error') + expect(CSV.parse(stdout.string)).to eq( + [ + %w[Error Message], + ['RuntimeError', 'some dangerous error'], + ], + ) + end + end end end diff --git a/spec/mailers/previews/report_mailer_preview.rb b/spec/mailers/previews/report_mailer_preview.rb index 291cc3ed4ae..4c247c4c57b 100644 --- a/spec/mailers/previews/report_mailer_preview.rb +++ b/spec/mailers/previews/report_mailer_preview.rb @@ -16,7 +16,7 @@ def monthly_key_metrics_report ReportMailer.tables_report( email: 'test@example.com', subject: 'Example Key Metrics Report', - message: 'Key Metrics Report February 2021', + message: monthly_key_metrics_report.preamble, attachment_format: :xlsx, reports: monthly_key_metrics_report.reports, ) diff --git a/spec/services/calendar_service_spec.rb b/spec/services/calendar_service_spec.rb index 904a6a9a11d..96c8c5727e7 100644 --- a/spec/services/calendar_service_spec.rb +++ b/spec/services/calendar_service_spec.rb @@ -164,6 +164,46 @@ it { is_expected.to eq(true) } end end + + describe '.fiscal_start_date' do + subject { described_class.fiscal_start_date(date) } + + context 'when the date is on or after October' do + let(:date) { Date.new(year, 11, 15) } + + it 'calculates the correct fiscal start date' do + expect(subject).to eq Date.new(2018, 10, 1) + end + end + + context 'when the date is before October' do + let(:date) { Date.new(year, 8, 15) } + + it 'calculates the correct fiscal start date' do + expect(subject).to eq Date.new(2017, 10, 1) + end + end + end + + describe '.fiscal_end_date' do + subject { described_class.fiscal_end_date(date) } + + context 'when the date is on or after October' do + let(:date) { Date.new(year, 11, 15) } + + it 'calculates the correct fiscal end date' do + expect(subject).to eq DateTime.new(2019, 9, 30) + end + end + + context 'when the date is before October' do + let(:date) { Date.new(year, 8, 15) } + + it 'calculates the correct fiscal end date' do + expect(subject).to eq DateTime.new(2018, 9, 30) + end + end + end end def holidays diff --git a/spec/services/doc_auth/acuant/acuant_client_spec.rb b/spec/services/doc_auth/acuant/acuant_client_spec.rb index ea74ae32ced..ab68738fce2 100644 --- a/spec/services/doc_auth/acuant/acuant_client_spec.rb +++ b/spec/services/doc_auth/acuant/acuant_client_spec.rb @@ -216,4 +216,51 @@ ) end end + + context 'when there is expected rxx http status code' do + shared_examples 'with http status' do |status| + it "generate response for status #{status} " do + instance_id = 'this-is-a-test-instance-id' + url = URI.join( + assure_id_url, "/AssureIDService/Document/#{instance_id}/Image" + ) + stub_request(:post, url).with(query: { side: 0, light: 0 }).to_return( + body: '', + status: status, + ) + + result = subject.post_front_image( + instance_id: instance_id, + image: DocAuthImageFixtures.document_front_image, + ) + expect(result.exception.message).not_to be_nil + case status + when 440 + expect(result.errors).to eql( + { + general: [DocAuth::Errors::IMAGE_SIZE_FAILURE], + front: [DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD], + }, + ) + when 438 + expect(result.errors).to eql( + { + general: [DocAuth::Errors::IMAGE_LOAD_FAILURE], + front: [DocAuth::Errors::IMAGE_LOAD_FAILURE_FIELD], + }, + ) + when 439 + expect(result.errors).to eql( + { + general: [DocAuth::Errors::PIXEL_DEPTH_FAILURE], + front: [DocAuth::Errors::PIXEL_DEPTH_FAILURE_FIELD], + }, + ) + end + end + end + it_should_behave_like 'with http status', 440 + it_should_behave_like 'with http status', 439 + it_should_behave_like 'with http status', 438 + end end diff --git a/spec/services/doc_auth/acuant/requests/get_results_request_spec.rb b/spec/services/doc_auth/acuant/requests/get_results_request_spec.rb index 4dd103ff2a2..8865965b7c5 100644 --- a/spec/services/doc_auth/acuant/requests/get_results_request_spec.rb +++ b/spec/services/doc_auth/acuant/requests/get_results_request_spec.rb @@ -26,7 +26,7 @@ it 'get general error for 4xx' do stub_request(:get, url).to_return(status: 440) response = described_class.new(config: config, instance_id: instance_id).fetch - expect(response.errors).to have_key(:general) + expect(response.errors).to include(:general, :front, :back) expect(response.network_error?).to eq(false) end diff --git a/spec/services/doc_auth/acuant/requests/upload_image_request_spec.rb b/spec/services/doc_auth/acuant/requests/upload_image_request_spec.rb index 84df287a0cd..34cf0438e7d 100644 --- a/spec/services/doc_auth/acuant/requests/upload_image_request_spec.rb +++ b/spec/services/doc_auth/acuant/requests/upload_image_request_spec.rb @@ -29,6 +29,36 @@ expect(response.exception).to be_nil expect(request_stub).to have_been_requested end + + context 'when http status is 4xx' do + shared_examples 'http expected 4xx status' do |http_status, general_failure, side_failure| + it "generate errors for #{http_status}" do + request_stub = stub_request(:post, url).with( + query: { side: 0, light: 0 }, + body: DocAuthImageFixtures.document_front_image, + ).to_return(body: '', status: http_status) + + request = described_class.new( + config: config, + image_data: DocAuthImageFixtures.document_front_image, + instance_id: instance_id, + side: :front, + ) + response = request.fetch + expect(response.success?).to eq(false) + expect(response.errors).to eq({ front: [side_failure], general: [general_failure] }) + expect(response.exception).not_to be_nil + expect(request_stub).to have_been_requested + end + end + + it_should_behave_like 'http expected 4xx status', 440, 'image_size_failure', + 'image_size_failure_field' + it_should_behave_like 'http expected 4xx status', 438, 'image_load_failure', + 'image_load_failure_field' + it_should_behave_like 'http expected 4xx status', 439, 'pixel_depth_failure', + 'pixel_depth_failure_field' + end end context 'with a back image' do diff --git a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb index 69f6c27c37c..5795ba59d60 100644 --- a/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb +++ b/spec/services/doc_auth/acuant/responses/get_results_response_spec.rb @@ -163,7 +163,7 @@ end end - context 'with a failed result of unknow document type' do + context 'with a failed result' do let(:http_response) do instance_double( Faraday::Response, diff --git a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb index 7a96f64a9a4..41b40f1fc8c 100644 --- a/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb +++ b/spec/services/doc_auth/mock/doc_auth_mock_client_spec.rb @@ -218,7 +218,10 @@ ) expect(response).to be_a(DocAuth::Response) expect(response.success?).to eq(false) - expect(response.errors).to eq(general: [DocAuth::Errors::IMAGE_SIZE_FAILURE]) + expect(response.errors).to eq( + { general: [DocAuth::Errors::IMAGE_SIZE_FAILURE], + front: [DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD] }, + ) end end end diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb index f63c85f2675..bbfcaec3c8c 100644 --- a/spec/services/doc_auth_router_spec.rb +++ b/spec/services/doc_auth_router_spec.rb @@ -230,22 +230,67 @@ def reload_ab_test_initializer! ) end - it 'translates http response errors and maintains exceptions' do - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :post_images, - response: DocAuth::Response.new( - success: false, - errors: { - general: [DocAuth::Errors::IMAGE_LOAD_FAILURE], - }, - exception: DocAuth::RequestError.new('Test 438 HTTP failure', 438), - ), - ) - - response = proxy.post_images(front_image: 'a', back_image: 'b') - - expect(response.errors).to eq(general: [I18n.t('doc_auth.errors.http.image_load')]) - expect(response.exception.message).to eq('Test 438 HTTP failure') + context 'translates http response errors and maintains exceptions' do + it 'translate general message' do + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_images, + response: DocAuth::Response.new( + success: false, + errors: { + general: [DocAuth::Errors::IMAGE_LOAD_FAILURE], + }, + exception: DocAuth::RequestError.new('Test 438 HTTP failure', 438), + ), + ) + + response = proxy.post_images(front_image: 'a', back_image: 'b') + expect(response.errors).to eq(general: [I18n.t('doc_auth.errors.http.image_load.top_msg')]) + expect(response.exception.message).to eq('Test 438 HTTP failure') + end + it 'translate related inline error messages for both sides' do + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_images, + response: DocAuth::Response.new( + success: false, + errors: { + general: [DocAuth::Errors::IMAGE_SIZE_FAILURE], + front: [DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD], + back: [DocAuth::Errors::IMAGE_SIZE_FAILURE_FIELD], + }, + exception: DocAuth::RequestError.new('Test 440 HTTP failure', 440), + ), + ) + + response = proxy.post_images(front_image: 'a', back_image: 'b') + + expect(response.errors).to eq( + general: [I18n.t('doc_auth.errors.http.image_size.top_msg')], + front: [I18n.t('doc_auth.errors.http.image_size.failed_short')], + back: [I18n.t('doc_auth.errors.http.image_size.failed_short')], + ) + expect(response.exception.message).to eq('Test 440 HTTP failure') + end + it 'translate related side specific inline error message' do + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_images, + response: DocAuth::Response.new( + success: false, + errors: { + general: [DocAuth::Errors::PIXEL_DEPTH_FAILURE], + front: [DocAuth::Errors::PIXEL_DEPTH_FAILURE_FIELD], + }, + exception: DocAuth::RequestError.new('Test 439 HTTP failure', 439), + ), + ) + + response = proxy.post_images(front_image: 'a', back_image: 'b') + + expect(response.errors).to eq( + general: [I18n.t('doc_auth.errors.http.pixel_depth.top_msg')], + front: [I18n.t('doc_auth.errors.http.pixel_depth.failed_short')], + ) + expect(response.exception.message).to eq('Test 439 HTTP failure') + end end it 'translates doc type error' do diff --git a/spec/services/reporting/active_users_count_report_spec.rb b/spec/services/reporting/active_users_count_report_spec.rb new file mode 100644 index 00000000000..5d9c80e3e1d --- /dev/null +++ b/spec/services/reporting/active_users_count_report_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +RSpec.describe Reporting::ActiveUsersCountReport do + let(:report_date) { Date.new(2023, 3, 1) } + + subject(:report) { Reporting::ActiveUsersCountReport.new(report_date) } + let(:sp1) { create(:service_provider) } + let(:sp2) { create(:service_provider) } + + before do + travel_to report_date + end + + describe '#result' do + it 'returns a report for active user' do + create( + :service_provider_identity, + user_id: 1, + service_provider_record: sp1, + last_ial1_authenticated_at: report_date - 5.days, + ) + create( + :service_provider_identity, + user_id: 1, + service_provider_record: sp2, + last_ial2_authenticated_at: report_date - 2.days, + ) + + create( + :service_provider_identity, + user_id: 2, + service_provider_record: sp1, + last_ial1_authenticated_at: report_date - 2.days, + ) + + create( + :service_provider_identity, + user_id: 3, + service_provider_record: sp1, + last_ial1_authenticated_at: Date.new(2022, 10, 1), + ) + create( + :service_provider_identity, + user_id: 3, + service_provider_record: sp2, + last_ial2_authenticated_at: Date.new(2022, 10, 10), + ) + + create( + :service_provider_identity, + user_id: 4, + service_provider_record: sp1, + last_ial1_authenticated_at: Date.new(2022, 12, 1), + ) + + active_users_count_table = report.generate_report + + expected_table = [ + ['Active Users', 'IAL1', 'IDV', 'Total', 'Range start', 'Range end'], + [ + 'Monthly February 2023', + 1, + 1, + 2, + Date.new(2023, 2, 1), + Date.new(2023, 2, 28), + ], + [ + 'Fiscal Year 2023', + 2, + 2, + 4, + Date.new(2022, 10, 1), + Date.new(2023, 9, 30), + ], + ] + + expect(active_users_count_table).to eq(expected_table) + + emailable_report = report.active_users_count_emailable_report + expect(emailable_report.title).to eq('Active Users') + expect(emailable_report.table).to eq active_users_count_table + expect(emailable_report.filename).to eq 'active_users_count' + end + end +end diff --git a/spec/services/reporting/monthly_active_users_count_report_spec.rb b/spec/services/reporting/monthly_active_users_count_report_spec.rb deleted file mode 100644 index 291ae027d5f..00000000000 --- a/spec/services/reporting/monthly_active_users_count_report_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -RSpec.describe Reporting::MonthlyActiveUsersCountReport do - let(:report_date) { Date.new(2023, 3, 1) } - - subject(:report) { Reporting::MonthlyActiveUsersCountReport.new(report_date) } - let(:sp1) { create(:service_provider) } - let(:sp2) { create(:service_provider) } - - before do - travel_to report_date - end - - describe '#result' do - it 'returns a report for monthly active user' do - create( - :service_provider_identity, - user_id: 1, - service_provider_record: sp1, - last_ial1_authenticated_at: report_date - 5.days, - ) - create( - :service_provider_identity, - user_id: 1, - service_provider_record: sp2, - last_ial2_authenticated_at: report_date - 2.days, - ) - - create( - :service_provider_identity, - user_id: 2, - service_provider_record: sp1, - last_ial1_authenticated_at: report_date - 2.days, - ) - monthly_active_users_count_table = report.monthly_active_users_count_report - - expected_table = [ - ['Monthly Active Users', 'Value'], - ['IAL1', 1], - ['IDV', 1], - ['Total', 2], - ] - - expect(monthly_active_users_count_table).to eq(expected_table) - - emailable_report = report.monthly_active_users_count_emailable_report - expect(emailable_report.title).to eq('February 2023 Active Users') - expect(emailable_report.table).to eq monthly_active_users_count_table - expect(emailable_report.filename).to eq 'monthly_active_users_count' - end - end -end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 0fb2cd3e0a2..df1ba972f36 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -175,7 +175,7 @@ def complete_all_doc_auth_steps_before_password_step(expect_accessible: false) fill_out_phone_form_ok if find('#idv_phone_form_phone').value.blank? click_continue verify_phone_otp - expect(page).to have_current_path(idv_review_path, wait: 10) + expect(page).to have_current_path(idv_enter_password_path, wait: 10) expect_page_to_have_no_accessibility_violations(page) if expect_accessible end @@ -248,7 +248,7 @@ def mock_doc_auth_trueid_http_non2xx_status(status) def mock_doc_auth_acuant_http_4xx_status(status, method = :post_front_image) DocAuth::Mock::DocAuthMockClient.mock_response!( method: method, - response: DocAuth::Mock::ResultResponse.create_image_error_response(status), + response: DocAuth::Mock::ResultResponse.create_image_error_response(status, 'front'), ) end diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index ca5dcdcf67b..df666b4862a 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -53,4 +53,12 @@ def api_image_submission_test_credential_part def click_try_again click_spinner_button_and_wait t('idv.failure.button.warning') end + + def click_sp_exit_link(sp_name: 'Test SP') + click_on "exit Login.gov and contact #{sp_name}" + end + + def click_submit_exit_button + click_on 'Submit and exit Login.gov' + end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index cb903f583d8..c786310c475 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -111,7 +111,7 @@ def fill_in_bad_piv_cac_credentials_and_submit def fill_in_credentials_and_submit(email, password) fill_in t('account.index.email'), with: email fill_in t('account.index.password'), with: password - click_button t('links.next') + click_button t('links.sign_in') end def continue_as(email = nil, password = VALID_PASSWORD) diff --git a/spec/support/features/webauthn_helper.rb b/spec/support/features/webauthn_helper.rb index 09acbb22143..963f6b2dda3 100644 --- a/spec/support/features/webauthn_helper.rb +++ b/spec/support/features/webauthn_helper.rb @@ -25,7 +25,7 @@ def fill_in_nickname_and_click_continue(nickname: 'mykey') end def mock_submit_without_pressing_button_on_hardware_key_on_setup - first('#continue-button').click + click_continue end def mock_press_button_on_hardware_key_on_setup @@ -37,11 +37,10 @@ def mock_press_button_on_hardware_key_on_setup set_hidden_field('attestation_object', attestation_object) set_hidden_field('client_data_json', setup_client_data_json) - button = first('#continue-button') if javascript_enabled? page.evaluate_script('document.querySelector("form").submit()') else - button.click + click_continue end end diff --git a/spec/support/idv_examples/max_attempts.rb b/spec/support/idv_examples/max_attempts.rb index ef0e19cdf47..be2c90027c1 100644 --- a/spec/support/idv_examples/max_attempts.rb +++ b/spec/support/idv_examples/max_attempts.rb @@ -52,7 +52,7 @@ fill_out_phone_form_ok verify_phone_otp - expect(page).to have_current_path(idv_review_path, wait: 10) + expect(page).to have_current_path(idv_enter_password_path, wait: 10) end end diff --git a/spec/support/matchers/accessibility.rb b/spec/support/matchers/accessibility.rb index 86632bacb33..296212cd7c3 100644 --- a/spec/support/matchers/accessibility.rb +++ b/spec/support/matchers/accessibility.rb @@ -235,3 +235,10 @@ def expect_page_to_have_no_accessibility_violations(page, validate_markup: true) expect(page).to be_uniquely_titled expect(page).to have_valid_markup if validate_markup end + +def activate_skip_link + page.evaluate_script('document.activeElement.blur()') + page.active_element.send_keys(:tab) + expect(page.active_element).to have_content(t('shared.skip_link'), wait: 5) + page.active_element.send_keys(:enter) +end diff --git a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb index a33c50c7484..91c7e113754 100644 --- a/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb +++ b/spec/views/idv/by_mail/enter_code/index.html.erb_spec.rb @@ -9,10 +9,12 @@ {} end - let(:should_prompt_user_to_request_another_letter) { true } + let(:can_request_another_letter) { true } let(:user_did_not_receive_letter) { false } + let(:last_date_letter_was_sent) { 2.days.ago } + before do allow(view).to receive(:step_indicator_steps).and_return({}) @@ -22,8 +24,9 @@ otp: '1234', ) - @should_prompt_user_to_request_another_letter = should_prompt_user_to_request_another_letter + @can_request_another_letter = can_request_another_letter @user_did_not_receive_letter = user_did_not_receive_letter + @last_date_letter_was_sent = last_date_letter_was_sent render end @@ -35,7 +38,7 @@ end context 'user is NOT allowed to request another GPO letter' do - let(:should_prompt_user_to_request_another_letter) { false } + let(:can_request_another_letter) { false } it 'does not include the send another letter link' do expect(rendered).not_to have_link(t('idv.messages.gpo.resend'), href: idv_request_letter_path) end @@ -84,7 +87,7 @@ end context 'user is NOT allowed to request another GPO letter' do - let(:should_prompt_user_to_request_another_letter) { false } + let(:can_request_another_letter) { false } it 'still has a special intro' do expect(rendered).to have_content( diff --git a/spec/views/idv/session_errors/warning.html.erb_spec.rb b/spec/views/idv/session_errors/warning.html.erb_spec.rb index 61471ad97af..2f6d15f38bc 100644 --- a/spec/views/idv/session_errors/warning.html.erb_spec.rb +++ b/spec/views/idv/session_errors/warning.html.erb_spec.rb @@ -14,6 +14,8 @@ assign(:remaining_attempts, remaining_attempts) assign(:try_again_path, try_again_path) + @step_indicator_steps = Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS + render end diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index 2927bccf663..c34b69ba605 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -12,6 +12,7 @@ let(:in_person_proofing_enabled_issuer) { nil } let(:acuant_sdk_upgrade_a_b_testing_enabled) { false } let(:use_alternate_sdk) { false } + let(:phone_question_ab_test_bucket) { :bypass_phone_question } let(:acuant_version) { '1.3.3.7' } before do @@ -41,6 +42,7 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + phone_question_ab_test_bucket: phone_question_ab_test_bucket, } end diff --git a/spec/views/sign_up/registrations/new.html.erb_spec.rb b/spec/views/sign_up/registrations/new.html.erb_spec.rb index 08e4dc37cc2..30989420c94 100644 --- a/spec/views/sign_up/registrations/new.html.erb_spec.rb +++ b/spec/views/sign_up/registrations/new.html.erb_spec.rb @@ -48,7 +48,7 @@ render expect(rendered).to have_link( - t('links.next'), + t('links.sign_in'), href: new_user_session_url(request_id: nil), ) end