diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 409137aa4b6..6500200f6a6 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -320,13 +320,17 @@ review-app:
{"name": "POSTGRES_WORKER_HOST", "value": "$CI_ENVIRONMENT_SLUG-identity-idp-chart-postgres.review-apps"},
{"name": "POSTGRES_WORKER_USERNAME", "value": "postgres"},
{"name": "POSTGRES_WORKER_PASSWORD", "value": "postgres"},
- {"name": "LOGIN_ENV", "value": "dev"},
{"name": "RAILS_OFFLINE", "value": "true"},
{"name": "REDIS_IRS_ATTEMPTS_API_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/2"},
{"name": "REDIS_THROTTLE_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/1"},
{"name": "REDIS_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379"},
{"name": "ASSET_HOST", "value": "https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"},
- {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}
+ {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"},
+ {"name": "LOGIN_DATACENTER", "value": "true" },
+ {"name": "LOGIN_DOMAIN", "value": "identitysandbox.gov"},
+ {"name": "LOGIN_ENV", "value": "$CI_ENVIRONMENT_SLUG" },
+ {"name": "LOGIN_HOST_ROLE", "value": "idp" },
+ {"name": "LOGIN_SKIP_REMOTE_CONFIG", "value": "true" }
]
EOF
)
@@ -343,13 +347,17 @@ review-app:
{"name": "POSTGRES_WORKER_HOST", "value": "$CI_ENVIRONMENT_SLUG-identity-idp-chart-postgres.review-apps"},
{"name": "POSTGRES_WORKER_USERNAME", "value": "postgres"},
{"name": "POSTGRES_WORKER_PASSWORD", "value": "postgres"},
- {"name": "LOGIN_ENV", "value": "dev"},
{"name": "RAILS_OFFLINE", "value": "true"},
{"name": "REDIS_IRS_ATTEMPTS_API_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/2"},
{"name": "REDIS_THROTTLE_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379/1"},
{"name": "REDIS_URL", "value": "redis://$CI_ENVIRONMENT_SLUG-identity-idp-chart-redis.review-apps:6379"},
{"name": "ASSET_HOST", "value": "https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"},
- {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"}
+ {"name": "DOMAIN_NAME", "value": "$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov"},
+ {"name": "LOGIN_DATACENTER", "value": "true" },
+ {"name": "LOGIN_DOMAIN", "value": "identitysandbox.gov"},
+ {"name": "LOGIN_ENV", "value": "$CI_ENVIRONMENT_SLUG" },
+ {"name": "LOGIN_HOST_ROLE", "value": "worker" },
+ {"name": "LOGIN_SKIP_REMOTE_CONFIG", "value": "true" }
]
EOF
)
@@ -366,6 +374,8 @@ review-app:
--set-json idp.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]"
$CI_ENVIRONMENT_SLUG ./charts
- echo "DNS may take a while to propagate, so be patient if it doesn't show up right away"
+ - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name review_app'"
+ - echo "Then run 'aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-identity-idp-chart-idp -n review-apps -- /app/bin/rails console'"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov
diff --git a/Gemfile.lock b/Gemfile.lock
index 614fd56a66a..12315d2a83e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,9 +1,9 @@
GIT
remote: https://github.com/18F/identity-hostdata.git
- revision: e33cbdb3a9c8826f6fc2b1f857fb713a4a233750
+ revision: 9e2e0441cd93307cbfc5d5b8d4b3b7b4219394fb
tag: v3.4.2
specs:
- identity-hostdata (3.4.1)
+ identity-hostdata (3.4.2)
activesupport (>= 6.1, < 8)
aws-sdk-s3 (~> 1.8)
@@ -139,17 +139,17 @@ GEM
ast (2.4.2)
awrence (1.2.1)
aws-eventstream (1.2.0)
- aws-partitions (1.684.0)
+ aws-partitions (1.792.0)
aws-sdk-cloudwatchlogs (1.49.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
- aws-sdk-core (3.168.4)
+ aws-sdk-core (3.179.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.61.0)
- aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sdk-kms (1.71.0)
+ aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-pinpoint (1.62.0)
aws-sdk-core (~> 3, >= 3.122.0)
@@ -157,10 +157,10 @@ GEM
aws-sdk-pinpointsmsvoice (1.29.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.117.2)
- aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sdk-s3 (1.132.0)
+ aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1)
- aws-sigv4 (~> 1.4)
+ aws-sigv4 (~> 1.6)
aws-sdk-ses (1.44.0)
aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.1)
@@ -170,7 +170,7 @@ GEM
aws-sdk-sqs (1.53.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
- aws-sigv4 (1.5.2)
+ aws-sigv4 (1.6.0)
aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.3.2)
dumb_delegator
diff --git a/app/assets/stylesheets/components/_file-input.scss b/app/assets/stylesheets/components/_file-input.scss
index a8c626ef081..ade8a41a785 100644
--- a/app/assets/stylesheets/components/_file-input.scss
+++ b/app/assets/stylesheets/components/_file-input.scss
@@ -1,4 +1,5 @@
@use 'uswds-core' as *;
+@use '../utilities/typography' as *;
// ===============================================
// Pending upstream Login Design System revisions:
@@ -45,15 +46,14 @@
.usa-file-input__banner-text {
@include u-font-family('sans');
+ @extend %h2;
color: color('primary');
display: block;
- font-size: 1.625rem;
letter-spacing: 0.4px;
line-height: 1.5;
// For content to appear as vertically centered, offset the larger line-height of the banner to
// match the space below the drag text.
margin-top: ((1.5rem - size('body', '2xs')) - ((1.625rem * 1.5) - 1.625rem)) * 0.5;
- text-transform: uppercase;
+ .usa-file-input__drag-text {
@include u-display('block');
diff --git a/app/components/webauthn_input_component.rb b/app/components/webauthn_input_component.rb
index 069f539ee9e..60d4dc22cf5 100644
--- a/app/components/webauthn_input_component.rb
+++ b/app/components/webauthn_input_component.rb
@@ -22,10 +22,16 @@ def call
:'lg-webauthn-input',
content,
**tag_options,
- hidden: true,
- platform: platform?.presence,
- 'passkey-supported-only': passkey_supported_only?.presence,
+ **initial_hidden_tag_options,
'show-unsupported-passkey': show_unsupported_passkey?.presence,
)
end
+
+ def initial_hidden_tag_options
+ if platform? && passkey_supported_only?
+ { hidden: true }
+ else
+ { class: 'js' }
+ end
+ end
end
diff --git a/app/controllers/concerns/reauthentication_required_concern.rb b/app/controllers/concerns/reauthentication_required_concern.rb
index 2057bac6afa..e5e6b3291c8 100644
--- a/app/controllers/concerns/reauthentication_required_concern.rb
+++ b/app/controllers/concerns/reauthentication_required_concern.rb
@@ -7,7 +7,6 @@ def confirm_recently_authenticated_2fa
non_remembered_device_authentication = user_session[:auth_method].present? &&
user_session[:auth_method] != 'remember_device'
return if recently_authenticated? && non_remembered_device_authentication
- return if in_multi_mfa_selection_flow?
analytics.user_2fa_reauthentication_required(
auth_method: user_session[:auth_method],
diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb
index 16f64a09205..82131fe8de2 100644
--- a/app/controllers/idv/hybrid_handoff_controller.rb
+++ b/app/controllers/idv/hybrid_handoff_controller.rb
@@ -176,7 +176,7 @@ def rate_limited_failure
throttle_type: :idv_send_link,
)
message = I18n.t(
- 'errors.doc_auth.send_link_throttle',
+ 'errors.doc_auth.send_link_limited',
timeout: distance_of_time_in_words(
Time.zone.now,
[rate_limiter.expires_at, Time.zone.now].compact.max,
diff --git a/app/controllers/idv/in_person/ssn_controller.rb b/app/controllers/idv/in_person/ssn_controller.rb
index 7fb74026d54..e59145adf4b 100644
--- a/app/controllers/idv/in_person/ssn_controller.rb
+++ b/app/controllers/idv/in_person/ssn_controller.rb
@@ -35,15 +35,15 @@ def update
analytics.idv_doc_auth_ssn_submitted(
**analytics_arguments.merge(form_response.to_h),
)
+ # This event is not currently logging but should be kept as decided in LG-10110
irs_attempts_api_tracker.idv_ssn_submitted(
ssn: params[:doc_auth][:ssn],
)
if form_response.success?
flow_session['pii_from_user'][:ssn] = params[:doc_auth][:ssn]
-
idv_session.invalidate_steps_after_ssn!
- redirect_to next_url
+ redirect_to idv_in_person_verify_info_url
else
@error_message = form_response.first_error_message
render :show, locals: extra_view_variables
@@ -73,10 +73,6 @@ def confirm_repeat_ssn
redirect_to idv_in_person_verify_info_url
end
- def next_url
- idv_in_person_verify_info_url
- end
-
def analytics_arguments
{
flow_path: flow_path,
diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb
index d1f5cd6ca05..20e722786a5 100644
--- a/app/controllers/idv/phone_controller.rb
+++ b/app/controllers/idv/phone_controller.rb
@@ -30,7 +30,7 @@ def new
Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer).
call(:verify_phone, :view, true)
- analytics.idv_phone_of_record_visited
+ analytics.idv_phone_of_record_visited(**ab_test_analytics_buckets)
render :new, locals: { gpo_letter_available: gpo_letter_available }
elsif async_state.missing?
analytics.proofing_address_result_missing
@@ -141,6 +141,7 @@ def set_idv_form
previous_params: idv_session.previous_phone_step_params,
allowed_countries:
PhoneNumberCapabilities::ADDRESS_IDENTITY_PROOFING_SUPPORTED_COUNTRY_CODES,
+ failed_phone_numbers: idv_session.failed_phone_step_numbers,
)
end
diff --git a/app/controllers/sign_up/completions_controller.rb b/app/controllers/sign_up/completions_controller.rb
index 98281429d12..d9a82d95cd3 100644
--- a/app/controllers/sign_up/completions_controller.rb
+++ b/app/controllers/sign_up/completions_controller.rb
@@ -17,7 +17,6 @@ def show
def update
track_completion_event('agency-page')
- irs_attempts_api_tracker.idv_reproof # if current_user.profiles&.last&.has_proofed_before?
update_verified_attributes
send_in_person_completion_survey
if decider.go_back_to_mobile_app?
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
index 59c112056e8..f3cf3e06570 100644
--- a/app/controllers/users/two_factor_authentication_controller.rb
+++ b/app/controllers/users/two_factor_authentication_controller.rb
@@ -366,7 +366,7 @@ def webauthn_params
def handle_too_many_confirmation_sends
flash[:error] = t(
- 'errors.messages.phone_confirmation_throttled',
+ 'errors.messages.phone_confirmation_limited',
timeout: distance_of_time_in_words(
Time.zone.now,
[phone_confirmation_rate_limiter.expires_at, Time.zone.now].compact.max,
diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb
index 3c9261d92df..ad1d34425b1 100644
--- a/app/forms/gpo_verify_form.rb
+++ b/app/forms/gpo_verify_form.rb
@@ -17,15 +17,16 @@ def initialize(user:, pii:, otp: nil)
def submit
result = valid?
- threatmetrix_check_failed = fraud_review_checker.fraud_check_failed?
+ fraud_check_failed = pending_profile&.fraud_pending_reason.present?
+
if result
pending_profile&.remove_gpo_deactivation_reason
if pending_in_person_enrollment?
UspsInPersonProofing::EnrollmentHelper.schedule_in_person_enrollment(user, pii)
pending_profile&.deactivate(:in_person_verification_pending)
- elsif fraud_review_checker.fraud_check_failed? && threatmetrix_enabled?
+ elsif fraud_check_failed && threatmetrix_enabled?
pending_profile&.deactivate_for_fraud_review
- elsif fraud_review_checker.fraud_check_failed?
+ elsif fraud_check_failed
pending_profile&.activate_after_fraud_review_unnecessary
else
activate_profile
@@ -40,7 +41,7 @@ def submit
enqueued_at: gpo_confirmation_code&.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
pending_in_person_enrollment: pending_in_person_enrollment?,
- threatmetrix_check_failed: threatmetrix_check_failed,
+ fraud_check_failed: fraud_check_failed,
},
)
end
diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb
index 71ae667d833..f6bc8c6841c 100644
--- a/app/forms/idv/api_image_upload_form.rb
+++ b/app/forms/idv/api_image_upload_form.rb
@@ -201,7 +201,7 @@ def limit_if_rate_limited
return unless document_capture_session
return unless rate_limited?
- errors.add(:limit, t('errors.doc_auth.throttled_heading'), type: :throttled)
+ errors.add(:limit, t('errors.doc_auth.rate_limited_heading'), type: :throttled)
end
def track_rate_limited
diff --git a/app/forms/idv/phone_form.rb b/app/forms/idv/phone_form.rb
index cb45eac0ef1..e572fb82b30 100644
--- a/app/forms/idv/phone_form.rb
+++ b/app/forms/idv/phone_form.rb
@@ -5,7 +5,7 @@ class PhoneForm
ALL_DELIVERY_METHODS = [:sms, :voice].freeze
attr_reader :user, :phone, :allowed_countries, :delivery_methods, :international_code,
- :otp_delivery_preference
+ :otp_delivery_preference, :failed_phone_numbers
validate :validate_valid_phone_for_allowed_countries
validate :validate_phone_delivery_methods
@@ -19,12 +19,14 @@ def initialize(
user:,
previous_params:,
allowed_countries: nil,
- delivery_methods: ALL_DELIVERY_METHODS
+ delivery_methods: ALL_DELIVERY_METHODS,
+ failed_phone_numbers: []
)
previous_params ||= {}
@user = user
@allowed_countries = allowed_countries
@delivery_methods = delivery_methods
+ @failed_phone_numbers = failed_phone_numbers
@international_code, @phone = determine_initial_values(
**previous_params.
@@ -59,9 +61,12 @@ def determine_initial_values(international_code: nil, phone: nil)
international_code ||= country_code_for(phone)
end
- phone = PhoneFormatter.format(phone, country_code: international_code)
-
- [international_code, phone]
+ if failed_phone_numbers.include?(Phonelib.parse(phone).e164)
+ [nil, nil]
+ else
+ phone = PhoneFormatter.format(phone, country_code: international_code)
+ [international_code, phone]
+ end
end
def country_code_for(phone)
diff --git a/app/forms/register_user_email_form.rb b/app/forms/register_user_email_form.rb
index 17493594957..a0d1efc3ffd 100644
--- a/app/forms/register_user_email_form.rb
+++ b/app/forms/register_user_email_form.rb
@@ -85,6 +85,8 @@ def process_successful_submission(request_id, instructions)
# already taken and if so, we act as if the user registration was successful.
if email_address_record&.user&.suspended?
send_suspended_user_email(email_address_record)
+ elsif blocked_email_address
+ send_suspended_user_email(blocked_email_address)
elsif email_taken? && user_unconfirmed?
update_user_language_preference
send_sign_up_unconfirmed_email(request_id)
@@ -175,4 +177,9 @@ def existing_user
def email_request_id(request_id)
request_id if request_id.present? && ServiceProviderRequestProxy.find_by(uuid: request_id)
end
+
+ def blocked_email_address
+ return @blocked_email_address if defined?(@blocked_email_address)
+ @blocked_email_address = SuspendedEmail.find_with_email(email)
+ end
end
diff --git a/app/forms/reset_password_form.rb b/app/forms/reset_password_form.rb
index 6fe9f582364..f9ed09dc00e 100644
--- a/app/forms/reset_password_form.rb
+++ b/app/forms/reset_password_form.rb
@@ -8,6 +8,9 @@ class ResetPasswordForm
def initialize(user)
@user = user
+ @active_profile = user.active_profile
+ @pending_profile = user.pending_profile
+
self.reset_password_token = @user.reset_password_token
end
@@ -23,7 +26,7 @@ def submit(params)
private
- attr_reader :success
+ attr_reader :success, :active_profile, :pending_profile
def valid_token
if !user.persisted?
@@ -55,11 +58,9 @@ def update_user
end
def mark_profile_inactive
- profile = user.active_profile
- return if profile.blank?
+ return if active_profile.blank?
- @profile_deactivated = true
- profile&.deactivate(:password_reset)
+ active_profile.deactivate(:password_reset)
Funnel::DocAuth::ResetSteps.call(user.id)
user.proofing_component&.destroy
end
@@ -82,7 +83,9 @@ def invalid_account?
def extra_analytics_attributes
{
user_id: user.uuid,
- profile_deactivated: (@profile_deactivated == true),
+ profile_deactivated: active_profile.present?,
+ pending_profile_invalidated: pending_profile.present?,
+ pending_profile_pending_reasons: (pending_profile&.pending_reasons || [])&.join(','),
}
end
end
diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx
index 9920f60ad67..456ba250c20 100644
--- a/app/javascript/packages/document-capture/components/document-capture.tsx
+++ b/app/javascript/packages/document-capture/components/document-capture.tsx
@@ -118,7 +118,7 @@ function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
pii: submissionError.pii,
})(ReviewIssuesStep)
: ReviewIssuesStep,
- title: t('errors.doc_auth.throttled_heading'),
+ title: t('errors.doc_auth.rate_limited_heading'),
},
] as FormStep[]
).concat(inPersonSteps)
diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx
index 001fc0742f6..5188f106a1d 100644
--- a/app/javascript/packages/document-capture/components/review-issues-step.tsx
+++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx
@@ -103,7 +103,7 @@ function ReviewIssuesStep({
return (
<>
}
>
-
= new Set(['NotAllowedError', 'TimeoutError']);
+
+const isExpectedWebauthnError = (error: Error): boolean =>
+ error instanceof DOMException && EXPECTED_DOM_EXCEPTIONS.has(error.name);
+
+export default isExpectedWebauthnError;
diff --git a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
index 18fb3e36093..7c6ee1a0ffb 100644
--- a/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
+++ b/app/javascript/packages/webauthn/webauthn-input-element.spec.ts
@@ -17,73 +17,45 @@ describe('WebauthnInputElement', () => {
quibble.reset();
});
- context('input for non-platform authenticator', () => {
- beforeEach(() => {
- document.body.innerHTML = ``;
- });
+ context('device does not support passkey', () => {
+ context('unsupported passkey not shown', () => {
+ beforeEach(() => {
+ isWebauthnPasskeySupported.returns(false);
+ document.body.innerHTML = ``;
+ });
- it('becomes visible', () => {
- const element = document.querySelector('lg-webauthn-input')!;
+ it('stays hidden', () => {
+ const element = document.querySelector('lg-webauthn-input')!;
- expect(element.hidden).to.be.false();
+ expect(element.hidden).to.be.true();
+ });
});
- });
- context('input for platform authenticator', () => {
- context('no passkey only restriction', () => {
+ context('unsupported passkey shown', () => {
beforeEach(() => {
- document.body.innerHTML = ``;
+ isWebauthnPasskeySupported.returns(false);
+ document.body.innerHTML = ``;
});
- it('becomes visible', () => {
+ it('becomes visible, with modifier class', () => {
const element = document.querySelector('lg-webauthn-input')!;
expect(element.hidden).to.be.false();
+ expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true();
});
});
+ });
- context('passkey supported only', () => {
- context('device does not support passkey', () => {
- context('unsupported passkey not shown', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(false);
- document.body.innerHTML = ``;
- });
-
- it('stays hidden', () => {
- const element = document.querySelector('lg-webauthn-input')!;
-
- expect(element.hidden).to.be.true();
- });
- });
-
- context('unsupported passkey shown', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(false);
- document.body.innerHTML = ``;
- });
-
- it('becomes visible, with modifier class', () => {
- const element = document.querySelector('lg-webauthn-input')!;
-
- expect(element.hidden).to.be.false();
- expect(element.classList.contains('webauthn-input--unsupported-passkey')).to.be.true();
- });
- });
- });
-
- context('device supports passkey', () => {
- beforeEach(() => {
- isWebauthnPasskeySupported.returns(true);
- document.body.innerHTML = ``;
- });
+ context('device supports passkey', () => {
+ beforeEach(() => {
+ isWebauthnPasskeySupported.returns(true);
+ document.body.innerHTML = ``;
+ });
- it('becomes visible', () => {
- const element = document.querySelector('lg-webauthn-input')!;
+ it('becomes visible', () => {
+ const element = document.querySelector('lg-webauthn-input')!;
- expect(element.hidden).to.be.false();
- });
- });
+ expect(element.hidden).to.be.false();
});
});
});
diff --git a/app/javascript/packages/webauthn/webauthn-input-element.ts b/app/javascript/packages/webauthn/webauthn-input-element.ts
index 1d2fd218fb6..11938ab51a6 100644
--- a/app/javascript/packages/webauthn/webauthn-input-element.ts
+++ b/app/javascript/packages/webauthn/webauthn-input-element.ts
@@ -2,27 +2,23 @@ import isWebauthnPasskeySupported from './is-webauthn-passkey-supported';
export class WebauthnInputElement extends HTMLElement {
connectedCallback() {
- this.toggleVisibleIfSupported();
+ this.toggleVisibleIfPasskeySupported();
}
get isPlatform(): boolean {
return this.hasAttribute('platform');
}
- get isOnlyPasskeySupported(): boolean {
- return this.hasAttribute('passkey-supported-only');
- }
-
get showUnsupportedPasskey(): boolean {
return this.hasAttribute('show-unsupported-passkey');
}
- isSupported(): boolean {
- return !this.isPlatform || !this.isOnlyPasskeySupported || isWebauthnPasskeySupported();
- }
+ toggleVisibleIfPasskeySupported() {
+ if (!this.hasAttribute('hidden')) {
+ return;
+ }
- toggleVisibleIfSupported() {
- if (this.isSupported()) {
+ if (isWebauthnPasskeySupported()) {
this.hidden = false;
} else if (this.showUnsupportedPasskey) {
this.hidden = false;
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 2df8b29f64b..42f3be18738 100644
--- a/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts
+++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.spec.ts
@@ -6,14 +6,17 @@ import type { WebauthnVerifyButtonDataset } from './webauthn-verify-button-eleme
describe('WebauthnVerifyButtonElement', () => {
const verifyWebauthnDevice = sinon.stub();
+ const trackError = sinon.stub();
before(async () => {
quibble('./verify-webauthn-device', verifyWebauthnDevice);
+ quibble('@18f/identity-analytics', { trackError });
await import('./webauthn-verify-button-element');
});
beforeEach(() => {
verifyWebauthnDevice.reset();
+ trackError.reset();
});
after(() => {
@@ -78,13 +81,33 @@ describe('WebauthnVerifyButtonElement', () => {
});
});
- it('submits with error name as input on thrown error', async () => {
+ it('submits with error name as input on thrown expected error', async () => {
+ const { form } = createElement();
+
+ verifyWebauthnDevice.throws(new DOMException('', 'NotAllowedError'));
+
+ const button = screen.getByRole('button', { name: 'Authenticate' });
+ await userEvent.click(button);
+ await expect(form.submit).to.eventually.be.called();
+
+ expect(Object.fromEntries(new window.FormData(form))).to.deep.equal({
+ credential_id: '',
+ authenticator_data: '',
+ client_data_json: '',
+ signature: '',
+ webauthn_error: 'NotAllowedError',
+ });
+ expect(trackError).not.to.have.been.called();
+ });
+
+ it('submits with error name as input and logs on thrown unexpected error', async () => {
const { form } = createElement();
class CustomError extends Error {
name = 'CustomError';
}
- verifyWebauthnDevice.throws(new CustomError());
+ const error = new CustomError();
+ verifyWebauthnDevice.throws(error);
const button = screen.getByRole('button', { name: 'Authenticate' });
await userEvent.click(button);
@@ -97,6 +120,7 @@ describe('WebauthnVerifyButtonElement', () => {
signature: '',
webauthn_error: 'CustomError',
});
+ expect(trackError).to.have.been.calledWith(error);
});
it('submits with verify result on successful verification', async () => {
diff --git a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts
index e8964761129..b876f970358 100644
--- a/app/javascript/packages/webauthn/webauthn-verify-button-element.ts
+++ b/app/javascript/packages/webauthn/webauthn-verify-button-element.ts
@@ -1,5 +1,7 @@
+import { trackError } from '@18f/identity-analytics';
import verifyWebauthnDevice from './verify-webauthn-device';
import type { VerifyCredentialDescriptor } from './verify-webauthn-device';
+import isExpectedWebauthnError from './is-expected-error';
export interface WebauthnVerifyButtonDataset extends DOMStringMap {
credentials: string;
@@ -50,6 +52,10 @@ class WebauthnVerifyButtonElement extends HTMLElement {
this.setInputValue('client_data_json', result.clientDataJSON);
this.setInputValue('signature', result.signature);
} catch (error) {
+ if (!isExpectedWebauthnError(error)) {
+ trackError(error);
+ }
+
this.setInputValue('webauthn_error', error.name);
}
diff --git a/app/javascript/packs/idv-phone-alert.ts b/app/javascript/packs/idv-phone-alert.ts
new file mode 100644
index 00000000000..0503bf6affc
--- /dev/null
+++ b/app/javascript/packs/idv-phone-alert.ts
@@ -0,0 +1,12 @@
+import type { PhoneInputElement } from '@18f/identity-phone-input';
+
+const alertElement = document.getElementById('phone-already-submitted-alert')!;
+const { iti, textInput: input } = document.querySelector('lg-phone-input') as PhoneInputElement;
+const failedPhoneNumbers: string[] = JSON.parse(alertElement.dataset.failedPhoneNumbers!);
+
+input.addEventListener('input', () => {
+ const isFailedPhoneNumber = failedPhoneNumbers.includes(
+ iti.getNumber(intlTelInputUtils.numberFormat.E164),
+ );
+ alertElement.hidden = !isFailedPhoneNumber;
+});
diff --git a/app/javascript/packs/webauthn-setup.ts b/app/javascript/packs/webauthn-setup.ts
index 09fbe0fa924..64a85949d32 100644
--- a/app/javascript/packs/webauthn-setup.ts
+++ b/app/javascript/packs/webauthn-setup.ts
@@ -1,4 +1,10 @@
-import { enrollWebauthnDevice, extractCredentials, longToByteArray } from '@18f/identity-webauthn';
+import {
+ enrollWebauthnDevice,
+ extractCredentials,
+ isExpectedWebauthnError,
+ longToByteArray,
+} from '@18f/identity-webauthn';
+import { trackError } from '@18f/identity-analytics';
import { forceRedirect } from '@18f/identity-url';
import type { Navigate } from '@18f/identity-url';
@@ -64,7 +70,13 @@ function webauthn() {
result.transports.join();
(document.getElementById('webauthn_form') as HTMLFormElement).submit();
})
- .catch((err) => reloadWithError(err.name, { force: true }));
+ .catch((error: Error) => {
+ if (!isExpectedWebauthnError(error)) {
+ trackError(error);
+ }
+
+ reloadWithError(error.name, { force: true });
+ });
});
}
diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb
index c19e0fea8b2..0d0e31b0068 100644
--- a/app/jobs/get_usps_proofing_results_job.rb
+++ b/app/jobs/get_usps_proofing_results_job.rb
@@ -212,6 +212,8 @@ def handle_unsupported_id_type(enrollment, response)
status_check_completed_at: Time.zone.now,
)
+ # send SMS and email
+ send_enrollment_status_sms_notification(enrollment: enrollment)
send_failed_email(enrollment.user, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
@@ -244,8 +246,10 @@ def handle_expired_status_update(enrollment, response, response_message)
status: :expired,
status_check_completed_at: Time.zone.now,
)
- # destroy phone number for expired
+
+ # destroy phone number for expired enrollments
enrollment.notification_phone_configuration&.destroy
+
begin
send_deadline_passed_email(enrollment.user, enrollment) unless enrollment.deadline_passed_sent
rescue StandardError => err
@@ -309,6 +313,9 @@ def handle_failed_status(enrollment, response)
proofed_at: proofed_at,
status_check_completed_at: Time.zone.now,
)
+
+ # send SMS and email
+ send_enrollment_status_sms_notification(enrollment: enrollment)
if response['fraudSuspected']
send_failed_fraud_email(enrollment.user, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
@@ -342,6 +349,9 @@ def handle_successful_status_update(enrollment, response)
proofed_at: proofed_at,
status_check_completed_at: Time.zone.now,
)
+
+ # send SMS and email
+ send_enrollment_status_sms_notification(enrollment: enrollment)
send_verified_email(enrollment.user, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
@@ -365,6 +375,8 @@ def handle_unsupported_secondary_id(enrollment, response)
proofed_at: proofed_at,
status_check_completed_at: Time.zone.now,
)
+ # send SMS and email
+ send_enrollment_status_sms_notification(enrollment: enrollment)
send_failed_email(enrollment.user, enrollment)
analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated(
**email_analytics_attributes(enrollment),
@@ -393,9 +405,6 @@ def process_enrollment_response(enrollment, response)
else
handle_unsupported_status(enrollment, response)
end
-
- # invoke job to send sms notification
- send_enrollment_status_sms_notification(enrollment: enrollment)
end
def send_verified_email(user, enrollment)
@@ -403,7 +412,7 @@ def send_verified_email(user, enrollment)
# rubocop:disable IdentityIdp/MailLaterLinter
UserMailer.with(user: user, email_address: email_address).in_person_verified(
enrollment: enrollment,
- ).deliver_later(**mail_delivery_params(enrollment.proofed_at))
+ ).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
@@ -423,7 +432,7 @@ def send_failed_email(user, enrollment)
# rubocop:disable IdentityIdp/MailLaterLinter
UserMailer.with(user: user, email_address: email_address).in_person_failed(
enrollment: enrollment,
- ).deliver_later(**mail_delivery_params(enrollment.proofed_at))
+ ).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
@@ -433,32 +442,33 @@ def send_failed_fraud_email(user, enrollment)
# rubocop:disable IdentityIdp/MailLaterLinter
UserMailer.with(user: user, email_address: email_address).in_person_failed_fraud(
enrollment: enrollment,
- ).deliver_later(**mail_delivery_params(enrollment.proofed_at))
+ ).deliver_later(**notification_delivery_params(enrollment))
# rubocop:enable IdentityIdp/MailLaterLinter
end
end
- def mail_delivery_params(proofed_at)
- return {} if proofed_at.blank?
- mail_delay_hours = IdentityConfig.store.in_person_results_delay_in_hours ||
- DEFAULT_EMAIL_DELAY_IN_HOURS
- wait_until = proofed_at + mail_delay_hours.hours
- return {} if mail_delay_hours == 0 || wait_until < Time.zone.now
- return { wait_until: wait_until, queue: :intentionally_delayed }
- end
-
# enqueue sms notification job when it's expired or success
# @param [InPersonEnrollment] enrollment
def send_enrollment_status_sms_notification(enrollment:)
- return unless IdentityConfig.store.in_person_send_proofing_notifications_enabled
- return if enrollment&.proofed_at.blank?
- sms_delay_hours = IdentityConfig.store.in_person_results_delay_in_hours ||
- DEFAULT_EMAIL_DELAY_IN_HOURS
- wait_until = enrollment.proofed_at + sms_delay_hours
- InPerson::SendProofingNotificationJob.set(
- wait_until: wait_until,
+ if IdentityConfig.store.in_person_send_proofing_notifications_enabled
+ InPerson::SendProofingNotificationJob.set(
+ **notification_delivery_params(enrollment),
+ ).perform_later(enrollment.id)
+ end
+ end
+
+ def notification_delivery_params(enrollment)
+ return {} unless enrollment.passed? || enrollment.failed?
+
+ wait_until = enrollment.status_check_completed_at + (
+ IdentityConfig.store.in_person_results_delay_in_hours || DEFAULT_EMAIL_DELAY_IN_HOURS
+ ).hours
+ return {} unless Time.zone.now < wait_until
+
+ {
+ wait_until:,
queue: :intentionally_delayed,
- ).perform_later(enrollment.id)
+ }
end
def email_analytics_attributes(enrollment)
@@ -466,7 +476,7 @@ def email_analytics_attributes(enrollment)
enrollment_code: enrollment.enrollment_code,
timestamp: Time.zone.now,
service_provider: enrollment.issuer,
- wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until],
+ wait_until: notification_delivery_params(enrollment)[:wait_until],
}
end
diff --git a/app/jobs/in_person/send_proofing_notification_job.rb b/app/jobs/in_person/send_proofing_notification_job.rb
index c23bb16668d..3230af4d620 100644
--- a/app/jobs/in_person/send_proofing_notification_job.rb
+++ b/app/jobs/in_person/send_proofing_notification_job.rb
@@ -6,75 +6,74 @@ class SendProofingNotificationJob < ApplicationJob
def perform(enrollment_id)
return unless IdentityConfig.store.in_person_proofing_enabled &&
IdentityConfig.store.in_person_send_proofing_notifications_enabled
- begin
- enrollment = InPersonEnrollment.find_by(
- { id: enrollment_id },
- include: [:notification_phone_configuration, :user],
- )
- return unless enrollment
- # skip when enrollment status not success/failed/expired and no phone configured
- if enrollment.skip_notification_sent_at_set?
- # log event
- analytics(user: enrollment.user).
- idv_in_person_usps_proofing_results_notification_job_skipped(
- enrollment_code: enrollment.enrollment_code,
- enrollment_id: enrollment.id,
- )
- return
- end
- analytics(user: enrollment.user).
- idv_in_person_usps_proofing_results_notification_job_started(
- enrollment_code: enrollment.enrollment_code,
- enrollment_id: enrollment.id,
+
+ enrollment = InPersonEnrollment.find_by(
+ { id: enrollment_id },
+ include: [:notification_phone_configuration, :user],
+ )
+
+ if enrollment.nil? || !enrollment.eligible_for_notification?
+ analytics(user: enrollment&.user || AnonymousUser.new).
+ idv_in_person_send_proofing_notification_job_skipped(
+ enrollment_code: enrollment&.enrollment_code,
+ enrollment_id: enrollment_id,
)
- if enrollment.expired?
- # no sending message for expired status
- enrollment.notification_phone_configuration&.destroy
- return
- end
+ return
+ end
- # only send sms when success or failed
- # send notification and log result
- phone = enrollment.notification_phone_configuration.formatted_phone
- message = notification_message(enrollment: enrollment)
- response = Telephony.send_notification(
- to: phone, message: message,
- country_code: Phonelib.parse(phone).country
+ analytics(user: enrollment.user).
+ idv_in_person_send_proofing_notification_job_started(
+ enrollment_code: enrollment.enrollment_code,
+ enrollment_id: enrollment.id,
)
- handle_telephony_result(enrollment: enrollment, phone: phone, telephony_result: response)
- # if notification sent successful
- enrollment.update(notification_sent_at: Time.zone.now) if response.success?
- ensure
- Rails.logger.error("Unknown enrollment with id #{enrollment_id}") unless enrollment.present?
- analytics(user: enrollment.present? ? enrollment.user : AnonymousUser.new).
- idv_in_person_usps_proofing_results_notification_job_completed(
- enrollment_code: enrollment&.enrollment_code, enrollment_id: enrollment_id,
- )
+ if enrollment.expired?
+ # no sending message for expired status
+ enrollment.notification_phone_configuration&.destroy
+ log_job_completed(enrollment: enrollment)
+ return
end
+
+ # send notification and log result when success or failed
+ phone = enrollment.notification_phone_configuration.formatted_phone
+ message = notification_message(enrollment: enrollment)
+ response = Telephony.send_notification(
+ to: phone, message: message,
+ country_code: Phonelib.parse(phone).country
+ )
+ handle_telephony_response(enrollment: enrollment, phone: phone, telephony_response: response)
+
+ enrollment.update(notification_sent_at: Time.zone.now) if response.success?
+
+ log_job_completed(enrollment: enrollment)
+ rescue StandardError => err
+ analytics(user: enrollment&.user || AnonymousUser.new).
+ idv_in_person_send_proofing_notification_job_exception(
+ enrollment_code: enrollment&.code,
+ enrollment_id: enrollment_id,
+ exception_class: err.class.to_s,
+ exception_message: err.message,
+ )
end
private
- def handle_telephony_result(enrollment:, phone:, telephony_result:)
- if telephony_result.success?
- analytics(user: enrollment.user).
- idv_in_person_usps_proofing_results_notification_sent_attempted(
- success: true,
- enrollment_code: enrollment.enrollment_code,
- enrollment_id: enrollment.id,
- telephony_result: telephony_result,
- )
- else
- analytics(user: enrollment.user).
- idv_in_person_usps_proofing_results_notification_sent_attempted(
- success: false,
- enrollment_code: enrollment.enrollment_code,
- enrollment_id: enrollment.id,
- telephony_result: telephony_result,
- )
- if telephony_result.error&.is_a?(Telephony::OptOutError)
- PhoneNumberOptOut.mark_opted_out(phone)
- end
+ def log_job_completed(enrollment:)
+ analytics(user: enrollment.user).
+ idv_in_person_send_proofing_notification_job_completed(
+ enrollment_code: enrollment.enrollment_code, enrollment_id: enrollment.id,
+ )
+ end
+
+ def handle_telephony_response(enrollment:, phone:, telephony_response:)
+ analytics(user: enrollment.user).
+ idv_in_person_send_proofing_notification_attempted(
+ success: telephony_response.success?,
+ enrollment_code: enrollment.enrollment_code,
+ enrollment_id: enrollment.id,
+ telephony_response: telephony_response.to_h,
+ )
+ if telephony_response.error&.is_a?(Telephony::OptOutError)
+ PhoneNumberOptOut.mark_opted_out(phone)
end
end
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index 967c5fe9011..b451f4afe04 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -6,6 +6,9 @@ class EmailAddress < ApplicationRecord
belongs_to :user, inverse_of: :email_addresses
validates :encrypted_email, presence: true
validates :email_fingerprint, presence: true
+ # rubocop:disable Rails/HasManyOrHasOneDependent
+ has_one :suspended_email
+ # rubocop:enable Rails/HasManyOrHasOneDependent
scope :confirmed, -> { where('confirmed_at IS NOT NULL') }
diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb
index 265cd342fff..ba51468cddd 100644
--- a/app/models/in_person_enrollment.rb
+++ b/app/models/in_person_enrollment.rb
@@ -133,8 +133,9 @@ def on_notification_sent_at_updated
end
end
- def skip_notification_sent_at_set?
- !notification_phone_configuration.present? || (!self.passed? && !self.failed? && !self.expired?)
+ def eligible_for_notification?
+ self.notification_phone_configuration.present? &&
+ (self.passed? || self.failed? || self.expired?)
end
private
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 727fe46991c..5cc0c49c2a1 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -33,7 +33,7 @@ class Profile < ApplicationRecord
attr_reader :personal_key
def fraud_review_pending?
- fraud_pending_reason.present? && !fraud_rejection?
+ fraud_review_pending_at.present?
end
def fraud_rejection?
@@ -147,26 +147,6 @@ def deactivate_for_gpo_verification
end
def deactivate_for_fraud_review
- ##
- # This is temporary. We are working on changing the way fraud review status
- # is computed. The goal is that a profile is only in fraud review when
- # `fraud_review_pending_at` is set. We will set this immediatly if a user
- # verifies with phone and when a user enters their GPO code.
- #
- # We currently look at `fraud_pending_reason` to determine if a user is in
- # fraud review. This allows us to change the writes on
- # `fraud_review_pending_at` without side-effects.
- #
- # Once the writes on `fraud_review_pending_at` are correct we can move the
- # reads to determine a user is fraud review pending to that column. At that
- # point we can set `fraud_pending_reason` when we create a profile and
- # deactivate the profile at the appropriate time for the given context
- # (i.e. immediatly for phone and after GPO code entry for GPO).
- #
- if fraud_pending_reason.nil?
- raise 'Attempting to deactivate a profile with a nil fraud pending reason'
- end
-
update!(
active: false,
fraud_review_pending_at: Time.zone.now,
@@ -244,10 +224,6 @@ def includes_phone_check?
proofing_components['address_check'] == 'lexis_nexis_address'
end
- def has_proofed_before?
- Profile.where(user_id: user_id).where.not(activated_at: nil).where.not(id: self.id).exists?
- end
-
def irs_attempts_api_tracker
@irs_attempts_api_tracker ||= IrsAttemptsApi::Tracker.new
end
diff --git a/app/models/suspended_email.rb b/app/models/suspended_email.rb
new file mode 100644
index 00000000000..2e0005c4c7e
--- /dev/null
+++ b/app/models/suspended_email.rb
@@ -0,0 +1,22 @@
+class SuspendedEmail < ApplicationRecord
+ belongs_to :email_address
+ validates :digested_base_email, presence: true
+
+ class << self
+ def generate_email_digest(email)
+ normalized_email = EmailNormalizer.new(email).normalized_email
+ OpenSSL::Digest::SHA256.hexdigest(normalized_email)
+ end
+
+ def create_from_email_adddress!(email_address)
+ create!(
+ digested_base_email: generate_email_digest(email_address.email),
+ email_address: email_address,
+ )
+ end
+
+ def find_with_email(email)
+ find_by(digested_base_email: generate_email_digest(email))&.email_address
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index d132e59ef93..86f4d283bc8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -122,6 +122,9 @@ def suspend!
OutOfBandSessionAccessor.new(unique_session_id).destroy if unique_session_id
update!(suspended_at: Time.zone.now, unique_session_id: nil)
analytics.user_suspended(success: true)
+ email_addresses.map do |email_address|
+ SuspendedEmail.create_from_email_adddress!(email_address)
+ end
end
def reinstate!
@@ -131,6 +134,9 @@ def reinstate!
end
update!(reinstated_at: Time.zone.now)
analytics.user_reinstated(success: true)
+ email_addresses.map do |email_address|
+ SuspendedEmail.find_with_email(email_address.email)&.destroy
+ end
end
def pending_profile
@@ -140,7 +146,9 @@ def pending_profile
pending = profiles.where(deactivation_reason: :in_person_verification_pending).or(
profiles.where.not(gpo_verification_pending_at: nil),
).or(
- profiles.where.not(fraud_pending_reason: nil),
+ profiles.where.not(fraud_review_pending_at: nil),
+ ).or(
+ profiles.where.not(fraud_rejection_at: nil),
).order(created_at: :desc).first
if pending.blank?
diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb
index 8d688615031..5ee0e3a5d62 100644
--- a/app/services/analytics_events.rb
+++ b/app/services/analytics_events.rb
@@ -1464,6 +1464,103 @@ def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra)
)
end
+ # Track sms notification attempt
+ # @param [boolean] success sms notification successful or not
+ # @param [String] enrollment_code enrollment_code
+ # @param [String] enrollment_id enrollment_id
+ # @param [Hash] telephony_response response from Telephony gem
+ # @param [Hash] extra extra information
+ def idv_in_person_send_proofing_notification_attempted(
+ success:,
+ enrollment_code:,
+ enrollment_id:,
+ telephony_response:,
+ **extra
+ )
+ track_event(
+ 'IdV: in person notification SMS send attempted',
+ success: success,
+ enrollment_code: enrollment_code,
+ enrollment_id: enrollment_id,
+ telephony_response: telephony_response,
+ **extra,
+ )
+ end
+
+ # Track sms notification job completion
+ # @param [String] enrollment_code enrollment_code
+ # @param [String] enrollment_id enrollment_id
+ # @param [Hash] extra extra information
+ def idv_in_person_send_proofing_notification_job_completed(
+ enrollment_code:,
+ enrollment_id:,
+ **extra
+ )
+ track_event(
+ 'SendProofingNotificationAndDeletePhoneNumberJob: job completed',
+ enrollment_code: enrollment_code,
+ enrollment_id: enrollment_id,
+ **extra,
+ )
+ end
+
+ # Tracks exceptions that are raised when running InPerson::SendProofingNotificationJob
+ # @param [String] enrollment_code
+ # @param [String] enrollment_id
+ # @param [String] exception_class
+ # @param [String] exception_message
+ # @param [Hash] extra extra information
+ def idv_in_person_send_proofing_notification_job_exception(
+ enrollment_code:,
+ enrollment_id:,
+ exception_class: nil,
+ exception_message: nil,
+ **extra
+ )
+ track_event(
+ 'SendProofingNotificationJob: Exception raised',
+ enrollment_code: enrollment_code,
+ enrollment_id: enrollment_id,
+ exception_class: exception_class,
+ exception_message: exception_message,
+ **extra,
+ )
+ end
+
+ # Track sms notification job skipped
+ # @param [String] enrollment_code enrollment_code
+ # @param [String] enrollment_id enrollment_id
+ # @param [Hash] extra extra information
+ def idv_in_person_send_proofing_notification_job_skipped(
+ enrollment_code:,
+ enrollment_id:,
+ **extra
+ )
+ track_event(
+ 'SendProofingNotificationAndDeletePhoneNumberJob: job skipped',
+ enrollment_code: enrollment_code,
+ enrollment_id: enrollment_id,
+ **extra,
+ )
+ end
+
+ # Track sms notification job started
+ # @param [String] enrollment_code enrollment_code
+ # @param [String] enrollment_id enrollment_id
+ # @param [Hash] extra extra information
+ def idv_in_person_send_proofing_notification_job_started(
+ enrollment_code:,
+ enrollment_id:,
+ **extra
+ )
+ track_event(
+ 'SendProofingNotificationAndDeletePhoneNumberJob: job started',
+ enrollment_code: enrollment_code,
+ enrollment_id: enrollment_id,
+ **extra,
+ )
+ end
+
# @param [String] flow_path Document capture path ("hybrid" or "standard")
# The user submitted the in person proofing switch_back step
def idv_in_person_switch_back_submitted(flow_path:, **extra)
@@ -1722,76 +1819,6 @@ def idv_in_person_usps_proofing_results_job_unexpected_response(
)
end
- # Track sms notification job completion
- # @param [String] enrollment_code enrollment_code
- # @param [String] enrollment_id enrollment_id
- # @param [Hash] extra extra information
- def idv_in_person_usps_proofing_results_notification_job_completed(enrollment_code:,
- enrollment_id:,
- **extra)
- track_event(
- 'SendProofingNotificationAndDeletePhoneNumberJob: job completed',
- enrollment_code: enrollment_code,
- enrollment_id: enrollment_id,
- **extra,
- )
- end
-
- # Track sms notification job skipped
- # @param [String] enrollment_code enrollment_code
- # @param [String] enrollment_id enrollment_id
- # @param [Hash] extra extra information
- def idv_in_person_usps_proofing_results_notification_job_skipped(
- enrollment_code:,
- enrollment_id:,
- **extra
- )
- track_event(
- 'SendProofingNotificationAndDeletePhoneNumberJob: job skipped',
- enrollment_code: enrollment_code,
- enrollment_id: enrollment_id,
- **extra,
- )
- end
-
- # Track sms notification job started
- # @param [String] enrollment_code enrollment_code
- # @param [String] enrollment_id enrollment_id
- # @param [Hash] extra extra information
- def idv_in_person_usps_proofing_results_notification_job_started(enrollment_code:,
- enrollment_id:,
- **extra)
- track_event(
- 'SendProofingNotificationAndDeletePhoneNumberJob: job started',
- enrollment_code: enrollment_code,
- enrollment_id: enrollment_id,
- **extra,
- )
- end
-
- # Track sms notification attempt
- # @param [boolean] success sms notification successful or not
- # @param [String] enrollment_code enrollment_code
- # @param [String] enrollment_id enrollment_id
- # @param [Telephony::Response] telephony_result
- # @param [Hash] extra extra information
- def idv_in_person_usps_proofing_results_notification_sent_attempted(
- success:,
- enrollment_code:,
- enrollment_id:,
- telephony_result:,
- **extra
- )
- track_event(
- 'IdV: in person notification SMS send attempted',
- success: success,
- enrollment_code: enrollment_code,
- enrollment_id: enrollment_id,
- telephony_result: telephony_result,
- **extra,
- )
- end
-
# Tracks if USPS in-person proofing enrollment request fails
# @param [String] context
# @param [String] reason
diff --git a/app/services/idv/phone_step.rb b/app/services/idv/phone_step.rb
index caf6f1fc4c9..d5f1959a31d 100644
--- a/app/services/idv/phone_step.rb
+++ b/app/services/idv/phone_step.rb
@@ -43,7 +43,11 @@ def async_state_done(async_state)
@idv_result = async_state.result
success = idv_result[:success]
- handle_successful_proofing_attempt if success
+ if success
+ handle_successful_proofing_attempt
+ else
+ handle_failed_proofing_attempt
+ end
delete_async
FormResponse.new(
@@ -170,5 +174,11 @@ def missing
def delete_async
idv_session.idv_phone_step_document_capture_session_uuid = nil
end
+
+ def handle_failed_proofing_attempt
+ return if failure_reason == :timeout
+
+ idv_session.add_failed_phone_step_number(idv_session.previous_phone_step_params[:phone])
+ end
end
end
diff --git a/app/services/idv/session.rb b/app/services/idv/session.rb
index 48b12bfbb5d..8cbef61007c 100644
--- a/app/services/idv/session.rb
+++ b/app/services/idv/session.rb
@@ -128,6 +128,16 @@ def user_phone_confirmation_session=(new_user_phone_confirmation_session)
session[:user_phone_confirmation_session] = new_user_phone_confirmation_session.to_h
end
+ def failed_phone_step_numbers
+ session[:failed_phone_step_params] ||= []
+ end
+
+ def add_failed_phone_step_number(phone)
+ parsed_phone = Phonelib.parse(phone)
+ phone_e164 = parsed_phone.e164
+ failed_phone_step_numbers << phone_e164 if !failed_phone_step_numbers.include?(phone_e164)
+ end
+
def in_person_enrollment?
ProofingComponent.find_by(user: current_user)&.document_check == Idp::Constants::Vendors::USPS
end
diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb
index 05563836e3a..2b141e89ad6 100644
--- a/app/services/idv/steps/doc_auth_base_step.rb
+++ b/app/services/idv/steps/doc_auth_base_step.rb
@@ -61,7 +61,7 @@ def rate_limited_response
redirect_to rate_limited_url
DocAuth::Response.new(
success: false,
- errors: { limit: I18n.t('errors.doc_auth.throttled_heading') },
+ errors: { limit: I18n.t('errors.doc_auth.rate_limited_heading') },
)
end
diff --git a/app/services/irs_attempts_api/tracker_events.rb b/app/services/irs_attempts_api/tracker_events.rb
index 9179bb36ad2..259b34f31bb 100644
--- a/app/services/irs_attempts_api/tracker_events.rb
+++ b/app/services/irs_attempts_api/tracker_events.rb
@@ -276,15 +276,6 @@ def idv_phone_upload_link_used
)
end
- # The user, who had previously successfully confirmed their identity, has
- # reproofed. All the normal events are also sent, this simply notes that
- # this is the second (or more) time they have gone through the process successfully.
- def idv_reproof
- track_event(
- :idv_reproof,
- )
- end
-
# @param [String] ssn
# User entered in SSN number during Identity verification
def idv_ssn_submitted(ssn:)
diff --git a/app/services/maintenance_window.rb b/app/services/maintenance_window.rb
deleted file mode 100644
index b2a01fdbbd2..00000000000
--- a/app/services/maintenance_window.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class MaintenanceWindow
- attr_reader :start, :finish, :now
-
- def initialize(start:, finish:, now: nil, display_time_zone: 'America/New_York')
- @start = start.in_time_zone(display_time_zone) if start.present?
- @finish = finish.in_time_zone(display_time_zone) if finish.present?
- @now = now || Time.zone.now
- end
-
- def active?
- (start...finish).cover?(now) if start && finish
- end
-end
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index 051a3b8fc2d..18946fc61b8 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -1,5 +1,4 @@
<% title t('titles.visitors.index') %>
-<%= render 'shared/maintenance_window_alert' %>
<% if decorated_session.sp_name %>
<%= render 'sign_up/registrations/sp_registration_heading' %>
diff --git a/app/views/idv/getting_started/show.html.erb b/app/views/idv/getting_started/show.html.erb
index 27c0bad7f64..ddc9be31c11 100644
--- a/app/views/idv/getting_started/show.html.erb
+++ b/app/views/idv/getting_started/show.html.erb
@@ -1,91 +1,89 @@
<% title @title %>
-<%= render 'shared/maintenance_window_alert' do %>
- <%= render JavascriptRequiredComponent.new(
- header: t('idv.getting_started.no_js_header'),
- intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name),
- ) do %>
+<%= render JavascriptRequiredComponent.new(
+ header: t('idv.getting_started.no_js_header'),
+ intro: t('idv.getting_started.no_js_intro', sp_name: @sp_name),
+ ) do %>
- <%= render AlertComponent.new(
- type: :error,
- class: [
- 'js-consent-form-alert',
- 'margin-bottom-4',
- flow_session[:error_message].blank? && 'display-none',
- ].select(&:present?),
- message: flow_session[:error_message].presence || t('errors.doc_auth.consent_form'),
- ) %>
+<%= render AlertComponent.new(
+ type: :error,
+ class: [
+ 'js-consent-form-alert',
+ 'margin-bottom-4',
+ flow_session[:error_message].blank? && 'display-none',
+ ].select(&:present?),
+ message: flow_session[:error_message].presence || t('errors.doc_auth.consent_form'),
+ ) %>
- <%= render PageHeadingComponent.new.with_content(@title) %>
-
- <%= t(
- 'doc_auth.info.getting_started_html',
- sp_name: @sp_name,
- link_html: new_tab_link_to(
- t('doc_auth.info.getting_started_learn_more'),
- help_center_redirect_path(
- category: 'verify-your-identity',
- article: 'how-to-verify-your-identity',
- flow: :idv,
- step: :getting_started,
- location: 'intro_paragraph',
- ),
+<%= render PageHeadingComponent.new.with_content(@title) %>
+
+ <%= t(
+ 'doc_auth.info.getting_started_html',
+ sp_name: @sp_name,
+ link_html: new_tab_link_to(
+ t('doc_auth.info.getting_started_learn_more'),
+ help_center_redirect_path(
+ category: 'verify-your-identity',
+ article: 'how-to-verify-your-identity',
+ flow: :idv,
+ step: :getting_started,
+ location: 'intro_paragraph',
),
- ) %>
-
+ ),
+ ) %>
+
- <%= t('doc_auth.getting_started.instructions.getting_started') %>
+ <%= t('doc_auth.getting_started.instructions.getting_started') %>
- <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %>
- <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %>
- <%= t('doc_auth.getting_started.instructions.text1') %>
- <% end %>
- <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %>
- <%= t('doc_auth.getting_started.instructions.text2') %>
- <% end %>
- <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %>
- <%= t('doc_auth.getting_started.instructions.text3') %>
- <% end %>
- <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %>
- <%= t('doc_auth.getting_started.instructions.text4') %>
- <% end %>
+ <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %>
+ <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet1')) do %>
+ <%= t('doc_auth.getting_started.instructions.text1') %>
<% end %>
-
- <%= simple_form_for(
- :doc_auth,
- url: url_for,
- method: 'put',
- html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' },
- ) do |f| %>
- <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %>
- <%= render ValidatedFieldComponent.new(
- form: f,
- name: :ial2_consent_given,
- as: :boolean,
- label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME),
- required: true,
- ) %>
+ <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet2')) do %>
+ <%= t('doc_auth.getting_started.instructions.text2') %>
+ <% end %>
+ <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet3')) do %>
+ <%= t('doc_auth.getting_started.instructions.text3') %>
+ <% end %>
+ <%= c.with_item(heading: t('doc_auth.getting_started.instructions.bullet4', app_name: APP_NAME)) do %>
+ <%= t('doc_auth.getting_started.instructions.text4') %>
<% end %>
-
- <%= new_tab_link_to(
- t('doc_auth.getting_started.instructions.learn_more'),
- policy_redirect_url(flow: :idv, step: :getting_started, location: :consent),
- ) %>
-
-
- <%= render(
- SpinnerButtonComponent.new(
- type: :submit,
- big: true,
- wide: true,
- spin_on_click: false,
- ).with_content(t('doc_auth.buttons.continue')),
- ) %>
-
<% end %>
- <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %>
+<%= simple_form_for(
+ :doc_auth,
+ url: url_for,
+ method: 'put',
+ html: { autocomplete: 'off', class: 'margin-top-2 margin-bottom-5 js-consent-continue-form' },
+ ) do |f| %>
+ <%= render ClickObserverComponent.new(event_name: 'IdV: consent checkbox toggled') do %>
+ <%= render ValidatedFieldComponent.new(
+ form: f,
+ name: :ial2_consent_given,
+ as: :boolean,
+ label: t('doc_auth.getting_started.instructions.consent', app_name: APP_NAME),
+ required: true,
+ ) %>
<% end %>
+
+ <%= new_tab_link_to(
+ t('doc_auth.getting_started.instructions.learn_more'),
+ policy_redirect_url(flow: :idv, step: :getting_started, location: :consent),
+ ) %>
+
+
+ <%= render(
+ SpinnerButtonComponent.new(
+ type: :submit,
+ big: true,
+ wide: true,
+ spin_on_click: false,
+ ).with_content(t('doc_auth.buttons.continue')),
+ ) %>
+
+<% end %>
+
+ <%= render 'shared/cancel', link: idv_cancel_path(step: 'getting_started') %>
<% end %>
<%= javascript_packs_tag_once('document-capture-welcome') %>
diff --git a/app/views/idv/gpo_verify/throttled.html.erb b/app/views/idv/gpo_verify/throttled.html.erb
index cc638e2b350..07026775dad 100644
--- a/app/views/idv/gpo_verify/throttled.html.erb
+++ b/app/views/idv/gpo_verify/throttled.html.erb
@@ -4,7 +4,7 @@
<%= t(
- 'errors.verify_profile.throttled',
+ 'errors.verify_profile.rate_limited',
timeout: distance_of_time_in_words(
Time.zone.now,
[@expires_at, Time.zone.now].compact.max,
diff --git a/app/views/idv/phone/new.html.erb b/app/views/idv/phone/new.html.erb
index d1454b85c95..8676e1d388a 100644
--- a/app/views/idv/phone/new.html.erb
+++ b/app/views/idv/phone/new.html.erb
@@ -65,6 +65,26 @@
required: true,
class: 'margin-bottom-4',
) %>
+ <%= render AlertComponent.new(
+ type: :warning,
+ class: 'margin-bottom-4',
+ id: 'phone-already-submitted-alert',
+ data: {
+ failed_phone_numbers: @idv_form.failed_phone_numbers,
+ },
+ hidden: true,
+ ) do %>
+ <%= t('idv.messages.phone.failed_number.alert_text') %>
+ <% if gpo_letter_available %>
+ <%= t(
+ 'idv.messages.phone.failed_number.gpo_alert_html',
+ link_html: link_to(
+ t('idv.messages.phone.failed_number.gpo_verify_link'),
+ idv_gpo_path,
+ ),
+ ) %>
+ <% end %>
+ <% end %>
<%= t('idv.titles.otp_delivery_method') %>
<%= t('idv.messages.otp_delivery_method_description') %>
@@ -115,4 +135,4 @@
<%= render 'idv/doc_auth/cancel', step: 'phone' %>
-<% javascript_packs_tag_once 'form-steps-wait' %>
+<% javascript_packs_tag_once 'form-steps-wait', 'idv-phone-alert' %>
diff --git a/app/views/idv/session_errors/throttled.html.erb b/app/views/idv/session_errors/throttled.html.erb
index ae644d260d0..e7fe95ac22d 100644
--- a/app/views/idv/session_errors/throttled.html.erb
+++ b/app/views/idv/session_errors/throttled.html.erb
@@ -1,6 +1,6 @@
<%= render(
'idv/shared/error',
- heading: t('errors.doc_auth.throttled_heading'),
+ heading: t('errors.doc_auth.rate_limited_heading'),
options: [
{
url: MarketingSite.contact_url,
@@ -11,7 +11,7 @@
) do %>
<%= t(
- 'errors.doc_auth.throttled_text_html',
+ 'errors.doc_auth.rate_limited_text_html',
timeout: distance_of_time_in_words(
Time.zone.now,
[@expires_at, Time.zone.now].compact.max,
diff --git a/app/views/idv/welcome/show.html.erb b/app/views/idv/welcome/show.html.erb
index 788a36fbb85..1c516a5a7bb 100644
--- a/app/views/idv/welcome/show.html.erb
+++ b/app/views/idv/welcome/show.html.erb
@@ -9,7 +9,6 @@
) %>
<% end %>
-<%= render 'shared/maintenance_window_alert' do %>
<%= render JavascriptRequiredComponent.new(
header: t('idv.welcome.no_js_header'),
intro: t('idv.welcome.no_js_intro', sp_name: decorated_session.sp_name || APP_NAME),
@@ -55,54 +54,53 @@
<%= f.submit t('doc_auth.buttons.continue') %>
<% end %>
- <%= render(
- 'shared/troubleshooting_options',
- heading_tag: :h3,
- heading: t('idv.troubleshooting.headings.missing_required_items'),
- options: [
- {
- url: help_center_redirect_path(
- category: 'verify-your-identity',
- article: 'accepted-state-issued-identification',
- flow: :idv,
- step: :welcome,
- location: 'missing_items',
- ),
- text: t('idv.troubleshooting.options.supported_documents'),
- new_tab: true,
- },
- {
- url: help_center_redirect_path(
- category: 'verify-your-identity',
- article: 'phone-number',
- flow: :idv,
- step: :welcome,
- location: 'missing_items',
- ),
- text: t('idv.troubleshooting.options.learn_more_address_verification_options'),
- new_tab: true,
- },
- decorated_session.sp_name && {
- url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'),
- text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_session.sp_name),
- new_tab: true,
- },
- ].select(&:present?),
- ) %>
+ <%= render(
+ 'shared/troubleshooting_options',
+ heading_tag: :h3,
+ heading: t('idv.troubleshooting.headings.missing_required_items'),
+ options: [
+ {
+ url: help_center_redirect_path(
+ category: 'verify-your-identity',
+ article: 'accepted-state-issued-identification',
+ flow: :idv,
+ step: :welcome,
+ location: 'missing_items',
+ ),
+ text: t('idv.troubleshooting.options.supported_documents'),
+ new_tab: true,
+ },
+ {
+ url: help_center_redirect_path(
+ category: 'verify-your-identity',
+ article: 'phone-number',
+ flow: :idv,
+ step: :welcome,
+ location: 'missing_items',
+ ),
+ text: t('idv.troubleshooting.options.learn_more_address_verification_options'),
+ new_tab: true,
+ },
+ decorated_session.sp_name && {
+ url: return_to_sp_failure_to_proof_url(step: 'welcome', location: 'missing_items'),
+ text: t('idv.troubleshooting.options.get_help_at_sp', sp_name: decorated_session.sp_name),
+ new_tab: true,
+ },
+ ].select(&:present?),
+ ) %>
-
<%= t('doc_auth.instructions.privacy') %>
-
- <%= t('doc_auth.info.privacy', app_name: APP_NAME) %>
-
-
- <%= new_tab_link_to(
- t('doc_auth.instructions.learn_more'),
- policy_redirect_url(flow: :idv, step: :welcome, location: :footer),
- ) %>
-
+ <%= t('doc_auth.instructions.privacy') %>
+
+ <%= t('doc_auth.info.privacy', app_name: APP_NAME) %>
+
+
+ <%= new_tab_link_to(
+ t('doc_auth.instructions.learn_more'),
+ policy_redirect_url(flow: :idv, step: :welcome, location: :footer),
+ ) %>
+
- <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %>
- <% end %>
+ <%= render 'shared/cancel', link: idv_cancel_path(step: 'welcome') %>
<% end %>
<%= javascript_packs_tag_once('document-capture-welcome') %>
diff --git a/app/views/shared/_maintenance_window_alert.html.erb b/app/views/shared/_maintenance_window_alert.html.erb
deleted file mode 100644
index b1d78a5a02f..00000000000
--- a/app/views/shared/_maintenance_window_alert.html.erb
+++ /dev/null
@@ -1,24 +0,0 @@
-<% maintenance_window = MaintenanceWindow.new(
- start: IdentityConfig.store.acuant_maintenance_window_start,
- finish: IdentityConfig.store.acuant_maintenance_window_finish,
- now: local_assigns[:now],
- ) %>
-<% if maintenance_window.active? %>
- <%= render AlertComponent.new(type: :warning, class: 'margin-bottom-2', text_tag: 'div') do %>
-
- <%= t(
- 'notices.maintenance.currently_under_maintenance_html',
- finish: l(
- maintenance_window.finish,
- format: t('time.formats.event_timestamp_with_zone'),
- ),
- ) %>
-
-
- <%= t('notices.maintenance.need_assistance') %>
- <%= link_to(t('notices.maintenance.contact_us'), MarketingSite.contact_url) %>
-
- <% end %>
-<% else %>
- <%= yield %>
-<% end %>
diff --git a/app/views/users/verify_personal_key/throttled.html.erb b/app/views/users/verify_personal_key/throttled.html.erb
index 83660193e0c..c20e71f6cdb 100644
--- a/app/views/users/verify_personal_key/throttled.html.erb
+++ b/app/views/users/verify_personal_key/throttled.html.erb
@@ -4,7 +4,7 @@
<%= t(
- 'errors.verify_personal_key.throttled',
+ 'errors.verify_personal_key.rate_limited',
timeout: distance_of_time_in_words(
Time.zone.now,
[@expires_at, Time.zone.now].compact.max,
diff --git a/config/application.yml.default b/config/application.yml.default
index b4b4fb48b11..10c9623bf9e 100644
--- a/config/application.yml.default
+++ b/config/application.yml.default
@@ -26,8 +26,6 @@ allowed_ialmax_providers: '[]'
account_reset_token_valid_for_days: 1
account_reset_wait_period_days: 1
account_suspended_support_code: EFGHI
-acuant_maintenance_window_start:
-acuant_maintenance_window_finish:
acuant_assure_id_password: ''
acuant_assure_id_subscription_id: ''
acuant_assure_id_url: ''
@@ -139,7 +137,6 @@ in_person_email_reminder_early_benchmark_in_days: 11
in_person_email_reminder_final_benchmark_in_days: 1
in_person_email_reminder_late_benchmark_in_days: 4
in_person_proofing_enabled: false
-in_person_send_proofing_notifications_enabled: false
in_person_enrollment_validity_in_days: 30
in_person_enrollments_ready_job_email_body_pattern: '\A\s*(?\d{16})\s*\Z'
in_person_enrollments_ready_job_cron: '0/10 * * * *'
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index d9143e5f5ca..33e855c2f84 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -119,17 +119,17 @@ en:
text4: Your password saves and encrypts your personal information.
headings:
address: Update your mailing address
- back: Back
+ back: Back of your driver’s license or state ID
capture_complete: We verified your ID
capture_scan_warning_html: We couldn’t read the barcode on your ID. If the
information below is incorrect, please %{link_html} of your
state‑issued ID.
capture_scan_warning_link: upload new photos
capture_troubleshooting_tips: Having trouble adding your state‑issued ID?
- document_capture: Add your state‑issued ID
+ document_capture: Add photos of your ID
document_capture_back: Back of your ID
document_capture_front: Front of your ID
- front: Front
+ front: Front of your driver’s license or state ID
getting_started: Let’s verify your identity for %{sp_name}
hybrid_handoff: How would you like to add your ID?
interstitial: We are processing your images
@@ -162,8 +162,8 @@ en:
capture_status_small_document: Move Closer
capture_status_tap_to_capture: Tap to Capture
document_capture_intro_acknowledgment: We’ll collect information about you by
- reading your state‑issued ID. We use this information to verify your
- identity.
+ reading your driver’s license or state ID card. We use this information
+ to verify your identity.
getting_started_html: '%{sp_name} needs to make sure you are you — not someone
pretending to be you. %{link_html}'
getting_started_learn_more: Learn more about what you need to verify your identity
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index dadb1cf26c6..6789e5a856d 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -144,17 +144,17 @@ es:
text4: Su contraseña guarda y encripta su información personal.
headings:
address: Actualice su dirección postal
- back: Parte Trasera
+ back: Reverso de su licencia de conducir o identificación estatal
capture_complete: Verificamos su identificación
capture_scan_warning_html: No pudimos leer el código de barras en su ID. Si la
información que aparece a continuación es incorrecta, por favor,
%{link_html} de su ID emitido por el estado.
capture_scan_warning_link: suba nuevas fotos
capture_troubleshooting_tips: '¿Tiene problemas para agregar su identificación emitida por el estado?'
- document_capture: Añada su documento de identidad expedido por el estado
+ document_capture: Incluir fotos de su identificación
document_capture_back: Parte trasera de su documento de identidad
document_capture_front: Parte delantera de su documento de identidad
- front: Parte Delantera
+ front: Anverso de su licencia de conducir o identificación estatal
getting_started: Vamos a verificar su identidad para %{sp_name}
hybrid_handoff: '¿Cómo desea añadir su documento de identidad?'
interstitial: Estamos procesando sus imágenes
@@ -190,7 +190,7 @@ es:
capture_status_small_document: Muévete mas cerca
capture_status_tap_to_capture: Toque para capturar
document_capture_intro_acknowledgment: Recopilaremos información sobre usted
- leyendo su documento de identidad expedido por el Estado. Usamos esta
+ leyendo su licencia de conducir o identificación estatal. Usamos esta
información para verificar su identidad.
getting_started_html: '%{sp_name} necesita asegurarse de que es usted realmente
y no alguien que se hace pasar por usted. %{link_html}'
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index 42d037b6a46..d0c7da71452 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -150,17 +150,17 @@ fr:
text4: Votre mot de passe sauvegarde et crypte vos informations personnelles.
headings:
address: Mettre à jour votre adresse postale
- back: Verso
+ back: Verso de votre permis de conduire ou de votre carte d’identité de l’État
capture_complete: Nous avons vérifié votre document d’identité
capture_scan_warning_html: Nous n’avons pas pu lire le code-barres de votre
pièce d’identité. Si les informations ci-dessous sont incorrectes,
veuillez %{link_html} de votre carte d’identité délivrée par l’État.
capture_scan_warning_link: télécharger de nouvelles photos
capture_troubleshooting_tips: Vous rencontrez des difficultés pour ajouter votre pièce d’identité?
- document_capture: Ajoutez votre carte d’identité délivrée par l’État
+ document_capture: Ajoutez des photos de votre pièce d’identité
document_capture_back: Verso de votre carte d’identité
document_capture_front: Recto de votre carte d’identité
- front: Recto
+ front: Recto de votre permis de conduire ou de votre carte d’identité de l’État
getting_started: Vérifions votre identité pour %{sp_name}
hybrid_handoff: Comment voulez-vous ajouter votre identifiant ?
interstitial: Nous traitons vos images
@@ -195,8 +195,8 @@ fr:
capture_status_small_document: Approchez-vous
capture_status_tap_to_capture: Appuyez pour capturer
document_capture_intro_acknowledgment: Nous recueillons des informations sur
- vous en lisant votre pièce d’identité délivrée par l’État. Nous
- utilisons ces informations pour vérifier votre identité.
+ vous en lisant votre permis de conduire ou votre carte d’identité de
+ l’État. Nous utilisons ces informations pour vérifier votre identité.
getting_started_html: '%{sp_name} doit s’assurer que c’est bien vous — et non
quelqu’un qui se fait passer pour vous. %{link_html}'
getting_started_learn_more: En savoir plus sur ce dont vous avez besoin pour vérifier votre identité
diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml
index 3600ddcd5c9..b13751eaba5 100644
--- a/config/locales/errors/en.yml
+++ b/config/locales/errors/en.yml
@@ -27,13 +27,13 @@ en:
document_capture_cancelled: You have cancelled uploading photos of your ID on your phone.
phone_step_incomplete: You must go to your phone and upload photos of your ID
before continuing. We sent you a link with instructions.
- send_link_throttle: You tried too many times, please try again in %{timeout}.
- You can also go back and choose to use your computer instead.
- throttled_heading: We couldn’t verify your ID
- throttled_subheading: Try taking new pictures
- throttled_text_html: 'For your security, we limit the number of times you can
+ rate_limited_heading: We couldn’t verify your ID
+ rate_limited_subheading: Try taking new pictures
+ rate_limited_text_html: 'For your security, we limit the number of times you can
attempt to verify a document online. Try again in
%{timeout}.'
+ send_link_limited: You tried too many times, please try again in %{timeout}. You
+ can also go back and choose to use your computer instead.
general: Oops, something went wrong. Please try again.
invalid_totp: Invalid code. Please try again.
max_password_attempts_reached: You’ve entered too many incorrect passwords. You
@@ -71,7 +71,7 @@ en:
personal_key_incorrect: Incorrect personal key
phone_carrier: Sorry, we are unable to support that phone carrier at this time.
Please select a different number and try again.
- phone_confirmation_throttled: You tried too many times, please try again in %{timeout}.
+ phone_confirmation_limited: You tried too many times, please try again in %{timeout}.
phone_duplicate: This account is already using the phone number you entered as
an authenticator. Please use a different phone number.
phone_required: Phone number is required
@@ -110,9 +110,9 @@ en:
must_select_additional_option: Select an additional authentication method.
must_select_option: Select an authentication method.
verify_personal_key:
- throttled: You tried too many times, please try again in %{timeout}.
+ rate_limited: You tried too many times, please try again in %{timeout}.
verify_profile:
- throttled: You tried too many times, please try again in %{timeout}.
+ rate_limited: You tried too many times, please try again in %{timeout}.
webauthn_platform_setup:
account_setup_error: We were unable to add face or touch unlock. Please try again or %{link}.
already_registered: Face or touch unlock is already registered on this device.
diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml
index 80355dc3322..b9104098488 100644
--- a/config/locales/errors/es.yml
+++ b/config/locales/errors/es.yml
@@ -28,14 +28,14 @@ es:
document_capture_cancelled: Ha cancelado la carga de fotos de su identificación en este teléfono.
phone_step_incomplete: Debe ir a su teléfono y cargar fotos de su identificación
antes de continuar. Te enviamos un enlace con instrucciones.
- send_link_throttle: Ha intentado demasiadas veces, por favor, inténtelo de nuevo
+ rate_limited_heading: No pudimos verificar la identificación
+ rate_limited_subheading: Intente tomar nuevas fotografÃas.
+ rate_limited_text_html: 'Por su seguridad, limitamos el número de veces que
+ puede intentar verificar un documento en lÃnea. Inténtelo de
+ nuevo en %{timeout}.'
+ send_link_limited: Ha intentado demasiadas veces, por favor, inténtelo de nuevo
en %{timeout}. También puede retroceder y elegir utilizar su computadora
como alternativa.
- throttled_heading: No pudimos verificar la identificación
- throttled_subheading: Intente tomar nuevas fotografÃas.
- throttled_text_html: 'Por su seguridad, limitamos el número de veces que puede
- intentar verificar un documento en lÃnea. Inténtelo de nuevo en
- %{timeout}.'
general: '¡Oops! Algo salió mal. Inténtelo de nuevo.'
invalid_totp: El código es inválido. Vuelva a intentarlo.
max_password_attempts_reached: Ha ingresado demasiadas contraseñas incorrectas.
@@ -75,7 +75,7 @@ es:
personal_key_incorrect: La clave personal es incorrecta
phone_carrier: Lo sentimos, no podemos admitir ese operador telefónico en este
momento. Por favor, seleccione un número diferente e inténtelo de nuevo.
- phone_confirmation_throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
+ phone_confirmation_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
phone_duplicate: Esta cuenta ya está utilizando el número de teléfono que
ingresó como autenticador. Por favor, use un número de teléfono
diferente.
@@ -116,9 +116,9 @@ es:
must_select_additional_option: Seleccione un método de autenticación adicional.
must_select_option: Seleccione un método de autenticación.
verify_personal_key:
- throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
+ rate_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
verify_profile:
- throttled: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
+ rate_limited: Lo intentaste muchas veces, vuelve a intentarlo en %{timeout}.
webauthn_platform_setup:
account_setup_error: No pudimos agregar el desbloqueo con la cara o con la
huella digital. Inténtelo de nuevo o %{link}.
diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml
index abe7843e95a..d1febdff5b2 100644
--- a/config/locales/errors/fr.yml
+++ b/config/locales/errors/fr.yml
@@ -32,14 +32,14 @@ fr:
phone_step_incomplete: Vous devez aller sur votre téléphone et télécharger des
photos de votre identifiant avant de continuer. Nous vous avons envoyé
un lien avec des instructions.
- send_link_throttle: Vous avez essayé trop de fois, veuillez réessayer dans
- %{timeout}. Vous pouvez également revenir en arrière et choisir
- d’utiliser votre ordinateur à la place.
- throttled_heading: Nous n’avons pas pu vérifier votre identité
- throttled_subheading: Essayez de prendre de nouvelles photos.
- throttled_text_html: 'Pour votre sécurité, nous limitons le nombre de fois où
+ rate_limited_heading: Nous n’avons pas pu vérifier votre identité
+ rate_limited_subheading: Essayez de prendre de nouvelles photos.
+ rate_limited_text_html: 'Pour votre sécurité, nous limitons le nombre de fois où
vous pouvez tenter de vérifier un document en ligne. Veuillez
réessayer dans %{timeout}.'
+ send_link_limited: Vous avez essayé trop de fois, veuillez réessayer dans
+ %{timeout}. Vous pouvez également revenir en arrière et choisir
+ d’utiliser votre ordinateur à la place.
general: Oups, une erreur s’est produite. Veuillez essayer de nouveau.
invalid_totp: Code non valide. Veuillez essayer de nouveau.
max_password_attempts_reached: Vous avez inscrit des mots de passe incorrects un
@@ -83,7 +83,7 @@ fr:
phone_carrier: Nous nous excusons, car nous ne pouvons pas prendre en charge cet
opérateur téléphonique pour le moment. Veuillez sélectionner un autre
numéro puis réessayer.
- phone_confirmation_throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
+ phone_confirmation_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
phone_duplicate: Ce compte utilise déjà le numéro de téléphone que vous avez
entré en tant qu’authentificateur. Veuillez utiliser un numéro de
téléphone différent.
@@ -126,9 +126,9 @@ fr:
must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire.
must_select_option: Sélectionnez une méthode d’authentification.
verify_personal_key:
- throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
+ rate_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
verify_profile:
- throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
+ rate_limited: Vous avez essayé plusieurs fois, essayez à nouveau dans %{timeout}.
webauthn_platform_setup:
account_setup_error: Nous n’avons pas pu ajouter le déverrouillage facial ni le
déverrouillage tactile. Veuillez réessayer ou %{link}.
diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml
index 9ec71063d31..6e4099cbb4e 100644
--- a/config/locales/idv/en.yml
+++ b/config/locales/idv/en.yml
@@ -213,6 +213,11 @@ en:
alert_html: 'Enter a phone number that is:'
description: We’ll check this number with records and send you a one-time code.
This is to help verify your identity.
+ failed_number:
+ alert_text: We couldn’t match you to this number.
+ gpo_alert_html: If you don’t have another number to try,
+ %{link_html} instead.
+ gpo_verify_link: verify by mail
rules:
- Based in the United States (including U.S. territories)
- Your primary number (the one you use the most often)
diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml
index dd8a0e31292..6a44773f345 100644
--- a/config/locales/idv/es.yml
+++ b/config/locales/idv/es.yml
@@ -224,6 +224,11 @@ es:
alert_html: 'Introduzca un número de teléfono que sea:'
description: Comprobaremos este número con los registros y le enviaremos un
código único. Esto es para ayudar a verificar su identidad.
+ failed_number:
+ alert_text: No pudimos asociarlo a este número.
+ gpo_alert_html: Si no dispone de otro número de teléfono,
+ %{link_html}.
+ gpo_verify_link: realice la verificación por correo
rules:
- Con base en Estados Unidos (incluidos los territorios de EE.UU.)
- Su número principal (el que utiliza con más frecuencia)
diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml
index 8c48a872496..f0bbd4c3cb9 100644
--- a/config/locales/idv/fr.yml
+++ b/config/locales/idv/fr.yml
@@ -237,6 +237,11 @@ fr:
alert_html: 'Entrez un numéro de téléphone qui est :'
description: Nous vérifierons ce numéro dans nos archives et vous enverrons un
code à usage unique. Ceci est pour aider à vérifier votre identité.
+ failed_number:
+ alert_text: Nous n’avons pas pu vous associer à ce numéro.
+ gpo_alert_html: Si vous n’avez pas d’autre numéro de téléphone
+ Ã essayer, %{link_html}.
+ gpo_verify_link: vérifiez plutôt par courrier
rules:
- Basé aux Etats-Unis (y compris les territoires américains)
- Votre numéro principal (celui que vous utilisez le plus souvent)
diff --git a/config/locales/notices/en.yml b/config/locales/notices/en.yml
index 55e754195a3..9de70481ed0 100644
--- a/config/locales/notices/en.yml
+++ b/config/locales/notices/en.yml
@@ -16,11 +16,6 @@ en:
use_diff_email:
link: create a new account
text_html: Or, %{link_html} using a different email address.
- maintenance:
- contact_us: Contact us
- currently_under_maintenance_html: We are currently under maintenance until
- %{finish} for some of our services.
- need_assistance: Need assistance?
password_changed: You changed your password.
phone_confirmed: A phone was added to your account.
piv_cac_configured: A PIV/CAC card was added to your account.
diff --git a/config/locales/notices/es.yml b/config/locales/notices/es.yml
index 15f3b495a25..e9ff0e44a59 100644
--- a/config/locales/notices/es.yml
+++ b/config/locales/notices/es.yml
@@ -16,11 +16,6 @@ es:
use_diff_email:
link: crear una cuenta nueva
text_html: O, %{link_html} utilizando un email diferente.
- maintenance:
- contact_us: Contacta con nosotros
- currently_under_maintenance_html: Actualmente estamos en mantenimiento hasta
- el %{finish} para algunos de nuestros servicios.
- need_assistance: '¿Necesita ayuda?'
password_changed: Ha cambiado su contraseña.
phone_confirmed: Un teléfono fue agregado a tu cuenta.
piv_cac_configured: Una tarjeta PIV/CAC fue agregada a tu cuenta.
diff --git a/config/locales/notices/fr.yml b/config/locales/notices/fr.yml
index e710595be57..24e554144bc 100644
--- a/config/locales/notices/fr.yml
+++ b/config/locales/notices/fr.yml
@@ -16,11 +16,6 @@ fr:
use_diff_email:
link: Créer un nouveau compte
text_html: Ou, %{link_html} en utilisant une adresse courriel différente.
- maintenance:
- contact_us: Nous contacter
- currently_under_maintenance_html: Nous sommes actuellement en maintenance
- jusqu’au %{finish} pour certains de nos services.
- need_assistance: Besoin d’assistance?
password_changed: Vous avez changé votre mot de passe.
phone_confirmed: Un téléphone a été ajouté à votre compte.
piv_cac_configured: Une carte PIV / CAC a été ajoutée à votre compte.
diff --git a/config/locales/telephony/en.yml b/config/locales/telephony/en.yml
index f8770beff4f..9e02a0167f4 100644
--- a/config/locales/telephony/en.yml
+++ b/config/locales/telephony/en.yml
@@ -39,11 +39,11 @@ en:
invalid_phone_number: The phone number entered is not valid.
opt_out: The phone number entered has opted out of text messages.
permanent_failure: The phone number entered is not valid.
+ rate_limited: That number is experiencing high message volume. Please try again
+ later.
sms_unsupported: The phone number entered doesn’t support text messaging. Try
the Phone call option.
temporary_failure: We are experiencing technical difficulties. Please try again later.
- throttled: That number is experiencing high message volume. Please try again
- later.
timeout: The server took too long to respond. Please try again.
unknown_failure: We are experiencing technical difficulties. Please try again later.
voice_unsupported: Invalid phone number. Check that you’ve entered the correct
diff --git a/config/locales/telephony/es.yml b/config/locales/telephony/es.yml
index 4db9376411d..682909ff6ec 100644
--- a/config/locales/telephony/es.yml
+++ b/config/locales/telephony/es.yml
@@ -40,12 +40,12 @@ es:
opt_out: El número de teléfono ingresado ha sido excluido de los mensajes de
texto.
permanent_failure: El número de teléfono ingresado no es válido.
+ rate_limited: Ese número está experimentando un alto volumen de mensajes. Por
+ favor, inténtelo de nuevo más tarde.
sms_unsupported: El número de teléfono ingresado no admite mensajes de texto.
Pruebe la opción de llamada telefónica.
temporary_failure: Estamos experimentando dificultades técnicas. Por favor,
inténtelo de nuevo más tarde.
- throttled: Ese número está experimentando un alto volumen de mensajes. Por
- favor, inténtelo de nuevo más tarde.
timeout: El servidor tardó demasiado en responder. Inténtalo de nuevo.
unknown_failure: Estamos experimentando dificultades técnicas. Por favor,
inténtelo de nuevo más tarde.
diff --git a/config/locales/telephony/fr.yml b/config/locales/telephony/fr.yml
index 5d9b8d670b8..41a35bbf284 100644
--- a/config/locales/telephony/fr.yml
+++ b/config/locales/telephony/fr.yml
@@ -41,12 +41,12 @@ fr:
invalid_phone_number: Le numéro de téléphone saisi n’est pas valide.
opt_out: Le numéro de téléphone entré a désactivé les messages texte.
permanent_failure: Le numéro de téléphone entré n’est pas valide.
+ rate_limited: Ce nombre connaît un volume de messages élevé. Veuillez réessayer
+ plus tard.
sms_unsupported: Le numéro de téléphone saisi ne prend pas en charge les
messages textuels. Veuillez essayer l’option d’appel téléphonique.
temporary_failure: Nous rencontrons des difficultés techniques. Veuillez
réessayer plus tard.
- throttled: Ce nombre connaît un volume de messages élevé. Veuillez réessayer
- plus tard.
timeout: Le serveur a pris trop de temps pour répondre. Veuillez réessayer.
unknown_failure: Nous rencontrons des difficultés techniques. Veuillez réessayer
plus tard.
diff --git a/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb b/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb
new file mode 100644
index 00000000000..a92e876c7f9
--- /dev/null
+++ b/db/primary_migrate/20230720162501_remove_reproof_at_from_profiles.rb
@@ -0,0 +1,5 @@
+class RemoveReproofAtFromProfiles < ActiveRecord::Migration[7.0]
+ def change
+ safety_assured { remove_column :profiles, :reproof_at, :date }
+ end
+end
diff --git a/db/primary_migrate/20230720183509_create_suspended_emails_table.rb b/db/primary_migrate/20230720183509_create_suspended_emails_table.rb
new file mode 100644
index 00000000000..494cce27e04
--- /dev/null
+++ b/db/primary_migrate/20230720183509_create_suspended_emails_table.rb
@@ -0,0 +1,9 @@
+class CreateSuspendedEmailsTable < ActiveRecord::Migration[7.0]
+ def change
+ create_table :suspended_emails do |t|
+ t.references :email_address, null: false
+ t.string :digested_base_email, null: false, index: true
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bcf0914a68b..cae30ecad3f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2023_07_07_144310) do
+ActiveRecord::Schema[7.0].define(version: 2023_07_20_183509) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "pgcrypto"
@@ -441,7 +441,6 @@
t.integer "deactivation_reason"
t.jsonb "proofing_components"
t.string "name_zip_birth_year_signature"
- t.date "reproof_at"
t.string "initiating_service_provider_issuer"
t.datetime "fraud_review_pending_at"
t.datetime "fraud_rejection_at"
@@ -452,7 +451,6 @@
t.index ["fraud_review_pending_at"], name: "index_profiles_on_fraud_review_pending_at"
t.index ["gpo_verification_pending_at"], name: "index_profiles_on_gpo_verification_pending_at"
t.index ["name_zip_birth_year_signature"], name: "index_profiles_on_name_zip_birth_year_signature"
- t.index ["reproof_at"], name: "index_profiles_on_reproof_at"
t.index ["ssn_signature"], name: "index_profiles_on_ssn_signature"
t.index ["user_id", "active"], name: "index_profiles_on_user_id_and_active", unique: true, where: "(active = true)"
t.index ["user_id"], name: "index_profiles_on_user_id"
@@ -572,6 +570,15 @@
t.index ["request_id"], name: "index_sp_return_logs_on_request_id", unique: true
end
+ create_table "suspended_emails", force: :cascade do |t|
+ t.bigint "email_address_id", null: false
+ t.string "digested_base_email", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["digested_base_email"], name: "index_suspended_emails_on_digested_base_email"
+ t.index ["email_address_id"], name: "index_suspended_emails_on_email_address_id"
+ end
+
create_table "users", id: :serial, force: :cascade do |t|
t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at", precision: nil
diff --git a/lib/app_artifacts.rb b/lib/app_artifacts.rb
index 9a3d33c797e..82160a429dd 100644
--- a/lib/app_artifacts.rb
+++ b/lib/app_artifacts.rb
@@ -36,7 +36,7 @@ def add_artifact(name, path)
private
def read_artifact(path)
- if Identity::Hostdata.in_datacenter?
+ if Identity::Hostdata.in_datacenter? && !ENV['LOGIN_SKIP_REMOTE_CONFIG']
secrets_s3.read_file(path)
else
read_local_artifact(path)
diff --git a/lib/identity_config.rb b/lib/identity_config.rb
index 9119edc93f1..1c2368a2368 100644
--- a/lib/identity_config.rb
+++ b/lib/identity_config.rb
@@ -99,8 +99,6 @@ def self.build_store(config_map)
config.add(:account_reset_token_valid_for_days, type: :integer)
config.add(:account_reset_wait_period_days, type: :integer)
config.add(:account_suspended_support_code, type: :string)
- config.add(:acuant_maintenance_window_start, type: :timestamp, allow_nil: true)
- config.add(:acuant_maintenance_window_finish, type: :timestamp, allow_nil: true)
config.add(:acuant_assure_id_password)
config.add(:acuant_assure_id_username)
config.add(:acuant_assure_id_subscription_id)
diff --git a/lib/tasks/remove_verified_at_for_non_verified.rake b/lib/tasks/remove_verified_at_for_non_verified.rake
new file mode 100644
index 00000000000..ee6a0d1c598
--- /dev/null
+++ b/lib/tasks/remove_verified_at_for_non_verified.rake
@@ -0,0 +1,68 @@
+namespace :profiles do
+ desc 'Remove verified_at if a profile is gpo, fraud pending or fraud rejected'
+
+ ##
+ # Usage:
+ #
+ # Print errant profiles
+ # bundle exec rake profiles:remove_verified_at_from_non_verified_profiles
+ #
+ # Commit updates
+ # bundle exec rake profiles:remove_verified_at_from_non_verified_profiles UPDATE_PROFILES=true
+ #
+ task remove_verified_at_from_non_verified_profiles: :environment do |_task, _args|
+ ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
+
+ update_profiles = ENV['UPDATE_PROFILES'] == 'true'
+
+ profiles = Profile.where('verified_at IS NOT NULL').
+ where('fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL OR
+ gpo_verification_pending_at IS NOT NULL')
+
+ profiles.each do |profile|
+ warn "#{profile.id},#{profile.verified_at}, #{profile.user.uuid}"
+ profile.update!(verified_at: nil) if update_profiles
+ end
+ end
+
+ ##
+ # Usage:
+ #
+ # Rollback the above:
+ #
+ # export BACKFILL_OUTPUT=''
+ # bundle exec rake profiles:rollback_remove_verified_at_from_non_verified_profiles
+ #
+ task rollback_remove_verified_at_from_non_verified_profiles: :environment do |_task, _args|
+ ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
+
+ profiles = ENV['VERIFIED_OUTPUT']
+
+ warn "Updating #{profiles.count} records"
+
+ profiles.split("\n").map do |profile_row|
+ profile_id, profile_verified_at = profile_row.split(',', 2)
+
+ Profile.find(profile_id).update(verified_at: profile_verified_at)
+ end
+ end
+
+ ##
+ # Usage:
+ # bundle exec rake profiles:validate_remove_verified_at_from_non_verified_profiles
+ #
+ task validate_remove_verified_at_from_non_verified_profiles: :environment do |_task, _args|
+ ActiveRecord::Base.connection.execute('SET statement_timeout = 60000')
+
+ profiles = Profile.where(
+ verified_at: nil,
+ ).where('fraud_review_pending_at IS NOT NULL OR fraud_rejection_at IS NOT NULL OR
+ gpo_verification_pending_at IS NOT NULL')
+
+ if profiles.empty?
+ warn 'remove verified_at from profiles that were not verified was successful'
+ else
+ warn "remove verified_at from profiles that were not verified left #{profiles.count} rows"
+ end
+ end
+end
diff --git a/lib/telephony/errors.rb b/lib/telephony/errors.rb
index e301b692cd6..62453eec2f9 100644
--- a/lib/telephony/errors.rb
+++ b/lib/telephony/errors.rb
@@ -68,10 +68,10 @@ def friendly_error_message_key
end
end
- class ThrottledError < TelephonyError
+ class RateLimitedError < TelephonyError
def friendly_error_message_key
- # i18n-tasks-use t('telephony.error.friendly_message.throttled')
- 'telephony.error.friendly_message.throttled'
+ # i18n-tasks-use t('telephony.error.friendly_message.rate_limited')
+ 'telephony.error.friendly_message.rate_limited'
end
end
diff --git a/lib/telephony/pinpoint/sms_sender.rb b/lib/telephony/pinpoint/sms_sender.rb
index ff6778af320..69fccf8e5e3 100644
--- a/lib/telephony/pinpoint/sms_sender.rb
+++ b/lib/telephony/pinpoint/sms_sender.rb
@@ -8,7 +8,7 @@ class SmsSender
'OPT_OUT' => OptOutError,
'PERMANENT_FAILURE' => PermanentFailureError,
'TEMPORARY_FAILURE' => TemporaryFailureError,
- 'THROTTLED' => ThrottledError,
+ 'THROTTLED' => RateLimitedError,
'TIMEOUT' => TimeoutError,
'UNKNOWN_FAILURE' => UnknownFailureError,
}.freeze
diff --git a/lib/telephony/pinpoint/voice_sender.rb b/lib/telephony/pinpoint/voice_sender.rb
index 3013df34146..7696109d4f4 100644
--- a/lib/telephony/pinpoint/voice_sender.rb
+++ b/lib/telephony/pinpoint/voice_sender.rb
@@ -81,7 +81,7 @@ def handle_pinpoint_error(err)
error_message = "#{err.class}: #{err.message}"
error_class = if err.is_a? Aws::PinpointSMSVoice::Errors::LimitExceededException
- Telephony::ThrottledError
+ Telephony::RateLimitedError
elsif err.is_a? Aws::PinpointSMSVoice::Errors::TooManyRequestsException
Telephony::DailyLimitReachedError
else
diff --git a/spec/components/webauthn_input_component_spec.rb b/spec/components/webauthn_input_component_spec.rb
index 766ddfd71e5..2e9f99d9931 100644
--- a/spec/components/webauthn_input_component_spec.rb
+++ b/spec/components/webauthn_input_component_spec.rb
@@ -6,12 +6,7 @@
subject(:rendered) { render_inline component }
it 'renders element with expected attributes' do
- element = rendered.css('lg-webauthn-input').first
-
- expect(element.attr('hidden')).to be_present
- expect(element.attr('platform')).to be_nil
- expect(element.attr('passkey-supported-only')).to be_nil
- expect(element.attr('show-unsupported-passkey')).to be_nil
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
it 'exposes boolean alias for platform option' do
@@ -28,66 +23,63 @@
context 'with platform option' do
context 'with platform option false' do
- let(:options) { { platform: false } }
+ let(:options) { super().merge(platform: false) }
- it 'renders without platform attribute' do
- expect(rendered).to have_css('lg-webauthn-input[hidden]:not([platform])', visible: false)
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
end
context 'with platform option true' do
- let(:options) { { platform: true } }
+ let(:options) { super().merge(platform: true) }
- it 'renders with platform attribute' do
- expect(rendered).to have_css('lg-webauthn-input[hidden][platform]', visible: false)
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
end
- end
- end
-
- context 'with passkey_supported_only option' do
- context 'with passkey_supported_only option false' do
- let(:options) { { passkey_supported_only: false } }
-
- it 'renders without passkey-supported-only attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden]:not([passkey-supported-only])',
- visible: false,
- )
- end
- end
-
- context 'with passkey_supported_only option true' do
- let(:options) { { passkey_supported_only: true } }
-
- it 'renders with passkey-supported-only attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden][passkey-supported-only]',
- visible: false,
- )
- end
- end
- end
-
- context 'with show_unsupported_passkey option' do
- context 'with show_unsupported_passkey option false' do
- let(:options) { { show_unsupported_passkey: false } }
-
- it 'renders without show-unsupported-passkey attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
- visible: false,
- )
- end
- end
-
- context 'with show_unsupported_passkey option true' do
- let(:options) { { show_unsupported_passkey: true } }
- it 'renders with show-unsupported-passkey attribute' do
- expect(rendered).to have_css(
- 'lg-webauthn-input[hidden][show-unsupported-passkey]',
- visible: false,
- )
+ context 'with passkey_supported_only option' do
+ context 'with passkey_supported_only option false' do
+ let(:options) { super().merge(passkey_supported_only: false) }
+
+ it 'renders as visible for js-enabled browsers' do
+ expect(rendered).to have_css('lg-webauthn-input.js:not([show-unsupported-passkey])')
+ end
+ end
+
+ context 'with passkey_supported_only option true' do
+ let(:options) { super().merge(passkey_supported_only: true) }
+
+ it 'renders as hidden' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
+ visible: false,
+ )
+ end
+
+ context 'with show_unsupported_passkey option' do
+ context 'with show_unsupported_passkey option false' do
+ let(:options) { super().merge(show_unsupported_passkey: false) }
+
+ it 'renders as hidden' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden]:not([show-unsupported-passkey])',
+ visible: false,
+ )
+ end
+ end
+
+ context 'with show_unsupported_passkey option true' do
+ let(:options) { super().merge(show_unsupported_passkey: true) }
+
+ it 'renders with show-unsupported-passkey attribute' do
+ expect(rendered).to have_css(
+ 'lg-webauthn-input[hidden][show-unsupported-passkey]',
+ visible: false,
+ )
+ end
+ end
+ end
+ end
end
end
end
@@ -96,7 +88,7 @@
let(:options) { super().merge(data: { foo: 'bar' }) }
it 'renders with additional attributes' do
- expect(rendered).to have_css('lg-webauthn-input[hidden][data-foo="bar"]', visible: false)
+ expect(rendered).to have_css('lg-webauthn-input[data-foo="bar"]')
end
end
end
diff --git a/spec/controllers/idv/gpo_verify_controller_spec.rb b/spec/controllers/idv/gpo_verify_controller_spec.rb
index c4720e654c7..2736311dd38 100644
--- a/spec/controllers/idv/gpo_verify_controller_spec.rb
+++ b/spec/controllers/idv/gpo_verify_controller_spec.rb
@@ -131,7 +131,7 @@
success: true,
errors: {},
pending_in_person_enrollment: false,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -178,7 +178,7 @@
success: true,
errors: {},
pending_in_person_enrollment: true,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -203,7 +203,6 @@
create(
:profile,
:with_pii,
- :fraud_review_pending,
fraud_pending_reason: 'threatmetrix_reject',
user: user,
)
@@ -215,7 +214,7 @@
success: true,
errors: {},
pending_in_person_enrollment: false,
- threatmetrix_check_failed: true,
+ fraud_check_failed: true,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -240,7 +239,6 @@
create(
:profile,
:with_pii,
- :fraud_review_pending,
fraud_pending_reason: 'threatmetrix_reject',
user: user,
)
@@ -252,7 +250,7 @@
success: true,
errors: {},
pending_in_person_enrollment: false,
- threatmetrix_check_failed: true,
+ fraud_check_failed: true,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -278,7 +276,6 @@
create(
:profile,
:with_pii,
- :fraud_review_pending,
fraud_pending_reason: 'threatmetrix_review',
user: user,
)
@@ -290,7 +287,7 @@
success: true,
errors: {},
pending_in_person_enrollment: false,
- threatmetrix_check_failed: true,
+ fraud_check_failed: true,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
)
@@ -312,7 +309,7 @@
success: false,
errors: otp_code_error_message,
pending_in_person_enrollment: false,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: nil,
error_details: otp_code_incorrect,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
@@ -343,7 +340,7 @@
success: false,
errors: otp_code_error_message,
pending_in_person_enrollment: false,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: nil,
error_details: otp_code_incorrect,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
@@ -387,7 +384,7 @@
success: false,
errors: otp_code_error_message,
pending_in_person_enrollment: false,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: nil,
error_details: otp_code_incorrect,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
@@ -397,7 +394,7 @@
success: true,
errors: {},
pending_in_person_enrollment: false,
- threatmetrix_check_failed: false,
+ fraud_check_failed: false,
enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at,
pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]],
).once
diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb
index 153b177cca9..08def1c0bf0 100644
--- a/spec/controllers/idv/image_uploads_controller_spec.rb
+++ b/spec/controllers/idv/image_uploads_controller_spec.rb
@@ -249,10 +249,10 @@
'IdV: doc auth image upload form submitted',
success: false,
errors: {
- limit: [I18n.t('errors.doc_auth.throttled_heading')],
+ limit: [I18n.t('errors.doc_auth.rate_limited_heading')],
},
error_details: {
- limit: [I18n.t('errors.doc_auth.throttled_heading')],
+ limit: [I18n.t('errors.doc_auth.rate_limited_heading')],
},
user_id: user.uuid,
attempts: IdentityConfig.store.doc_auth_max_attempts,
diff --git a/spec/controllers/idv/in_person/ssn_controller_spec.rb b/spec/controllers/idv/in_person/ssn_controller_spec.rb
index 7ef46e95e25..c393733e1ca 100644
--- a/spec/controllers/idv/in_person/ssn_controller_spec.rb
+++ b/spec/controllers/idv/in_person/ssn_controller_spec.rb
@@ -84,61 +84,167 @@
expect(response).to redirect_to idv_in_person_step_url(step: :address)
end
end
+ end
+ end
+
+ describe '#show' do
+ context 'when in_person_ssn_info_controller_enabled is true' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_ssn_info_controller_enabled).
+ and_return(true)
+ end
+ let(:analytics_name) { 'IdV: doc auth ssn visited' }
+ let(:analytics_args) do
+ {
+ analytics_id: 'In Person Proofing',
+ flow_path: 'standard',
+ irs_reproofing: false,
+ step: 'ssn',
+ }.merge(ab_test_args)
+ end
+
+ it 'renders the show template' do
+ get :show
+
+ expect(response).to render_template :show
+ end
+
+ it 'sends analytics_visited event' do
+ get :show
+
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ end
+
+ it 'updates DocAuthLog ssn_view_count' do
+ doc_auth_log = DocAuthLog.create(user_id: user.id)
+
+ expect { get :show }.to(
+ change { doc_auth_log.reload.ssn_view_count }.from(0).to(1),
+ )
+ end
+
+ context 'with an ssn in session' do
+ let(:referer) { idv_document_capture_url }
+ before do
+ flow_session['pii_from_user'][:ssn] = ssn
+ request.env['HTTP_REFERER'] = referer
+ end
+
+ context 'referer is not verify_info' do
+ it 'redirects to verify_info' do
+ get :show
+
+ expect(response).to redirect_to(idv_in_person_verify_info_url)
+ end
+ end
+
+ context 'referer is verify_info' do
+ let(:referer) { idv_in_person_verify_info_url }
+ it 'does not redirect' do
+ get :show
+
+ expect(response).to render_template :show
+ end
+ end
+ end
+ end
+ end
+
+ describe '#update' do
+ context 'when in_person_ssn_info_controller_enabled is true' do
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_ssn_info_controller_enabled).
+ and_return(true)
+ end
- describe '#show' do
- let(:analytics_name) { 'IdV: doc auth ssn visited' }
+ context 'valid ssn' do
+ let(:params) { { doc_auth: { ssn: ssn } } }
+ let(:analytics_name) { 'IdV: doc auth ssn submitted' }
let(:analytics_args) do
{
analytics_id: 'In Person Proofing',
flow_path: 'standard',
irs_reproofing: false,
step: 'ssn',
+ success: true,
+ errors: {},
+ pii_like_keypaths: [[:errors, :ssn], [:error_details, :ssn]],
}.merge(ab_test_args)
end
- it 'renders the show template' do
- get :show
-
- expect(response).to render_template :show
+ let(:idv_session) do
+ {
+ applicant: Idp::Constants::MOCK_IDV_APPLICANT,
+ resolution_successful: true,
+ profile_confirmation: true,
+ vendor_phone_confirmation: true,
+ user_phone_confirmation: true,
+ }
end
- it 'sends analytics_visited event' do
- get :show
+ it 'sends analytics_submitted event' do
+ put :update, params: params
expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
end
- it 'updates DocAuthLog ssn_view_count' do
- doc_auth_log = DocAuthLog.create(user_id: user.id)
-
- expect { get :show }.to(
- change { doc_auth_log.reload.ssn_view_count }.from(0).to(1),
+ it 'logs attempts api event' do
+ expect(@irs_attempts_api_tracker).to receive(:idv_ssn_submitted).with(
+ ssn: ssn,
)
+
+ put :update, params: params
end
- context 'with an ssn in session' do
- let(:referer) { idv_document_capture_url }
- before do
- flow_session['pii_from_user'][:ssn] = ssn
- request.env['HTTP_REFERER'] = referer
- end
+ it 'merges ssn into pii session value' do
+ put :update, params: params
- context 'referer is not verify_info' do
- it 'redirects to verify_info' do
- get :show
+ expect(flow_session['pii_from_user'][:ssn]).to eq(ssn)
+ end
- expect(response).to redirect_to(idv_in_person_verify_info_url)
- end
- end
+ it 'invalidates steps after ssn' do
+ put :update, params: params
- context 'referer is verify_info' do
- let(:referer) { idv_in_person_verify_info_url }
- it 'does not redirect' do
- get :show
+ expect(subject.idv_session.applicant).to be_blank
+ expect(subject.idv_session.resolution_successful).to be_blank
+ expect(subject.idv_session.profile_confirmation).to be_blank
+ expect(subject.idv_session.vendor_phone_confirmation).to be_blank
+ expect(subject.idv_session.user_phone_confirmation).to be_blank
+ end
- expect(response).to render_template :show
- end
- end
+ it 'redirects to the expected page' do
+ put :update, params: params
+
+ expect(response).to redirect_to idv_in_person_verify_info_url
+ end
+ end
+
+ context 'invalid ssn' do
+ let(:params) { { doc_auth: { ssn: 'i am not an ssn' } } }
+ let(:analytics_name) { 'IdV: doc auth ssn submitted' }
+ let(:analytics_args) do
+ {
+ analytics_id: 'In Person Proofing',
+ flow_path: 'standard',
+ irs_reproofing: false,
+ step: 'ssn',
+ success: false,
+ errors: {
+ ssn: ['Enter a nine-digit Social Security number'],
+ },
+ error_details: { ssn: [:invalid] },
+ pii_like_keypaths: [[:errors, :ssn], [:error_details, :ssn]],
+ }.merge(ab_test_args)
+ end
+
+ render_views
+
+ it 'renders the show template with an error message' do
+ put :update, params: params
+
+ expect(response).to have_rendered(:show)
+ expect(@analytics).to have_received(:track_event).with(analytics_name, analytics_args)
+ expect(response.body).to include('Enter a nine-digit Social Security number')
end
end
end
diff --git a/spec/controllers/idv/phone_controller_spec.rb b/spec/controllers/idv/phone_controller_spec.rb
index 274909c3cc7..18ff8310a9a 100644
--- a/spec/controllers/idv/phone_controller_spec.rb
+++ b/spec/controllers/idv/phone_controller_spec.rb
@@ -11,6 +11,7 @@
let(:normalized_phone) { '7035550000' }
let(:bad_phone) { '+1 (703) 555-5555' }
let(:international_phone) { '+81 54 354 3643' }
+ let(:timeout_phone) { '7035555888' }
describe 'before_actions' do
it 'includes authentication before_action' do
@@ -328,6 +329,7 @@
)
expect(subject.idv_session.vendor_phone_confirmation).to eq true
expect(subject.idv_session.user_phone_confirmation).to eq false
+ expect(subject.idv_session.failed_phone_step_numbers).to be_empty
end
context 'with full vendor outage' do
@@ -440,6 +442,22 @@
expect(subject.idv_session.vendor_phone_confirmation).to be_falsy
expect(subject.idv_session.user_phone_confirmation).to be_falsy
+ expect(subject.idv_session.failed_phone_step_numbers).to contain_exactly('+17035555555')
+ end
+
+ it 'renders timeout page and does not set phone confirmation' do
+ user = build(:user, with: { phone: '+1 (415) 555-0130', phone_confirmed_at: Time.zone.now })
+ stub_verify_steps_one_and_two(user)
+
+ put :create, params: { idv_phone_form: { phone: timeout_phone } }
+
+ expect(response).to redirect_to idv_phone_path
+ get :new
+ expect(response).to redirect_to idv_phone_errors_timeout_path
+
+ expect(subject.idv_session.vendor_phone_confirmation).to be_falsy
+ expect(subject.idv_session.user_phone_confirmation).to be_falsy
+ expect(subject.idv_session.failed_phone_step_numbers).to be_empty
end
it 'tracks event with invalid phone' do
diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb
index 0cdfdf9ae82..a1dc9aef164 100644
--- a/spec/controllers/sign_up/completions_controller_spec.rb
+++ b/spec/controllers/sign_up/completions_controller_spec.rb
@@ -341,7 +341,6 @@
issuer: 'foo',
request_url: 'http://example.com',
}
- expect(@irs_attempts_api_tracker).not_to receive(:idv_reproof)
patch :update
end
@@ -372,7 +371,6 @@
expect(additional_profile.initiating_service_provider).to be_nil
expect(additional_profile.verified_at).to be_present
- expect(@irs_attempts_api_tracker).to receive(:idv_reproof)
patch :update
end
@@ -383,7 +381,6 @@
ial2: false,
request_url: 'http://example.com',
}
- expect(@irs_attempts_api_tracker).not_to receive(:idv_reproof)
patch :update
diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb
index cca9e053abd..f4b0ef01117 100644
--- a/spec/controllers/users/reset_passwords_controller_spec.rb
+++ b/spec/controllers/users/reset_passwords_controller_spec.rb
@@ -235,6 +235,8 @@
error_details: reset_password_error_details,
user_id: user.uuid,
profile_deactivated: false,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
}
expect(@analytics).to have_received(:track_event).
@@ -266,6 +268,8 @@
error_details: password_short_error,
user_id: user.uuid,
profile_deactivated: false,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
}
expect(@analytics).to receive(:track_event).
@@ -341,6 +345,8 @@
errors: {},
user_id: user.uuid,
profile_deactivated: false,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
}
expect(@analytics).to have_received(:track_event).
@@ -389,6 +395,8 @@
errors: {},
user_id: user.uuid,
profile_deactivated: true,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
}
expect(@analytics).to have_received(:track_event).
@@ -434,6 +442,8 @@
errors: {},
user_id: user.uuid,
profile_deactivated: false,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
}
expect(@analytics).to have_received(:track_event).
diff --git a/spec/controllers/users/two_factor_authentication_controller_spec.rb b/spec/controllers/users/two_factor_authentication_controller_spec.rb
index bd18c1d5e20..4c0fe5032cb 100644
--- a/spec/controllers/users/two_factor_authentication_controller_spec.rb
+++ b/spec/controllers/users/two_factor_authentication_controller_spec.rb
@@ -623,7 +623,7 @@ def index
expect(flash[:error]).to eq(
I18n.t(
- 'errors.messages.phone_confirmation_throttled',
+ 'errors.messages.phone_confirmation_limited',
timeout: timeout,
),
)
@@ -676,7 +676,7 @@ def index
expect(flash[:error]).to eq(
I18n.t(
- 'errors.messages.phone_confirmation_throttled',
+ 'errors.messages.phone_confirmation_limited',
timeout: timeout,
),
)
diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb
index 67e7a38f842..7e0b5177bde 100644
--- a/spec/factories/profiles.rb
+++ b/spec/factories/profiles.rb
@@ -37,6 +37,11 @@
deactivation_reason { :in_person_verification_pending }
end
+ trait :fraud_pending_reason do
+ fraud_pending_reason { 'threatmetrix_review' }
+ proofing_components { { threatmetrix_review_status: 'review' } }
+ end
+
trait :fraud_review_pending do
fraud_pending_reason { 'threatmetrix_review' }
fraud_review_pending_at { 15.days.ago }
diff --git a/spec/factories/suspended_emails.rb b/spec/factories/suspended_emails.rb
new file mode 100644
index 00000000000..a018f738036
--- /dev/null
+++ b/spec/factories/suspended_emails.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :suspended_email do
+ digested_base_email { 'test_digest' }
+ association :email_address
+ end
+end
diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb
index 7ee0c172226..203b39d2333 100644
--- a/spec/features/idv/analytics_spec.rb
+++ b/spec/features/idv/analytics_spec.rb
@@ -50,7 +50,7 @@
'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false },
'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false },
'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_results: { exception: nil, timed_out: false, threatmetrix_review_status: 'pass', context: { device_profiling_adjudication_reason: 'device_profiling_result_pass', double_address_verification: false, 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 } } } },
- 'IdV: phone of record visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
+ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
'IdV: phone confirmation form' => { success: true, errors: {}, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' }, otp_delivery_preference: 'sms' },
'IdV: phone confirmation vendor' => { success: true, errors: {}, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, area_code: '202', country_code: 'US', phone_fingerprint: anything },
'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' }, adapter: :test, errors: {}, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything },
@@ -85,7 +85,7 @@
'IdV: doc auth verify visited' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false },
'IdV: doc auth verify submitted' => { flow_path: 'standard', step: 'verify', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, analytics_id: 'Doc Auth', irs_reproofing: false },
'IdV: doc auth verify proofing results' => { success: true, errors: {}, address_edited: false, address_line2_present: false, ssn_is_unique: true, acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, 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', double_address_verification: false, 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 } } } },
- 'IdV: phone of record visited' => { proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
+ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
'IdV: USPS address letter requested' => { resend: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } },
'IdV: review info visited' => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, getting_started_ab_test_bucket: :welcome, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } },
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index 8efe4181a4d..906d3efcebe 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -67,7 +67,7 @@
timeout = distance_of_time_in_words(
RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes,
)
- message = strip_tags(t('errors.doc_auth.throttled_text_html', timeout: timeout))
+ message = strip_tags(t('errors.doc_auth.rate_limited_text_html', timeout: timeout))
expect(page).to have_content(message)
expect(page).to have_current_path(idv_session_errors_throttled_path)
end
diff --git a/spec/features/idv/doc_auth/getting_started_spec.rb b/spec/features/idv/doc_auth/getting_started_spec.rb
index 661ce15beeb..0d3ad5a7e16 100644
--- a/spec/features/idv/doc_auth/getting_started_spec.rb
+++ b/spec/features/idv/doc_auth/getting_started_spec.rb
@@ -5,7 +5,6 @@
include DocAuthHelper
let(:fake_analytics) { FakeAnalytics.new }
- let(:maintenance_window) { [] }
let(:sp_name) { 'Test SP' }
before do
diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
index 302dd710bde..cca341a2c8e 100644
--- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
+++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb
@@ -144,7 +144,7 @@
freeze_time do
idv_send_link_max_attempts.times do
expect(page).to_not have_content(
- I18n.t('errors.doc_auth.send_link_throttle', timeout: timeout),
+ I18n.t('errors.doc_auth.send_link_limited', timeout: timeout),
)
fill_in :doc_auth_phone, with: '415-555-0199'
@@ -161,7 +161,7 @@
expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true)
expect(page).to have_content(
I18n.t(
- 'errors.doc_auth.send_link_throttle',
+ 'errors.doc_auth.send_link_limited',
timeout: timeout,
),
)
diff --git a/spec/features/idv/doc_auth/welcome_spec.rb b/spec/features/idv/doc_auth/welcome_spec.rb
index d1b77ff1625..42334e19a77 100644
--- a/spec/features/idv/doc_auth/welcome_spec.rb
+++ b/spec/features/idv/doc_auth/welcome_spec.rb
@@ -5,15 +5,11 @@
include DocAuthHelper
let(:fake_analytics) { FakeAnalytics.new }
- let(:maintenance_window) { [] }
let(:sp_name) { 'Test SP' }
before do
allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
allow_any_instance_of(ServiceProviderSessionDecorator).to receive(:sp_name).and_return(sp_name)
- start, finish = maintenance_window
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start)
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish)
visit_idp_from_sp_with_ial2(:oidc)
sign_in_and_2fa_user
@@ -80,20 +76,4 @@
),
)
end
-
- context 'during the acuant maintenance window' do
- let(:maintenance_window) do
- [Time.zone.parse('2020-01-01T00:00:00Z'), Time.zone.parse('2020-01-01T23:59:59Z')]
- end
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
-
- around do |ex|
- travel_to(now) { ex.run }
- end
-
- it 'renders the warning banner but no other content' do
- expect(page).to have_content('We are currently under maintenance')
- expect(page).to_not have_content(t('doc_auth.headings.welcome'))
- end
- end
end
diff --git a/spec/features/idv/end_to_end_idv_spec.rb b/spec/features/idv/end_to_end_idv_spec.rb
index 73e1d7dd20f..c6f0f633a02 100644
--- a/spec/features/idv/end_to_end_idv_spec.rb
+++ b/spec/features/idv/end_to_end_idv_spec.rb
@@ -165,6 +165,10 @@ def validate_phone_page
t('two_factor_authentication.otp_delivery_preference.sms'), visible: false
)
+ # displays phone number by default
+ phone_field = find_field(t('two_factor_authentication.phone_label'))
+ expect(phone_field.value).not_to be_empty
+
# displays error if invalid phone number is entered
fill_in :idv_phone_form_phone, with: '578190'
click_idv_send_security_code
diff --git a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb
index 6ba031d7320..4fdf2b4185f 100644
--- a/spec/features/idv/steps/gpo_otp_verification_step_spec.rb
+++ b/spec/features/idv/steps/gpo_otp_verification_step_spec.rb
@@ -18,9 +18,7 @@
ssn: '123-45-6789',
dob: '1970-01-01',
},
- fraud_review_pending_at: fraud_review_pending_timestamp,
fraud_pending_reason: fraud_pending_reason,
- fraud_rejection_at: fraud_rejection_timestamp,
)
end
let(:gpo_confirmation_code) do
@@ -32,9 +30,7 @@
end
let(:user) { profile.user }
let(:threatmetrix_enabled) { false }
- let(:fraud_review_pending_timestamp) { nil }
let(:fraud_pending_reason) { nil }
- let(:fraud_rejection_timestamp) { nil }
let(:redirect_after_verification) { nil }
let(:profile_should_be_active) { true }
let(:fraud_review_pending) { false }
@@ -48,7 +44,6 @@
context 'ThreatMetrix disabled, but we have ThreatMetrix status on proofing component' do
let(:threatmetrix_enabled) { false }
- let(:fraud_review_pending_timestamp) { 1.day.ago }
let(:fraud_pending_reason) { 'threatmetrix_review' }
it_behaves_like 'gpo otp verification'
end
@@ -57,12 +52,10 @@
let(:threatmetrix_enabled) { true }
context 'ThreatMetrix says "pass"' do
- let(:fraud_review_pending_timestamp) { nil }
it_behaves_like 'gpo otp verification'
end
context 'ThreatMetrix says "review"' do
- let(:fraud_review_pending_timestamp) { 1.day.ago }
let(:fraud_pending_reason) { 'threatmetrix_review' }
let(:profile_should_be_active) { false }
let(:fraud_review_pending) { true }
@@ -70,15 +63,14 @@
end
context 'ThreatMetrix says "reject"' do
- let(:fraud_rejection_timestamp) { 1.day.ago }
- let(:fraud_pending_reason) { 'threatmetrix_review' }
+ let(:fraud_pending_reason) { 'threatmetrix_reject' }
let(:profile_should_be_active) { false }
let(:fraud_review_pending) { true }
it_behaves_like 'gpo otp verification'
end
context 'No ThreatMetrix result on proofing component' do
- let(:fraud_review_pending_timestamp) { nil }
+ let(:fraud_pending_reason) { nil }
it_behaves_like 'gpo otp verification'
end
end
diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb
index 0127ab3f154..924ebd6b120 100644
--- a/spec/features/idv/steps/phone_step_spec.rb
+++ b/spec/features/idv/steps/phone_step_spec.rb
@@ -76,16 +76,125 @@
end
context "when the user's information cannot be verified" do
- it 'reports the number the user entered' do
+ before do
start_idv_from_sp
complete_idv_steps_before_phone_step
fill_out_phone_form_fail
+ end
+
+ it 'reports the number the user entered' do
click_idv_send_security_code
expect(page).to have_content(t('idv.failure.phone.warning.heading'))
expect(page).to have_content('+1 703-555-5555')
end
+ context 'resubmission after number failed verification' do
+ it 'phone field is empty after invalid submission' do
+ phone_field = find_field(t('two_factor_authentication.phone_label'))
+
+ expect(phone_field.value).not_to be_empty
+
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+ expect(phone_field.value).to be_empty
+ end
+
+ it 'succeeds to otp verification with valid number resubmission' do
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+
+ fill_out_phone_form_ok
+ click_idv_send_security_code
+ expect(page).to have_current_path(idv_otp_verification_path)
+ end
+
+ context 'displays alert message if same nubmer is resubmitted' do
+ context 'gpo verification is enabled' do
+ it 'includes verify link' do
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+
+ fill_out_phone_form_fail
+
+ expect(page).to have_content(t('idv.messages.phone.failed_number.alert_text'))
+
+ expect(page).to have_content(
+ strip_tags(
+ t(
+ 'idv.messages.phone.failed_number.gpo_alert_html',
+ link_html: t('idv.messages.phone.failed_number.gpo_verify_link'),
+ ),
+ ),
+ )
+
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+ end
+ end
+
+ context 'gpo verification is disabled' do
+ before do
+ allow(IdentityConfig.store).to receive(:enable_usps_verification).and_return(false)
+ end
+
+ it 'does not display verify link' do
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+
+ fill_out_phone_form_fail
+
+ expect(page).to have_content(t('idv.messages.phone.failed_number.alert_text'))
+ expect(page).not_to have_content(
+ strip_tags(
+ t(
+ 'idv.messages.phone.failed_number.gpo_alert_html',
+ link_html: t('idv.messages.phone.failed_number.gpo_verify_link'),
+ ),
+ ),
+ )
+
+ click_idv_send_security_code
+ click_on t('idv.failure.phone.warning.try_again_button')
+
+ expect(page).to have_current_path(idv_phone_path)
+ end
+ end
+ end
+ end
+
+ context 'phone number submission times out' do
+ it 'does not display failed alert message' do
+ timeout_phone_number = '7035555888'
+ start_idv_from_sp
+ complete_idv_steps_before_phone_step
+ fill_out_phone_form_ok(timeout_phone_number)
+ click_idv_send_security_code
+ click_on t('idv.failure.button.warning')
+
+ expect(page).to have_current_path(idv_phone_path)
+
+ fill_out_phone_form_ok(timeout_phone_number)
+
+ expect(page).not_to have_content(t('idv.messages.phone.failed_number.alert_text'))
+
+ click_idv_send_security_code
+ click_on t('idv.failure.button.warning')
+
+ expect(page).to have_current_path(idv_phone_path)
+ end
+ end
+
it 'goes to the cancel page when cancel link is clicked' do
start_idv_from_sp
complete_idv_steps_before_phone_step
@@ -161,6 +270,7 @@
click_idv_continue_for_step(:phone)
click_on t('idv.failure.phone.warning.try_again_button')
end
+ fill_out_phone_form_fail
click_idv_continue_for_step(:phone)
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 7584acf90f3..c2905b8c7d7 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
@@ -75,6 +75,51 @@
expect(current_path).to eq account_path
end
+ scenario 'user can select 2 MFA methods and complete after reauthn window' do
+ allow(IdentityConfig.store).to receive(:reauthn_window).and_return(10)
+ sign_in_before_2fa
+
+ expect(current_path).to eq authentication_methods_setup_path
+
+ click_2fa_option('backup_code')
+ click_2fa_option('auth_app')
+
+ click_continue
+
+ expect(current_path).to eq authenticator_setup_path
+ fill_in t('forms.totp_setup.totp_step_1'), with: 'App'
+
+ secret = find('#qr-code').text
+ totp = generate_totp_code(secret)
+
+ fill_in :code, with: totp
+ check t('forms.messages.remember_device')
+ click_submit_default
+
+ expect(current_path).to eq backup_code_setup_path
+ travel_to((IdentityConfig.store.reauthn_window + 5).seconds.from_now) do
+ click_continue
+ expect(current_path).to eq login_two_factor_options_path
+
+ find("label[for='two_factor_options_form_selection_auth_app']").click
+ click_on t('forms.buttons.continue')
+
+ totp = generate_totp_code(secret)
+ fill_in :code, with: totp
+
+ click_submit_default
+ click_continue
+
+ expect(page).to have_link(t('components.download_button.label'))
+
+ click_continue
+
+ expect(page).to have_content(t('notices.backup_codes_configured'))
+
+ expect(current_path).to eq account_path
+ end
+ end
+
scenario 'user can select 1 MFA methods and will be prompted to add another method' do
sign_in_before_2fa
diff --git a/spec/features/users/sign_up_spec.rb b/spec/features/users/sign_up_spec.rb
index 1f3a0cfb054..cf4df12e992 100644
--- a/spec/features/users/sign_up_spec.rb
+++ b/spec/features/users/sign_up_spec.rb
@@ -123,7 +123,7 @@
# whether it says '9 minutes' or '10 minutes' depends on how
# slowly the test runs.
throttled_message = I18n.t(
- 'errors.messages.phone_confirmation_throttled',
+ 'errors.messages.phone_confirmation_limited',
timeout: '(10|9) minutes',
)
diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb
index 5ce217fb300..73045e44b40 100644
--- a/spec/features/webauthn/hidden_spec.rb
+++ b/spec/features/webauthn/hidden_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe 'webauthn hide' do
+ include JavascriptDriverHelper
+
describe 'security key' do
let(:option_id) { 'two_factor_options_form_selection_webauthn' }
@@ -102,9 +104,11 @@
end
def webauthn_option_hidden?
- page.find("label[for=#{option_id}]")
- false
- rescue Capybara::ElementNotFound
- true
+ label = page.find("label[for=#{option_id}]", visible: :all)
+ if javascript_enabled?
+ !label.visible?
+ else
+ label.ancestor('.js,[hidden]', visible: :all).present?
+ end
end
end
diff --git a/spec/forms/gpo_verify_form_spec.rb b/spec/forms/gpo_verify_form_spec.rb
index 4e9b69843e7..d6d54a9b23e 100644
--- a/spec/forms/gpo_verify_form_spec.rb
+++ b/spec/forms/gpo_verify_form_spec.rb
@@ -151,11 +151,9 @@
context 'ThreatMetrix rejection' do
let(:pending_profile) do
- create(:profile, :verify_by_mail_pending, :fraud_review_pending, user: user)
+ create(:profile, :verify_by_mail_pending, :fraud_pending_reason, user: user)
end
- let(:threatmetrix_review_status) { 'reject' }
-
before do
allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled)
end
@@ -174,7 +172,7 @@
it 'notes that threatmetrix failed' do
result = subject.submit
- expect(result.extra).to include(threatmetrix_check_failed: true)
+ expect(result.extra).to include(fraud_check_failed: true)
end
context 'threatmetrix is not required for verification' do
@@ -196,7 +194,7 @@
it 'notes that threatmetrix failed' do
result = subject.submit
- expect(result.extra).to include(threatmetrix_check_failed: true)
+ expect(result.extra).to include(fraud_check_failed: true)
end
end
end
diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb
index cc1bb11624b..d5048799bd5 100644
--- a/spec/forms/idv/api_image_upload_form_spec.rb
+++ b/spec/forms/idv/api_image_upload_form_spec.rb
@@ -68,7 +68,7 @@
form.submit
expect(form.valid?).to eq(false)
- expect(form.errors[:limit]).to eq([I18n.t('errors.doc_auth.throttled_heading')])
+ expect(form.errors[:limit]).to eq([I18n.t('errors.doc_auth.rate_limited_heading')])
end
end
end
diff --git a/spec/forms/register_user_email_form_spec.rb b/spec/forms/register_user_email_form_spec.rb
index ddbbdca081c..8f25d7c68b6 100644
--- a/spec/forms/register_user_email_form_spec.rb
+++ b/spec/forms/register_user_email_form_spec.rb
@@ -8,7 +8,7 @@
it_behaves_like 'email validation'
describe '#submit' do
- let(:email_domain) { 'test.com' }
+ let(:email_domain) { 'gmail.com' }
let(:registered_email_address) { 'taken@' + email_domain }
let(:unregistered_email_address) { 'not_taken@' + email_domain }
let(:registered_and_confirmed_user) do
@@ -68,6 +68,32 @@
end
end
+ context 'email submission with special characters' do
+ context 'mx record are gmail' do
+ shared_examples 'blocked email address' do |email_address|
+ it 'sends the email with error code' do
+ user = create(*registered_and_confirmed_user)
+ user.suspend!
+
+ subject.submit(email: email_address, terms_accepted: '1')
+
+ expect_delivered_email_count(1)
+ expect_delivered_email(
+ to: [registered_email_address],
+ subject: t('user_mailer.suspended_create_account.subject'),
+ )
+ expect(subject.send(:blocked_email_address).user).to eq(user)
+ end
+ end
+ context 'when email contains a plus sign' do
+ it_behaves_like 'blocked email address', 'taken+1@gmail.com'
+ end
+ context 'when email contains a dot' do
+ it_behaves_like 'blocked email address', 'tak.en@gmail.com'
+ end
+ end
+ end
+
let(:variation_of_preexisting_email) { 'TAKEN@' + email_domain }
context 'when email is already taken' do
let!(:existing_user) { create(*registered_and_confirmed_user) }
diff --git a/spec/forms/reset_password_form_spec.rb b/spec/forms/reset_password_form_spec.rb
index 22d078d25f6..45c16209764 100644
--- a/spec/forms/reset_password_form_spec.rb
+++ b/spec/forms/reset_password_form_spec.rb
@@ -60,7 +60,6 @@
form = ResetPasswordForm.new(user)
password = 'valid password'
- extra = { user_id: '123', profile_deactivated: false }
user_updater = instance_double(UpdateUser)
allow(UpdateUser).to receive(:new).
with(user: user, attributes: { password: password }).and_return(user_updater)
@@ -69,7 +68,10 @@
expect(form.submit(password: password).to_h).to eq(
success: true,
errors: {},
- **extra,
+ user_id: '123',
+ profile_deactivated: false,
+ pending_profile_invalidated: false,
+ pending_profile_pending_reasons: '',
)
end
end
@@ -151,6 +153,39 @@
end
end
+ context 'when the user has a pending profile' do
+ it 'includes that the profile was not deactivated in the form response' do
+ profile = create(:profile, :verify_by_mail_pending, :in_person_verification_pending)
+ user = profile.user
+ user.update(reset_password_sent_at: Time.zone.now)
+
+ form = ResetPasswordForm.new(user)
+
+ result = form.submit(password: 'a good and powerful password')
+
+ expect(result.success?).to eq(true)
+ expect(result.extra[:pending_profile_invalidated]).to eq(true)
+ expect(result.extra[:pending_profile_pending_reasons]).to eq(
+ 'gpo_verification_pending,in_person_verification_pending',
+ )
+ end
+ end
+
+ context 'when the user does not have a pending profile' do
+ it 'includes that the profile was not deactivated in the form response' do
+ user = create(:user)
+ user.update(reset_password_sent_at: Time.zone.now)
+
+ form = ResetPasswordForm.new(user)
+
+ result = form.submit(password: 'a good and powerful password')
+
+ expect(result.success?).to eq(true)
+ expect(result.extra[:pending_profile_invalidated]).to eq(false)
+ expect(result.extra[:pending_profile_pending_reasons]).to eq('')
+ end
+ end
+
context 'when the unconfirmed email address has been confirmed by another account' do
it 'does not raise an error and is not successful' do
user = create(:user, :unconfirmed)
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 c2219499287..a40e3787314 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
@@ -55,7 +55,7 @@ describe('document-capture/components/review-issues-step', () => {
,
);
- expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok();
+ expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok();
expect(getByText('3 attempts', { selector: 'strong' })).to.be.ok();
expect(getByText('remaining')).to.be.ok();
expect(getByRole('button', { name: 'idv.failure.button.warning' })).to.be.ok();
@@ -89,7 +89,7 @@ describe('document-capture/components/review-issues-step', () => {
,
);
- expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok();
+ expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok();
expect(getByText('3 attempts', { selector: 'strong' })).to.be.ok();
expect(getByText('remaining')).to.be.ok();
expect(getByRole('button', { name: 'idv.failure.button.try_online' })).to.be.ok();
@@ -131,7 +131,7 @@ describe('document-capture/components/review-issues-step', () => {
,
);
- expect(getByText('errors.doc_auth.throttled_heading')).to.be.ok();
+ expect(getByText('errors.doc_auth.rate_limited_heading')).to.be.ok();
expect(getByText('One attempt remaining')).to.be.ok();
expect(getByText('An unknown error occurred')).to.be.ok();
expect(getByRole('button', { name: 'idv.failure.button.warning' })).to.be.ok();
diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb
index 17b4ecfa25b..daf7c6fe90f 100644
--- a/spec/jobs/get_usps_proofing_results_job_spec.rb
+++ b/spec/jobs/get_usps_proofing_results_job_spec.rb
@@ -194,6 +194,8 @@
end
before do
+ allow(IdentityConfig.store).
+ to(receive(:in_person_results_delay_in_hours).and_return(nil))
allow(Rails).to receive(:cache).and_return(
ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url),
)
@@ -217,7 +219,7 @@
let!(:pending_enrollments) do
[
create(
- :in_person_enrollment, :pending, :with_notification_phone_configuration,
+ :in_person_enrollment, :pending,
selected_location_details: { name: 'BALTIMORE' },
issuer: 'http://localhost:3000'
),
@@ -482,18 +484,15 @@
it 'sends deadline passed email on response with expired status' do
stub_request_expired_proofing_results
- allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled).
- and_return(true)
user = pending_enrollment.user
expect(pending_enrollment.deadline_passed_sent).to be false
- expect(pending_enrollment.notification_phone_configuration).not_to be_nil
freeze_time do
expect do
job.perform(Time.zone.now)
end.to have_enqueued_mail(UserMailer, :in_person_deadline_passed).with(
params: { user: user, email_address: user.email_addresses.first },
args: [{ enrollment: pending_enrollment }],
- ).on_queue(:default)
+ ).at(:no_wait).on_queue(:default)
pending_enrollment.reload
expect(pending_enrollment.deadline_passed_sent).to be true
expect(job_analytics).to have_logged_event(
@@ -505,8 +504,6 @@
wait_until: nil,
job_name: 'GetUspsProofingResultsJob',
)
- expect(pending_enrollment.notification_phone_configuration).to be_nil
- expect(pending_enrollment.notification_sent_at).to be_nil
end
end
@@ -613,14 +610,12 @@
to(receive(:in_person_results_delay_in_hours).and_return(0))
user = pending_enrollment.user
- freeze_time do
- expect do
- job.perform(Time.zone.now)
- end.to have_enqueued_mail(UserMailer, :in_person_verified).with(
- params: { user: user, email_address: user.email_addresses.first },
- args: [{ enrollment: pending_enrollment }],
- ).on_queue(:default)
- end
+ expect do
+ job.perform(Time.zone.now)
+ end.to have_enqueued_mail(UserMailer, :in_person_verified).with(
+ params: { user: user, email_address: user.email_addresses.first },
+ args: [{ enrollment: pending_enrollment }],
+ ).at(:no_wait).on_queue(:default)
end
end
end
@@ -639,13 +634,17 @@
request_passed_proofing_results_response,
)
- it 'logs details about the success' do
+ it 'invokes the SendProofingNotificationJob and logs details about the success' do
allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled).
and_return(true)
- expect do
- job.perform(Time.zone.now)
- end.to have_enqueued_job(InPerson::SendProofingNotificationJob).
- with(pending_enrollment.id).on_queue(:intentionally_delayed)
+ expected_wait_until = nil
+ freeze_time do
+ expected_wait_until = 1.hour.from_now
+ expect do
+ job.perform(Time.zone.now)
+ end.to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id).at(expected_wait_until).on_queue(:intentionally_delayed)
+ end
expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time)
expect(job_analytics).to have_logged_event(
'GetUspsProofingResultsJob: Enrollment status updated',
@@ -661,7 +660,7 @@
enrollment_code: pending_enrollment.enrollment_code,
service_provider: anything,
timestamp: anything,
- wait_until: nil,
+ wait_until: expected_wait_until,
job_name: 'GetUspsProofingResultsJob',
)
end
@@ -682,7 +681,11 @@
)
it 'logs failure details' do
- job.perform(Time.zone.now)
+ expected_wait_until = nil
+ freeze_time do
+ expected_wait_until = 1.hour.from_now
+ job.perform(Time.zone.now)
+ end
expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time)
expect(job_analytics).to have_logged_event(
@@ -699,7 +702,7 @@
enrollment_code: pending_enrollment.enrollment_code,
service_provider: anything,
timestamp: anything,
- wait_until: nil,
+ wait_until: expected_wait_until,
job_name: 'GetUspsProofingResultsJob',
)
end
@@ -720,7 +723,11 @@
)
it 'logs fraud failure details' do
- job.perform(Time.zone.now)
+ expected_wait_until = nil
+ freeze_time do
+ expected_wait_until = 1.hour.from_now
+ job.perform(Time.zone.now)
+ end
expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time)
expect(job_analytics).to have_logged_event(
@@ -738,7 +745,7 @@
enrollment_code: pending_enrollment.enrollment_code,
service_provider: anything,
timestamp: anything,
- wait_until: nil,
+ wait_until: expected_wait_until,
job_name: 'GetUspsProofingResultsJob',
)
end
@@ -759,7 +766,11 @@
)
it 'logs a message about the unsupported ID' do
- job.perform Time.zone.now
+ expected_wait_until = nil
+ freeze_time do
+ expected_wait_until = 1.hour.from_now
+ job.perform Time.zone.now
+ end
expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time)
expect(job_analytics).to have_logged_event(
@@ -777,7 +788,7 @@
enrollment_code: pending_enrollment.enrollment_code,
service_provider: anything,
timestamp: anything,
- wait_until: nil,
+ wait_until: expected_wait_until,
job_name: 'GetUspsProofingResultsJob',
)
end
@@ -1138,10 +1149,12 @@
it 'logs a message about enrollment with secondary ID' do
allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled).
and_return(true)
- expect do
- job.perform Time.zone.now
- end.to have_enqueued_job(InPerson::SendProofingNotificationJob).
- with(pending_enrollment.id).on_queue(:intentionally_delayed)
+ freeze_time do
+ expect do
+ job.perform Time.zone.now
+ end.to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id).at(1.hour.from_now).on_queue(:intentionally_delayed)
+ end
expect(pending_enrollment.proofed_at).to eq(transaction_end_date_time)
expect(pending_enrollment.profile.active).to eq(false)
expect(job_analytics).to have_logged_event(
@@ -1154,6 +1167,87 @@
)
end
end
+
+ context 'sms notifications enabled' do
+ let(:pending_enrollment) do
+ create(
+ :in_person_enrollment,
+ :pending,
+ :with_notification_phone_configuration,
+ capture_secondary_id_enabled: true,
+ )
+ end
+
+ before do
+ allow(IdentityConfig.store).to receive(:in_person_send_proofing_notifications_enabled).
+ and_return(true)
+ end
+
+ context 'enrollment is expired' do
+ it 'deletes the notification phone configuration without sending an sms' do
+ stub_request_expired_proofing_results
+
+ expect(pending_enrollment.notification_phone_configuration).to_not be_nil
+
+ job.perform(Time.zone.now)
+
+ expect(pending_enrollment.reload.notification_phone_configuration).to be_nil
+ expect(pending_enrollment.notification_sent_at).to be_nil
+ end
+ end
+
+ context 'enrollment has passed proofing' do
+ it 'invokes the SendProofingNotificationJob for the enrollment' do
+ stub_request_passed_proofing_results
+
+ expect(pending_enrollment.notification_phone_configuration).to_not be_nil
+ expect(pending_enrollment.notification_sent_at).to be_nil
+
+ expect { job.perform(Time.zone.now) }.
+ to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id)
+ end
+ end
+
+ context 'enrollment has failed proofing' do
+ it 'invokes the SendProofingNotificationJob for the enrollment' do
+ stub_request_failed_proofing_results
+
+ expect(pending_enrollment.notification_phone_configuration).to_not be_nil
+ expect(pending_enrollment.notification_sent_at).to be_nil
+
+ expect { job.perform(Time.zone.now) }.
+ to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id)
+ end
+ end
+
+ context 'enrollment has failed proofing due to unsupported secondary ID' do
+ it 'invokes the SendProofingNotificationJob for the enrollment' do
+ stub_request_passed_proofing_secondary_id_type_results
+
+ expect(pending_enrollment.notification_phone_configuration).to_not be_nil
+ expect(pending_enrollment.notification_sent_at).to be_nil
+
+ expect { job.perform(Time.zone.now) }.
+ to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id)
+ end
+ end
+
+ context 'enrollment has failed proofing due to unsupported ID type' do
+ it 'invokes the SendProofingNotificationJob for the enrollment' do
+ stub_request_passed_proofing_unsupported_id_results
+
+ expect(pending_enrollment.notification_phone_configuration).to_not be_nil
+ expect(pending_enrollment.notification_sent_at).to be_nil
+
+ expect { job.perform(Time.zone.now) }.
+ to have_enqueued_job(InPerson::SendProofingNotificationJob).
+ with(pending_enrollment.id)
+ end
+ end
+ end
end
end
diff --git a/spec/jobs/in_person/send_proofing_notification_job_spec.rb b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
index edecbc0de8d..0313100d903 100644
--- a/spec/jobs/in_person/send_proofing_notification_job_spec.rb
+++ b/spec/jobs/in_person/send_proofing_notification_job_spec.rb
@@ -54,12 +54,11 @@
let(:in_person_proofing_enabled) { false }
let(:in_person_send_proofing_notifications_enabled) { true }
it 'returns without doing anything' do
- allow(InPersonEnrollment).to receive(:find).and_return(passed_enrollment)
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_completed,
)
expect(job).not_to receive(:poll)
expect(job).not_to receive(:process_batch)
@@ -70,13 +69,11 @@
let(:in_person_proofing_enabled) { true }
let(:in_person_send_proofing_notifications_enabled) { false }
it 'returns without doing anything' do
- allow(InPersonEnrollment).to receive(:find).and_return(passed_enrollment)
-
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_completed,
)
expect(job).not_to receive(:poll)
expect(job).not_to receive(:process_batch)
@@ -86,16 +83,25 @@
context 'ipp and job enabled' do
let(:in_person_proofing_enabled) { true }
let(:in_person_send_proofing_notifications_enabled) { true }
+ context 'enrollment does not exist' do
+ it 'returns without doing anything' do
+ bad_id = (InPersonEnrollment.all.pluck(:id).max || 0) + 1
+ expect(analytics).not_to receive(
+ :idv_in_person_send_proofing_notification_job_started,
+ )
+ expect(analytics).to receive(
+ :idv_in_person_send_proofing_notification_job_skipped,
+ )
+ job.perform(bad_id)
+ end
+ end
context 'without notification phone notification' do
it 'returns without doing anything' do
- allow(InPersonEnrollment).to receive(:find).
- and_return(passed_enrollment_without_notification)
-
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_skipped,
)
job.perform(passed_enrollment_without_notification.id)
end
@@ -103,85 +109,97 @@
context 'with notification phone configuration' do
it 'sends notification successfully when enrollment is successful and enrollment updated' do
allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
- allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment)
freeze_time do
now = Time.zone.now
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_completed,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_sent_attempted,
+ :idv_in_person_send_proofing_notification_attempted,
)
- expect(passed_enrollment.notification_sent_at).to eq(nil)
+ expect(passed_enrollment.reload.notification_sent_at).to be_nil
job.perform(passed_enrollment.id)
- expect(passed_enrollment.notification_sent_at).to eq(now)
- expect(passed_enrollment.reload_notification_phone_configuration).to eq(nil)
+ expect(passed_enrollment.reload.notification_sent_at).to eq(now)
+ expect(passed_enrollment.reload.notification_phone_configuration).to be_nil
end
end
it 'sends notification successfully when enrollment failed' do
allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
- allow(InPersonEnrollment).to receive(:find_by).and_return(failing_enrollment)
freeze_time do
now = Time.zone.now
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_completed,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_sent_attempted,
+ :idv_in_person_send_proofing_notification_attempted,
)
job.perform(failing_enrollment.id)
- expect(failing_enrollment.notification_sent_at).to eq(now)
- expect(failing_enrollment.reload_notification_phone_configuration).to eq(nil)
+ expect(failing_enrollment.reload.notification_sent_at).to eq(now)
+ expect(failing_enrollment.reload.notification_phone_configuration).to be_nil
end
end
it 'sends no notification and phone removed when enrollment expired' do
allow(Telephony).to receive(:send_notification).and_return(sms_success_response)
- allow(InPersonEnrollment).to receive(:find_by).and_return(expired_enrollment)
freeze_time do
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_started,
+ :idv_in_person_send_proofing_notification_job_started,
)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_job_completed,
+ :idv_in_person_send_proofing_notification_job_completed,
)
expect(analytics).not_to receive(
- :idv_in_person_usps_proofing_results_notification_sent_attempted,
+ :idv_in_person_send_proofing_notification_attempted,
)
job.perform(expired_enrollment.id)
- expect(expired_enrollment.notification_sent_at).to be_nil
- expect(expired_enrollment.reload_notification_phone_configuration).to eq(nil)
+ expect(expired_enrollment.reload.notification_sent_at).to be_nil
+ expect(expired_enrollment.reload.notification_phone_configuration).to be_nil
end
end
end
context 'when failed to send notification' do
it 'logs sms send failure when number is opt out and enrollment not updated' do
allow(Telephony).to receive(:send_notification).and_return(sms_opt_out_response)
- allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_sent_attempted,
+ :idv_in_person_send_proofing_notification_attempted,
)
job.perform(passed_enrollment.id)
- expect(passed_enrollment.notification_sent_at).to eq(nil)
+ expect(passed_enrollment.reload.notification_sent_at).to be_nil
end
it 'logs sms send failure for delivery failure' do
allow(Telephony).to receive(:send_notification).and_return(sms_failure_response)
- allow(InPersonEnrollment).to receive(:find_by).and_return(passed_enrollment)
expect(analytics).to receive(
- :idv_in_person_usps_proofing_results_notification_sent_attempted,
+ :idv_in_person_send_proofing_notification_attempted,
)
job.perform(passed_enrollment.id)
- expect(passed_enrollment.notification_sent_at).to eq(nil)
+ expect(passed_enrollment.reload.notification_sent_at).to be_nil
+ end
+ end
+ context 'when an exception is raised' do
+ it 'logs the exception details' do
+ allow(InPersonEnrollment).
+ to receive(:find_by).
+ and_raise(ActiveRecord::DatabaseConnectionError)
+
+ job.perform(passed_enrollment.id)
+
+ expect(analytics).to have_logged_event(
+ 'SendProofingNotificationJob: Exception raised',
+ enrollment_code: nil,
+ enrollment_id: passed_enrollment.id,
+ exception_class: 'ActiveRecord::DatabaseConnectionError',
+ exception_message: 'Database connection error',
+ )
end
end
end
diff --git a/spec/lib/telephony/pinpoint/sms_sender_spec.rb b/spec/lib/telephony/pinpoint/sms_sender_spec.rb
index aade8ef3256..5353255f28d 100644
--- a/spec/lib/telephony/pinpoint/sms_sender_spec.rb
+++ b/spec/lib/telephony/pinpoint/sms_sender_spec.rb
@@ -108,7 +108,7 @@ def ==(other)
response = subject.deliver(message: 'hello!', to: '+11234567890', country_code: 'US')
expect(response.success?).to eq(false)
- expect(response.error).to eq(Telephony::ThrottledError.new(raised_error_message))
+ expect(response.error).to eq(Telephony::RateLimitedError.new(raised_error_message))
expect(response.extra[:delivery_status]).to eq('THROTTLED')
expect(response.extra[:request_id]).to eq('fake-message-request-id')
end
diff --git a/spec/lib/telephony/pinpoint/voice_sender_spec.rb b/spec/lib/telephony/pinpoint/voice_sender_spec.rb
index 9a73f23535f..bf73199dfdb 100644
--- a/spec/lib/telephony/pinpoint/voice_sender_spec.rb
+++ b/spec/lib/telephony/pinpoint/voice_sender_spec.rb
@@ -118,7 +118,7 @@ def mock_build_backup_client
'Aws::PinpointSMSVoice::Errors::LimitExceededException: This is a test message'
expect(response.success?).to eq(false)
- expect(response.error).to eq(Telephony::ThrottledError.new(error_message))
+ expect(response.error).to eq(Telephony::RateLimitedError.new(error_message))
end
end
diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb
index 1f8876c9362..652e43f5531 100644
--- a/spec/models/in_person_enrollment_spec.rb
+++ b/spec/models/in_person_enrollment_spec.rb
@@ -389,10 +389,13 @@
end
end
- describe 'skip_notification_sent_at_set?' do
+ describe 'eligible_for_notification?' do
let(:passed_enrollment) do
create(:in_person_enrollment, :passed, :with_notification_phone_configuration)
end
+ let(:failed_enrollment) do
+ create(:in_person_enrollment, :failed, :with_notification_phone_configuration)
+ end
let(:expired_enrollment) do
create(:in_person_enrollment, :expired, :with_notification_phone_configuration)
end
@@ -406,14 +409,16 @@
create(:in_person_enrollment, :failed)
end
- it 'returns false when status of passed/failed/expired and notification configuration' do
- expect(passed_enrollment.skip_notification_sent_at_set?).to eq(false)
- expect(expired_enrollment.skip_notification_sent_at_set?).to eq(false)
+ it 'returns true when status of passed/failed/expired and notification configuration' do
+ expect(passed_enrollment.eligible_for_notification?).to eq(true)
+ expect(failed_enrollment.eligible_for_notification?).to eq(true)
+ expect(expired_enrollment.eligible_for_notification?).to eq(true)
end
+
it 'returns false when status of incomplete or without notification configuration' do
- expect(incomplete_enrollment.skip_notification_sent_at_set?).to eq(true)
- expect(passed_enrollment_without_notification.skip_notification_sent_at_set?).to eq(true)
- expect(failed_enrollment_without_notification.skip_notification_sent_at_set?).to eq(true)
+ expect(incomplete_enrollment.eligible_for_notification?).to eq(false)
+ expect(passed_enrollment_without_notification.eligible_for_notification?).to eq(false)
+ expect(failed_enrollment_without_notification.eligible_for_notification?).to eq(false)
end
end
diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb
index afb877f08dc..d6839f54dbb 100644
--- a/spec/models/profile_spec.rb
+++ b/spec/models/profile_spec.rb
@@ -242,82 +242,6 @@
end
end
- # TODO: remove entire describe block
- describe '#has_proofed_before' do
- it 'is false when the user has only been activated once' do
- expect(profile.activated_at).to be_nil
- expect(profile.active).to eq(false)
- expect(profile.deactivation_reason).to be_nil
- expect(profile.fraud_review_pending?).to eq(false)
- expect(profile.gpo_verification_pending_at).to be_nil
- expect(profile.has_proofed_before?).to eq(false) # won't change
- expect(profile.initiating_service_provider).to be_nil
- expect(profile.verified_at).to be_nil # to change
-
- profile.activate
-
- expect(profile.activated_at).to be_present
- expect(profile.active).to eq(true)
- expect(profile.deactivation_reason).to be_nil
- expect(profile.fraud_review_pending?).to eq(false)
- expect(profile.gpo_verification_pending_at).to be_nil
- expect(profile.has_proofed_before?).to eq(false) # unchanged
- expect(profile.initiating_service_provider).to be_nil
- expect(profile.verified_at).to be_present # changed
- end
-
- it 'is true when the user is re-activated' do
- existing_profile = create(:profile, user: user)
-
- # profile before
- expect(profile.activated_at).to be_nil # to change
- expect(profile.active).to eq(false) # to change
- expect(profile.deactivation_reason).to be_nil
- expect(profile.fraud_review_pending?).to eq(false)
- expect(profile.gpo_verification_pending_at).to be_nil
- expect(profile.has_proofed_before?).to eq(false) # to change
- expect(profile.initiating_service_provider).to be_nil
- expect(profile.verified_at).to be_nil # to change
-
- # existing_profile before
- expect(existing_profile.activated_at).to be_nil # will change !!!
- expect(existing_profile.active).to eq(false) # won't change
- expect(existing_profile.deactivation_reason).to be_nil
- expect(existing_profile.fraud_review_pending?).to eq(false)
- expect(existing_profile.gpo_verification_pending_at).to be_nil
- expect(existing_profile.initiating_service_provider).to be_nil
- expect(existing_profile.verified_at).to be_nil # to change
-
- existing_profile.activate
- profile.activate
-
- existing_profile.reload
- profile.reload
-
- # profile after
- expect(profile.activated_at).to be_present # changed
- expect(profile.active).to eq(true) # changed
- expect(profile.deactivation_reason).to be_nil
- expect(profile.fraud_review_pending?).to eq(false)
- expect(profile.gpo_verification_pending_at).to be_nil
- expect(profile.has_proofed_before?).to eq(true) # changed
- expect(profile.initiating_service_provider).to be_nil
- expect(profile.verified_at).to be_present # fix pending
-
- # existing_profile after
-
- # Now, existing_profile should be deactivated
- expect(existing_profile.activated_at).to be_present
- expect(existing_profile.active).to eq(false)
-
- expect(existing_profile.deactivation_reason).to be_nil
- expect(existing_profile.fraud_review_pending?).to eq(false)
- expect(existing_profile.gpo_verification_pending_at).to be_nil
- expect(existing_profile.initiating_service_provider).to be_nil
- expect(existing_profile.verified_at).to be_present # fix pending
- end
- end
-
describe '#activate' do
it 'activates current Profile, de-activates all other Profile for the user' do
active_profile = create(:profile, :active, user: user)
diff --git a/spec/models/suspended_email_spec.rb b/spec/models/suspended_email_spec.rb
new file mode 100644
index 00000000000..bfb06ec4002
--- /dev/null
+++ b/spec/models/suspended_email_spec.rb
@@ -0,0 +1,44 @@
+require 'rails_helper'
+
+RSpec.describe SuspendedEmail, type: :model do
+ describe 'associations' do
+ it { should belong_to(:email_address).class_name('EmailAddress') }
+ end
+
+ describe 'validations' do
+ it { should validate_presence_of(:digested_base_email) }
+ end
+
+ describe '.generate_email_digest' do
+ it 'generates the correct digest for a given email' do
+ email = 'test@example.com'
+ expected_digest = Digest::SHA256.hexdigest('test@example.com')
+
+ expect(SuspendedEmail.generate_email_digest(email)).to eq(expected_digest)
+ end
+ end
+
+ describe '.blocked_email_address' do
+ context 'when the email is not blocked' do
+ it 'returns nil' do
+ email = 'not_blocked@example.com'
+
+ expect(SuspendedEmail.find_with_email(email)).to be_nil
+ end
+ end
+
+ context 'when the email is blocked' do
+ it 'returns the original email address' do
+ blocked_email = FactoryBot.create(:email_address, email: 'blocked@example.com')
+ digested_base_email = SuspendedEmail.generate_email_digest('blocked@example.com')
+ FactoryBot.create(
+ :suspended_email,
+ digested_base_email: digested_base_email,
+ email_address: blocked_email,
+ )
+
+ expect(SuspendedEmail.find_with_email('blocked@example.com')).to eq(blocked_email)
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4f488a917b6..0ce1af29431 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -689,7 +689,7 @@
end
describe 'user suspension' do
- let(:user) { User.new }
+ let(:user) { create(:user) }
let(:cannot_reinstate_message) { :user_is_not_suspended }
let(:cannot_suspend_message) { :user_already_suspended }
@@ -768,6 +768,10 @@
UpdateUser.new(user: user, attributes: { unique_session_id: mock_session_id }).call
end
+ it 'creates SuspendedEmail records for each email address' do
+ expect { user.suspend! }.to(change { SuspendedEmail.count }.by(1))
+ end
+
it 'updates the suspended_at attribute with the current time' do
expect do
user.suspend!
@@ -822,9 +826,17 @@
describe '#reinstate!' do
before do
- user.suspended_at = Time.zone.now
+ user.suspend!
user.reinstated_at = nil
end
+
+ it 'destroys SuspendedEmail records for each email address' do
+ email_address = user.email_addresses.last
+ expect { user.reinstate! }.
+ to(change { SuspendedEmail.find_with_email(email_address.email) }.
+ from(email_address).to(nil))
+ end
+
it 'updates the reinstated_at attribute with the current time' do
expect do
user.reinstate!
diff --git a/spec/presenters/image_upload_response_presenter_spec.rb b/spec/presenters/image_upload_response_presenter_spec.rb
index beb44410db3..93aee571862 100644
--- a/spec/presenters/image_upload_response_presenter_spec.rb
+++ b/spec/presenters/image_upload_response_presenter_spec.rb
@@ -64,7 +64,7 @@
FormResponse.new(
success: false,
errors: {
- limit: t('errors.doc_auth.throttled_heading'),
+ limit: t('errors.doc_auth.rate_limited_heading'),
},
)
end
@@ -113,7 +113,7 @@
FormResponse.new(
success: false,
errors: {
- limit: t('errors.doc_auth.throttled_heading'),
+ limit: t('errors.doc_auth.rate_limited_heading'),
},
extra: extra_attributes,
)
@@ -123,7 +123,7 @@
expected = {
success: false,
result_failed: false,
- errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }],
+ errors: [{ field: :limit, message: t('errors.doc_auth.rate_limited_heading') }],
redirect: idv_session_errors_throttled_url,
remaining_attempts: 0,
ocr_pii: nil,
@@ -141,7 +141,7 @@
expected = {
success: false,
result_failed: false,
- errors: [{ field: :limit, message: t('errors.doc_auth.throttled_heading') }],
+ errors: [{ field: :limit, message: t('errors.doc_auth.rate_limited_heading') }],
redirect: idv_hybrid_mobile_capture_complete_url,
remaining_attempts: 0,
ocr_pii: nil,
diff --git a/spec/services/idv/session_spec.rb b/spec/services/idv/session_spec.rb
index fc5a1cb1082..e0ca88a89f9 100644
--- a/spec/services/idv/session_spec.rb
+++ b/spec/services/idv/session_spec.rb
@@ -74,6 +74,29 @@
end
end
+ describe '#add_failed_phone_step_number' do
+ it 'adds uniq phone numbers in e164 format' do
+ subject.add_failed_phone_step_number('+1703-555-1212')
+ subject.add_failed_phone_step_number('703555-7575')
+
+ expect(subject.failed_phone_step_numbers.length).to eq(2)
+
+ # add duplicates
+ subject.add_failed_phone_step_number('(703) 555-1234')
+ subject.add_failed_phone_step_number('1703555-1212')
+
+ expect(subject.failed_phone_step_numbers).to eq(
+ ['+17035551212', '+17035557575', '+17035551234'],
+ )
+ end
+ end
+
+ describe '#failed_phone_step_numbers' do
+ it 'defaults to an empy array' do
+ expect(subject.failed_phone_step_numbers).to eq([])
+ end
+ end
+
describe '#create_profile_from_applicant_with_password' do
before do
subject.applicant = Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN
diff --git a/spec/services/maintenance_window_spec.rb b/spec/services/maintenance_window_spec.rb
deleted file mode 100644
index 9f1767b27a1..00000000000
--- a/spec/services/maintenance_window_spec.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe MaintenanceWindow do
- subject(:maintenance_window) do
- MaintenanceWindow.new(
- start: start,
- finish: finish,
- now: now,
- display_time_zone: display_time_zone,
- )
- end
-
- let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') }
- let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') }
- let(:now) { nil }
- let(:display_time_zone) { 'America/Los_Angeles' }
-
- describe '#active?' do
- context 'when now is during the maintenance window' do
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
- it { expect(maintenance_window.active?).to eq(true) }
- end
-
- context 'when now is outside the maintenance window' do
- let(:now) { '2020-12-31T00:00:00Z' }
- it { expect(maintenance_window.active?).to eq(false) }
- end
-
- context 'when both start and finish are empty' do
- let(:start) { nil }
- let(:finish) { nil }
-
- it 'is falsey' do
- expect(maintenance_window.active?).to be_falsey
- end
- end
- end
-
- describe '#start' do
- it 'is formatted in the display_time_zone' do
- expect(maintenance_window.start.time_zone.name).to eq(display_time_zone)
- end
-
- context 'with an empty value' do
- let(:start) { nil }
- it { expect(maintenance_window.start).to eq(nil) }
- end
- end
-
- describe '#finish' do
- it 'is formatted in the display_time_zone' do
- expect(maintenance_window.finish.time_zone.name).to eq(display_time_zone)
- end
-
- context 'with an empty value' do
- let(:finish) { nil }
- it { expect(maintenance_window.finish).to eq(nil) }
- end
- end
-end
diff --git a/spec/views/devise/sessions/new.html.erb_spec.rb b/spec/views/devise/sessions/new.html.erb_spec.rb
index d79724d77dd..4ddfa9c7a79 100644
--- a/spec/views/devise/sessions/new.html.erb_spec.rb
+++ b/spec/views/devise/sessions/new.html.erb_spec.rb
@@ -151,26 +151,4 @@
)
end
end
-
- context 'during the acuant maintenance window' do
- let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') }
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
- let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') }
-
- before do
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start)
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish)
- end
-
- around do |ex|
- travel_to(now) { ex.run }
- end
-
- it 'renders the warning banner and the normal form' do
- render
-
- expect(rendered).to have_content('We are currently under maintenance')
- expect(rendered).to have_selector('input.email')
- end
- end
end
diff --git a/spec/views/idv/getting_started/show.html.erb_spec.rb b/spec/views/idv/getting_started/show.html.erb_spec.rb
index 1f6c1ef41f6..a63321effe8 100644
--- a/spec/views/idv/getting_started/show.html.erb_spec.rb
+++ b/spec/views/idv/getting_started/show.html.erb_spec.rb
@@ -35,28 +35,6 @@
end
end
- context 'during the acuant maintenance window' do
- let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') }
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
- let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') }
-
- before do
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start)
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish)
- end
-
- around do |ex|
- travel_to(now) { ex.run }
- end
-
- it 'renders the warning banner but no other content' do
- render
-
- expect(rendered).to have_content('We are currently under maintenance')
- expect(rendered).to_not have_content(t('doc_auth.headings.welcome'))
- end
- end
-
it 'includes code to track clicks on the consent checkbox' do
selector = [
'lg-click-observer[event-name="IdV: consent checkbox toggled"]',
diff --git a/spec/views/idv/session_errors/throttled.html.erb_spec.rb b/spec/views/idv/session_errors/throttled.html.erb_spec.rb
index 66a0778a5bb..39c18d68eef 100644
--- a/spec/views/idv/session_errors/throttled.html.erb_spec.rb
+++ b/spec/views/idv/session_errors/throttled.html.erb_spec.rb
@@ -46,13 +46,13 @@
context 'with liveness feature disabled' do
it 'renders expected heading' do
- expect(rendered).to have_text(t('errors.doc_auth.throttled_heading'))
+ expect(rendered).to have_text(t('errors.doc_auth.rate_limited_heading'))
end
end
context 'with liveness feature enabled' do
it 'renders expected heading' do
- expect(rendered).to have_text(t('errors.doc_auth.throttled_heading'))
+ expect(rendered).to have_text(t('errors.doc_auth.rate_limited_heading'))
end
end
end
diff --git a/spec/views/idv/welcome/show.html.erb_spec.rb b/spec/views/idv/welcome/show.html.erb_spec.rb
index 7a76176812f..4de2cfaa7e9 100644
--- a/spec/views/idv/welcome/show.html.erb_spec.rb
+++ b/spec/views/idv/welcome/show.html.erb_spec.rb
@@ -31,28 +31,6 @@
end
end
- context 'during the acuant maintenance window' do
- let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') }
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
- let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') }
-
- before do
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start)
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish)
- end
-
- around do |ex|
- travel_to(now) { ex.run }
- end
-
- it 'renders the warning banner but no other content' do
- render
-
- expect(rendered).to have_content('We are currently under maintenance')
- expect(rendered).to_not have_content(t('doc_auth.headings.welcome'))
- end
- end
-
context 'without service provider' do
it 'renders troubleshooting options' do
render
diff --git a/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb b/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb
deleted file mode 100644
index ba063a41540..00000000000
--- a/spec/views/shared/_maintenance_window_alert.html.erb_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'rails_helper'
-
-RSpec.describe 'shared/_maintenance_window_alert.html.erb' do
- let(:start) { Time.zone.parse('2020-01-01T00:00:00Z') }
- let(:finish) { Time.zone.parse('2020-01-01T23:59:59Z') }
-
- before do
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_start).and_return(start)
- allow(IdentityConfig.store).to receive(:acuant_maintenance_window_finish).and_return(finish)
- end
-
- subject(:render_partial) do
- render(
- 'shared/maintenance_window_alert',
- now: now,
- ) { 'contents of block' }
- end
-
- context 'during the maintenance window' do
- let(:now) { Time.zone.parse('2020-01-01T12:00:00Z') }
-
- it 'renders a warning and not the contents of the block' do
- render_partial
-
- expect(rendered).to have_content('We are currently under maintenance')
-
- formatted_finish = l(
- finish.in_time_zone('America/New_York'),
- format: t('time.formats.event_timestamp_with_zone'),
- )
- expect(rendered).to have_content(formatted_finish)
-
- expect(rendered).to_not have_content('contents of block')
- end
- end
-
- context 'outside the maintenance window' do
- let(:now) { Time.zone.parse('2020-01-03T00:00:00Z') }
-
- it 'renders the contents of the block but no warning' do
- render_partial
-
- expect(rendered).to have_content('contents of block')
-
- expect(rendered).to_not have_content('We are currently under maintenance')
- end
- end
-end