From a529e474b10bf3e4af06200e76418436ebdeac14 Mon Sep 17 00:00:00 2001
From: Stephen Shelton
Date: Mon, 11 Aug 2025 14:46:41 -0400
Subject: [PATCH 1/9] changelog: internal, ci, update nginx image for easier
deployment (#12417)
---
dockerfiles/nginx-prod.conf | 19 ++++++++++++++-----
dockerfiles/nginx.Dockerfile | 9 +++++++++
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/dockerfiles/nginx-prod.conf b/dockerfiles/nginx-prod.conf
index e5b041464ac..018b4ec6c6b 100644
--- a/dockerfiles/nginx-prod.conf
+++ b/dockerfiles/nginx-prod.conf
@@ -1,10 +1,12 @@
-# user nginx;
+# user nginx;
worker_processes 2;
worker_rlimit_nofile 2048;
pid /var/run/nginx.pid;
daemon off;
load_module /usr/lib/nginx/modules/ngx_http_headers_more_filter_module.so;
+# Main context error log
+error_log /dev/stdout info;
events {
worker_connections 1024;
@@ -60,10 +62,16 @@ http {
# Add CloudFront source address ranges to trusted CIDR range for real ip computation
include /etc/nginx/cloudfront-ips.conf;
- # logging
+ # HTTP context logging
access_log /dev/stdout;
error_log /dev/stdout info;
+ client_body_temp_path /var/lib/nginx/tmp/client_body;
+ proxy_temp_path /var/lib/nginx/tmp/proxy_temp;
+ fastcgi_temp_path /var/lib/nginx/tmp/fastcgi_temp;
+ uwsgi_temp_path /var/lib/nginx/tmp/uwsgi_temp;
+ scgi_temp_path /var/lib/nginx/tmp/scgi_temp;
+
# Specify a key=value format useful for machine parsing
log_format kv escape=json
'{'
@@ -128,8 +136,9 @@ http {
ssl_protocols TLSv1.2;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 5m;
- ssl_stapling on;
- ssl_stapling_verify on;
+ # Disable SSL stapling for self-signed certificates to avoid warnings
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
resolver_timeout 5s;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
@@ -232,4 +241,4 @@ http {
proxy_pass https://0.0.0.0:3000;
}
}
-}
+}
\ No newline at end of file
diff --git a/dockerfiles/nginx.Dockerfile b/dockerfiles/nginx.Dockerfile
index ad189fa8db1..8622577f059 100644
--- a/dockerfiles/nginx.Dockerfile
+++ b/dockerfiles/nginx.Dockerfile
@@ -8,6 +8,15 @@ COPY ./dockerfiles/nginx-prod.conf /etc/nginx/nginx.conf
COPY ./dockerfiles/status-map.conf /etc/nginx/
RUN /update-ips.sh
+RUN mkdir -p /var/lib/nginx/tmp/client_body \
+ /var/lib/nginx/tmp/proxy_temp \
+ /var/lib/nginx/tmp/fastcgi_temp \
+ /var/lib/nginx/tmp/uwsgi_temp \
+ /var/lib/nginx/tmp/scgi_temp \
+ /var/lib/nginx/logs && \
+ chown -R 100:1000 /var/lib/nginx && \
+ chmod -R 755 /var/lib/nginx
+
# Generate and place SSL certificates for nginx (used only by ALB)
RUN mkdir /keys
RUN openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 1825 \
From 1f7d42bbc96047b886e78fe514a1352bef52fa9a Mon Sep 17 00:00:00 2001
From: Mitchell Henke
Date: Tue, 12 Aug 2025 09:13:51 -0500
Subject: [PATCH 2/9] Re-enable accessibility testing on IDV pages test
(#12419)
changelog: Internal, Maintenance, Re-enable accessibility testing on IDV pages test
---
spec/features/accessibility/idv_pages_spec.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/spec/features/accessibility/idv_pages_spec.rb b/spec/features/accessibility/idv_pages_spec.rb
index 6769d230166..814d33340dc 100644
--- a/spec/features/accessibility/idv_pages_spec.rb
+++ b/spec/features/accessibility/idv_pages_spec.rb
@@ -33,7 +33,7 @@
visit idv_path
expect_page_to_have_no_accessibility_violations(page)
- complete_all_doc_auth_steps_before_password_step
+ complete_all_doc_auth_steps_before_password_step(expect_accessible: true)
fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD
click_continue
@@ -44,7 +44,7 @@
scenario 'doc auth steps accessibility on mobile', driver: :headless_chrome_mobile do
sign_in_and_2fa_user
visit idv_path
- complete_all_doc_auth_steps_before_password_step
+ complete_all_doc_auth_steps_before_password_step(expect_accessible: true)
fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD
click_continue
From dbfa1a03a8580e0d64211f9d61c73487ec3ede33 Mon Sep 17 00:00:00 2001
From: Shane Chesnutt
Date: Tue, 12 Aug 2025 11:35:19 -0400
Subject: [PATCH 3/9] LG-16371 Use constants for idv document types (#12398)
[skip changelog]
---
.../concerns/idv/document_capture_concern.rb | 9 ++++----
app/forms/idv/api_image_upload_form.rb | 3 ++-
app/forms/idv/choose_id_type_form.rb | 6 +++---
app/forms/idv/doc_pii_form.rb | 7 +++----
.../document-capture/context/upload.tsx | 4 ++--
app/jobs/socure_docv_results_job.rb | 2 +-
.../doc_auth/classification_concern.rb | 4 ++--
.../doc_auth/document_classifications.rb | 21 +++++++++++++++++++
app/services/doc_auth/error_generator.rb | 7 ++-----
.../doc_auth/lexis_nexis/doc_pii_reader.rb | 8 +++----
.../doc_auth/lexis_nexis/document_types.rb | 6 ++++++
.../lexis_nexis/requests/true_id_request.rb | 4 ++--
.../lexis_nexis/responses/true_id_response.rb | 10 +++++----
.../doc_auth/mock/doc_auth_mock_client.rb | 2 +-
app/services/doc_auth/response.rb | 11 ----------
.../doc_auth/socure/document_types.rb | 6 ++++++
.../socure/requests/document_request.rb | 5 ++++-
.../socure/responses/docv_result_response.rb | 8 +++----
.../resolution/progressive_proofer.rb | 2 +-
lib/idp/constants.rb | 14 +++++++++++++
spec/forms/idv/choose_id_type_form_spec.rb | 20 +++++++++++-------
.../components/documents-step-spec.tsx | 4 ++--
.../shared/_document_capture.html.erb_spec.rb | 2 +-
23 files changed, 105 insertions(+), 60 deletions(-)
create mode 100644 app/services/doc_auth/document_classifications.rb
create mode 100644 app/services/doc_auth/lexis_nexis/document_types.rb
create mode 100644 app/services/doc_auth/socure/document_types.rb
diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb
index 9d1a68d873e..bf2cba4aa42 100644
--- a/app/controllers/concerns/idv/document_capture_concern.rb
+++ b/app/controllers/concerns/idv/document_capture_concern.rb
@@ -157,15 +157,15 @@ def validation_requirements_met?
def document_type_mismatch?
# Reject passports when feature is disabled but user submitted a passport
return true if !IdentityConfig.store.doc_auth_passports_enabled &&
- submitted_id_type == 'passport'
+ submitted_id_type == Idp::Constants::DocumentTypes::PASSPORT
# Reject when user requested passport flow but submitted a different document type
return true if document_capture_session.passport_requested? &&
- submitted_id_type != 'passport'
+ submitted_id_type != Idp::Constants::DocumentTypes::PASSPORT
# Reject when user didn't request passport flow but submitted a passport
return true if !document_capture_session.passport_requested? &&
- submitted_id_type == 'passport'
+ submitted_id_type == Idp::Constants::DocumentTypes::PASSPORT
false
end
@@ -175,7 +175,8 @@ def submitted_id_type
end
def id_type_requested
- document_capture_session.passport_requested? ? 'passport' : 'state_id'
+ document_capture_session.passport_requested? ? Idp::Constants::DocumentTypes::PASSPORT :
+ Idp::Constants::DocumentTypes::STATE_ID_CARD
end
def track_document_issuing_state(user, state)
diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb
index 96c84321735..29552a331fc 100644
--- a/app/forms/idv/api_image_upload_form.rb
+++ b/app/forms/idv/api_image_upload_form.rb
@@ -187,7 +187,8 @@ def document_type
return nil if document_capture_session.nil?
@document_type ||= passport_requested? \
- ? 'Passport' : 'DriversLicense'
+ ? DocAuth::LexisNexis::DocumentTypes::PASSPORT :
+ DocAuth::LexisNexis::DocumentTypes::DRIVERS_LICENSE
end
def validate_pii_from_doc(client_response)
diff --git a/app/forms/idv/choose_id_type_form.rb b/app/forms/idv/choose_id_type_form.rb
index 49886ff009e..aa5c03a7a07 100644
--- a/app/forms/idv/choose_id_type_form.rb
+++ b/app/forms/idv/choose_id_type_form.rb
@@ -18,13 +18,13 @@ def submit(params)
end
def chosen_id_type_valid?
- return true if DocAuth::Response::ID_TYPE_SLUGS.value? @chosen_id_type
+ return true if Idp::Constants::DocumentTypes::SUPPORTED_ID_TYPES.include?(@chosen_id_type)
errors.add(
:chosen_id_type,
:invalid,
message: "
- `choose_id_type` #{@chosen_id_type} is invalid,
- expected one of #{DocAuth::Response::ID_TYPE_SLUGS.values}
+ `chosen_id_type` #{@chosen_id_type} is invalid,
+ expected one of #{Idp::Constants::DocumentTypes::SUPPORTED_ID_TYPES}
",
)
false
diff --git a/app/forms/idv/doc_pii_form.rb b/app/forms/idv/doc_pii_form.rb
index 87e73dc4968..0910d11d3c1 100644
--- a/app/forms/idv/doc_pii_form.rb
+++ b/app/forms/idv/doc_pii_form.rb
@@ -42,7 +42,7 @@ def submit
def self.pii_like_keypaths(document_type:)
keypaths = [[:pii]]
- document_attrs = document_type&.downcase&.include?('passport') ?
+ document_attrs = document_type&.downcase&.include?(Idp::Constants::DocumentTypes::PASSPORT) ?
DocPiiPassport.pii_like_keypaths :
DocPiiStateId.pii_like_keypaths
@@ -76,7 +76,6 @@ def self.present_error(existing_errors)
PII_ERROR_KEYS = %i[name dob address1 state zipcode jurisdiction state_id_number
dob_min_age].freeze
- STATE_ID_TYPES = ['drivers_license', 'state_id_card', 'identification_card'].freeze
attr_reader :pii_from_doc
@@ -103,10 +102,10 @@ def dob_valid?
def id_doc_type_valid?
case id_doc_type
- when *STATE_ID_TYPES
+ when *Idp::Constants::DocumentTypes::STATE_ID_TYPES
state_id_validation = DocPiiStateId.new(pii: pii_from_doc)
state_id_validation.valid? || errors.merge!(state_id_validation.errors)
- when 'passport', 'passport_card'
+ when *Idp::Constants::DocumentTypes::PASSPORT_TYPES
passport_validation = DocPiiPassport.new(pii: pii_from_doc)
passport_validation.valid? || errors.merge!(passport_validation.errors)
else
diff --git a/app/javascript/packages/document-capture/context/upload.tsx b/app/javascript/packages/document-capture/context/upload.tsx
index 9b341a6ed10..6a3180ca2a9 100644
--- a/app/javascript/packages/document-capture/context/upload.tsx
+++ b/app/javascript/packages/document-capture/context/upload.tsx
@@ -10,7 +10,7 @@ const UploadContext = createContext({
statusPollInterval: undefined as number | undefined,
isMockClient: false,
flowPath: 'standard' as FlowPath,
- idType: 'state_id',
+ idType: 'state_id_card',
formData: {} as Record,
submitAttempts: 0,
});
@@ -175,7 +175,7 @@ interface UploadContextProviderProps {
flowPath: FlowPath;
/**
- * The ID type, one of "state_id" or "passport".
+ * The ID type, one of "state_id_card" or "passport".
*/
idType: string;
diff --git a/app/jobs/socure_docv_results_job.rb b/app/jobs/socure_docv_results_job.rb
index 44d84da1f34..ee27949a5a1 100644
--- a/app/jobs/socure_docv_results_job.rb
+++ b/app/jobs/socure_docv_results_job.rb
@@ -270,7 +270,7 @@ def validate_mrz(doc_pii_response)
def document_type
@document_type ||= document_capture_session.passport_requested? \
- ? 'Passport' : 'DriversLicense'
+ ? DocAuth::Socure::DocumentTypes::PASSPORT : DocAuth::Socure::DocumentTypes::DRIVERS_LICENSE
end
def user_uuid
diff --git a/app/services/doc_auth/classification_concern.rb b/app/services/doc_auth/classification_concern.rb
index 0630f329abd..fa527a2ac0b 100644
--- a/app/services/doc_auth/classification_concern.rb
+++ b/app/services/doc_auth/classification_concern.rb
@@ -29,8 +29,8 @@ def doc_side_class_ok?(classification_info, doc_side)
!side_type.present? ||
(
IdentityConfig.store.doc_auth_passports_enabled ?
- DocAuth::Response::ID_TYPE_SLUGS.key?(side_type) :
- DocAuth::Response::STATE_ID_TYPE_SLUGS.key?(side_type)
+ DocAuth::DocumentClassifications::ALL_CLASSIFICATIONS.include?(side_type) :
+ DocAuth::DocumentClassifications::STATE_ID_CLASSIFICATIONS.include?(side_type)
) ||
side_type == 'Unknown'
end
diff --git a/app/services/doc_auth/document_classifications.rb b/app/services/doc_auth/document_classifications.rb
new file mode 100644
index 00000000000..d6068eda005
--- /dev/null
+++ b/app/services/doc_auth/document_classifications.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module DocAuth::DocumentClassifications
+ PASSPORT = 'Passport'
+ IDENTIFICATION_CARD = 'Identification Card'
+ DRIVERS_LICENSE = 'Drivers License'
+
+ ALL_CLASSIFICATIONS = [PASSPORT, IDENTIFICATION_CARD, DRIVERS_LICENSE].freeze
+ STATE_ID_CLASSIFICATIONS = [IDENTIFICATION_CARD, DRIVERS_LICENSE].freeze
+
+ CLASSIFICATION_TO_DOCUMENT_TYPE = {
+ PASSPORT => Idp::Constants::DocumentTypes::PASSPORT,
+ DRIVERS_LICENSE => Idp::Constants::DocumentTypes::DRIVERS_LICENSE,
+ IDENTIFICATION_CARD => Idp::Constants::DocumentTypes::IDENTIFICATION_CARD,
+ }.freeze
+
+ STATE_ID_CLASSIFICATION_TO_DOCUMENT_TYPE = {
+ DRIVERS_LICENSE => Idp::Constants::DocumentTypes::DRIVERS_LICENSE,
+ IDENTIFICATION_CARD => Idp::Constants::DocumentTypes::IDENTIFICATION_CARD,
+ }.freeze
+end
diff --git a/app/services/doc_auth/error_generator.rb b/app/services/doc_auth/error_generator.rb
index 0fe9373592c..af4932ec2cd 100644
--- a/app/services/doc_auth/error_generator.rb
+++ b/app/services/doc_auth/error_generator.rb
@@ -9,7 +9,6 @@ def handle(response_info)
end
class IdTypeErrorHandler < ErrorHandler
- SUPPORTED_ID_CLASSNAME = ['Identification Card', 'Drivers License', 'Passport'].freeze
ACCEPTED_ISSUER_TYPES = [DocAuth::LexisNexis::IssuerTypes::STATE_OR_PROVINCE.name,
DocAuth::LexisNexis::IssuerTypes::COUNTRY.name,
DocAuth::LexisNexis::IssuerTypes::UNKNOWN.name].freeze
@@ -24,7 +23,7 @@ def get_id_type_errors(classification_info)
error_result = ErrorResult.new
both_side_ok = true
document_type = classification_info.with_indifferent_access.dig('Front', 'ClassName')
- is_passport = document_type == 'Passport'
+ is_passport = document_type == DocAuth::DocumentClassifications::PASSPORT
sides = is_passport ? ['Front'] : ['Front', 'Back']
sides.each do |side|
side_class = classification_info.with_indifferent_access.dig(side, 'ClassName')
@@ -32,7 +31,7 @@ def get_id_type_errors(classification_info)
side_issuer_type = classification_info.with_indifferent_access.dig(side, 'IssuerType')
side_ok = !side_class.present? ||
- SUPPORTED_ID_CLASSNAME.include?(side_class) ||
+ DocAuth::DocumentClassifications::ALL_CLASSIFICATIONS.include?(side_class) ||
side_class == 'Unknown'
country_ok = !side_country.present? || supported_country_codes.include?(side_country)
issuer_type_ok = !side_issuer_type.present? ||
@@ -294,8 +293,6 @@ class ErrorGenerator
'Visible Photo Characteristics': { type: FRONT, msg_key: Errors::VISIBLE_PHOTO_CHECK },
}.freeze
- SUPPORTED_ID_CLASSNAME = ['Identification Card', 'Drivers License', 'Passport'].freeze
-
def initialize(config)
@config = config
end
diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb
index 7093a7f1995..50b9bca04f9 100644
--- a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb
+++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb
@@ -33,9 +33,9 @@ def read_pii(true_id_product)
doc_issue_type: id_doc_issue_type,
)
- if id_doc_type == 'drivers_license' || id_doc_type == 'state_id_card'
+ if Idp::Constants::DocumentTypes::SUPPORTED_STATE_ID_TYPES.include?(id_doc_type)
generate_state_id_pii
- elsif id_doc_type == 'passport' || id_doc_type == 'passport_card'
+ elsif Idp::Constants::DocumentTypes::PASSPORT_TYPES.include?(id_doc_type)
generate_passport_pii
end
end
@@ -185,9 +185,9 @@ def generate_passport_pii
def determine_id_doc_type(doc_class_name:, doc_issue_type:)
val = if IdentityConfig.store.doc_auth_passports_enabled
- DocAuth::Response::ID_TYPE_SLUGS[doc_class_name]
+ DocumentClassifications::CLASSIFICATION_TO_DOCUMENT_TYPE[doc_class_name]
else
- DocAuth::Response::STATE_ID_TYPE_SLUGS[doc_class_name]
+ DocumentClassifications::STATE_ID_CLASSIFICATION_TO_DOCUMENT_TYPE[doc_class_name]
end
# If the DocIssueType is 'Passport Card',
diff --git a/app/services/doc_auth/lexis_nexis/document_types.rb b/app/services/doc_auth/lexis_nexis/document_types.rb
new file mode 100644
index 00000000000..0a6e697c97f
--- /dev/null
+++ b/app/services/doc_auth/lexis_nexis/document_types.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module DocAuth::LexisNexis::DocumentTypes
+ PASSPORT = 'Passport'
+ DRIVERS_LICENSE = 'DriversLicense'
+end
diff --git a/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb b/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb
index 6d6a43c7cee..1d617188ae4 100644
--- a/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb
+++ b/app/services/doc_auth/lexis_nexis/requests/true_id_request.rb
@@ -59,7 +59,7 @@ def body
def id_front_image
# TrueID front_image required whether driver's license or passport
case document_type
- when 'Passport'
+ when DocumentTypes::PASSPORT
passport_image
else
front_image
@@ -109,7 +109,7 @@ def acuant_sdk_source?
end
def back_image_required?
- document_type == 'DriversLicense'
+ document_type == DocumentTypes::DRIVERS_LICENSE
end
def encode(image)
diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
index 915828da461..a88b208f580 100644
--- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
+++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb
@@ -132,10 +132,11 @@ def parsed_response_body
end
def id_doc_type_expected?
- expected_id_type = passport_requested ? ['passport'] :
- ['drivers_license', 'state_id_card']
+ expected_id_types = passport_requested ?
+ Idp::Constants::DocumentTypes::SUPPORTED_PASSPORT_TYPES :
+ Idp::Constants::DocumentTypes::SUPPORTED_STATE_ID_TYPES
- expected_id_type.include?(id_type)
+ expected_id_types.include?(id_type)
end
def id_type
@@ -143,7 +144,8 @@ def id_type
end
def passport_pii?
- @passport_pii ||= ['passport', 'passport_card'].include?(id_type)
+ @passport_pii ||=
+ Idp::Constants::DocumentTypes::PASSPORT_TYPES.include?(id_type)
end
def transaction_status
diff --git a/app/services/doc_auth/mock/doc_auth_mock_client.rb b/app/services/doc_auth/mock/doc_auth_mock_client.rb
index 35312df08fc..9ecf00464d9 100644
--- a/app/services/doc_auth/mock/doc_auth_mock_client.rb
+++ b/app/services/doc_auth/mock/doc_auth_mock_client.rb
@@ -70,7 +70,7 @@ def post_images(
return mocked_response_for_method(__method__) if method_mocked?(__method__)
instance_id = SecureRandom.uuid
- if document_type == 'Passport'
+ if document_type == DocAuth::LexisNexis::DocumentTypes::PASSPORT
passport_image_response = post_passport_image(
image: passport_image,
instance_id: instance_id,
diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb
index e5c50d5e769..3e8ce11e1ad 100644
--- a/app/services/doc_auth/response.rb
+++ b/app/services/doc_auth/response.rb
@@ -8,17 +8,6 @@ class Response
:selfie_live, :selfie_quality_good
attr_accessor :vendor_errors
- ID_TYPE_SLUGS = {
- 'Identification Card' => 'state_id_card',
- 'Drivers License' => 'drivers_license',
- 'Passport' => 'passport',
- }.freeze
-
- STATE_ID_TYPE_SLUGS = {
- 'Identification Card' => 'state_id_card',
- 'Drivers License' => 'drivers_license',
- }.freeze
-
def initialize(
success:,
errors: {},
diff --git a/app/services/doc_auth/socure/document_types.rb b/app/services/doc_auth/socure/document_types.rb
new file mode 100644
index 00000000000..033d0911be5
--- /dev/null
+++ b/app/services/doc_auth/socure/document_types.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module DocAuth::Socure::DocumentTypes
+ PASSPORT = 'Passport'
+ DRIVERS_LICENSE = 'DriversLicense'
+end
diff --git a/app/services/doc_auth/socure/requests/document_request.rb b/app/services/doc_auth/socure/requests/document_request.rb
index bff892be4d0..5163385f93e 100644
--- a/app/services/doc_auth/socure/requests/document_request.rb
+++ b/app/services/doc_auth/socure/requests/document_request.rb
@@ -7,6 +7,9 @@ class DocumentRequest < DocAuth::Socure::Request
attr_reader :customer_user_id, :redirect_url, :language,
:liveness_checking_required, :passport_requested
+ PASSPORT_DOCUMENT_TYPE = 'passport'
+ DRIVERS_LICENSE_DOCUMENT_TYPE = 'license'
+
def initialize(
customer_user_id:,
redirect_url:,
@@ -76,7 +79,7 @@ def use_case_key
end
def document_type
- passport_requested ? 'passport' : 'license'
+ passport_requested ? PASSPORT_DOCUMENT_TYPE : DRIVERS_LICENSE_DOCUMENT_TYPE
end
end
end
diff --git a/app/services/doc_auth/socure/responses/docv_result_response.rb b/app/services/doc_auth/socure/responses/docv_result_response.rb
index e63a8a83fc5..8dbc1bcba34 100644
--- a/app/services/doc_auth/socure/responses/docv_result_response.rb
+++ b/app/services/doc_auth/socure/responses/docv_result_response.rb
@@ -127,7 +127,7 @@ def error_messages
end
def read_pii
- if id_doc_type == 'passport'
+ if id_doc_type == Idp::Constants::DocumentTypes::PASSPORT
return Pii::Passport.new(
first_name: get_data(DATA_PATHS[:first_name]),
middle_name: get_data(DATA_PATHS[:middle_name]),
@@ -235,14 +235,14 @@ def parse_date(date_string)
def id_type_supported?
if passports_enabled?
- DocAuth::Response::ID_TYPE_SLUGS.key?(document_id_type)
+ DocAuth::DocumentClassifications::ALL_CLASSIFICATIONS.include?(document_id_type)
else
- DocAuth::Response::STATE_ID_TYPE_SLUGS.key?(document_id_type)
+ DocAuth::DocumentClassifications::STATE_ID_CLASSIFICATIONS.include?(document_id_type)
end
end
def id_type_expected?
- if id_doc_type == 'passport'
+ if id_doc_type == Idp::Constants::DocumentTypes::PASSPORT
passport_requested
else
!passport_requested
diff --git a/app/services/proofing/resolution/progressive_proofer.rb b/app/services/proofing/resolution/progressive_proofer.rb
index 98895995f5e..0ef58f2e845 100644
--- a/app/services/proofing/resolution/progressive_proofer.rb
+++ b/app/services/proofing/resolution/progressive_proofer.rb
@@ -191,7 +191,7 @@ def process_state_id(
end
def passport_applicant?(applicant_pii)
- applicant_pii[:id_doc_type] == 'passport'
+ applicant_pii[:id_doc_type] == Idp::Constants::DocumentTypes::PASSPORT
end
end
end
diff --git a/lib/idp/constants.rb b/lib/idp/constants.rb
index b4471b792be..bcc0be909f3 100644
--- a/lib/idp/constants.rb
+++ b/lib/idp/constants.rb
@@ -23,6 +23,20 @@ module Vendors
SOURCE_CHECK = [AAMVA, AAMVA_UNSUPPORTED_JURISDICTION, STATE_ID_MOCK].freeze
end
+ module DocumentTypes
+ PASSPORT = 'passport'
+ PASSPORT_CARD = 'passport_card'
+ DRIVERS_LICENSE = 'drivers_license'
+ STATE_ID_CARD = 'state_id_card'
+ IDENTIFICATION_CARD = 'identification_card'
+
+ SUPPORTED_PASSPORT_TYPES = [PASSPORT].freeze
+ SUPPORTED_STATE_ID_TYPES = [DRIVERS_LICENSE, STATE_ID_CARD].freeze
+ SUPPORTED_ID_TYPES = [*SUPPORTED_PASSPORT_TYPES, *SUPPORTED_STATE_ID_TYPES].freeze
+ PASSPORT_TYPES = [PASSPORT, PASSPORT_CARD].freeze
+ STATE_ID_TYPES = [DRIVERS_LICENSE, STATE_ID_CARD, IDENTIFICATION_CARD].freeze
+ end
+
# US State and Territory codes are
# taken from the FIPS standard, which
# can be found at:
diff --git a/spec/forms/idv/choose_id_type_form_spec.rb b/spec/forms/idv/choose_id_type_form_spec.rb
index 746b618673f..042c8b041a5 100644
--- a/spec/forms/idv/choose_id_type_form_spec.rb
+++ b/spec/forms/idv/choose_id_type_form_spec.rb
@@ -4,19 +4,23 @@
let(:subject) { Idv::ChooseIdTypeForm.new }
describe '#submit' do
- context 'when the form is valid' do
- let(:params) { { choose_id_type_preference: 'passport' } }
+ Idp::Constants::DocumentTypes::SUPPORTED_ID_TYPES.each do |id_type|
+ context "when the choose_id_type_preference is '#{id_type}'" do
+ let(:params) { { choose_id_type_preference: id_type } }
- it 'returns a successful form response' do
- result = subject.submit(params)
+ it 'returns a successful form response' do
+ result = subject.submit(params)
- expect(result).to be_kind_of(FormResponse)
- expect(result.success?).to eq(true)
- expect(result.errors).to be_empty
+ expect(result).to be_kind_of(FormResponse)
+ expect(result.success?).to eq(true)
+ expect(result.errors).to be_empty
+ end
end
end
+
context 'when the choose_id_type_preference is nil' do
let(:params) { { choose_id_type_preference: nil } }
+
it 'returns a failed form response when id type is nil' do
result = subject.submit(params)
@@ -25,8 +29,10 @@
expect(result.errors).not_to be_empty
end
end
+
context 'when the choose_id_type_preference is not supported type' do
let(:params) { { choose_id_type_preference: 'unknown-type' } }
+
it 'returns a failed form response when id type is nil' do
result = subject.submit(params)
diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.tsx b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx
index 3567683a6ec..f92fb9124ab 100644
--- a/spec/javascript/packages/document-capture/components/documents-step-spec.tsx
+++ b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx
@@ -102,7 +102,7 @@ describe('document-capture/components/documents-step', () => {
it('renders the hybrid flow warning if the flow is hybrid', () => {
const { getByText } = render(
-
+
undefined}
@@ -123,7 +123,7 @@ describe('document-capture/components/documents-step', () => {
it('does not render the hybrid flow warning if the flow is standard (default)', () => {
const { queryByText } = render(
-
+
undefined}
diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb
index 20a06504670..c393fd4e187 100644
--- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb
+++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb
@@ -7,7 +7,7 @@
let(:sp_name) { nil }
let(:sp_issuer) { nil }
let(:flow_path) { 'standard' }
- let(:id_type) { 'state_id' }
+ let(:id_type) { Idp::Constants::DocumentTypes::STATE_ID_CARD }
let(:choose_id_type_path) { 'choose_id_type' }
let(:failure_to_proof_url) { return_to_sp_failure_to_proof_path }
let(:in_person_proofing_enabled) { false }
From 333ad2b8a1a358fdf122d7a489fdaf45dfe287b6 Mon Sep 17 00:00:00 2001
From: Malick Diarra
Date: Tue, 12 Aug 2025 16:33:01 -0400
Subject: [PATCH 4/9] changelog: Upcoming Features, One account, fix bucket
name (#12424)
---
config/initializers/ab_tests.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb
index 230ad2aec19..716c5c19ef2 100644
--- a/config/initializers/ab_tests.rb
+++ b/config/initializers/ab_tests.rb
@@ -147,7 +147,7 @@ def self.all
:one_account_recognize_all_profiles,
].to_set,
buckets: {
- one_account_user_verification_enabled_percentage:
+ one_account_user_verification_enabled:
IdentityConfig.store.one_account_user_verification_enabled_percentage,
},
) do |user:, user_session:, **|
From 15b202bf07b7663d076763fbc80b8fd45ea52f60 Mon Sep 17 00:00:00 2001
From: Mitchell Henke
Date: Tue, 12 Aug 2025 16:11:01 -0500
Subject: [PATCH 5/9] Enable additional Rubocop rules for Capybara tests
(#12413)
* Create Rubocop rule to enforce stricter path assertions in feature tests
changelog: Internal, Testing, Create Rubocop rule to enforce stricter path assertions in feature tests
Co-authored-by: Doug Price
* lint fixes
* add exception to new rubocop rule
---------
Co-authored-by: Doug Price
---
.rubocop.yml | 37 +-
Gemfile.lock | 5 +-
.../capybara_current_path_equality_linter.rb | 46 ++
.../capybara_current_url_expect_linter.rb | 63 +++
.../completions_cancel_spec.rb | 3 +-
spec/features/idv/cancel_spec.rb | 21 +-
.../idv/doc_auth/document_capture_spec.rb | 18 +-
.../doc_auth/redo_document_capture_spec.rb | 13 +-
.../idv/doc_auth/verify_info_step_spec.rb | 474 +++++++++---------
spec/features/idv/hybrid_mobile/entry_spec.rb | 12 +-
spec/features/idv/in_person_spec.rb | 6 +-
spec/features/idv/outage_spec.rb | 2 +-
spec/features/idv/sp_follow_up_spec.rb | 14 +-
.../multiple_emails/sp_sign_in_spec.rb | 8 +-
.../authorization_confirmation_spec.rb | 6 +-
.../openid_connect/openid_connect_spec.rb | 16 +-
.../phishing_resistant_required_spec.rb | 24 +-
.../redirect_uri_validation_spec.rb | 4 +-
.../remember_device/cookie_expiration_spec.rb | 2 +-
.../user_opted_preference_spec.rb | 12 +-
.../saml/authorization_confirmation_spec.rb | 12 +-
.../saml/ial1/account_creation_spec.rb | 13 +-
spec/features/saml/ial1_sso_spec.rb | 30 +-
spec/features/saml/ial2_sso_spec.rb | 2 +-
.../saml/phishing_resistant_required_spec.rb | 20 +-
.../saml/redirect_uri_validation_spec.rb | 2 +-
spec/features/saml/saml_spec.rb | 12 +-
.../two_factor_authentication/sign_in_spec.rb | 15 +-
spec/features/users/sign_in_spec.rb | 21 +-
.../visitors/email_confirmation_spec.rb | 8 +-
spec/features/visitors/set_password_spec.rb | 4 +-
spec/features/webauthn/hidden_spec.rb | 4 +-
spec/features/webauthn/sign_in_spec.rb | 4 +-
...ybara_current_path_equality_linter_spec.rb | 66 +++
...capybara_current_url_expect_linter_spec.rb | 72 +++
spec/support/features/doc_auth_helper.rb | 20 +-
spec/support/features/doc_capture_helper.rb | 10 -
spec/support/features/session_helper.rb | 32 +-
spec/support/idv_examples/sp_handoff.rb | 2 +-
.../idv_examples/sp_requested_attributes.rb | 8 +-
spec/support/saml_auth_helper.rb | 2 +-
.../shared_examples/account_creation.rb | 28 +-
spec/support/shared_examples/sign_in.rb | 23 +-
43 files changed, 739 insertions(+), 457 deletions(-)
create mode 100644 lib/linters/capybara_current_path_equality_linter.rb
create mode 100644 lib/linters/capybara_current_url_expect_linter.rb
create mode 100644 spec/lib/linters/capybara_current_path_equality_linter_spec.rb
create mode 100644 spec/lib/linters/capybara_current_url_expect_linter_spec.rb
diff --git a/.rubocop.yml b/.rubocop.yml
index 9fb15de4d44..82e55a94c32 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -6,7 +6,6 @@
require:
- rubocop-rails
- rubocop-rspec
- - rubocop-capybara
- ./lib/linters/i18n_helper_html_linter.rb
- ./lib/linters/analytics_event_name_linter.rb
- ./lib/linters/localized_validation_message_linter.rb
@@ -15,9 +14,12 @@ require:
- ./lib/linters/redirect_back_linter.rb
- ./lib/linters/url_options_linter.rb
- ./lib/linters/errors_add_linter.rb
+ - ./lib/linters/capybara_current_url_expect_linter.rb
+ - ./lib/linters/capybara_current_path_equality_linter.rb
plugins:
- rubocop-performance
+ - rubocop-capybara
AllCops:
Exclude:
@@ -46,12 +48,45 @@ Bundler/DuplicatedGem:
Bundler/InsecureProtocolSource:
Enabled: true
+Capybara/AmbiguousClick:
+ Enabled: false
+
Capybara/CurrentPathExpectation:
Enabled: true
+Capybara/FindAllFirst:
+ Enabled: true
+
+Capybara/MatchStyle:
+ Enabled: true
+
+Capybara/RedundantWithinFind:
+ Enabled: true
+
+Capybara/RSpec/PredicateMatcher:
+ Enabled: true
+
+Capybara/SpecificActions:
+ Enabled: true
+
+Capybara/SpecificFinders:
+ Enabled: false
+
+Capybara/SpecificMatcher:
+ Enabled: false
+
+Capybara/VisibilityMatcher:
+ Enabled: false
+
Gemspec/DuplicatedAssignment:
Enabled: true
+IdentityIdp/CapybaraCurrentUrlExpectLinter:
+ Enabled: true
+
+IdentityIdp/CapybaraCurrentPathEqualityLinter:
+ Enabled: true
+
IdentityIdp/I18nHelperHtmlLinter:
Enabled: true
Include:
diff --git a/Gemfile.lock b/Gemfile.lock
index de081646de0..954aff1d128 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -633,8 +633,9 @@ GEM
rubocop-ast (1.46.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
- rubocop-capybara (2.21.0)
- rubocop (~> 1.41)
+ rubocop-capybara (2.22.1)
+ lint_roller (~> 1.1)
+ rubocop (~> 1.72, >= 1.72.1)
rubocop-performance (1.25.0)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
diff --git a/lib/linters/capybara_current_path_equality_linter.rb b/lib/linters/capybara_current_path_equality_linter.rb
new file mode 100644
index 00000000000..7d02262bada
--- /dev/null
+++ b/lib/linters/capybara_current_path_equality_linter.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module IdentityIdp
+ # This lint is similar to
+ # https://docs.rubocop.org/rubocop-capybara/cops_capybara.html#capybaracurrentpathexpectation
+ # and our `IdentityIdp::CapybaraCurrentUrlExpectLinter` in that it is intended to help prevent
+ # race conditions. A comparison of the `current_path` or `current_url` relies on page
+ # navigation timing which can be asynchronous and lead to inconsistent results.
+ #
+ # These cases typically come up when trying to support multiple action paths in a feature
+ # test. We should prefer being explicit about what actions are expected rather than relying
+ # on changing a shared helper, even if it results in a more verbose test.
+ # Using method parameters to control the flow is another option. The critical part is the
+ # conditional should be stable regardless of the timing of browser activity.
+ #
+ # @example
+ # #bad
+ # return if page.current_path == idv_document_capture_path
+ #
+ class CapybaraCurrentPathEqualityLinter < RuboCop::Cop::Base
+ include RangeHelp
+
+ MSG = 'Do not compare equality of `current_path` in Capybara feature specs - instead,' \
+ ' use the `have_current_path` matcher on `page` or avoid it entirely'
+
+ RESTRICT_ON_SEND = %i[== !=].freeze
+
+ def_node_matcher :current_path_equality_lhs, <<~PATTERN
+ (send (send {(send nil? :page) nil?} :current_path) ${:== :!=} (...))
+ PATTERN
+
+ def_node_matcher :current_path_equality_rhs, <<~PATTERN
+ (send (...) ${:== :!=} (send {(send nil? :page) nil?} :current_path))
+ PATTERN
+
+ def on_send(node)
+ if current_path_equality_lhs(node) || current_path_equality_rhs(node)
+ add_offense(node)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/linters/capybara_current_url_expect_linter.rb b/lib/linters/capybara_current_url_expect_linter.rb
new file mode 100644
index 00000000000..5857ef72be3
--- /dev/null
+++ b/lib/linters/capybara_current_url_expect_linter.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module IdentityIdp
+ # This lint is a very similar to
+ # https://docs.rubocop.org/rubocop-capybara/cops_capybara.html#capybaracurrentpathexpectation
+ # but for `current_url` instead of `current_path`. The reasoning is the same in that it
+ # ensures usage of Capybara's waiting functionality. This linter is not autocorrectable.
+ #
+ # @example
+ # #bad
+ # expect(current_url).to eq authentication_methods_setup_url
+ # expect(current_url).to eq 'http://localhost:3001/auth/result'
+ #
+ # #good
+ # expect(page).to have_current_path(authentication_methods_setup_path)
+ # expect(page).to have_current_path('http://localhost:3001/auth/result', url: true)
+ #
+ class CapybaraCurrentUrlExpectLinter < RuboCop::Cop::Base
+ include RangeHelp
+
+ MSG = 'Do not set an RSpec expectation on `current_url` in ' \
+ 'Capybara feature specs - instead, use the ' \
+ '`have_current_path` matcher on `page`'
+
+ RESTRICT_ON_SEND = %i[expect].freeze
+
+ # @!method expectation_set_on_current_url(node)
+ def_node_matcher :expectation_set_on_current_url, <<~PATTERN
+ (send nil? :expect (send {(send nil? :page) nil?} :current_url))
+ PATTERN
+
+ # Supported matchers: eq(...) / match(/regexp/) / match('regexp')
+ # @!method as_is_matcher(node)
+ def_node_matcher :as_is_matcher, <<~PATTERN
+ (send
+ #expectation_set_on_current_url ${:to :to_not :not_to}
+ ${(send nil? :eq ...) (send nil? :match (...)) (send nil? :include ...) (send nil? :start_with ...)})
+ PATTERN
+
+ # @!method regexp_node_matcher(node)
+ def_node_matcher :regexp_node_matcher, <<~PATTERN
+ (send
+ #expectation_set_on_current_url ${:to :to_not :not_to}
+ $(send nil? :match ${str dstr xstr}))
+ PATTERN
+
+ def on_send(node)
+ expectation_set_on_current_url(node) do
+ as_is_matcher(node.parent) do
+ add_offense(node.parent.loc.selector)
+ end
+
+ regexp_node_matcher(node.parent) do
+ add_offense(node.parent.loc.selector)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/account_creation/completions_cancel_spec.rb b/spec/features/account_creation/completions_cancel_spec.rb
index 7bd3fbbd06e..2065a0b5c8d 100644
--- a/spec/features/account_creation/completions_cancel_spec.rb
+++ b/spec/features/account_creation/completions_cancel_spec.rb
@@ -27,6 +27,7 @@
url: true,
ignore_query: true,
)
- expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied')
+ params = UriService.params(current_url)
+ expect(params['error']).to eq('access_denied')
end
end
diff --git a/spec/features/idv/cancel_spec.rb b/spec/features/idv/cancel_spec.rb
index b954e52ec49..66902e536d2 100644
--- a/spec/features/idv/cancel_spec.rb
+++ b/spec/features/idv/cancel_spec.rb
@@ -20,13 +20,13 @@
end
it 'shows the user a cancellation message with the option to go back to the step' do
+ expect(page).to have_current_path(idv_agreement_path)
expect(page).to have_content(t('doc_auth.headings.verify_identity'))
- original_path = current_path
click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'agreement'))
expect(page).to have_content(t('idv.cancel.headings.prompt.standard'))
- expect(page).to have_current_path(idv_cancel_path, ignore_query: true)
expect(fake_analytics).to have_logged_event(
'IdV: cancellation visited',
hash_including(step: 'agreement'),
@@ -35,12 +35,13 @@
expect(page).to have_unique_form_landmark_labels
expect(page).to have_button(t('idv.cancel.actions.start_over'))
+ expect(page).to have_no_button(t('idv.cancel.actions.exit', app_name: APP_NAME))
expect(page).to have_button(t('idv.cancel.actions.account_page'))
expect(page).to have_button(t('idv.cancel.actions.keep_going'))
click_on(t('idv.cancel.actions.keep_going'))
+ expect(page).to have_current_path(idv_agreement_path)
expect(page).to have_content(t('doc_auth.headings.lets_go'))
- expect(page).to have_current_path(original_path)
expect(fake_analytics).to have_logged_event(
'IdV: cancellation go back',
step: 'agreement',
@@ -51,8 +52,8 @@
expect(page).to have_content(t('doc_auth.headings.verify_identity'))
click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'agreement'))
expect(page).to have_content(t('idv.cancel.headings.prompt.standard'))
- expect(page).to have_current_path(idv_cancel_path, ignore_query: true)
expect(fake_analytics).to have_logged_event(
'IdV: cancellation visited',
hash_including(step: 'agreement'),
@@ -66,8 +67,8 @@
click_on t('idv.cancel.actions.start_over')
- expect(page).to have_content(t('doc_auth.instructions.getting_started'))
expect(page).to have_current_path(idv_welcome_path)
+ expect(page).to have_content(t('doc_auth.instructions.getting_started'))
expect(fake_analytics).to have_logged_event(
'IdV: start over',
step: 'agreement',
@@ -78,8 +79,8 @@
expect(page).to have_content(t('doc_auth.headings.verify_identity'))
click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'agreement'))
expect(page).to have_content(t('idv.cancel.headings.prompt.standard'))
- expect(page).to have_current_path(idv_cancel_path, ignore_query: true)
expect(fake_analytics).to have_logged_event(
'IdV: cancellation visited',
hash_including(step: 'agreement'),
@@ -118,6 +119,7 @@
expect(page).to have_content(t('doc_auth.info.ssn'))
click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'ssn'))
expect(page).to have_content(t('idv.cancel.headings.prompt.standard'))
expect(fake_analytics).to have_logged_event(
'IdV: cancellation visited',
@@ -174,8 +176,8 @@
click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'agreement'))
expect(page).to have_content(t('idv.cancel.headings.prompt.standard'))
- expect(page).to have_current_path(idv_cancel_path, ignore_query: true)
expect(fake_analytics).to have_logged_event(
'IdV: cancellation visited',
hash_including(step: 'agreement'),
@@ -194,7 +196,10 @@
url: true,
ignore_query: true,
)
- expect(current_url).to start_with('http://localhost:7654/auth/result?error=access_denied')
+
+ params = UriService.params(current_url)
+ expect(params['error']).to eq('access_denied')
+
expect(fake_analytics).to have_logged_event(
'IdV: cancellation confirmed',
step: 'agreement',
diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb
index d4e00216fb8..f009108fe2c 100644
--- a/spec/features/idv/doc_auth/document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/document_capture_spec.rb
@@ -164,7 +164,7 @@
complete_doc_auth_steps_before_document_capture_step
end
- it 'user can go through verification uploading ID and selfie on seprerate pages' do
+ it 'user can go through verification uploading ID and selfie on separate pages' do
expect(page).to have_current_path(idv_document_capture_url)
expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1'))
attach_images
@@ -553,22 +553,13 @@
fill_out_ssn_form_ok
click_idv_continue
complete_verify_step
- # expect(page).to have_content(t('doc_auth.headings.document_capture_selfie'))
expect(page).to have_current_path(idv_phone_url)
end
end
end
context 'when a selfie is required by the SP' do
- context 'on mobile platform', allow_browser_log: true do
- before do
- # mock mobile device as cameraCapable, this allows us to process
- allow_any_instance_of(ActionController::Parameters)
- .to receive(:[]).and_wrap_original do |impl, param_name|
- param_name.to_sym == :skip_hybrid_handoff ? '' : impl.call(param_name)
- end
- end
-
+ context 'on mobile platform', driver: :headless_chrome_mobile, allow_browser_log: true do
context 'with a passing selfie' do
it 'proceeds to the next page with valid info, including a selfie image' do
perform_in_browser(:mobile) do
@@ -647,11 +638,6 @@
# when there are multiple doc auth errors on front and back
it 'shows the correct error message for the given error' do
perform_in_browser(:mobile) do
- if page.current_path == idv_how_to_verify_path
- click_button t('forms.buttons.continue_online')
- else
- click_continue
- end
use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
click_continue
click_button 'Take photo'
diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb
index 296caf18b84..90e9c292b12 100644
--- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb
+++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb
@@ -415,15 +415,7 @@
end
context 'when a selfie is required by the SP' do
- context 'on mobile platform', allow_browser_log: true do
- before do
- # mock mobile device as cameraCapable, this allows us to process
- allow_any_instance_of(ActionController::Parameters)
- .to receive(:[]).and_wrap_original do |impl, param_name|
- param_name.to_sym == :skip_hybrid_handoff ? '' : impl.call(param_name)
- end
- end
-
+ context 'on mobile platform', driver: :headless_chrome_mobile, allow_browser_log: true do
context 'with a passing selfie' do
it 'proceeds to the next page with valid info, including a selfie image' do
perform_in_browser(:mobile) do
@@ -463,9 +455,6 @@
# when there are multiple doc auth errors on front and back
it 'shows the correct error message for the given error' do
perform_in_browser(:mobile) do
- if page.current_path == idv_how_to_verify_path
- click_button t('forms.buttons.continue_online')
- end
use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml')
click_continue
click_button 'Take photo'
diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb
index 17e0529eb57..d0427cc7226 100644
--- a/spec/features/idv/doc_auth/verify_info_step_spec.rb
+++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb
@@ -21,307 +21,309 @@
}
end
- before do
- allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
- allow_any_instance_of(ApplicationController).to receive(:attempts_api_tracker).and_return(
- attempts_api_tracker,
- )
- sign_in_and_2fa_user(user)
- complete_doc_auth_steps_before_ssn_step
- end
-
- context 'with good ssn' do
+ context 'no outage' do
before do
- fill_out_ssn_form_ok
- click_idv_continue
- end
-
- it 'allows the user to enter in a new address and displays updated info' do
- click_link t('idv.buttons.change_address_label')
- fill_in 'idv_form_zipcode', with: '12345'
- fill_in 'idv_form_address2', with: 'Apt 3E'
-
- click_button t('forms.buttons.submit.update')
-
- expect(page).to have_current_path(idv_verify_info_path)
-
- expect(page).to have_content('12345')
- expect(page).to have_content('Apt 3E')
-
- complete_verify_step
-
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth verify proofing results',
- hash_including(
- address_edited: true,
- address_line2_present: true,
- analytics_id: 'Doc Auth',
- ),
+ allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics)
+ allow_any_instance_of(ApplicationController).to receive(:attempts_api_tracker).and_return(
+ attempts_api_tracker,
)
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_ssn_step
end
- it 'allows the user to enter in a new ssn and displays updated info' do
- click_link t('idv.buttons.change_ssn_label')
-
- expect(page).to have_current_path(idv_ssn_path)
- expect(page).to_not have_content(t('doc_auth.headings.capture_complete'))
- expect(
- find_field(t('idv.form.ssn_label')).value,
- ).to eq(DocAuthHelper::GOOD_SSN.gsub(/\D/, ''))
-
- fill_in t('idv.form.ssn_label'), with: '900456789'
- click_button t('forms.buttons.submit.update')
+ context 'with good ssn' do
+ before do
+ fill_out_ssn_form_ok
+ click_idv_continue
+ end
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth redo_ssn submitted',
- )
+ it 'allows the user to enter in a new address and displays updated info' do
+ click_link t('idv.buttons.change_address_label')
+ fill_in 'idv_form_zipcode', with: '12345'
+ fill_in 'idv_form_address2', with: 'Apt 3E'
- expect(page).to have_current_path(idv_verify_info_path)
+ click_button t('forms.buttons.submit.update')
- expect(page).to have_text('9**-**-***9')
- check t('forms.ssn.show')
- expect(page).to have_text('900-45-6789')
- end
+ expect(page).to have_current_path(idv_verify_info_path)
- it 'logs analytics event on submit' do
- complete_verify_step
+ expect(page).to have_content('12345')
+ expect(page).to have_content('Apt 3E')
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth verify proofing results',
- hash_including(address_edited: false, address_line2_present: false),
- )
- end
- end
+ complete_verify_step
- it 'does not proceed to the next page if resolution fails' do
- fill_out_ssn_form_with_ssn_that_fails_resolution
- click_idv_continue
- complete_verify_step
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth verify proofing results',
+ hash_including(
+ address_edited: true,
+ address_line2_present: true,
+ analytics_id: 'Doc Auth',
+ ),
+ )
+ end
- expect(page).to have_current_path(idv_session_errors_warning_path)
- expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
- click_on t('idv.failure.button.warning')
+ it 'allows the user to enter in a new ssn and displays updated info' do
+ click_link t('idv.buttons.change_ssn_label')
- expect(page).to have_current_path(idv_verify_info_path)
- end
+ expect(page).to have_current_path(idv_ssn_path)
+ expect(page).to_not have_content(t('doc_auth.headings.capture_complete'))
+ expect(
+ find_field(t('idv.form.ssn_label')).value,
+ ).to eq(DocAuthHelper::GOOD_SSN.gsub(/\D/, ''))
- it 'does not proceed to the next page if resolution raises an exception' do
- fill_out_ssn_form_with_ssn_that_raises_exception
+ fill_in t('idv.form.ssn_label'), with: '900456789'
+ click_button t('forms.buttons.submit.update')
- click_idv_continue
- complete_verify_step
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth redo_ssn submitted',
+ )
- expect(fake_analytics).to have_logged_event(
- 'IdV: doc auth exception visited',
- step_name: 'verify_info',
- remaining_submit_attempts: 5,
- )
- expect(page).to have_current_path(idv_session_errors_exception_path)
+ expect(page).to have_current_path(idv_verify_info_path)
- click_on t('idv.failure.button.warning')
+ expect(page).to have_text('9**-**-***9')
+ check t('forms.ssn.show')
+ expect(page).to have_text('900-45-6789')
+ end
- expect(page).to have_current_path(idv_verify_info_path)
- end
+ it 'logs analytics event on submit' do
+ complete_verify_step
- context 'resolution rate limiting' do
- let(:max_resolution_attempts) { 3 }
- before do
- allow(IdentityConfig.store).to receive(:idv_max_attempts)
- .and_return(max_resolution_attempts)
+ expect(fake_analytics).to have_logged_event(
+ 'IdV: doc auth verify proofing results',
+ hash_including(address_edited: false, address_line2_present: false),
+ )
+ end
+ end
+ it 'does not proceed to the next page if resolution fails' do
fill_out_ssn_form_with_ssn_that_fails_resolution
click_idv_continue
- end
+ complete_verify_step
- # proof_ssn_max_attempts is 10, vs 5 for resolution, so it doesn't get triggered
- it 'rate limits resolution and continues when it expires' do
- expect(attempts_api_tracker).to receive(:idv_rate_limited).with(
- limiter_type: :idv_resolution,
- ).twice
+ expect(page).to have_current_path(idv_session_errors_warning_path)
+ expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_info'))
+ click_on t('idv.failure.button.warning')
- (max_resolution_attempts - 2).times do
- complete_verify_step
- expect(page).to have_current_path(idv_session_errors_warning_path)
- click_try_again
- end
+ expect(page).to have_current_path(idv_verify_info_path)
+ end
- # Check that last attempt shows correct warning text
- complete_verify_step
- expect(page).to have_current_path(idv_session_errors_warning_path)
- expect(page).to have_content(
- strip_tags(
- t('idv.failure.attempts_html.one'),
- ),
- )
- click_try_again
+ it 'does not proceed to the next page if resolution raises an exception' do
+ fill_out_ssn_form_with_ssn_that_raises_exception
+ click_idv_continue
complete_verify_step
- expect(page).to have_current_path(idv_session_errors_failure_path)
- expect(page).not_to have_css('.step-indicator__step--current', text: text, wait: 5)
+
expect(fake_analytics).to have_logged_event(
- 'Rate Limit Reached',
- limiter_type: :idv_resolution,
+ 'IdV: doc auth exception visited',
step_name: 'verify_info',
+ remaining_submit_attempts: 5,
)
+ expect(page).to have_current_path(idv_session_errors_exception_path)
- visit idv_verify_info_url
- expect(page).to have_current_path(idv_session_errors_failure_path)
-
- # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp
- RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset!
- travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do
- sign_in_and_2fa_user(user)
- complete_doc_auth_steps_before_verify_step
- complete_verify_step
+ click_on t('idv.failure.button.warning')
- expect(page).to have_current_path(idv_phone_path)
- expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited
- end
+ expect(page).to have_current_path(idv_verify_info_path)
end
- it 'allows user to cancel identify verification' do
- click_link t('links.cancel')
- expect(page).to have_current_path(idv_cancel_path(step: 'verify'))
- end
- end
+ context 'resolution rate limiting' do
+ let(:max_resolution_attempts) { 3 }
+ before do
+ allow(IdentityConfig.store).to receive(:idv_max_attempts)
+ .and_return(max_resolution_attempts)
- context 'ssn rate limiting' do
- # Simulates someone trying same SSN with second account
- let(:max_resolution_attempts) { 4 }
- let(:max_ssn_attempts) { 3 }
+ fill_out_ssn_form_with_ssn_that_fails_resolution
+ click_idv_continue
+ end
- before do
- allow(IdentityConfig.store).to receive(:idv_max_attempts)
- .and_return(max_resolution_attempts)
+ # proof_ssn_max_attempts is 10, vs 5 for resolution, so it doesn't get triggered
+ it 'rate limits resolution and continues when it expires' do
+ expect(attempts_api_tracker).to receive(:idv_rate_limited).with(
+ limiter_type: :idv_resolution,
+ ).twice
- allow(IdentityConfig.store).to receive(:proof_ssn_max_attempts)
- .and_return(max_ssn_attempts)
+ (max_resolution_attempts - 2).times do
+ complete_verify_step
+ expect(page).to have_current_path(idv_session_errors_warning_path)
+ click_try_again
+ end
- fill_out_ssn_form_with_ssn_that_fails_resolution
- click_idv_continue
- (max_ssn_attempts - 1).times do
+ # Check that last attempt shows correct warning text
complete_verify_step
expect(page).to have_current_path(idv_session_errors_warning_path)
+ expect(page).to have_content(
+ strip_tags(
+ t('idv.failure.attempts_html.one'),
+ ),
+ )
click_try_again
- end
- end
- it 'rate limits ssn and continues when it expires' do
- expect(attempts_api_tracker).to receive(:idv_rate_limited).with(
- limiter_type: :proof_ssn,
- ).twice
+ complete_verify_step
+ expect(page).to have_current_path(idv_session_errors_failure_path)
+ expect(page).not_to have_css('.step-indicator__step--current', text: text, wait: 5)
+ expect(fake_analytics).to have_logged_event(
+ 'Rate Limit Reached',
+ limiter_type: :idv_resolution,
+ step_name: 'verify_info',
+ )
- complete_verify_step
- expect(page).to have_current_path(idv_session_errors_ssn_failure_path)
- expect(fake_analytics).to have_logged_event(
- 'Rate Limit Reached',
- limiter_type: :proof_ssn,
- step_name: 'verify_info',
- )
+ visit idv_verify_info_url
+ expect(page).to have_current_path(idv_session_errors_failure_path)
- visit idv_verify_info_url
- # second rate limit event
- expect(page).to have_current_path(idv_session_errors_ssn_failure_path)
+ # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp
+ RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset!
+ travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_verify_step
+ complete_verify_step
- # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp
- RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset!
- travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do
- sign_in_and_2fa_user(user)
- complete_doc_auth_steps_before_verify_step
- complete_verify_step
+ expect(page).to have_current_path(idv_phone_path)
+ expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited
+ end
+ end
- expect(page).to have_current_path(idv_phone_path)
- expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited
+ it 'allows user to cancel identify verification' do
+ click_link t('links.cancel')
+ expect(page).to have_current_path(idv_cancel_path(step: 'verify'))
end
end
- it 'continues to next step if ssn successful on last attempt' do
- click_link t('idv.buttons.change_ssn_label')
+ context 'ssn rate limiting' do
+ # Simulates someone trying same SSN with second account
+ let(:max_resolution_attempts) { 4 }
+ let(:max_ssn_attempts) { 3 }
+
+ before do
+ allow(IdentityConfig.store).to receive(:idv_max_attempts)
+ .and_return(max_resolution_attempts)
+
+ allow(IdentityConfig.store).to receive(:proof_ssn_max_attempts)
+ .and_return(max_ssn_attempts)
+
+ fill_out_ssn_form_with_ssn_that_fails_resolution
+ click_idv_continue
+ (max_ssn_attempts - 1).times do
+ complete_verify_step
+ expect(page).to have_current_path(idv_session_errors_warning_path)
+ click_try_again
+ end
+ end
+
+ it 'rate limits ssn and continues when it expires' do
+ expect(attempts_api_tracker).to receive(:idv_rate_limited).with(
+ limiter_type: :proof_ssn,
+ ).twice
- expect(page).to have_current_path(idv_ssn_path)
- expect(page).to_not have_content(t('doc_auth.headings.capture_complete'))
- expect(
- find_field(t('idv.form.ssn_label')).value,
- ).not_to eq(DocAuthHelper::GOOD_SSN.gsub(/\D/, ''))
+ complete_verify_step
+ expect(page).to have_current_path(idv_session_errors_ssn_failure_path)
+ expect(fake_analytics).to have_logged_event(
+ 'Rate Limit Reached',
+ limiter_type: :proof_ssn,
+ step_name: 'verify_info',
+ )
- fill_in t('idv.form.ssn_label'), with: '900456789'
- click_button t('forms.buttons.submit.update')
- complete_verify_step
+ visit idv_verify_info_url
+ # second rate limit event
+ expect(page).to have_current_path(idv_session_errors_ssn_failure_path)
- expect(page).to have_current_path(idv_phone_path)
- expect(fake_analytics).not_to have_logged_event(
- 'Rate Limit Reached',
- limiter_type: :proof_ssn,
- step_name: 'verify_info',
- )
- end
- end
+ # Manual expiration is needed because Redis timestamp doesn't always match ruby timestamp
+ RateLimiter.new(user: user, rate_limit_type: :idv_resolution).reset!
+ travel_to(IdentityConfig.store.idv_attempt_window_in_hours.hours.from_now + 1) do
+ sign_in_and_2fa_user(user)
+ complete_doc_auth_steps_before_verify_step
+ complete_verify_step
- context 'AAMVA' do
- let(:mock_state_id_jurisdiction) do
- [Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION]
- end
+ expect(page).to have_current_path(idv_phone_path)
+ expect(RateLimiter.new(user: user, rate_limit_type: :idv_resolution)).to_not be_limited
+ end
+ end
- context 'when the user lives in an AAMVA supported state' do
- it 'performs a resolution and state ID check' do
- allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return(
- mock_state_id_jurisdiction,
- )
- expect_any_instance_of(Proofing::Mock::IdMockClient).to receive(:proof).with(
- hash_including(
- **Idp::Constants::MOCK_IDV_APPLICANT,
- ),
- ).and_call_original
+ it 'continues to next step if ssn successful on last attempt' do
+ click_link t('idv.buttons.change_ssn_label')
- complete_ssn_step
+ expect(page).to have_current_path(idv_ssn_path)
+ expect(page).to_not have_content(t('doc_auth.headings.capture_complete'))
+ expect(
+ find_field(t('idv.form.ssn_label')).value,
+ ).not_to eq(DocAuthHelper::GOOD_SSN.gsub(/\D/, ''))
+
+ fill_in t('idv.form.ssn_label'), with: '900456789'
+ click_button t('forms.buttons.submit.update')
complete_verify_step
+
+ expect(page).to have_current_path(idv_phone_path)
+ expect(fake_analytics).not_to have_logged_event(
+ 'Rate Limit Reached',
+ limiter_type: :proof_ssn,
+ step_name: 'verify_info',
+ )
end
end
- context 'when the user does not live in an AAMVA supported state' do
- it 'does not perform the state ID check' do
- allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return(
- IdentityConfig.store.aamva_supported_jurisdictions -
+ context 'AAMVA' do
+ let(:mock_state_id_jurisdiction) do
+ [Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_JURISDICTION]
+ end
+
+ context 'when the user lives in an AAMVA supported state' do
+ it 'performs a resolution and state ID check' do
+ allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return(
mock_state_id_jurisdiction,
- )
- expect_any_instance_of(Proofing::Mock::IdMockClient).to_not receive(:proof)
+ )
+ expect_any_instance_of(Proofing::Mock::IdMockClient).to receive(:proof).with(
+ hash_including(
+ **Idp::Constants::MOCK_IDV_APPLICANT,
+ ),
+ ).and_call_original
+
+ complete_ssn_step
+ complete_verify_step
+ end
+ end
- complete_ssn_step
- complete_verify_step
+ context 'when the user does not live in an AAMVA supported state' do
+ it 'does not perform the state ID check' do
+ allow(IdentityConfig.store).to receive(:aamva_supported_jurisdictions).and_return(
+ IdentityConfig.store.aamva_supported_jurisdictions -
+ mock_state_id_jurisdiction,
+ )
+ expect_any_instance_of(Proofing::Mock::IdMockClient).to_not receive(:proof)
+
+ complete_ssn_step
+ complete_verify_step
+ end
end
end
- end
- context 'async missing' do
- it 'allows resubmitting form' do
- complete_ssn_step
+ context 'async missing' do
+ it 'allows resubmitting form' do
+ complete_ssn_step
- allow(DocumentCaptureSession).to receive(:find_by)
- .and_return(nil)
+ allow(DocumentCaptureSession).to receive(:find_by)
+ .and_return(nil)
- complete_verify_step
- expect(fake_analytics).to have_logged_event('IdV: proofing resolution result missing')
- expect(page).to have_content(t('idv.failure.timeout'))
- expect(page).to have_current_path(idv_verify_info_path)
- allow(DocumentCaptureSession).to receive(:find_by).and_call_original
- complete_verify_step
- expect(page).to have_current_path(idv_phone_path)
+ complete_verify_step
+ expect(fake_analytics).to have_logged_event('IdV: proofing resolution result missing')
+ expect(page).to have_content(t('idv.failure.timeout'))
+ expect(page).to have_current_path(idv_verify_info_path)
+ allow(DocumentCaptureSession).to receive(:find_by).and_call_original
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_path)
+ end
end
- end
- context 'async timed out' do
- it 'allows resubmitting form' do
- complete_ssn_step
+ context 'async timed out' do
+ it 'allows resubmitting form' do
+ complete_ssn_step
- allow(DocumentCaptureSession).to receive(:find_by)
- .and_return(nil)
+ allow(DocumentCaptureSession).to receive(:find_by)
+ .and_return(nil)
- complete_verify_step
- expect(page).to have_content(t('idv.failure.timeout'))
- expect(page).to have_current_path(idv_verify_info_path)
- allow(DocumentCaptureSession).to receive(:find_by).and_call_original
- complete_verify_step
- expect(page).to have_current_path(idv_phone_path)
+ complete_verify_step
+ expect(page).to have_content(t('idv.failure.timeout'))
+ expect(page).to have_current_path(idv_verify_info_path)
+ allow(DocumentCaptureSession).to receive(:find_by).and_call_original
+ complete_verify_step
+ expect(page).to have_current_path(idv_phone_path)
+ end
end
end
@@ -330,7 +332,13 @@
allow_any_instance_of(OutageStatus).to receive(:any_phone_vendor_outage?).and_return(true)
visit_idp_from_sp_with_ial2(:oidc)
sign_in_and_2fa_user(user)
- complete_doc_auth_steps_before_verify_step
+ complete_doc_auth_steps_before_welcome_step
+ click_idv_continue # Acknowledge mail-only alert
+ complete_welcome_step
+ complete_agreement_step
+ complete_hybrid_handoff_step
+ complete_document_capture_step
+ complete_ssn_step
end
it 'should be at the verify step page' do
diff --git a/spec/features/idv/hybrid_mobile/entry_spec.rb b/spec/features/idv/hybrid_mobile/entry_spec.rb
index 408c91908ca..79bd4992aed 100644
--- a/spec/features/idv/hybrid_mobile/entry_spec.rb
+++ b/spec/features/idv/hybrid_mobile/entry_spec.rb
@@ -32,12 +32,12 @@
Capybara.using_session('mobile') do
visit link_to_visit
# Should have redirected to the actual doc capture url
- expect(current_url).to eql(idv_hybrid_mobile_document_capture_url)
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_path)
# Confirm that we end up on the LN / Mock page even if we try to
# go to the Socure one.
visit idv_hybrid_mobile_socure_document_capture_url
- expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url)
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_path)
end
end
@@ -54,12 +54,12 @@
Capybara.using_session('mobile') do
visit link_to_visit
# Should have redirected to the actual doc capture url
- expect(current_url).to eql(idv_hybrid_mobile_socure_document_capture_url)
+ expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_path)
# Confirm that we end up on the LN / Mock page even if we try to
# go to the Socure one.
visit idv_hybrid_mobile_document_capture_url
- expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_url)
+ expect(page).to have_current_path(idv_hybrid_mobile_socure_document_capture_path)
end
end
end
@@ -79,7 +79,7 @@
Capybara.using_session('mobile') do
visit link_to_visit
# Should have redirected to the actual doc capture url
- expect(current_url).to eql(idv_hybrid_mobile_document_capture_url)
+ expect(page).to have_current_path(idv_hybrid_mobile_document_capture_path)
end
end
end
@@ -99,7 +99,7 @@
Capybara.using_session('mobile') do
visit link_to_visit
- expect(current_url).to eql(root_url)
+ expect(page).to have_current_path(root_path)
end
end
end
diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb
index 2ff829c7046..e8719542272 100644
--- a/spec/features/idv/in_person_spec.rb
+++ b/spec/features/idv/in_person_spec.rb
@@ -232,13 +232,15 @@
end
end
- context 'the user fails remote docauth and starts IPP', allow_browser_log: true do
+ context 'the user fails remote doc auth and starts IPP', allow_browser_log: true do
before do
allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(true)
visit_idp_from_sp_with_ial2(:oidc, **{ client_id: ipp_service_provider.issuer })
sign_in_via_branded_page(user)
- complete_doc_auth_steps_before_document_capture_step
+ complete_welcome_step
+ complete_agreement_step
+ complete_hybrid_handoff_step
# Fail docauth
complete_document_capture_step_with_yml(
diff --git a/spec/features/idv/outage_spec.rb b/spec/features/idv/outage_spec.rb
index b6e71a21aff..f5107b38f10 100644
--- a/spec/features/idv/outage_spec.rb
+++ b/spec/features/idv/outage_spec.rb
@@ -152,7 +152,7 @@ def sign_in_with_idv_required(user:, sms_or_totp: :sms)
click_on t('links.exit_login', app_name: APP_NAME)
- expect(current_url).to eq 'https://example.com/'
+ expect(page).to have_current_path('https://example.com/', url: true)
end
it 'skips the hybrid handoff screen and proceeds to doc capture' do
diff --git a/spec/features/idv/sp_follow_up_spec.rb b/spec/features/idv/sp_follow_up_spec.rb
index 2821443da76..735a5d1f820 100644
--- a/spec/features/idv/sp_follow_up_spec.rb
+++ b/spec/features/idv/sp_follow_up_spec.rb
@@ -25,7 +25,7 @@
open_last_email
click_email_link_matching(/return_to_sp\/account_verified_cta/)
- expect(current_url).to eq(post_idv_follow_up_url)
+ expect(page).to have_current_path(post_idv_follow_up_url, url: true)
end
scenario 'receiving an email after passing fraud review' do
@@ -43,7 +43,7 @@
open_last_email
click_email_link_matching(/return_to_sp\/account_verified_cta/)
- expect(current_url).to eq(post_idv_follow_up_url)
+ expect(page).to have_current_path(post_idv_follow_up_url, url: true)
end
context 'after entering a verify-by-mail code' do
@@ -69,10 +69,10 @@
click_button t('idv.gpo.form.submit')
acknowledge_and_confirm_personal_key
- expect(page).to have_current_path(idv_sp_follow_up_path)
+ expect(page).to have_current_path(idv_sp_follow_up_url, url: true)
click_on t('idv.by_mail.sp_follow_up.connect_account')
- expect(current_url).to eq(post_idv_follow_up_url)
+ expect(page).to have_current_path(post_idv_follow_up_url, url: true)
end
scenario 'canceling on the CTA and visiting from the account page' do
@@ -97,10 +97,10 @@
click_button t('idv.gpo.form.submit')
acknowledge_and_confirm_personal_key
- expect(page).to have_current_path(idv_sp_follow_up_path)
+ expect(page).to have_current_path(idv_sp_follow_up_url, url: true)
click_on t('idv.by_mail.sp_follow_up.go_to_account')
- expect(current_url).to eq(account_url)
+ expect(page).to have_current_path(account_path)
expect(page).to have_content(
t(
@@ -115,7 +115,7 @@
),
)
- expect(current_url).to eq(post_idv_follow_up_url)
+ expect(page).to have_current_path(post_idv_follow_up_url, url: true)
end
end
end
diff --git a/spec/features/multiple_emails/sp_sign_in_spec.rb b/spec/features/multiple_emails/sp_sign_in_spec.rb
index 54c2705713f..551a582a918 100644
--- a/spec/features/multiple_emails/sp_sign_in_spec.rb
+++ b/spec/features/multiple_emails/sp_sign_in_spec.rb
@@ -11,12 +11,12 @@
expect(emails.count).to eq(2)
- emails.each do |email|
+ emails.each.with_index do |email, index|
visit_idp_from_oidc_sp(scope: 'openid email')
signin(email, user.password)
fill_in_code_with_last_phone_otp
click_submit_default
- click_agree_and_continue if current_path == sign_up_completed_path
+ click_agree_and_continue if index == 0
expect(oidc_decoded_id_token[:email]).to eq(emails.first)
expect(oidc_decoded_id_token[:all_emails]).to be_nil
@@ -76,12 +76,12 @@
expect(emails.count).to eq(2)
- emails.each do |email|
+ emails.each.with_index do |email, index|
visit authn_request
signin(email, user.password)
fill_in_code_with_last_phone_otp
click_submit_default_twice
- click_agree_and_continue if current_path == sign_up_completed_path
+ click_agree_and_continue if index == 0
click_submit_default
xmldoc = SamlResponseDoc.new('feature', 'response_assertion')
diff --git a/spec/features/openid_connect/authorization_confirmation_spec.rb b/spec/features/openid_connect/authorization_confirmation_spec.rb
index cf56f6db45d..77b740c7ded 100644
--- a/spec/features/openid_connect/authorization_confirmation_spec.rb
+++ b/spec/features/openid_connect/authorization_confirmation_spec.rb
@@ -37,7 +37,7 @@ def create_user_and_remember_device
second_email = create(:email_address, user: user1)
sign_in_user(user1, second_email.email)
visit_idp_from_ial1_oidc_sp
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
expect(page).to have_content shared_email
continue_as(second_email.email)
@@ -50,7 +50,7 @@ def create_user_and_remember_device
second_email = create(:email_address, user: user1)
sign_in_user(user1, second_email.email)
visit_idp_from_ial1_oidc_sp
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
expect(page).to have_content second_email.email
continue_as(second_email.email)
@@ -108,7 +108,7 @@ def create_user_and_remember_device
it 'it allows the user to switch accounts prior to continuing to the SP' do
sign_in_user(user1)
visit_idp_from_ial1_oidc_sp
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
continue_as(user2.email, user2.password)
diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb
index 7f2c843fc61..a041aeaf9fd 100644
--- a/spec/features/openid_connect/openid_connect_spec.rb
+++ b/spec/features/openid_connect/openid_connect_spec.rb
@@ -529,9 +529,9 @@
state: state,
)
- current_url_no_port = URI(current_url).tap { |uri| uri.port = nil }.to_s
- expect(current_url_no_port).to include(
- "http://www.example.com/openid_connect/logout?id_token_hint=#{id_token}",
+ expect(page).to have_current_path(
+ "http://www.example.com/openid_connect/logout?id_token_hint=#{id_token}&post_logout_redirect_uri=gov.gsa.openidconnect.test://result/signout&state=#{state}",
+ url: true,
)
expect(page).to have_content(t('openid_connect.logout.errors.id_token_hint_present'))
end
@@ -933,12 +933,12 @@
it 'displays the branded page' do
visit_idp_from_ial1_oidc_sp
- expect(current_url).to eq(root_url)
+ expect(page).to have_current_path(root_path)
expect_branded_experience
visit_idp_from_ial1_oidc_sp
- expect(current_url).to eq(root_url)
+ expect(page).to have_current_path(root_path)
expect_branded_experience
end
end
@@ -951,7 +951,7 @@
sign_in_live_with_2fa(user)
sp = ServiceProvider.find_by(issuer: 'urn:gov:gsa:openidconnect:sp:server')
- expect(current_url).to eq(sign_up_completed_url)
+ expect(page).to have_current_path(sign_up_completed_path)
expect(page).to have_content(
t(
'titles.sign_up.completion_first_sign_in',
@@ -1100,7 +1100,7 @@
sp = ServiceProvider.find_by(issuer: 'urn:gov:gsa:openidconnect:sp:server')
click_link t('links.cancel')
- expect(current_url).to eq new_user_session_url(request_id: sp_request_id)
+ expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id))
expect(page).to have_content t('links.back_to_sp', sp: sp.friendly_name)
end
end
@@ -1118,7 +1118,7 @@
confirm_email_in_a_different_browser(email)
click_agree_and_continue
- expect(current_url).to eq new_user_session_url
+ expect(page).to have_current_path(new_user_session_path)
expect(page)
.to have_content t('instructions.go_back_to_mobile_app', friendly_name: 'Example iOS App')
end
diff --git a/spec/features/openid_connect/phishing_resistant_required_spec.rb b/spec/features/openid_connect/phishing_resistant_required_spec.rb
index b3664efeccc..db29d217889 100644
--- a/spec/features/openid_connect/phishing_resistant_required_spec.rb
+++ b/spec/features/openid_connect/phishing_resistant_required_spec.rb
@@ -64,7 +64,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -78,7 +78,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -92,7 +92,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -103,7 +103,7 @@
sign_in_and_2fa_user(user_with_phishing_resistant_2fa)
visit_idp_from_ial1_oidc_sp_requesting_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
end
context 'adding an ineligible method after authenticating with phishing-resistant' do
@@ -142,7 +142,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -156,7 +156,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -170,7 +170,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -181,7 +181,7 @@
sign_in_and_2fa_user(user_with_phishing_resistant_2fa)
visit_idp_from_ial1_oidc_sp_requesting_phishing_resistant(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
end
context 'adding an ineligible method after authenticating with phishing-resistant' do
@@ -220,7 +220,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -234,7 +234,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -248,7 +248,7 @@
sign_in_before_2fa(user)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -259,7 +259,7 @@
sign_in_and_2fa_user(user_with_phishing_resistant_2fa)
visit_idp_from_ial1_oidc_sp_defaulting_to_aal3(prompt: 'select_account')
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
end
context 'adding an ineligible method after authenticating with phishing-resistant' do
diff --git a/spec/features/openid_connect/redirect_uri_validation_spec.rb b/spec/features/openid_connect/redirect_uri_validation_spec.rb
index 693bc943522..e7279778fff 100644
--- a/spec/features/openid_connect/redirect_uri_validation_spec.rb
+++ b/spec/features/openid_connect/redirect_uri_validation_spec.rb
@@ -73,12 +73,12 @@
sp_redirect_uri = "http://localhost:7654/auth/result?error=access_denied&state=#{state}"
click_on t('links.back_to_sp', sp: 'Test SP')
- expect(current_url).to eq(sp_redirect_uri)
+ expect(page).to have_current_path(sp_redirect_uri, url: true)
visit new_user_session_path(request_id: '123', redirect_uri: 'evil.com')
click_on t('links.back_to_sp', sp: 'Test SP')
- expect(current_url).to eq(sp_redirect_uri)
+ expect(page).to have_current_path(sp_redirect_uri, url: true)
end
end
diff --git a/spec/features/remember_device/cookie_expiration_spec.rb b/spec/features/remember_device/cookie_expiration_spec.rb
index 7591f1d9df9..7e977553301 100644
--- a/spec/features/remember_device/cookie_expiration_spec.rb
+++ b/spec/features/remember_device/cookie_expiration_spec.rb
@@ -10,7 +10,7 @@
expire_cookies
sign_in_user(user)
- expect(current_url).to match(%r{/account})
+ expect(page).to have_current_path(account_path)
end
def sign_in_user_with_remember_device
diff --git a/spec/features/remember_device/user_opted_preference_spec.rb b/spec/features/remember_device/user_opted_preference_spec.rb
index c6cee94e1cc..27b2d381f3f 100644
--- a/spec/features/remember_device/user_opted_preference_spec.rb
+++ b/spec/features/remember_device/user_opted_preference_spec.rb
@@ -23,7 +23,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('/login/two_factor/authenticator')
+ expect(page).to have_current_path login_two_factor_authenticator_path
expect(page).to have_unchecked_field('remember_device')
end
end
@@ -50,7 +50,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('/login/two_factor/webauthn')
+ expect(page).to have_current_path login_two_factor_webauthn_path
expect(page).to have_unchecked_field('remember_device')
end
@@ -78,7 +78,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('login/two_factor/sms')
+ expect(page).to have_current_path login_two_factor_path(otp_delivery_preference: 'sms')
expect(page).to have_unchecked_field('remember_device')
end
end
@@ -98,7 +98,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('/login/two_factor/authenticator')
+ expect(page).to have_current_path login_two_factor_authenticator_path
expect(page).to have_unchecked_field('remember_device')
end
end
@@ -126,7 +126,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('login/two_factor/webauthn')
+ expect(page).to have_current_path login_two_factor_webauthn_path
expect(page).to have_unchecked_field('remember_device')
end
end
@@ -145,7 +145,7 @@
end
it 'requires the user to 2fa again and has an unchecked remember device checkbox upon sign in' do
- expect(current_url).to include('login/two_factor/sms')
+ expect(page).to have_current_path login_two_factor_path(otp_delivery_preference: 'sms')
expect(page).to have_unchecked_field('remember_device')
end
end
diff --git a/spec/features/saml/authorization_confirmation_spec.rb b/spec/features/saml/authorization_confirmation_spec.rb
index 98e6eaa90e5..69b5f8885f6 100644
--- a/spec/features/saml/authorization_confirmation_spec.rb
+++ b/spec/features/saml/authorization_confirmation_spec.rb
@@ -40,11 +40,11 @@ def create_user_and_remember_device
sign_in_user(user1, second_email.email)
visit request_url
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
expect(page).to have_content shared_email
continue_as(shared_email)
- expect(current_url).to eq(complete_saml_url)
+ expect(page).to have_current_path complete_saml_path
end
context 'with requested attributes contains only email' do
@@ -92,14 +92,14 @@ def create_user_and_remember_device
sign_in_user(user1)
visit request_url
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
continue_as(user2.email, user2.password)
# Can't remember both users' devices?
fill_in_code_with_last_phone_otp
click_submit_default
- expect(current_url).to eq(complete_saml_url)
+ expect(page).to have_current_path complete_saml_path
end
it 'does not render an error if a user goes back after opting to switch accounts' do
@@ -163,7 +163,7 @@ def create_user_and_remember_device
# second visit
visit request_url
- expect(current_url).to eq(request_url)
+ expect(page).to have_current_path(request_url, url: true)
end
it 'redirects to the account page with no sp in session' do
@@ -193,7 +193,7 @@ def create_user_and_remember_device
click_agree_and_continue
- expect(current_url).to eq complete_saml_url
+ expect(page).to have_current_path complete_saml_path
expect(page.get_rack_session.keys).to include('sp')
end
end
diff --git a/spec/features/saml/ial1/account_creation_spec.rb b/spec/features/saml/ial1/account_creation_spec.rb
index 34f0d952511..61a2d2c4b83 100644
--- a/spec/features/saml/ial1/account_creation_spec.rb
+++ b/spec/features/saml/ial1/account_creation_spec.rb
@@ -10,7 +10,7 @@
click_link t('links.create_account')
click_link t('links.cancel')
- expect(current_url).to eq new_user_session_url(request_id: sp_request_id)
+ expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id))
end
end
@@ -22,13 +22,14 @@
click_confirmation_link_in_email('test@test.com')
click_link t('links.cancel_account_creation')
- expect(current_url).to eq sign_up_cancel_url
+ expect(page).to have_current_path(sign_up_cancel_path)
expect do
click_button t('forms.buttons.cancel')
end.to change(User, :count).by(-1)
- expect(current_url).to eq \
- new_user_session_url(request_id: ServiceProviderRequestProxy.last.uuid)
+ expect(page).to have_current_path(
+ new_user_session_path(request_id: ServiceProviderRequestProxy.last.uuid),
+ )
end
it 'redirects to the password page after cancelling the cancellation' do
@@ -39,12 +40,12 @@
previous_url = current_url
click_link t('links.cancel_account_creation')
- expect(current_url).to eq sign_up_cancel_url
+ expect(page).to have_current_path(sign_up_cancel_path)
expect do
click_link t('links.go_back')
end.to change(User, :count).by(0)
- expect(current_url).to eq previous_url
+ expect(page).to have_current_path(previous_url, url: true)
end
end
end
diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb
index 5fd8a166e48..df9e6bbd7ba 100644
--- a/spec/features/saml/ial1_sso_spec.rb
+++ b/spec/features/saml/ial1_sso_spec.rb
@@ -25,7 +25,7 @@
click_agree_and_continue
- expect(current_url).to eq complete_saml_url
+ expect(page).to have_current_path complete_saml_path
expect(page.get_rack_session.keys).to include('sp')
end
end
@@ -40,7 +40,7 @@
click_submit_default_twice
click_agree_and_continue
- expect(current_url).to eq complete_saml_url
+ expect(page).to have_current_path complete_saml_path
visit root_path
expect(page).to have_current_path account_path
@@ -54,7 +54,7 @@
visit saml_authn_request_url
- expect(current_url).to match new_user_session_path
+ expect(page).to have_current_path new_user_session_path
expect(page).to have_content(sp_content)
expect(page).to_not have_css('.usa-accordion__heading')
end
@@ -84,7 +84,7 @@
session['warden.user.user.session']['last_request_at'] = 30.minutes.ago.to_i
end
- expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id), wait: 5)
+ expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id))
allow(IdentityConfig.store).to receive(:session_check_delay).and_call_original
allow(IdentityConfig.store).to receive(:session_check_frequency).and_call_original
@@ -93,7 +93,7 @@
click_submit_default
# SAML does internal redirect using JavaScript prior to showing consent screen
- expect(page).to have_current_path(sign_up_completed_path, wait: 5)
+ expect(page).to have_current_path(sign_up_completed_path)
click_agree_and_continue
expect(page).to have_current_path(test_saml_decode_assertion_path)
@@ -113,7 +113,7 @@
it 'redirects user to verify attributes page' do
sp = ServiceProvider.find_by(issuer: 'http://localhost:3000')
- expect(current_url).to eq(sign_up_completed_url)
+ expect(page).to have_current_path(sign_up_completed_path)
expect(page).to have_content(
t(
'titles.sign_up.completion_first_sign_in',
@@ -124,7 +124,7 @@
it 'returns to sp after clicking continue' do
click_agree_and_continue
- expect(current_url).to eq(complete_saml_url)
+ expect(page).to have_current_path complete_saml_path
end
it 'it confirms the user wants to continue to the SP after signing in again' do
@@ -138,10 +138,10 @@
visit saml_authn_request
- expect(current_url).to match(user_authorization_confirmation_path)
+ expect(page).to have_current_path(user_authorization_confirmation_path)
continue_as(user.email)
- expect(current_url).to eq(complete_saml_url)
+ expect(page).to have_current_path complete_saml_path
end
end
@@ -174,7 +174,7 @@
find_link(t('i18n.locale.es'), visible: false).click
end
- expect(current_url).to eq root_url(locale: :es, trailing_slash: true)
+ expect(page).to have_current_path(root_path(locale: :es, trailing_slash: true))
expect_branded_experience
end
end
@@ -184,12 +184,12 @@
request_url = saml_authn_request_url
visit request_url
- expect(current_url).to eq root_url
+ expect(page).to have_current_path(root_path)
expect_branded_experience
visit request_url
- expect(current_url).to eq root_url
+ expect(page).to have_current_path(root_path)
expect_branded_experience
end
end
@@ -204,7 +204,7 @@
sp = ServiceProvider.find_by(issuer: 'http://localhost:3000')
click_link t('links.cancel')
- expect(current_url).to eq new_user_session_url(request_id: sp_request_id)
+ expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id))
expect(page).to have_content t('links.back_to_sp', sp: sp.friendly_name)
end
end
@@ -227,7 +227,7 @@
fill_in_code_with_last_phone_otp
click_submit_default
- expect(current_url).to match new_user_session_path
+ expect(page).to have_current_path complete_saml_path
click_submit_default
click_agree_and_continue
click_submit_default
@@ -258,7 +258,7 @@
fill_in_code_with_last_phone_otp
click_submit_default
- expect(current_url).to match new_user_session_path
+ expect(page).to have_current_path complete_saml_path(locale: 'es')
click_submit_default
click_agree_and_continue
click_submit_default
diff --git a/spec/features/saml/ial2_sso_spec.rb b/spec/features/saml/ial2_sso_spec.rb
index f5573dbe36b..2aecc83f21a 100644
--- a/spec/features/saml/ial2_sso_spec.rb
+++ b/spec/features/saml/ial2_sso_spec.rb
@@ -96,7 +96,7 @@ def sign_out_user
perform_id_verification_with_gpo_without_confirming_code(user)
- expect(current_url).to eq expected_gpo_return_to_sp_url
+ expect(page).to have_current_path(expected_gpo_return_to_sp_url, url: true)
visit account_path
click_link(t('account.index.verification.reactivate_button'))
diff --git a/spec/features/saml/phishing_resistant_required_spec.rb b/spec/features/saml/phishing_resistant_required_spec.rb
index 772a8b4b824..18fbd4ef1d2 100644
--- a/spec/features/saml/phishing_resistant_required_spec.rb
+++ b/spec/features/saml/phishing_resistant_required_spec.rb
@@ -75,7 +75,7 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -94,7 +94,7 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -113,7 +113,7 @@
authn_context: Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF,
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -171,7 +171,7 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -189,7 +189,7 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -207,7 +207,7 @@
issuer: sp1_issuer, authn_context: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -265,7 +265,7 @@
issuer: aal3_issuer, authn_context: nil
},
)
- expect(current_url).to eq(login_two_factor_piv_cac_url)
+ expect(page).to have_current_path(login_two_factor_piv_cac_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:piv_cac)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -283,7 +283,7 @@
issuer: aal3_issuer, authn_context: nil
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -301,7 +301,7 @@
issuer: aal3_issuer, authn_context: nil
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
click_on t('two_factor_authentication.login_options_link_text')
expect(has_2fa_option?(:webauthn_platform)).to eq(true)
expect(has_2fa_option?(:sms)).to eq(false)
@@ -316,7 +316,7 @@
},
)
- expect(current_url).to eq(login_two_factor_webauthn_url)
+ expect(page).to have_current_path(login_two_factor_webauthn_path)
end
context 'adding an ineligible method after authenticating with phishing-resistant' do
diff --git a/spec/features/saml/redirect_uri_validation_spec.rb b/spec/features/saml/redirect_uri_validation_spec.rb
index a2e0cc06e80..321f2522f5d 100644
--- a/spec/features/saml/redirect_uri_validation_spec.rb
+++ b/spec/features/saml/redirect_uri_validation_spec.rb
@@ -23,7 +23,7 @@
click_agree_and_continue
click_submit_default_twice
- expect(current_url).to eq sp.acs_url
+ expect(page).to have_current_path(sp.acs_url, url: true)
end
end
end
diff --git a/spec/features/saml/saml_spec.rb b/spec/features/saml/saml_spec.rb
index 049f6a59ad5..cb7fa4c8173 100644
--- a/spec/features/saml/saml_spec.rb
+++ b/spec/features/saml/saml_spec.rb
@@ -23,7 +23,7 @@
click_agree_and_continue
click_submit_default_twice
- expect(current_url).to eq sp.acs_url
+ expect(page).to have_current_path(sp.acs_url, url: true)
end
end
@@ -159,8 +159,8 @@
end
it 'redirects to /test/saml/decode_assertion after submitting the form' do
- expect(page.current_url)
- .to eq(saml_settings.assertion_consumer_service_url)
+ expect(page)
+ .to have_current_path(saml_settings.assertion_consumer_service_url, url: true)
end
it 'stores SP identifier in Identity model' do
@@ -518,7 +518,7 @@
)
expect(fake_analytics.events['SAML Auth'].count).to eq 2
- expect(current_url).to eq sp.acs_url
+ expect(page).to have_current_path(sp.acs_url, url: true)
end
it 'logs one SAML Auth Requested event and two SAML Auth events for IAL2 request' do
@@ -565,7 +565,7 @@
)
expect(fake_analytics.events['SAML Auth'].count).to eq 2
- expect(current_url).to eq sp.acs_url
+ expect(page).to have_current_path(sp.acs_url, url: true)
end
end
@@ -595,7 +595,7 @@
)
expect(fake_analytics.events['SAML Auth'].count).to eq 2
- expect(current_url).to eq sp.acs_url
+ expect(page).to have_current_path(sp.acs_url, url: true)
end
end
end
diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb
index 1d4adda1a56..4b1dcc9dd25 100644
--- a/spec/features/two_factor_authentication/sign_in_spec.rb
+++ b/spec/features/two_factor_authentication/sign_in_spec.rb
@@ -529,17 +529,26 @@ def attempt_to_bypass_2fa
sign_in_before_2fa(user)
click_link t('links.help'), match: :first
- expect(current_url).to eq MarketingSite.help_url
+ expect(page).to have_current_path(
+ MarketingSite.help_url,
+ url: true,
+ )
visit login_two_factor_path(otp_delivery_preference: 'sms')
click_link t('links.contact'), match: :first
- expect(current_url).to eq MarketingSite.contact_url
+ expect(page).to have_current_path(
+ MarketingSite.contact_url,
+ url: true,
+ )
visit login_two_factor_path(otp_delivery_preference: 'sms')
click_link t('links.privacy_policy'), match: :first
- expect(current_url).to eq MarketingSite.security_and_privacy_practices_url
+ expect(page).to have_current_path(
+ MarketingSite.security_and_privacy_practices_url,
+ url: true,
+ )
end
end
diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb
index 57dfb7d2e8a..80f7d55cffc 100644
--- a/spec/features/users/sign_in_spec.rb
+++ b/spec/features/users/sign_in_spec.rb
@@ -64,8 +64,8 @@
click_on t('account.login.piv_cac')
fill_in_piv_cac_credentials_and_submit(user, user.piv_cac_configurations.first.x509_dn_uuid)
- expect(current_url).to eq rules_of_use_url
- accept_rules_of_use_and_continue_if_displayed
+ expect(page).to have_current_path rules_of_use_path
+ accept_rules_of_use_and_continue
expect(oidc_redirect_url).to start_with service_provider.redirect_uris.first
end
@@ -93,13 +93,13 @@
click_on t('account.login.piv_cac')
fill_in_piv_cac_credentials_and_submit(user, user.piv_cac_configurations.first.x509_dn_uuid)
- expect(current_url).to eq capture_password_url
+ expect(page).to have_current_path(capture_password_path)
fill_in 'Password', with: user.password
click_submit_default
- expect(current_url).to eq rules_of_use_url
- accept_rules_of_use_and_continue_if_displayed
+ expect(page).to have_current_path rules_of_use_path
+ accept_rules_of_use_and_continue
expect(oidc_redirect_url).to start_with service_provider.redirect_uris.first
end
@@ -538,7 +538,7 @@
fill_in_credentials_and_submit(user.email, user.password)
- expect(current_url).to eq new_user_session_url(request_id: '123')
+ expect(page).to have_current_path(new_user_session_path(request_id: '123'))
expect(page).to have_content t('errors.general')
end
end
@@ -675,7 +675,6 @@
it 'signs out the user if they choose to cancel' do
user = create(:user, :fully_registered)
signin(user.email, user.password)
- accept_rules_of_use_and_continue_if_displayed
click_link t('two_factor_authentication.login_options_link_text')
click_on t('links.cancel')
@@ -754,7 +753,7 @@
click_on t('account.login.piv_cac')
fill_in_piv_cac_credentials_and_submit(user, 'foo')
- expect(current_url).to eq account_url
+ expect(page).to have_current_path(account_path)
Capybara.reset_session!
@@ -762,7 +761,7 @@
click_on t('account.login.piv_cac')
fill_in_piv_cac_credentials_and_submit(user, 'bar')
- expect(current_url).to eq account_url
+ expect(page).to have_current_path(account_path)
end
end
@@ -846,7 +845,7 @@
click_agree_and_continue
- expect(current_url).to eq complete_saml_url
+ expect(page).to have_current_path complete_saml_path
end
it 'returns ial2 info for a verified user' do
@@ -875,7 +874,7 @@
click_agree_and_continue
- expect(current_url).to eq complete_saml_url
+ expect(page).to have_current_path complete_saml_path
end
end
diff --git a/spec/features/visitors/email_confirmation_spec.rb b/spec/features/visitors/email_confirmation_spec.rb
index e38158bcc7c..05dea4ab10d 100644
--- a/spec/features/visitors/email_confirmation_spec.rb
+++ b/spec/features/visitors/email_confirmation_spec.rb
@@ -13,7 +13,7 @@
check t('sign_up.terms', app_name: APP_NAME)
click_submit_default
- expect(page).to have_current_path(sign_up_verify_email_url)
+ expect(page).to have_current_path(sign_up_verify_email_path)
expect(page).not_to have_content(t('errors.registration.terms'))
end
@@ -47,7 +47,7 @@
click_button t('forms.buttons.continue')
- expect(current_url).to eq authentication_methods_setup_url
+ expect(page).to have_current_path(authentication_methods_setup_path)
expect(page).to_not have_content t('devise.confirmations.confirmed_but_must_set_password')
end
@@ -91,7 +91,7 @@
visit sign_up_create_email_confirmation_url(confirmation_token: @raw_confirmation_token)
- expect(current_url).to eq account_url
+ expect(page).to have_current_path(account_path)
end
end
@@ -102,9 +102,9 @@
visit sign_up_create_email_confirmation_url(confirmation_token: @raw_confirmation_token)
+ expect(page).to have_current_path(new_user_session_path)
action = t('devise.confirmations.sign_in')
expect(page).to have_content t('devise.confirmations.already_confirmed', action:)
- expect(current_url).to eq new_user_session_url
end
end
diff --git a/spec/features/visitors/set_password_spec.rb b/spec/features/visitors/set_password_spec.rb
index 6c7edfe9d99..b4bbf7ef0c9 100644
--- a/spec/features/visitors/set_password_spec.rb
+++ b/spec/features/visitors/set_password_spec.rb
@@ -7,8 +7,8 @@
fill_in t('forms.password'), with: ''
click_button t('forms.buttons.continue')
+ expect(page).to have_current_path sign_up_create_password_path
expect(page).to have_content t('errors.messages.blank')
- expect(current_url).to eq sign_up_create_password_url
end
context 'password field is blank when JS is on', js: true do
@@ -78,8 +78,8 @@
click_button t('forms.buttons.continue')
+ expect(page).to have_current_path sign_up_create_password_path
expect(page).to have_content('characters')
- expect(current_url).to eq sign_up_create_password_url
end
scenario 'visitor gets password help message' do
diff --git a/spec/features/webauthn/hidden_spec.rb b/spec/features/webauthn/hidden_spec.rb
index e7c318df07a..90a905ab266 100644
--- a/spec/features/webauthn/hidden_spec.rb
+++ b/spec/features/webauthn/hidden_spec.rb
@@ -106,7 +106,7 @@
expect(webauthn_option_hidden?).to eq(false)
choose t('two_factor_authentication.login_options.webauthn_platform')
click_continue
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
end
context 'if the webauthn credential is not their default mfa method when signing in' do
@@ -126,7 +126,7 @@
click_on t('two_factor_authentication.login_options_link_text')
choose t('two_factor_authentication.login_options.webauthn_platform')
click_continue
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
end
end
end
diff --git a/spec/features/webauthn/sign_in_spec.rb b/spec/features/webauthn/sign_in_spec.rb
index 78d1e602bcb..99f6986212d 100644
--- a/spec/features/webauthn/sign_in_spec.rb
+++ b/spec/features/webauthn/sign_in_spec.rb
@@ -80,7 +80,7 @@
mock_webauthn_verification_challenge
sign_in_user(user)
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
mock_cancelled_webauthn_authentication { click_webauthn_authenticate_button }
expect(page).to have_content(t('two_factor_authentication.webauthn_platform_header_text'))
@@ -104,7 +104,7 @@
expect(page).to have_current_path(login_two_factor_options_path)
select_2fa_option('webauthn_platform', visible: :all)
click_continue
- expect(current_url).to eq(login_two_factor_webauthn_url(platform: true))
+ expect(page).to have_current_path(login_two_factor_webauthn_path(platform: true))
end
end
end
diff --git a/spec/lib/linters/capybara_current_path_equality_linter_spec.rb b/spec/lib/linters/capybara_current_path_equality_linter_spec.rb
new file mode 100644
index 00000000000..331f619cc80
--- /dev/null
+++ b/spec/lib/linters/capybara_current_path_equality_linter_spec.rb
@@ -0,0 +1,66 @@
+require 'rubocop'
+require 'rubocop/rspec/cop_helper'
+require 'rubocop/rspec/expect_offense'
+
+require 'rails_helper'
+require_relative '../../../lib/linters/capybara_current_path_equality_linter'
+
+RSpec.describe RuboCop::Cop::IdentityIdp::CapybaraCurrentPathEqualityLinter do
+ include CopHelper
+ include RuboCop::RSpec::ExpectOffense
+
+ let(:config) { RuboCop::Config.new }
+ let(:cop) { RuboCop::Cop::IdentityIdp::CapybaraCurrentPathEqualityLinter.new(config) }
+
+ it 'registers offense when doing equality check on method/variable' do
+ expect_offense(<<~RUBY)
+ current_path == a_path
+ ^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'registers offense when doing inequality check on method/variable' do
+ expect_offense(<<~RUBY)
+ current_path != a_path
+ ^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'registers offense when doing equality check on method/variable' do
+ expect_offense(<<~RUBY)
+ page.current_path == a_path
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'registers offense when doing equality check on method/variable with explicit page call' do
+ expect_offense(<<~RUBY)
+ page.current_path == a_path
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'registers offense when doing inequality check on method/variable with explicit page call' do
+ expect_offense(<<~RUBY)
+ page.current_path != a_path
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'registers offense when doing equality check on method/variable on left-hand side' do
+ expect_offense(<<~RUBY)
+ a_path == current_path
+ ^^^^^^^^^^^^^^^^^^^^^^ IdentityIdp/CapybaraCurrentPathEqualityLinter: Do not compare equality of `current_path` in Capybara feature specs - instead, use the `have_current_path` matcher on `page` or avoid it entirely
+ RUBY
+ end
+
+ it 'does not register offense for unrelated comparisons' do
+ expect_no_offenses(<<~RUBY)
+ 1 == 1
+ RUBY
+
+ expect_no_offenses(<<~RUBY)
+ 1 != 1
+ RUBY
+ end
+end
diff --git a/spec/lib/linters/capybara_current_url_expect_linter_spec.rb b/spec/lib/linters/capybara_current_url_expect_linter_spec.rb
new file mode 100644
index 00000000000..41115daf261
--- /dev/null
+++ b/spec/lib/linters/capybara_current_url_expect_linter_spec.rb
@@ -0,0 +1,72 @@
+require 'rubocop'
+require 'rubocop/rspec/cop_helper'
+require 'rubocop/rspec/expect_offense'
+
+require 'rails_helper'
+require_relative '../../../lib/linters/capybara_current_url_expect_linter'
+
+RSpec.describe RuboCop::Cop::IdentityIdp::CapybaraCurrentUrlExpectLinter do
+ include CopHelper
+ include RuboCop::RSpec::ExpectOffense
+
+ let(:config) { RuboCop::Config.new }
+ let(:cop) { RuboCop::Cop::IdentityIdp::CapybaraCurrentUrlExpectLinter.new(config) }
+
+ it 'registers offense when expecting current_url with eq' do
+ expect_offense(<<~RUBY)
+ expect(current_url).to eq root_url
+ ^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'registers an offense with negation' do
+ expect_offense(<<~RUBY)
+ expect(current_url).not_to eq root_url
+ ^^^^^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'registers offense when calling expecting current_url with include' do
+ expect_offense(<<~RUBY)
+ expect(current_url).to include('/')
+ ^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'registers offense when calling expecting current_url with start_with' do
+ expect_offense(<<~RUBY)
+ expect(current_url).to start_with('/')
+ ^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'registers offense when calling expecting current_url with match regular expression' do
+ expect_offense(<<~RUBY)
+ expect(current_url).to match /localhost/
+ ^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'registers offense when calling expecting current_url with match string' do
+ expect_offense(<<~RUBY)
+ expect(current_url).to match 'localhost'
+ ^^ IdentityIdp/CapybaraCurrentUrlExpectLinter: Do not set an RSpec expectation on `current_url` in Capybara feature specs - instead, use the `have_current_path` matcher on `page`
+ RUBY
+ end
+
+ it 'does not register offense for correct usage' do
+ expect_no_offenses(<<~RUBY)
+ expect(page).to have_current_path(root_path)
+ RUBY
+
+ expect_no_offenses(<<~RUBY)
+ expect(page).to have_current_path('http://localhost:4001', url: true)
+ RUBY
+ end
+
+ it 'does not register offense for unrelated expectations' do
+ expect_no_offenses(<<~RUBY)
+ expect(user.created_at).to eq 3
+ RUBY
+ end
+end
diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb
index 45e9a37e7bb..d1a988aa707 100644
--- a/spec/support/features/doc_auth_helper.rb
+++ b/spec/support/features/doc_auth_helper.rb
@@ -43,8 +43,10 @@ def click_send_link
end
def complete_doc_auth_steps_before_welcome_step(expect_accessible: false)
- visit idv_welcome_url unless current_path == idv_welcome_url
- click_idv_continue if current_path == idv_mail_only_warning_path
+ # rubocop:disable IdentityIdp/CapybaraCurrentPathEqualityLinter
+ # This should be refactored at some point to not require the path conditional
+ visit idv_welcome_path unless current_path == idv_welcome_path
+ # rubocop:enable IdentityIdp/CapybaraCurrentPathEqualityLinter
expect_page_to_have_no_accessibility_violations(page) if expect_accessible
end
@@ -60,11 +62,7 @@ def complete_doc_auth_steps_before_agreement_step(expect_accessible: false)
end
def complete_agreement_step
- find(
- 'label',
- text: t('doc_auth.instructions.consent', app_name: APP_NAME),
- wait: 5,
- ).click
+ check t('doc_auth.instructions.consent', app_name: APP_NAME)
click_on t('doc_auth.buttons.continue')
end
@@ -84,9 +82,9 @@ def complete_hybrid_handoff_step
def complete_doc_auth_steps_before_document_capture_step(expect_accessible: false)
complete_doc_auth_steps_before_hybrid_handoff_step(expect_accessible: expect_accessible)
# JavaScript-enabled mobile devices will skip directly to document capture, so stop as complete.
- return if page.current_path == idv_document_capture_path
- if IdentityConfig.store.in_person_proofing_opt_in_enabled &&
- page.current_path == idv_how_to_verify_path
+ return if page.mode == :headless_chrome_mobile
+
+ if IdentityConfig.store.in_person_proofing_opt_in_enabled
click_on t('forms.buttons.continue_online')
end
complete_hybrid_handoff_step
@@ -97,7 +95,7 @@ def complete_up_to_how_to_verify_step_for_opt_in_ipp(remote: true)
complete_doc_auth_steps_before_welcome_step
complete_welcome_step
complete_agreement_step
- return if page.current_path == idv_hybrid_handoff_path && remote
+ return if page.mode != :headless_chrome_mobile && remote
if remote
click_on t('forms.buttons.continue_online')
else
diff --git a/spec/support/features/doc_capture_helper.rb b/spec/support/features/doc_capture_helper.rb
index 3b796c4f7ff..da8ea9fb357 100644
--- a/spec/support/features/doc_capture_helper.rb
+++ b/spec/support/features/doc_capture_helper.rb
@@ -32,16 +32,6 @@ def using_doc_capture_session(user = user_with_2fa)
end
end
- def complete_doc_capture_steps_before_document_capture_step(user = user_with_2fa)
- complete_doc_capture_steps_before_first_step(user) unless
- current_path == idv_hybrid_mobile_document_capture_path
- end
-
- def complete_doc_capture_steps_before_capture_complete_step(user = user_with_2fa)
- complete_doc_capture_steps_before_document_capture_step(user)
- attach_and_submit_images
- end
-
def mock_doc_captured(user_id, response = DocAuth::Response.new(success: true))
user = User.find(user_id)
user.document_capture_sessions.last.store_result_from_response(response)
diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb
index 3ee33593170..4082561ed26 100644
--- a/spec/support/features/session_helper.rb
+++ b/spec/support/features/session_helper.rb
@@ -17,8 +17,6 @@ def sign_up_with(email)
end
def choose_another_security_option(option)
- accept_rules_of_use_and_continue_if_displayed
-
click_link t('two_factor_authentication.login_options_link_text')
expect(page).to have_current_path login_two_factor_options_path
@@ -243,24 +241,23 @@ def click_send_one_time_code
def sign_in_live_with_2fa(user = user_with_2fa)
sign_in_user(user)
+ expect(page).to have_current_path(login_two_factor_path(otp_delivery_preference: 'sms'))
uncheck(t('forms.messages.remember_device'))
fill_in_code_with_last_phone_otp
click_submit_default
+ expect(page).to_not have_current_path(login_two_factor_path(otp_delivery_preference: 'sms'))
user
end
def fill_in_code_with_last_phone_otp
- accept_rules_of_use_and_continue_if_displayed
fill_in I18n.t('components.one_time_code_input.label'), with: last_phone_otp
end
def fill_in_code_with_last_totp(user)
- accept_rules_of_use_and_continue_if_displayed
fill_in 'code', with: last_totp(user)
end
- def accept_rules_of_use_and_continue_if_displayed
- return unless current_path == rules_of_use_path
+ def accept_rules_of_use_and_continue
check 'rules_of_use_form[terms_accepted]'
click_button t('forms.buttons.continue')
end
@@ -325,43 +322,43 @@ def session_store
def sign_up_user_from_sp_without_confirming_email(email)
sp_request_id = ServiceProviderRequestProxy.last.uuid
- expect(current_url).to eq new_user_session_url
+ expect(page).to have_current_path(new_user_session_path)
expect_branded_experience
click_sign_in_from_landing_page_then_click_create_account
- expect(current_url).to eq sign_up_email_url
+ expect(page).to have_current_path sign_up_email_path
expect_branded_experience
visit_landing_page_and_click_create_account_with_request_id(sp_request_id)
- expect(current_url).to eq sign_up_email_url
+ expect(page).to have_current_path sign_up_email_path
expect_branded_experience
submit_form_with_invalid_email
- expect(current_url).to eq sign_up_email_url
+ expect(page).to have_current_path sign_up_email_path
expect_branded_experience
submit_form_with_valid_but_wrong_email
- expect(current_url).to eq sign_up_verify_email_url
+ expect(page).to have_current_path sign_up_verify_email_path
expect_branded_experience
click_link_to_use_a_different_email
- expect(current_url).to eq sign_up_email_url
+ expect(page).to have_current_path sign_up_email_path
expect_branded_experience
submit_form_with_valid_email(email)
- expect(current_url).to eq sign_up_verify_email_url
+ expect(page).to have_current_path sign_up_verify_email_path
expect(last_email.html_part.body.raw_source).to include "?_request_id=#{sp_request_id}"
expect_branded_experience
click_link_to_resend_the_email
- expect(current_url).to eq sign_up_verify_email_url(resend: true)
+ expect(page).to have_current_path sign_up_verify_email_path(resend: true)
expect_branded_experience
attempt_to_confirm_email_with_invalid_token(sp_request_id)
@@ -488,6 +485,13 @@ def confirm_email(email)
def confirm_email_and_password(email)
find_link(t('links.create_account')).click
submit_form_with_valid_email(email)
+
+ if I18n.locale != I18n.default_locale
+ expect(page).to have_current_path(sign_up_verify_email_path(locale: I18n.locale))
+ else
+ expect(page).to have_current_path(sign_up_verify_email_path)
+ end
+
click_confirmation_link_in_email(email)
submit_form_with_valid_password
end
diff --git a/spec/support/idv_examples/sp_handoff.rb b/spec/support/idv_examples/sp_handoff.rb
index ee43a76e175..8a2c1f901a5 100644
--- a/spec/support/idv_examples/sp_handoff.rb
+++ b/spec/support/idv_examples/sp_handoff.rb
@@ -173,7 +173,7 @@ def expect_successful_saml_handoff
if javascript_enabled?
expect(page).to have_current_path test_saml_decode_assertion_path
else
- expect(current_url).to eq @saml_authn_request
+ expect(page).to have_current_path(@saml_authn_request, url: true)
end
expect(xmldoc.phone_number.children.children.to_s).to eq(Phonelib.parse(profile_phone).e164)
end
diff --git a/spec/support/idv_examples/sp_requested_attributes.rb b/spec/support/idv_examples/sp_requested_attributes.rb
index 39541f07e72..b4ca0638725 100644
--- a/spec/support/idv_examples/sp_requested_attributes.rb
+++ b/spec/support/idv_examples/sp_requested_attributes.rb
@@ -64,12 +64,16 @@
click_submit_default
if sp == :oidc
- expect(current_url).to include('http://localhost:7654/auth/result')
+ expect(page).to have_current_path(
+ 'http://localhost:7654/auth/result',
+ url: true,
+ ignore_query: true,
+ )
elsif sp == :saml
if javascript_enabled?
expect(page).to have_current_path(test_saml_decode_assertion_path)
else
- expect(current_url).to include(api_saml_auth_url(path_year: PATH_YEAR))
+ expect(page).to have_current_path(api_saml_auth_url(path_year: PATH_YEAR))
end
end
end
diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb
index fcd0ab10632..60173c44685 100644
--- a/spec/support/saml_auth_helper.rb
+++ b/spec/support/saml_auth_helper.rb
@@ -231,7 +231,7 @@ def login_and_confirm_sp(user, protocol)
fill_in_code_with_last_phone_otp
protocol == :saml ? click_submit_default_twice : click_submit_default
- expect(current_url).to match new_user_session_path
+ expect(page).to have_current_path(sign_up_completed_path)
expect(page).to have_content(t('titles.sign_up.completion_first_sign_in', sp: 'Test SP'))
click_agree_and_continue
diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb
index 6ac469d7354..22c1cb8c824 100644
--- a/spec/support/shared_examples/account_creation.rb
+++ b/spec/support/shared_examples/account_creation.rb
@@ -5,8 +5,8 @@
register_user
click_agree_and_continue
- if :sp == :saml
- expect(current_url).to eq UriService.add_params(@saml_authn_request, locale: :es)
+ if sp == :saml
+ expect(page).to have_current_path complete_saml_path(locale: :es)
elsif sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -21,7 +21,7 @@
register_user_with_authenticator_app
click_agree_and_continue
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -51,9 +51,11 @@
end
if sp == :oidc
- redirect_uri = URI(current_url)
-
- expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result')
+ expect(page).to have_current_path(
+ 'http://localhost:7654/auth/result',
+ url: true,
+ ignore_query: true,
+ )
end
end
end
@@ -64,7 +66,7 @@
register_user_with_piv_cac
click_agree_and_continue
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -99,9 +101,11 @@
end
if sp == :oidc
- redirect_uri = URI(current_url)
-
- expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result')
+ expect(page).to have_current_path(
+ 'http://localhost:7654/auth/result',
+ url: true,
+ ignore_query: true,
+ )
end
end
end
@@ -122,7 +126,7 @@
continue_as(first_email)
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -143,7 +147,7 @@
continue_as(second_email)
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb
index 9c0867a4f7f..2a15d57447b 100644
--- a/spec/support/shared_examples/sign_in.rb
+++ b/spec/support/shared_examples/sign_in.rb
@@ -10,12 +10,12 @@
fill_in_code_with_last_phone_otp
sp == :saml ? click_submit_default_twice : click_submit_default
- expect(current_url).to eq(sign_up_completed_url(locale: 'es'))
+ expect(page).to have_current_path(sign_up_completed_path(locale: 'es'))
click_agree_and_continue
if sp == :saml
- expect(current_url).to eq UriService.add_params(complete_saml_url, locale: 'es')
+ expect(page).to have_current_path(complete_saml_url(locale: 'es'))
elsif sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -47,9 +47,9 @@
fill_in_credentials_and_submit(user.email, user.password)
fill_in_code_with_last_phone_otp
click_submit_default
- click_submit_default if current_path == complete_saml_path
+ click_submit_default if sp == :saml
click_agree_and_continue
- click_submit_default if current_path == complete_saml_path
+ click_submit_default if sp == :saml
expect(analytics).to have_logged_event(
'SP redirect initiated',
@@ -85,7 +85,7 @@
click_continue
continue_as
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -146,7 +146,7 @@
click_submit_default
click_agree_and_continue
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -167,7 +167,6 @@
fill_in_credentials_and_submit(user.email, new_password)
fill_in_code_with_last_phone_otp
click_submit_default
- click_submit_default if current_path == complete_saml_path
expect(page).to have_current_path reactivate_account_path
@@ -181,7 +180,7 @@
click_agree_and_continue
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
if sp == :oidc
redirect_uri = URI(oidc_redirect_url)
@@ -403,7 +402,7 @@ def ial1_sign_in_with_personal_key_goes_to_sp(sp)
click_submit_default
click_agree_and_continue
- expect(current_url).to eq complete_saml_url if sp == :saml
+ expect(page).to have_current_path complete_saml_path if sp == :saml
return unless sp == :oidc
@@ -436,7 +435,7 @@ def ial2_sign_in_with_piv_cac_goes_to_sp(sp)
fill_in_piv_cac_credentials_and_submit(user)
# capture password before redirecting to SP
- expect(current_url).to eq capture_password_url
+ expect(page).to have_current_path(capture_password_path)
fill_in_password_and_submit(user.password)
# With JavaScript disabled, user needs to manually click button to proceed
@@ -475,7 +474,7 @@ def no_authn_context_sign_in_with_piv_cac_goes_to_sp(sp)
fill_in_piv_cac_credentials_and_submit(user)
# capture password before redirecting to SP
- expect(current_url).to eq capture_password_url
+ expect(page).to have_current_path(capture_password_path)
fill_in_password_and_submit(user.password)
# With JavaScript disabled, user needs to manually click button to proceed
@@ -500,7 +499,7 @@ def ial2_sign_in_with_piv_cac_gets_sign_in_failure_error(sp)
click_on t('account.login.piv_cac')
fill_in_piv_cac_credentials_and_submit(user)
- expect(current_url).to eq capture_password_url
+ expect(page).to have_current_path(capture_password_path)
max_allowed_attempts = IdentityConfig.store.password_max_attempts
(max_allowed_attempts - 1).times do
From 78ba9e49e8d2b7a0a681c019e5da1fbdb28f02aa Mon Sep 17 00:00:00 2001
From: Vraj Mohan
Date: Tue, 12 Aug 2025 14:29:20 -0700
Subject: [PATCH 6/9] Create model/table to hold duplicate profiles (#12422)
This will replace duplicate_profile_confirmations.
changelog: Upcoming Features, One Account, Create model/table to hold duplicate profiles
---
app/models/duplicate_profile.rb | 4 ++++
.../20250811221924_create_duplicate_profiles.rb | 16 ++++++++++++++++
db/schema.rb | 14 +++++++++++++-
3 files changed, 33 insertions(+), 1 deletion(-)
create mode 100644 app/models/duplicate_profile.rb
create mode 100644 db/primary_migrate/20250811221924_create_duplicate_profiles.rb
diff --git a/app/models/duplicate_profile.rb b/app/models/duplicate_profile.rb
new file mode 100644
index 00000000000..f7c3b0720bf
--- /dev/null
+++ b/app/models/duplicate_profile.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class DuplicateProfile < ApplicationRecord
+end
diff --git a/db/primary_migrate/20250811221924_create_duplicate_profiles.rb b/db/primary_migrate/20250811221924_create_duplicate_profiles.rb
new file mode 100644
index 00000000000..b1bdc37a22f
--- /dev/null
+++ b/db/primary_migrate/20250811221924_create_duplicate_profiles.rb
@@ -0,0 +1,16 @@
+class CreateDuplicateProfiles < ActiveRecord::Migration[8.0]
+ def change
+ create_table :duplicate_profiles do |t|
+ t.string :service_provider, limit: 255, null: false, comment: 'sensitive=false'
+ t.bigint :profile_ids, array:true, null: false, comment: 'sensitive=false'
+ t.datetime :closed_at, null: true, comment: 'sensitive=false'
+ t.boolean :self_serviced, null: true, comment: 'sensitive=false'
+ t.boolean :fraud_investigation_conclusive, null: true, comment: 'sensitive=false'
+
+ t.timestamps comment: 'sensitive=false'
+ end
+
+ add_index(:duplicate_profiles, [:service_provider, :profile_ids], unique: true)
+ add_index(:duplicate_profiles, :profile_ids, using: 'gin')
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index cc1dc1d388e..b817dfad87e 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[8.0].define(version: 2025_08_08_215829) do
+ActiveRecord::Schema[8.0].define(version: 2025_08_11_221924) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_catalog.plpgsql"
@@ -231,6 +231,18 @@
t.index ["profile_id"], name: "index_duplicate_profile_confirmations_on_profile_id"
end
+ create_table "duplicate_profiles", force: :cascade do |t|
+ t.string "service_provider", limit: 255, null: false, comment: "sensitive=false"
+ t.bigint "profile_ids", null: false, comment: "sensitive=false", array: true
+ t.datetime "closed_at", comment: "sensitive=false"
+ t.boolean "self_serviced", comment: "sensitive=false"
+ t.boolean "fraud_investigation_conclusive", comment: "sensitive=false"
+ t.datetime "created_at", null: false, comment: "sensitive=false"
+ t.datetime "updated_at", null: false, comment: "sensitive=false"
+ t.index ["profile_ids"], name: "index_duplicate_profiles_on_profile_ids", using: :gin
+ t.index ["service_provider", "profile_ids"], name: "index_duplicate_profiles_on_service_provider_and_profile_ids", unique: true
+ end
+
create_table "email_addresses", force: :cascade do |t|
t.bigint "user_id", comment: "sensitive=false"
t.string "confirmation_token", limit: 255, comment: "sensitive=true"
From 23f3f673d06040a20f623905da447eaf6f5977e2 Mon Sep 17 00:00:00 2001
From: Vraj Mohan
Date: Wed, 13 Aug 2025 08:57:37 -0700
Subject: [PATCH 7/9] Remove association with table to be dropped (#12421)
We will be dropping `duplicate_profile_confirmations`. This prepares for
that by dropping the association.
changelog: Upcoming Features, One Account, Remove association with table to be dropped
---
app/models/profile.rb | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/models/profile.rb b/app/models/profile.rb
index 0bbe5519995..b6dc89b7d37 100644
--- a/app/models/profile.rb
+++ b/app/models/profile.rb
@@ -16,7 +16,6 @@ class Profile < ApplicationRecord
# rubocop:enable Rails/InverseOf
has_many :gpo_confirmation_codes, dependent: :destroy
has_one :in_person_enrollment, dependent: :destroy
- has_many :duplicate_profile_confirmations, dependent: :destroy
validates :active, uniqueness: { scope: :user_id, if: :active? }
From 9f88c05417344c7892e570fa065b90b726279885 Mon Sep 17 00:00:00 2001
From: Kevin Masters <135744319+kevinsmaster5@users.noreply.github.com>
Date: Wed, 13 Aug 2025 16:43:21 -0400
Subject: [PATCH 8/9] Revise translation for duplicate account page (#12425)
* changelog: Upcoming Features, MVP One Account, implement translation for one account fraud review
* replace mis-altered translations
---
app/views/duplicate_profiles_detected/show.html.erb | 1 +
config/locales/en.yml | 2 +-
config/locales/es.yml | 2 +-
config/locales/fr.yml | 2 +-
config/locales/zh.yml | 2 +-
5 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/app/views/duplicate_profiles_detected/show.html.erb b/app/views/duplicate_profiles_detected/show.html.erb
index 24350e9bae6..a821f1d5b25 100644
--- a/app/views/duplicate_profiles_detected/show.html.erb
+++ b/app/views/duplicate_profiles_detected/show.html.erb
@@ -14,6 +14,7 @@
),
),
app_name: APP_NAME,
+ sp_name: decorated_sp_session.sp_name,
) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index d0e1700596f..04f15dabd89 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -745,7 +745,7 @@ duplicate_profiles_detected.dont_recognize_account: I don’t recognize an accou
duplicate_profiles_detected.duplicate: Duplicate
duplicate_profiles_detected.get_help: Get Help
duplicate_profiles_detected.heading: We found other accounts that may be yours
-duplicate_profiles_detected.intro_html: '%{app_name} requires that you only have one identity verified %{app_name} account. %{link_html}'
+duplicate_profiles_detected.intro_html: 'The %{sp_name} requires that you only have one identity verified %{app_name} account. %{link_html}'
duplicate_profiles_detected.intro.link: Learn more about duplicate accounts.
duplicate_profiles_detected.intro2: 'You need to delete duplicate accounts before signing into %{app_name}. Here’s what to do:'
duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}'
diff --git a/config/locales/es.yml b/config/locales/es.yml
index de05e435224..33f206a005c 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -756,7 +756,7 @@ duplicate_profiles_detected.dont_recognize_account: No reconozco una de estas cu
duplicate_profiles_detected.duplicate: Duplicate
duplicate_profiles_detected.get_help: Obtener ayuda
duplicate_profiles_detected.heading: Encontramos otras cuentas que pueden ser suyas
-duplicate_profiles_detected.intro_html: '%{app_name} requiere que usted tenga una sola cuenta de %{app_name} en la cual haya verificado su identidad. %{link_html}'
+duplicate_profiles_detected.intro_html: '%{sp_name} requiere que usted tenga una sola cuenta de %{app_name} en la cual haya verificado su identidad. %{link_html}'
duplicate_profiles_detected.intro.link: Obtenga más información acerca de las cuentas duplicadas.
duplicate_profiles_detected.intro2: 'Antes de iniciar sesión en %{app_name}, necesita eliminar las cuentas duplicadas. Esto es lo que tiene que hacer:'
duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}'
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 41b9d507d9f..db0dbfa54a2 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -745,7 +745,7 @@ duplicate_profiles_detected.dont_recognize_account: Je ne reconnais pas l’un d
duplicate_profiles_detected.duplicate: Duplicate
duplicate_profiles_detected.get_help: Obtenir de l’aide
duplicate_profiles_detected.heading: Nous avons trouvé d’autres comptes susceptibles de vous appartenir
-duplicate_profiles_detected.intro_html: Pour accéder à %{app_name}, vous devez disposer d’un seul compte %{app_name} avec votre identité vérifiée. %{link_html}
+duplicate_profiles_detected.intro_html: Pour accéder à %{sp_name}, vous devez disposer d’un seul compte %{app_name} avec votre identité vérifiée. %{link_html}
duplicate_profiles_detected.intro.link: En savoir plus sur les comptes en double.
duplicate_profiles_detected.intro2: 'Vous devez supprimer les comptes en double avant de vous connecter à %{app_name}. Voici comment faire :'
duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}'
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index 7952e6857f6..911ea031348 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -756,7 +756,7 @@ duplicate_profiles_detected.dont_recognize_account: 我不认识上边的一个
duplicate_profiles_detected.duplicate: Duplicate
duplicate_profiles_detected.get_help: 获取帮助
duplicate_profiles_detected.heading: 我们发现了其他可能属于你的帐户
-duplicate_profiles_detected.intro_html: '%{app_name} 规定你只能拥有一个身份经过验证的 %{app_name} 帐户。%{link_html}'
+duplicate_profiles_detected.intro_html: '%{sp_name} 规定你只能拥有一个身份经过验证的 %{app_name} 帐户。%{link_html}'
duplicate_profiles_detected.intro.link: 了解更多关于重复帐户的信息。
duplicate_profiles_detected.intro2: '你需要在登录 %{app_name}. 之前删除重复帐户。操作步骤如下:'
duplicate_profiles_detected.last_sign_in_at_html: ' Last login: %{timestamp_html}'
From e60adb567464c06579872c6477e952e14dbe4a68 Mon Sep 17 00:00:00 2001
From: Shane Chesnutt
Date: Wed, 13 Aug 2025 18:00:45 -0400
Subject: [PATCH 9/9] LG-16396 Add improved illustrations to application
(#12423)
changelog: User-Facing Improvements, Illustration Improvements, Improve visual consistency of IdV.
---
app/assets/images/come-back.svg | 2 +-
app/assets/images/email/letter-success.png | Bin 27873 -> 23753 bytes
app/assets/images/empty-loc.svg | 2 +-
app/assets/images/idv/computer-to-phone.svg | 1 +
app/assets/images/idv/continue-online.svg | 1 +
app/assets/images/idv/in-person.svg | 1 -
app/assets/images/idv/interstitial_icons.svg | 2 +-
app/assets/images/idv/laptop-icon.svg | 2 +-
app/assets/images/idv/mobile-phone-icon.svg | 2 +-
app/assets/images/idv/phone-icon.svg | 1 -
app/assets/images/idv/post-office.svg | 1 +
app/assets/images/idv/remote.svg | 1 -
.../images/idv/switch-back-to-computer.svg | 1 +
app/assets/images/info-pin-map.svg | 2 +-
app/assets/images/user-access.svg | 2 +-
app/assets/images/user-signup.svg | 2 +-
.../components/in-person-switch-back-step.tsx | 2 +-
app/presenters/idv/how_to_verify_presenter.rb | 2 +-
app/views/idv/cancellations/destroy.html.erb | 1 -
app/views/idv/how_to_verify/show.html.erb | 6 +++---
app/views/idv/hybrid_handoff/show.html.erb | 8 ++++----
.../capture_complete/show.html.erb | 6 ++++--
app/views/idv/link_sent/show.html.erb | 18 ++++++++----------
config/locales/en.yml | 2 +-
config/locales/es.yml | 2 +-
config/locales/fr.yml | 2 +-
config/locales/zh.yml | 2 +-
27 files changed, 37 insertions(+), 37 deletions(-)
create mode 100644 app/assets/images/idv/computer-to-phone.svg
create mode 100644 app/assets/images/idv/continue-online.svg
delete mode 100644 app/assets/images/idv/in-person.svg
delete mode 100644 app/assets/images/idv/phone-icon.svg
create mode 100644 app/assets/images/idv/post-office.svg
delete mode 100644 app/assets/images/idv/remote.svg
create mode 100644 app/assets/images/idv/switch-back-to-computer.svg
diff --git a/app/assets/images/come-back.svg b/app/assets/images/come-back.svg
index 8d5e23f4d02..200e0803902 100644
--- a/app/assets/images/come-back.svg
+++ b/app/assets/images/come-back.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/assets/images/email/letter-success.png b/app/assets/images/email/letter-success.png
index de287b7c8c52c8d9770b73d415d6fa90fffde3db..cfb3eb573a1c43b818f8c566ddab38f72aa82ebf 100644
GIT binary patch
literal 23753
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
literal 27873
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