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 zcmXV1WmsEXkjCAE6?Z5O#ogUCc(CHd-HKbGXrQ>eySo)y+}*Xf`{vu-ANR?Vo80m9 zp1E^oqSRDm(NTy{prD}8<>jO_prD|cA(tK!0^|-=A`T+t1KCMV&lL)alJCC@S{lp& zg4~35)sU5ds-7S@hJ1mu5?2<7f~xz8`f3Uf1!ZU~FD0(!1%1|q+({zqei3MUUe2W0 zzG8pY&S@M^z}!G+zwttG`2ySQTnp^11@b8N6YY+?e0*S0fRk!F5u?ta!g~?pDTlYj zEO6LpFl+z7X7C}56VezYR#o_?R##iBySE!>om8Knq@SFh{L227o&9|BCB1EZ-uIzq zlHa{N`ukA6)1>52bT>D*+u7g0p(CaOoItr!0$r;vSlgAec8K zT2kUPgElheus#$WRBE{BA^(j(WNNr2DWK`PY~)7-{GTsTP5t`BOhA_5u8;Re6e|}$ zTCwB5f9UlSNd7fj11EPpML)ZH)U?#qF7%`5OduPm?-N&wSz;jEE`E7w$<@G9v- zs8V=6Ybq)L?E(Uzv1GEenN?V$EYc~#qEuz`*$#o+hK8inV~bgM|4Ks=KvL2_&4V6U znc>F=8kG!&fzhMV7hjyfpYWT2qr1QL{*YNYuXO?`zreqkCDZ#3@k%Sq8B!iKlfX*QJo4sQLDBp_ z=9=#UoEcFwl>xe$#j2-v#6g_fwe%p_@Ni;EP)PEOK~2Zbyj`CLK_$o%&9P%q&<9X7 zF>;CE*Sm6X99jwcg*Wgo9%xpaS4)$ z8PgCv;yK@FAipOS&}X|Mq6w{ZUz>{*ubumWS4mV>qK2HH-wOIRj`;fqqgWm7d`Xk- zYNvl*c*QrOO0N=~38<~XmWOk`QRZr8pja-SyC1frjo30h+-f_BqJ#H(*(Y0LHox2= z9&Xf<%a>JW?>rtoS2sc)0sHYf$v)UU;&HJvTg+;L@rBigmf6r>_?Foat_o?m!{bbN zwCBqpif#(9PeFX)7};7GojDy7R`H<8e#_6ri?G@68sPM3B^txhvPVrGMoa$2(8zei zdA{O`<#8q$Uo_7xhwNPy>-ftYejF-0e@f7YSj`940!m*UHs)}^#??xkDMV-fK=t+G zPyUL77FktQL-*wqxTzV=+WBn5m!g9U8Go}Lx%4Z9|5gO1u!o|KbGeS^3!Js~+2dHM z>7)yhGjiWbeqW}Yt!H@y;iG2{}rp3f{d8j3r7KaIl?es92*D{}e`#Jas zRuTWQ*|%x}HG`*b7&CBmsXTh8QQI-dGVqFEioffQ34h9H?fP}1GIYE>iUZiEaf#s2 z8usgmJcNir!cW0+(#UT6Y0@8~9;T|w^}G{M-0j7W8^jzIlf2~-0a;B>VRgZ@+wk)} zu-pspS-;o1sgUs&o*mu?>Gl4ch~|RS~atJO>d&s0cTb z?&<8fzWxu35bLV(Vi}PJ`^tpWCn0C-Nw%XF_7IvuXgPX7CKj-xgB>$Oq*B!l$5$M;hd3<+6*AiE+Di?xZs3hT*bR=;aBx^}u1KG*My1ye!)9)Yifv$Za3VG8)$O`nlW6(?>+ED)~=zZU5jMK9#4(lX~YMgn24qDtD{ zecD#9uUw}xny`jZ|3ANTzvZp8>M+IVD5B_an}t!&b?#*n;ISHO&;4H{>6tvjkhhh{ zfecR{h;tN$znN7;>x?5WV3PU#6SM(HaP*+~0vT}5$bdEAd|0n*+@=-$ix;`N~| z4GnKQ8zE|6RsMq^zbQ<&#XMV-Wuygjw-id3+Z#5~o*EKZ1jc0+3t@etSE)Z8ZDkrqUEVpsS{* zvSH$#{+ow76z8T%t@Zkw!*)XYTGsrY?Y>c?3ls3$=4qU>W^gM>#R>dq7q{DZ#iRxI zDY~+H^Y+jBN?!(ds)7u1EC15j-FI*cz!L8mP9wL$C;wj0NM^%hjZ+GB!40Sv;-053-2-F@2=gA0{|Gs_d9 z_AxEk@F!*7<=b|TvFXbNG5b^=jV1Q@8hsZpMl*0|;@`XOuC7j2NUXMkpe-f09tX_n z5p7j*-zonG;Mxfp-a4T2CF*yY45Q1dKPm;?t8I8}KdF7EhduK8frSa+B zSDhOjFBDFH=yawOq*=fW$?Cm7O(tQ&I2sZO%?0RnhLqhyC8~xub~0o1J&PAOO`ld{ z^_A{Znx!8&Qjuvz^2aZ$E$e08G*}iwQ==*M_HfJ`Rm6PLz&7zGcUsIkJrPkXWbxtHoo^QRA1JT~+fu}o3b^i{MxV2H$)y+^RMg;yrfB>N_QwKO_tytB&d)Ed&AIFicm;PH~aL~Ztq6xY(~ za{I0g6pNtS*ulA*6`=$(%e?z(=ymLEf)Xom+(T%$*=S=3LN{H~UfcF(bIdp@KD59p z^{)Bwl($Zd%{}LR^P+#wp9gLjTaG6@{pqQMt>15>{n1h51h`Rd{D0^V&wW!e#>A|r zPu9E6``NpL8NhzuI+vQ>(|90Kwq*BLQC>p`>CpQeUUmd7Q$2vkeo?#^xUv_?BDvmHyOBAgEs}ITIb7mbOi&xa*?rqb#o{(B=VW^2tc5YY#p`TG?A%$jVU%?~e1g?;yj zBEzTM8ch#j?qIz1VlQOAH_x6x{Cjc%lyJ2BAIfdDdg718@arSKGT)#j>-s~dZYGR} z17Cu44oV@S?Y+Pbrrmn7_Kb+8fW}<8JLF(v$0XIT% zKfRg1Bzi{z_wjTXJo!K;IOD&Ur4qO(<9QZbvT?7ov7y=R*xRAH|03r%*og8|4DNXU zMS)S1H8sRN+TH7ApW&gnr-yUQ?RD5#S>v`}kXWIiamioVHKV6rV%Mv6j!E*A3L(0sT5e?*i|GfZF{pIpNd7zKm!!mk4)}1l_M1 zi!73z!9gAP$+TQ%fD<2!St-t`H3z0cc@cc7xGt9_$^p%Fl7!0iG*>lsWBry6)$N`k zE`Y8v=j$tZ{X8|f=sX$d(!h9eP1O4f^LsC1p4GcwRny)D`}~NZjT6?4PsrwVm*O0( z)dB&u^F?S?C7&mDpZ()V&e~uPy3H7a5IR9+qjQCB&yHU<;aDGFf;HL@of+oz-2TMm zpPHYT4{^vXuU8XqJ5e^uRsL6g?xXv&*NB>djgE`^lHbOmQ}B^*XYY7LOu5X6*p_=P zSR7g52Z<=t*!YRB1{-lFl(a{nA-Aby_0$+a9dGDZu=Vu%t@^4Ck7;A|w*w+p(|qoS zF{Y;cJ;x*+ojEO}G%8L!N+ioH^`0=Ms7<_)trS5{x6G&o8TP`@SC!$Ykc80BjhL`K zn2iXCWL5iCLz=i!YnN8a22cfkoVN`bl*oY(N!MN zHqL>Ip|^OVwRD?*IVkpQ9d&ZO9U(mM!Jr08j1Ovmy+do7i5dH$A2HB5-pFv7AZb@8 zMeszpRDuvUM{5Dm(S1fFUd+$>G5(d4LbVX@<@}=OYhS#K>{?J?U4-t%MMZwmwI@hj z?PKjFW7bafs8c&FdPb<+05jBt=K2YD(R(`Q{WG)AM~(!MR#0LXt!?{jRdkZ*W=4qt zhUn7qX3fWq&90XYwXcVDB&f({?uP@MdZk+991%{C3H9MSw%!GrJjpsWI@V!Y;Pb>gBEf+;AlPO~Ks%*+nx>kyRUNe3?7byIk6Ugr1x>Tzbfng2h z7`XUS%QB;oRBzQ|&GF~gJozWyndr2Mo$JW*la#=5aX)wBcFbnjwcp=59a`Q<`F0=i zuQv|X%8^I%EIj`tkolZUhJd_}TcX6KLS5!Odg)h)Eu@ELGQ9l!~evtIU5YCn0`3Bp&~LH+DU*_K-AuUZGFrM|L&jvGx8E z*fiqseCipvr(L5HuR;9 zl|tyrZ>V{hfMti>p)0+DF^d6w{D8eXS(jt{SFNaV+$SfD;iXnL) z$_xPj$?dr8{8_CP1buicKp4J$AjFE$W*jBG}I;) z(xO`UL#*}B&r2$;N@Dvwq&Ay-8rK0(rH6ukagQ9cW4mbgMj|H9b~JEt;c>#Y9a0Se zZOuA{f=>eBHS>E{fPdM~w3F)@9@9*Z_q+A;l59>kr{#7GzL<|4RLw=$bd;d$`awYR zrxGRZD(=b-JGopHuZ4M@r23`OPp;Ki4j-EJObq@cMV~@m1>MwbD0H@ssiOk>V3}mr z(l9HugBHeWfwAG0o=8G%TT|0dz)f(zpW#*1&gY-M77!_y7?#G%Wq6%XR*63YaH?I zCr=Fb3~3y zlSqtyC>+Y@GlSVX%_ITD_W_q|ZNo;V>s~ouR81^a@RWOaRV0fo7=hMCbp`Exh5xR< zfS-GIwT!aKQtT}2fca54Ik`G`5;p}qKsjA2L_??Zm|mv=UQm+IXuJ~qIM-CS)4SnD zNNG>zj^N8e60ayyG~i6W1<1@Xb>v2_mYo+pXoHOXn!|4{z-%^Ii>w6r7KHX&sg}JU z%O9O4Mbhn_W8|pCsCn z-$1Aw4Ejnz%?V(Zh)PJrV9rML*X-niBPzs-bi6B6rwtLl<-z(DfFDDPQzaKftT7a;JAmpQTfXeBHAJ1^*G`r&y*P zs5PW$H-U=ks@eHUZj$zUb5>G*=+n8G72Q}$kDEo&m&RhggJYqMcvY@jnpIig{f3oG z=f}$X`dxG+P)?(*rDV<*s1(ljhtuDq+$zH?>(X!(TElC_zn31m^ao&V<6bfz=~?HDxIKXR4YCsLytl=@;Y=|kM%`*CbcxO(d0`pjiWfQ1wz591zroBD_Gma5oG>+`MJ#EMEtj$LmX_n3vYLt9m z<6Xv1Fbl`Oy_)E35)EYhiwl&au|gNye+pY<7H%KY?JJ0{u}z^7WL{A3PH!FCj~4+1 zDD4@7zwi|^B(!7)-|QB7d!SXjpA_gozK!R(&wlN*6xe?E=nb2?#ZC1Z$l%osYfjWI zIzCQkn4v|Mj1rAcw=FFjKB)F>?&wOjn;QG&1{J1wkoZ9)C$e!u3EV@u7hk+AJFO1s z+U!|nDF3*5`A4GnWIor{f};_Ys|)b&nO++=r#(zSY`>i0-K!^;+`A`!|1_rEdKUd=4O7 zZCK2b?sUHQvnW!AlU@=}I3h#T~ZBV^-2^ag=PA9YM z{@*&G+0xgH^P1@O&m1#eXs3Q2hpAjzL7&|41K59@=)M)6m99v-x1-!onJqCLXY^rF z_wDp~z%V6_4RL?R#b?;Gj)Hq4*+Nb%0cD8Z*epHNeC+gKOBpCez@6vrDwlko+c2{+ zXr-0*%e$rxZ=b{y0U3^Vdh>}qPtLJ36E4(5+b?PyL2_qe$l0V;t#Smk1r5$)( zvHFRO{KOn+W_xGbyknB**S8$f{!z|7D5WvGB}f?00L(7qiP!q3E>TPBoH-T(2)K?5 zd~7F3%CCNr?Mxa`&AsgDduV$T%detq1e!UUTv8c_VZAe_EL+W>HeUWD@c1k^$BPmU zRDS#Kw3hQGcuu4~*rNvuE2-ZhZD-1H2cHfFKgeQ#0MsEgk=s+LF+lNiY#Z@*$;x6; zg|F98e9#M$)Zf5I=?*cgRno?zv1s|2FlJ;t5gh05Z`jPi80I@0Z=scy2c%*-yo!rIG zi*PI{DKA}eu(2&p{Y65dMq%uCG#+zHo~z6GeEkrrSkK!Q6iLmju-1{h$JQhIMjyH1 zN3ENLQOZbC6ut8dfZY%E(GkXw_bT-7|L&bstay=0R@&GPE$k?vvealn^|ue(5rNwy z8DZ9xc>yP8Q{Et=XIWoa^9F1)_?TbeQBT-M7%CYHx^0!IG>juVk!f4|d`LxP>s_n5 zp0b%@5<-fKnQBNIaQ_}cZ&x83-GDGIo8OKX+j~8MwI@3C1AhkYB<@uwY0Wii(a~eE zm7#j#b4AjCp!^xyG>cMsvSi{~zgOy1FKH&PRk4T9lx0}v!1m=W{H2**H|T8r6eW>b zrEnKr`l8$qWCKsX>Ph(@zW3!&9`mD!WUI+f&e22y@iSzCzrWE`aO_PM;trJCqcYzq z7KDk&*lo@lBI9tA{R;Dd!zoitpK zuL@Q2bI?KX<7V<=b4Xwtka86Jc%hZel@$sJuh@m2`8y|7^S*G4OKhI~54E0QwaI+= zl3?h%Z>EKyUkex~YkgDtVyPQgB$c}v<@Gtr{+^B+X_XuDIZm0q$#*J+5Of-@vMI@1 zswFYQ?x)G-;F!$QSOeZCQe-NA1{~t&D_Za257Q*bEOHak#K^RT%qa9DhJnFWddQca zj%~zphlMY^aAoKL^`)bKcj(&(v|SuGZZ-af=o&DGef24WJ#h259#r+yU!xCy8;P{_Lgy72J~d^<>N6v>PEy_F6imq>y_T#1 z*iDBeF_nI}hiFWBq822|Dl;QBh%??AO=uzEU+CffArc&+jO)`ieU&`MMT0#TxS9TR z0b|ve%KME<;_594{apZp9-QZ2BxzWXA2u;&EF!!GlK#NEjgh9D=82;zo{bs);VpyB za9G%UK{4`?35%as;9{MK01=F5J@OB6_^c~mXfaAhNJXR#p(5&EjffDU+2YOJIyt|%PmC> zd>2x>4PBJ&YY)MXp_F>Uc(!ud)gz2{mf^si#cRX(tq5 z#H%%2D|ZYd2h!=ubKl=4$D#|Ze@yY~wWUx+6-+2fDC664n-W-C*JCn-4r3W(c8hna1snW=rlq z74eojSH^>|6#%g3#b!mZoKNWaWv7C(y{RgaQSdt*BgJA7=J>J=z!jo|cCo9lO;l3z zaMs*<9MjS;ft_T9$O#OkUcQA!&1anPvUFU?`NAR%Gv+Kh4^Er&XE6ex$vk(JIBG>i z7wyqDq^Xa2+KDZ$&40asP~*7Vmb5=5S(0BXX)0@Y_gBE!+@1stNj^#${A!65>iE<{ zKez$DfqinoRs7!IQ0=`?X8#bMP^Dww8jQ)1v+^ik+ ze+|gX1EY-wc?Mo6;mFjz<=VWb@MP$?AbNuZ;o3;mw~!5|1%)-9;Juu)pXUgk;OiH_ zMeF>Jr}PkB_e6xReLaId$ridyVk?yQIjvg})38OKIuW|NeORQHk3F z+vJp)B9BV+l!+oW=0=|_I9dWLZ@x<=H;M43FFlUb7=ED_Gzw14si8hjbu zh@xZVUJppW&5VH=gT|4{*rTHqSF})gtZ{l208W@4p^w<*Bqxb5GeN97$436Iq4afE zf&T0Mvp~CX4X1mdTXs+AX}lk>!nlp)2kvwoW{g^6PMFH7Nl?{OR$Dk@Sy2e@c=FYh z|IU?CANF2aH55b>T9>`IGN+`)fmM9$N8b{! z0{-hJ-Qp!+R-i*n#7`4pap<>hl~V&{136js)aG&~!Ok{3nEAtW}K3A)FJA z_(rj3I)3p${=Bk)aC>$YQ=MBcep-2c10;k!hY4^|3Rau8OwirN3CyW_DR;1oJ}eF) zReZV)oilSyalB*L@!m|~b$B0#H|qO`EuzT=&(j~DA3h=c26WKg2@NT3ut4U&2rP^M z0yVgW7rFLq!bB`Qi-3m%NC~sa23<}!BI@Dbk^{brJ{^MwPxS%^Au<`^N^sO~k}kXm zwyNr8Iqb{t<$mJ6_DHZ$Fj?;;8tmyfkc2m;*9W&%2bWQagcdtnlUph0+g6wQ?|zsN zp3viv^9_&WZ0Qins?TEnjbn&xR0sCIxbkHPrg`$4_mI`)xo#t@%*Ug8pV#8Pl@f%8 zlIt2t>o*NY$ls7$a+TARqwe-5hgZ+x>(t)3+>mHlb(`83Yf@=fxDeS>+bS|q60u~! zElk7zVXP%XED9>2;arE`4xfrAs%8r>L-$ICm4~mJcJ=9(^oh!3Y7#y2X$xg+#%?OY z8*QI<0Y|E!&aL>*lU5%5I{zmZ*T!L9(xa8%C|U;6wR(oiW|Xr{A}PdPNKsgknUgzw91L^5NMT1$kAimazcL-;oLw#o+om%@Si zBxf1TTalFAq47sokj|<#|CdfH@WT>?9#GFqIb073miZrg6uu7kXLLOO{Zi6}m#$cz zcV$ZRl}I8Z$<@e}L_8Z}mI7I|i(Vfg@sG13{BmXI{wLsZELIX9|EhiPi>*zubj9563*BUIy2GIJwoURgIE0Z{E&hAS;#IWxMzWGOkT?{`SQ0+bCNX;^F^(t{f&@bw);XG>ORk9|S4dLT zA{?X~%6PplUjMl-a#c>d=wRI|@$J{&dU^?k=dLo8Mpv^Y)h2zJ9UZP@9naEwMRDIk zoNqa=rvJ}TWN8Qs3CzSSR?=EK402!{md^?@M3TS4;1O)l=ko?@d(TFHPA1e_*u*h2 zi$mPB*4eTEriGb~viL&8VucWOs5cRGJ=RbqiWeO`*l?efjS4c4Bk6nQ3%pz^Y(~*7A~&zb(R+hx$r{jA`j9 zTFZ$7n9ZiRN(A~CQ~I45y$vN2{1z0JWahwp>@6ra<=1o8{7jFoOfo&!_j(TLnu;uW zrYBl~>l0uh`zAx;()*Z{o_Jp&h+(87tQ&v)I(6nMn0BEXJqu1L;lZ6P|Kbi{_C6vH zWt8;aMyfaVES!pl5XTR)0d?ucf*ZbjqKCn%UBWr(U-|Jyhr_LhV7vsw4s8ePPjqV3 z#+I%UCm2}b03E*;?Ei`H7wqQN-o}?OD(e8X4Zvt0d^_@SwU*wKnS;Q|YGgZM?*1BQ zEci>`UEC#r=(s$;!lIPPJ=T;`sO(Qh@oB5b&!~cV_Q~;ry4cc~SctTBzZa|8fPi-T1raF7@^r_NC`F!D>z_=@Vbp%QUU~a3Geg3PY+au>8+t zVKU=QI}bL9rfl+L=mNY^1xbJ=?mYE~3eSjFDk119Bdplcq>X-vAg=OB2J{c%i1iTsE?`||93Er=h9+;0A4>&{sdx}BLw5F%?&v= ztPx~J21CWCvf}%2^62ov>To?U9Iuc*&ZZ## z8jPHvDhzQ5@+V-2)tzE+AIlq2uooptj0kH*Nad3=HEV7w8|o#ZgM^TCqfx=Wl9M1C z)|M36YfTOD;;H0{%=qCK-Y!zu)9F@(h!t;g?d)ya zGfY72XKcCh=&oKa%8f{wD22)1oDQY~V;~PlzI;}f>--U)%Ap)Mr&sr}Ro>>WosFp{X z9!HebbgMp0b5Ivp*Sf7+5@6D$^oK_QwD-;Ah2s>e+Pr(?7B)cjq`gFpe({%r7^{&2 zKO7n}xs3@9IYLIXUYK3|Ipne=s&I6z*~>1EM)~Go;?k&BsFC-&^H`?0LPEh{2*!j% z9uXb{xHh>*pC2nT84ir7rsEP1ekg3n@Y1A+c)Y?D4k=F~weT}W#r>rX%_koL^Nd}=d?J#iOJx^KiF52S3va$Xf8HUJLLqv@3A^2FoH&P-2Zms{2r z4)Ek-Btdv-2uRW8-EX0?OIYm5=1x&*AF2%kqDOPmDSjYcJ1|3;}#bxO8 z&7X1U3fGoXtyQhffy?m_yJ(>6XV=AoiB+xBd~mXj6@75pL+C!E>-M};E%)!B(Xm(n z-w1Ob+v0?n<2~mJEtGF7f3B;dlu zAr7tEX7u_Gt@tXvf8KeVl#D1Gav;%{9Nh@7pP7(-gCSCTgyTRY2))bUAD7n@#=67* zv55%US`iJh?R_LfxNY%!JCEM-{P4CK_!M|Qk-iWM1|@2Tfxc;Xc+~1TD&TPk^-IHj zT;#SOxK49cP=+d?#t^oJ9%~TKF{-vPYS9QTDLp)!u2hs+OBpGP8N>BV>(9>5i;BtA&c=O}QS(#k80f2SHBNFT@G0ESr1b1Pj_?{35}h;e7HOek zdhS%B9BVWFMvjeMJsJWlDoFDdcQEm2O&sp%~JIcd@Zouf3B~@Opdpl zDA}i(5XSgfhZg2E&A<}&H1@j=&5w4?ikf5Ixx_y03f9{jDajhcq#vFI6u*EOvg}Ca zgv67!%r->o1+AC4LJh5IpwRDW9*Z~;NVx?aqq`>dS;Z1zxkZsXb`WZGnXW~|D@=bo zc59O2ckgVvt$?+{Hgek2ebIs7 zp&cNNxKvyN#i!?NqpT|q*y&b1nvt=r`3`tPY}bFWi?~X@+$r)nOid#bT&F7i_XsO~ zm+C{}6myLgkk^2^t4ne*3cMpkk-kA!fde`nzmzAs~u0*@LqwzuEYNb>8(H#+Bg$MDO;WW^veRKKU)GN^< zEd*1YVD4I%0eSs!DkWG71XBq# zt=-6lKmBbg!I;oCHV=78Fl4&+Hp>TnW`k1K#5qBa#BQ|mS6mJk<5N%m2*N224}N3u zyY=N1Z#zI}_BNuPzIpOzPUC_OtTRaqtmyO_{}`>3Pp0P?KN133G?|QczKIR)IZSE5 zZu?`3Q0&7DsDX$GMdXOEM!{!Fx?Q4R^`uBXkWY)u(~fJr3H{PeUNt$vbmmuIK++># z)C7ENCzH|)@l<2X$)S;h6-@~j+*usOoI`z)h?B1E+@F-Of!Z^wH_9?AWFgkZM*C+; za|Ul?^|{eW`LhtCF1c>RWg@EBqS1~St}fHXjSy_&d9*8d@ekbqQQ*By1rrr_?gnIX zhS~=VG@JUI0~uBkEmi;aV%0>gk_ytl6=*(NLPbC;`|E5$1&=yQoCc;_#FuFeJ3*?G z$c>yy=t6H_jQCo_WPy%SY9qE$EIJ2h0q1-q`e5q>j;z~;pMxr`bQOSJEHvNgj5QX@ z{nV~`c_|%!7W|t12N}AN&v^fSUenZEALi4=ISNVb-#D=!5^T;W@~%E>*9BXi;FIxr zwVI{-U{PTzehaa%Lu#OP{BO|b7?Yt^egVI|C+!^sd%e69$D%;BJL5drSSIN8cRy9j z(ezVCSUQcy`WApf70}l~(z9$3)mi+XoR#vvQc8in-#e89N3k~SocLDP8%guj>#c(Y zBWV!h(boD~X*8B+V2)d??;JwTo01+Nf?}Ux&lZZ(w2gqh)&id7;u8z8bR{F}D)j9T z;iH20W&S1oZ*|_bq~g;>UTl&^KBqxGc946L8~l$CE}&J(9in$Nh&RU3DeLp4Jk zH`y#(`GqFAbdMqHDJ4a#kt6k~TMcoN?4wtC4@>{%l19tS$preh(46hZn>m zbL{7)|HWwDJ8=P6Q67eg`DCs97GTdeUEbd-fG~fP4Vh}PnI%`+2>^dDoz59(3f;EM z#~DxIcDx*xR;G~_qi!4j7_My=G1v3U-${%Ji+zVkVy(DZo!fFH}Ku6dPtNn}oN!w%PF=fzY%{z1x?(2w9V zhRZ>PJx@Czl+%Ei2?h6aQsvp_!U8Nv7@5zK*Gry@FTDvyDxW6GU@8h=p;A5^`{2h? z%~vdOJ(8vDK&6vl7$^dx)-c$S(JFH`1^?=6b41=obZht-K%y5bmNiTK4a}ir$&S&C zs&sb##Y3ENJhowPj0I#;aA^mzas ziR=jq16VZ8iX&IMfZ)}420tgECi49GHqt(jKr=MNeRo5}qFNCdGh14NMebJ>eXq!5tk2{1!FY|ZgU0wf){Fne?=dDF zhiGFnPXmE`hk-%o*Z5F$^rmT7g;QJhvb=`46Bu$D{znlGy|3KiFA~dj`J{==8Y;w| zY}6%8Y0a?qhyT%t?My{R_VH(#tw2HwElvby~yzHT=PIk$mCk7+%`6tsV>=d3N8>^%Q;zEQl; z`%2eS);CQo;Az}3b$I%{dH|U0PPbSINnMp~n!rIF5e^oXMasHc+E@Jlnp;bkrb~CP zn8vw6%CDE^MZTOymreMTCYI0l&y7hmW1QbV_a@4&UOq2;ZW+lq+1m5UYiD6)^x!(N zkT3`M438{0bi{%!$JbJLQpAhtZieT^RvIp_NQ6CvEKH;H!{g%241h_-lc}q8iRR>l zvUSHOPrY$6Mp2R4VLn8bI*X&Dg#VTUM=kLFv@=$P5tYib%mDS4N*a!4NkuJ395jJ@ zd|)|UxpJdxL@Y~ZTs!T3L_s3=v_!gbQkMTZa`da1x-r$Li***J&FMkbkN*?C8$skm zMZm78g4J%_78eye8g+memr#fm#>cmB9crXz!AfIgZQ8oCVx*0t z(Q2bCTg(vc5lzz{iZ>cH^#`F4pz(pZmPSjnn!z-UoTJF(NpY<#=AR~jwT0Vu?)Szc zq<;TcMWIzkS+<$f&JBY1WL2!D2yp+jkV6Ez`o!Ag_naFCgnCtgTySCp~+_&y9YTS$rat)}*en6d|C1RB3fU50Wbvfh_o33D~g(7vwJTz-I zj8B-2D3o=eU8Zh=<#we^ja)rkn9&|ZL+agAxrVFW1Tfc3(X+rG?RPRc z;Aux$wq!+`^oOR5M$MiKKY4RQ;#qc-WlJ+jlm5`q z_J|m_!e_A0>>0#oyWXy#pkby$8rP7=v(R_06LyqkOPt}RN<*}-g4Xj)0 z3|8LoxG1~Tjwe|BI3GXLSS@wdl|Bdl}gI)iXgfiiSszU0>I{L$9qjybZ=?&xBgA zqbyrmiDslf;4}8jhu)Q9iTbB7XtxSY;s+1>`gT?odXq`np2J7sC4ckx(0MYw?~Wp6 z(!dnc70&9JYK6*@E6{@4&hFoGLaY_<+hyH-RmEB-`+DK`pZ;@m!+^pa*IseS$Dj!` z?deRD>VPNmxz%rnG^8D6*`_O4@(89s@HVQY_;7PA=@0m9rSM_~d&Z}%fhN(^X!1P| zJvKn7^Ca7iev|U^fBtJ2xcC0Va%ffUVb#D%HGmi=IK9fZlEuHVNl`d?CD=27s_16y{eDo?NE9*?lFS`F{u=rnpuAPShg(EDRbc}{p6$UtM z)0%XQ&6x==ES?Wzvu48RoPHReHH&|jr%d4=bT4Gkjqey4;ch+Bw`&jd9vXtFyY@oQ zp`-qEiy3H1ZDN+xxIxScFiQKQlIOgT9@6 zz!DF}v(VZy=}fYp)pTRRvubVaz4X<*7Ty$|Z&jy0uF{Yj^WvTdf3=oCX|-wjv%mH| zn6+Uex2pCSUOY@!Sk~2NV^s+ZpM5$UUa=ewz3Sze3se$laiMsf`q=Ma`m@i2B^nIK z?_w@ulC_2RJ7&|*@m|tLR-6ORe(>F33?XZ;z3gJMmMV27H-fcp-NXH3-8~PpJz8N> z9^cFZU-}yCf9|=&ockl@xl;*bshDfM}gMXp~>WY zEWH2wmo0;5KmKQFuJK^c*pv&i7_~VIauvXFgYfiW_H)2!fs-Rp`uuG`wnt~fV_+TA z6>LwSdeEBM`KC)VOVF(sb07PywYp+J4+DiOHvM7gqEu}J0UI?F^4eww*;R5EaZX!v z2!{?s2NNwgc(`Jg4Xj7gDng~gEw1~2ZiP!jY(ReQY*(!2nD&Iy{2D7K~qFii>p>E2&6-NG(=wzwC`{)ND#_O%EnoH!#uqj`0GX zXw5t_%meGzstSWK3#`J%I8|D<9^VXGPq-fxDj`a72*FC??0M(FS*NiN`#zg}rv-EK z@MDE5jBjntnVDLw7&l*g`6YGj>bP2R{Zyw-cj!EP1kU~1-L`660o}i?tJAf~M3H*> z*amp~$Zw1UO2t&LWY7vj0lWA`uSq3dC{~!Cr>45JJHw0S!?PcH7fjBcWxPyY(ifu0 zy%~~Pv{>!oxqtUfD_W4=szPCU2Zr+-t>>m&27W zd7F`Np|y4H-+r^LT|~5~pZ&nQ3nx}c%LIDTiWY)FS$ylZ8<-e~4O)|K*m?++AS=O= zL5u4W<}M})S3AG34qWom)Ex!1idZ2n6`4+(4UfdNJuD)o?ZZ&0e!B1e%!Nu&55Yo& ztBAstguV6#G_k9gYG|o!${*kT?fc&Lj`z$2LSALM46nXtvW)H2ijK(*du04DeCzqI z!=s0P36nIrk$AOC9H>tmX=MB)Y}ok(Y~H_-$G6aBjG~>cJ+=$n^Tw5F7GYWicU^bI zCB~zq=7E+=|NhbUzx~s!?Yn=^ecT1Cr&T3WZ^7c*C{J)}Yh>aioV(zH z$Z|y$Ev!)a!zVrjCr)3IYPnX5S*f&a1G{QOtK2t*2c+@F?HWD~MIcG6&;|U5=e`V2 zAA1TKNh1VH@ch9o@aS`oz=exn5$PfpO|*DT;0NEGdc1>H7AvIX)4{8$Jw!_D?gz5& z0cd6DsyD@s&WmF8l`VhAzZ*@xg6?r(K>mNc{x4%|5esqM-|#wc-kGi@kT2KbVuiGP z7Y1l)z3rXvJ;r{%G0k#x`B{9FY4ZFaGY1;uCS?hI*$QxOxGr)&Hlabwsu^O1G}6Kn zh=Barr=1Hkrs&(w<8>!Hb_3a4|2SUeOzi`D<8|P?=?#z;O395Zz@E$IHfbnKgFCmu zw6o5*mj^*iZx6Wz8?*lPUqp5jvg^bS`0Dew*DYnigl1S+0(1*M&Md9PvmzQ-pB#aX z&wRG^u6g=4f#CJJuquxYnX92tEre6|rzOCCwa``gw_iZ+VQ*P3RuMODGu=R$W(pRP z=pN#W>NB4Mc%gZsZVGn+(e*d)$4Zd9{;X=HP<;jCh4DtC9Za(vx`r4NaNd7C7lb@R zueu`lVwDFl?cq!R{#X243p{g6)OrGiY8V_;W{_R3)C|YHJXY{;A#2KdlE!r4p3OW&Z6r4v>g`wpa}epNAj09DY-;+uFE! z>Usp?_rzow_8vLO_C?X%=yo!8kxV8QIE(9u!y zuMb{pybkhp6W2vtKY1D=%Hp?JUWdAzye#546oyWW!SKoIDuRko6BtL~d%1JRJ;aUx zQwHN1UF;@aJZ}bcl!6;ncQXsbjVOzkPONk{wR(fAH}=7!C*kOE{V3LtAAStJzazCl zSy%ETSjaW6SPhphzf3!?e_Ah`I@8VH66aeME=0bzGFS$u<4Y~-+6q*G>*(m<+Yx`C z9G!pzM_z!5a`KpimCzvqoi{dn+`xL%7Tt1I9gO#w+dmafojuL_3vW8tla3ND@wd1^ zg@q>HtnzPF=!7*V{zmI+^LBkK23^1}KJ(e8)(Gmzn_ywh;b-3Tg~%ZM=?iDWl-{n; zH2K=9FC2M#xIBL1Ei(D~^D^x3eFul($Z*xyuyDckDB7kUVRsm@?ltTVxe2PEtE&SR z%$))K)2H&27J37t$e=JeC`>_W8^yOL-k1^AlQh9EPuM!~o~P7%IN0>TU58-!1$|x< z#;vgH;=GYJ!9uV%Q%rZzaY|ngoU!yY_qzDk2Coa{*F>J~iw?h5lwTw99TFzbdh$h0 zeqn(tZ6`;@VaMLXFg_7KR9z$#?qKn%HF+6^tKPI-$LBwI@X?R4t9(l}E+R5(#wjrW z)EUs*-3@q)s&7lgVPaE~tT0j{o(SQM>fN*u2$mYNk}bCrC&ys(w!PYY$)hVM$fIB( zajxN+1M^_Uv_5|B#WfTzo+FEoYea=!I(S{F_m5wDoQ1alRYC<`xk={s?b>$)jtrfE z>Y(fdlUwuJ_jZF4wn>J(Uh#zOwvzpkEGg9-YW0~DdsP-Qk;-Xa*n6gkJtJ=W@RB6jG+wnrPH`6k#QGqHL z8=rt3dk(=cSooH;qzHJoZi*h#cyo>w>M4v z#^tf>$&m@ISRrU#*%XHKV2Z=fH$9}ChvynyG`tpg{`oayLWS2#zBc^ZyQboL<8Lg7 zA6yof!w;4f({SB*&+OiRq!zKdqo^qhCbXr6CF6~O_#LRy+CpacPlIKPPh&ybo{$S6 z2hru)Cf~XOsHi#@;P0)2dtm?JV-?#&q57*oehTtTUM(yn&dQoGZ7Q69_EP>^t}#@J zx_-LUm7(Di>;-oajvcS5Vgv*0Hx{LdO|`V8g(ZVG0M@WnlV~bHE9;4;x3XB%ZWcUs zI|%fxDfSVfh2OFTrIzunH1>@(gF3N7e!BO5uvAZ+m38d+3p}RDOXullx7Q|CXjP3(xv<^E3Tb-_OU4_#l?CLhx3-XZb7#W9!qcF)w>val zefz5OWDDz==XSBG?jsfD@|wn=l7)q}WbPt()63rgcRutj$eK7SYhcl7{DYWYGdcD+ zIf()FgRFLUKh%h|suWtjdzmcx@!j9r@YZ*|?bFRZ;APmK=nhVnE33Mn9(oF%9C`xsPOaU`pjbWdnIFPiFS(M( z`hKNyEeqTjv)Wd(^?$Z$y~pUTeq{?zZyG; zbYoT5z5U8pJOdUVYhguDYfYle{8MMK7}tE?;!+b2<>XQ1=&|GQQgLmY{WY`?Y%O?WO0R$>Uj33BfY>V%o9$0Jn%IC#%e-61X*h zH(RSpPbXNkc(pLg-1dlYQ?+?p$le1-_-FoUvta@I%$zYzD_Y`v-$DH#pg4Sd(Q5{E zVugI-ZSHspS&A0MvYy?yQEO!#Wbs41PKYW}yb^_d9~~NoZ9Dd|7fiLeR5EmkZQ_n1 zw*$-q>k=lw`;LFt`+vxO&SdY}6;J~wShe}y{fGGtj(IxhE(+wPJDZ=|sdWc&5DDx_ zv=ykvC5#d)!GAgDRhheoXl0$ZuyO#4lOv;W`jQ2{8z@{tyiO=)&urbn#R{#fnjvH@ z3#woC_CLAkr(lW2x`Zjn(oIA#4FavMh4be4DG?}YkNx(K+VZX94kBq?!X&Yh`NVf` zg|9vIZODQ+xA0XjI|rtpGL>H!e1U9dLG+!w_wyRujKCV|Tdn(Nj&%uB;Jb+#)h3RN zj=}TW_wdi$IsLHkv^g*m@y0H)JD5hSj-4EaUv13J!isYXhnOW?W;K3Wx9?`|U#6oh z4HD?Mx#&GB)B|f_)j(FlWdi12p-b2*m|9qA#p;T`x(1%!z7etnhrY*8{c+<|z2kki zw@YK?tZDni(OO^@*IM4YsoTV75b8)Cdt|7UJct#7!`{~y6e@aa&sg6w460kPsvheS z*2t<=uf>~h4YRh^g5%u40<){34i;C@1S>CMh3q`C9qNO%sItZiRz`U7EmybzR%Yoc z?jG3*MIujPgDMJsYxu+&(gsuxzUGR70GwfM;@#&u+@IKlS3iIrgI(ZMVPYmj~J zV6NX-H;;J)P2(Pz9}$dzD09#R}nW;ftVJY z_Ksdf9C{OwYfXM!ac*u>FXp^t0Tqq9B~s~^AfTkzv|eG3-PSO8hmN@9h?SXfo}(qYLS zD_Cua5Uc6E)3{h=En2v)RuU^q>tF?|&5*@Pkfmseby=~p)B`J6ZG=v&1X+kynsr#Q zvQ&c=tXx60)#Ot;`k+?g#7a<$Xtlan=?0_lu&E0Hq?lD-*Hp0NA1hcnf>Nq&ACiTICCEy&iWXpB6xCA$g{Q!h8?0dE2|@;S zle4n01htA*5o-e@yOtr=CCn36uyO>6h?gwv%T}r7=$HFO#Gj$6E)reLR#R^jzqAaZ5(OxT9Il>B7j&K`7>vOTPb_)e*Max92a9ypG z6dKdh=%DRFCSO>=${7U6dfoIhPB{zeMH?w|MIXj4=Y$XqeMH`>!zcHRgXB9Of90-s$wZJ{{ zvJjnU6+x`xtSe?-{RAwz#0pkU=^Y=xT{j*3#%iRMg~*~+1hGO;tY$}aDML%-cCh3U z0+!tJg9jgbSU0>q4*T_2fBaNkM$?G{do4vw#OmbJ`A!i3{9C@P7c4^7UVGWaH-aUX zSPLu1Py)B=rZ>aNLQtq$-LgV3b`@z|*ni_P!D<0YozBfs^WL#^wRH^*Kvys(?+RmA*1Bo|tYEbOSHJ$%8-P-6Ue>Jc+152Q zATH3()ankmFEUHsv4Yhi^o)P~S$F0~oy!MJq zJ_eSS!U|T4;kkydjq1$-#p=DMf3U4<4o$?$J%^T7!wOalf;CzMqT^XO!cMzdS_Wdp z<5sp-ur;xQ)uLQ;`6c&JN+WZJ+EKJj#0t-uja#)YR#lh9nmEyFrS%cFN1kjaj6T46{})k1*;&Kh!zUg zPxn61kbblpAz0Xm`D!LqF-f5uD^|t93RZzI5v?QRhvEC%?}Du-HbXtBw_u$;?;N<{ zk`KnZfQS{VVqpcVV3>#&^7xU*n6-637qEIzFTpaksyuGK_VP zR2GE`MGM6%E3y)-bV3E#+-3tUkySR&s+d^8DlqHTJ={Om-Mt0~xyhtF3KzzI3tj30un$I<#a!t=V&3d8XdMUIyc zEEJ`_?mnJ=fS)N{u1KNFw|M4KsDbsKRW%5fw!sQko5F-^C82POZlKnjgFWwC-~aY0v+3D+8SLtsYc0OSHXL|Kfgcf9X*8>6xyRpF literal 27873 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 \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/computer-to-phone.svg b/app/assets/images/idv/computer-to-phone.svg new file mode 100644 index 00000000000..27fc4e078ba --- /dev/null +++ b/app/assets/images/idv/computer-to-phone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/idv/continue-online.svg b/app/assets/images/idv/continue-online.svg new file mode 100644 index 00000000000..3115794f047 --- /dev/null +++ b/app/assets/images/idv/continue-online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/idv/in-person.svg b/app/assets/images/idv/in-person.svg deleted file mode 100644 index 991b04e93ae..00000000000 --- a/app/assets/images/idv/in-person.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/idv/interstitial_icons.svg b/app/assets/images/idv/interstitial_icons.svg index d3c03fe6115..307d7ba9b40 100644 --- a/app/assets/images/idv/interstitial_icons.svg +++ b/app/assets/images/idv/interstitial_icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/laptop-icon.svg b/app/assets/images/idv/laptop-icon.svg index ec77f8f121a..68cfbbe32fa 100644 --- a/app/assets/images/idv/laptop-icon.svg +++ b/app/assets/images/idv/laptop-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/mobile-phone-icon.svg b/app/assets/images/idv/mobile-phone-icon.svg index a84148c5b4a..da6f2d9eb02 100644 --- a/app/assets/images/idv/mobile-phone-icon.svg +++ b/app/assets/images/idv/mobile-phone-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/idv/phone-icon.svg b/app/assets/images/idv/phone-icon.svg deleted file mode 100644 index 7369806eb70..00000000000 --- a/app/assets/images/idv/phone-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/idv/post-office.svg b/app/assets/images/idv/post-office.svg new file mode 100644 index 00000000000..42116563b90 --- /dev/null +++ b/app/assets/images/idv/post-office.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/idv/remote.svg b/app/assets/images/idv/remote.svg deleted file mode 100644 index 9de0edb4620..00000000000 --- a/app/assets/images/idv/remote.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/images/idv/switch-back-to-computer.svg b/app/assets/images/idv/switch-back-to-computer.svg new file mode 100644 index 00000000000..411545cab14 --- /dev/null +++ b/app/assets/images/idv/switch-back-to-computer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/info-pin-map.svg b/app/assets/images/info-pin-map.svg index e8fa70716e3..e16c696d224 100644 --- a/app/assets/images/info-pin-map.svg +++ b/app/assets/images/info-pin-map.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/user-access.svg b/app/assets/images/user-access.svg index 0194234a206..6238d37cc97 100644 --- a/app/assets/images/user-access.svg +++ b/app/assets/images/user-access.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/user-signup.svg b/app/assets/images/user-signup.svg index c40bf10f2c8..4914f6ceb2e 100644 --- a/app/assets/images/user-signup.svg +++ b/app/assets/images/user-signup.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx b/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx index bcf11f094c0..dcb72975465 100644 --- a/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx +++ b/app/javascript/packages/document-capture/components/in-person-switch-back-step.tsx @@ -15,7 +15,7 @@ function InPersonSwitchBackStep({ onChange }: FormStepComponentProps) { <> {t('in_person_proofing.headings.switch_back')} {t('doc_auth.instructions.switch_back_image')} diff --git a/app/presenters/idv/how_to_verify_presenter.rb b/app/presenters/idv/how_to_verify_presenter.rb index 95ad910b26f..4f0918ed0d6 100644 --- a/app/presenters/idv/how_to_verify_presenter.rb +++ b/app/presenters/idv/how_to_verify_presenter.rb @@ -63,7 +63,7 @@ def online_submit end def post_office_asset_url - 'idv/in-person.svg' + 'idv/post-office.svg' end def post_office_asset_alt_text diff --git a/app/views/idv/cancellations/destroy.html.erb b/app/views/idv/cancellations/destroy.html.erb index 47f56b2a197..dd11f7dfa78 100644 --- a/app/views/idv/cancellations/destroy.html.erb +++ b/app/views/idv/cancellations/destroy.html.erb @@ -2,7 +2,6 @@ <%= render StatusPageComponent.new(status: :error) do |c| %> <% c.with_header { t('idv.cancel.headings.confirmation.hybrid') } %> -

<%= t('doc_auth.instructions.switch_back') %>

<%= image_tag(asset_url('idv/switch.png'), width: 193, height: 109, alt: t('doc_auth.instructions.switch_back_image')) %> <% end %> diff --git a/app/views/idv/how_to_verify/show.html.erb b/app/views/idv/how_to_verify/show.html.erb index d46ddadb23d..110f73322f7 100644 --- a/app/views/idv/how_to_verify/show.html.erb +++ b/app/views/idv/how_to_verify/show.html.erb @@ -21,7 +21,7 @@ <% end %> <% end %> -
+
<%= image_tag( asset_url(@presenter.online_asset_url), @@ -72,13 +72,13 @@
-
+
<%= image_tag( asset_url(@presenter.post_office_asset_url), width: 88, height: 88, - class: 'margin-right-1 margin-top-3', + class: 'margin-top-3', alt: @presenter.post_office_asset_alt_text, ) %>
diff --git a/app/views/idv/hybrid_handoff/show.html.erb b/app/views/idv/hybrid_handoff/show.html.erb index f3bd8e9eb84..a3490ac36c7 100644 --- a/app/views/idv/hybrid_handoff/show.html.erb +++ b/app/views/idv/hybrid_handoff/show.html.erb @@ -16,7 +16,7 @@
<%= image_tag( - asset_url('idv/phone-icon.svg'), + asset_url('idv/mobile-phone-icon.svg'), alt: t('image_description.camera_mobile_phone'), width: 88, height: 88, @@ -61,18 +61,18 @@
<% if @post_office_enabled %>
-
+
<%= image_tag( asset_url(@presenter.post_office_asset_url), width: 88, height: 88, - class: 'margin-right-1 margin-top-3', + class: 'margin-top-3', alt: @presenter.post_office_asset_alt_text, ) %>
- +
<%= simple_form_for( @idv_how_to_verify_form, diff --git a/app/views/idv/hybrid_mobile/capture_complete/show.html.erb b/app/views/idv/hybrid_mobile/capture_complete/show.html.erb index fb69577ef1b..f2584efd3c3 100644 --- a/app/views/idv/hybrid_mobile/capture_complete/show.html.erb +++ b/app/views/idv/hybrid_mobile/capture_complete/show.html.erb @@ -7,8 +7,10 @@ ) %> <% end %> +
+ <%= image_tag(asset_url('idv/switch-back-to-computer.svg'), width: 207, height: 88, alt: t('doc_auth.instructions.switch_back_image')) %> +
+ <% self.title = t('titles.doc_auth.switch_back') %> <%= render PageHeadingComponent.new.with_content(t('doc_auth.instructions.switch_back')) %> - -<%= image_tag(asset_url('idv/switch.png'), width: 193, height: 109, alt: t('doc_auth.instructions.switch_back_image')) %> diff --git a/app/views/idv/link_sent/show.html.erb b/app/views/idv/link_sent/show.html.erb index 4bef8552cde..4b4109ae486 100644 --- a/app/views/idv/link_sent/show.html.erb +++ b/app/views/idv/link_sent/show.html.erb @@ -17,18 +17,16 @@ <%= t('doc_auth.info.link_sent_complete_no_polling') %> <% end %> <% end %> +
+ <%= image_tag asset_url('idv/computer-to-phone.svg'), width: 207, height: 88, alt: t('image_description.camera_mobile_phone') %> +
<%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.text_message')) %>
-
- <%= image_tag asset_url('idv/phone-icon.svg'), width: 88, height: 88, alt: t('image_description.camera_mobile_phone') %> -
-
-

- <%= t('doc_auth.info.you_entered') %> - <%= local_assigns[:phone] %> -

-

<%= t('doc_auth.info.link_sent') %>

-
+

+ <%= t('doc_auth.info.you_entered') %> + <%= local_assigns[:phone] %> +

+

<%= t('doc_auth.info.link_sent') %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index 04f15dabd89..618ff1b8f94 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -711,7 +711,7 @@ doc_auth.instructions.bullet4: Re-enter your %{app_name} password doc_auth.instructions.consent: By checking this box, you are letting %{app_name} ask for, use, keep, and share your personal information. We will use it to verify your identity. doc_auth.instructions.getting_started: 'You’ll need to:' doc_auth.instructions.learn_more: Learn more about our privacy and security measures -doc_auth.instructions.switch_back: Switch back to your computer to finish verifying your identity. +doc_auth.instructions.switch_back: Switch back to your computer to finish verifying your identity doc_auth.instructions.switch_back_image: Arrow pointing from phone to computer doc_auth.instructions.test_ssn: In the test environment only SSNs that begin with “900-” or “666-” are considered valid. Do not enter real PII in this field. doc_auth.instructions.text1: Other forms of ID are not accepted. We’ll check that you are the person on your ID. diff --git a/config/locales/es.yml b/config/locales/es.yml index 33f206a005c..2607d1b02d2 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -722,7 +722,7 @@ doc_auth.instructions.bullet4: Volver a ingresar su contraseña de %{app_name} doc_auth.instructions.consent: Al marcar esta casilla, usted permite que %{app_name} solicite, utilice, conserve y comparta su información personal. La utilizaremos para verificar su identidad. doc_auth.instructions.getting_started: 'Necesitará:' doc_auth.instructions.learn_more: Obtenga más información sobre nuestras medidas de privacidad y seguridad -doc_auth.instructions.switch_back: Vuelva a su computadora para finalizar la verificación de su identidad. +doc_auth.instructions.switch_back: Vuelva a su computadora para finalizar la verificación de su identidad doc_auth.instructions.switch_back_image: Flecha que apunta del teléfono a la computadora doc_auth.instructions.test_ssn: En el entorno de prueba, solo se consideran válidos los números de Seguro Social que comienzan con “900-” o “666-”. No ingrese IIP real en este campo. doc_auth.instructions.text1: No se aceptan otras formas de identificación. Revisaremos que usted sea la persona que figura en su identificación. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index db0dbfa54a2..ac66332f363 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -711,7 +711,7 @@ doc_auth.instructions.bullet4: Saisir à nouveau votre mot de passe %{app_name} doc_auth.instructions.consent: En cochant cette case, vous autorisez %{app_name} à demander, utiliser, conserver et partager vos renseignements personnels. Nous les utiliserons pour confirmer votre identité. doc_auth.instructions.getting_started: 'Vous devrez :' doc_auth.instructions.learn_more: En savoir plus sur nos mesures de confidentialité et de sécurité -doc_auth.instructions.switch_back: Revenir à votre ordinateur pour continuer à confirmer votre identité. +doc_auth.instructions.switch_back: Revenir à votre ordinateur pour continuer à confirmer votre identité doc_auth.instructions.switch_back_image: Flèche pointant du téléphone vers l’ordinateur doc_auth.instructions.test_ssn: Dans l’environnement de test, seuls les numéros de sécurité sociale commençant par “900-” ou “666-” sont considérés comme valides. Ne saisissez pas de vraies IPI dans ce champ. doc_auth.instructions.text1: Les autres pièces d’identité ne sont pas acceptées. Nous vérifierons que vous êtes la personne figurant sur la pièce d’identité. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 911ea031348..8b5b1a59ea4 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -722,7 +722,7 @@ doc_auth.instructions.bullet4: 重新输入你 %{app_name} 密码 doc_auth.instructions.consent: 在此框打勾,意味着你允许 %{app_name} 索要、使用、保留并分享你的个人信息。我们会使用这些信息来验证你的身份。 doc_auth.instructions.getting_started: '你需要:' doc_auth.instructions.learn_more: 了解有关我们隐私和安全措施的更多信息。 -doc_auth.instructions.switch_back: 然后再返回你的电脑完成验证身份。 +doc_auth.instructions.switch_back: 然后再返回你的电脑完成验证身份 doc_auth.instructions.switch_back_image: 从手机指向电脑的箭头 doc_auth.instructions.test_ssn: 在测试环境中只有以 900- 或者 666- 打头的社保号才被视为是对的。请勿在该字段中输入你真实的显示个人身份的信息。 doc_auth.instructions.text1: 其他形式的身份证件不被接受。我们要查看你是身份证件上的人。