diff --git a/.circleci/config.yml b/.circleci/config.yml index cfa5be2cef3..9a31b401136 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,13 +105,6 @@ commands: fail_only: true failure_message: ":smokeybear::red_circle: Smoke tests failed in environment: $MONITOR_ENV" include_project_field: false - store-smoketest-results: - steps: - - store_test_results: - path: tmp/capybara - - store_artifacts: - path: tmp/capybara - destination: capybara jobs: build: @@ -221,7 +214,6 @@ jobs: command: | bin/smoke_test --remote --no-source-env - notify-slack-smoke-test-status - - store-smoketest-results smoketest-int: working_directory: ~/identity-idp executor: ruby_browsers @@ -237,7 +229,6 @@ jobs: command: | bin/smoke_test --remote --no-source-env - notify-slack-smoke-test-status - - store-smoketest-results smoketest-staging: working_directory: ~/identity-idp executor: ruby_browsers @@ -253,7 +244,6 @@ jobs: command: | bin/smoke_test --remote --no-source-env - notify-slack-smoke-test-status - - store-smoketest-results smoketest-prod: working_directory: ~/identity-idp executor: ruby_browsers @@ -267,7 +257,7 @@ jobs: command: | bin/smoke_test --remote --no-source-env - notify-slack-smoke-test-status - - store-smoketest-results + workflows: version: 2 release: diff --git a/.eslintrc b/.eslintrc index 3e4269b137b..ed988201927 100644 --- a/.eslintrc +++ b/.eslintrc @@ -25,7 +25,6 @@ "indent": "off", "max-classes-per-file": "off", "newline-per-chained-call": "off", - "no-console": "error", "no-empty": ["error", { "allowEmptyCatch": true }], "no-param-reassign": ["off", "never"], "no-confusing-arrow": "off", @@ -37,12 +36,12 @@ "message": "Use CustomEvent constructor with polyfill for Internet Explorer" }, { - "selector": "AssignmentExpression[left.property.name='href'][right.type=/(Template)?Literal/]", - "message": "Do not assign window.location.href to a string or string template to avoid losing i18n parameters" + "selector": "ArrayExpression > SpreadElement", + "message": "Don't use array spread, issue with IE 11 (use Array.from instead)" }, { - "selector": "CallExpression[callee.object.name=/^(it|describe|context)$/][callee.property.name='only'] > MemberExpression", - "message": "Test exclusivity should not be committed" + "selector": "AssignmentExpression[left.property.name='href'][right.type=/(Template)?Literal/]", + "message": "Do not assign window.location.href to a string or string template to avoid losing i18n parameters" } ], "no-unused-expressions": "off", @@ -76,7 +75,8 @@ { "files": "spec/javascripts/**/*", "rules": { - "react/jsx-props-no-spreading": "off" + "react/jsx-props-no-spreading": "off", + "no-restricted-syntax": "off" } } ] diff --git a/Gemfile b/Gemfile index 269666ddbd1..30a5010e00d 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'faraday' gem 'foundation_emails' gem 'hiredis' gem 'http_accept_language' -gem 'identity-doc-auth', github: '18F/identity-doc-auth', tag: 'v0.4.0' +gem 'identity-doc-auth', github: '18F/identity-doc-auth', tag: 'v0.3.3' gem 'identity-hostdata', github: '18F/identity-hostdata', tag: 'v0.4.3' require File.join(__dir__, 'lib', 'lambda_jobs', 'git_ref.rb') gem 'identity-idp-functions', github: '18F/identity-idp-functions', ref: LambdaJobs::GIT_REF @@ -109,7 +109,6 @@ group :test do gem 'rack_session_access', '>= 0.2.0' gem 'rack-test', '>= 1.1.0' gem 'rails-controller-testing', '>= 1.0.4' - gem 'rspec-retry' gem 'shoulda-matchers', '~> 4.0', require: false gem 'timecop' gem 'webdrivers', '~> 4.0' @@ -118,6 +117,6 @@ group :test do end group :production do - gem 'aamva', github: '18F/identity-aamva-api-client-gem', tag: 'v3.6.0' + gem 'aamva', github: '18F/identity-aamva-api-client-gem', tag: 'v3.4.1' gem 'lexisnexis', github: '18F/identity-lexisnexis-api-client-gem', tag: 'v2.5.1.pre' end diff --git a/Gemfile.lock b/Gemfile.lock index 8aad49c7fa5..be60bb9d55a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,22 +1,21 @@ GIT remote: https://github.com/18F/identity-aamva-api-client-gem.git - revision: dbf3d2e102603530a29cb43308b9aa639efaea1f - tag: v3.6.0 + revision: 149b5b480f0319ec39410e497bb4bbffd1652014 + tag: v3.4.1 specs: - aamva (3.6.0) + aamva (3.4.1) dotenv faraday hashie - proofer (>= 2.7.1) retries xmldsig GIT remote: https://github.com/18F/identity-doc-auth.git - revision: 164a507fd17ecffe4ef2289120d89156358b3a80 - tag: v0.4.0 + revision: 4e1e09d7e5eb673dfbc1e301feacc74859d25d29 + tag: v0.3.3 specs: - identity-doc-auth (0.4.0) + identity-doc-auth (0.3.3) activesupport faraday @@ -30,11 +29,10 @@ GIT GIT remote: https://github.com/18F/identity-idp-functions.git - revision: eb8aa1657173af64fd9fcad2ab4df2a5741eb51d - ref: eb8aa1657173af64fd9fcad2ab4df2a5741eb51d + revision: 8c16776e19b211d15bda7246d99ff95155d60c11 + ref: 8c16776e19b211d15bda7246d99ff95155d60c11 specs: - identity-idp-functions (0.11.0) - aamva (>= 3.5.0) + identity-idp-functions (0.10.0) aws-sdk-s3 (>= 1.73) aws-sdk-ssm (>= 1.55) retries (>= 0.0.5) @@ -573,8 +571,6 @@ GEM rspec-expectations (~> 3.9) rspec-mocks (~> 3.9) rspec-support (~> 3.9) - rspec-retry (0.6.2) - rspec-core (> 3.3) rspec-support (3.10.0) rubocop (1.4.2) parallel (~> 1.10) @@ -801,7 +797,6 @@ DEPENDENCIES rotp (~> 6.1) rqrcode rspec-rails (~> 4.0) - rspec-retry rubocop (~> 1.4.0) rubocop-rails (>= 2.5.2) ruby-progressbar diff --git a/README.md b/README.md index 603f18672c6..53bcf042807 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,6 @@ We recommend using [Homebrew](https://brew.sh/), [rbenv](https://github.com/rben MONITOR_ENV=INT ./bin/smoke_test --remote ``` - For remote smoke tests, we save a screenshot of failed test scenarios to help debugging in `tmp/capybara`, and they are exported to CircleCI as build artifacts as well. - #### Speeding up local development and testing To automatically run the test that corresponds to the file you are editing, diff --git a/app/assets/images/wait.gif b/app/assets/images/wait.gif new file mode 100644 index 00000000000..209efd816c0 Binary files /dev/null and b/app/assets/images/wait.gif differ diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index d25ed58d006..866b616e704 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -2,6 +2,39 @@ class AnalyticsController < ApplicationController skip_before_action :verify_authenticity_token def create + results.each do |event, result| + next if result.nil? + + analytics.track_event(event, result.to_h) + end head :ok end + + private + + def results + { + Analytics::FRONTEND_BROWSER_CAPABILITIES => platform_authenticator_result, + } + end + + def platform_authenticator_result + return unless current_user + return if platform_authenticator_results_saved? || platform_authenticator_available?.nil? + + session[:platform_authenticator_analytics_saved] = true + extra = { platform_authenticator: platform_authenticator_available? } + FormResponse.new(success: true, errors: {}, extra: extra) + end + + def platform_authenticator_available? + @platform_authenticator_available ||= begin + available = params.dig(:platform_authenticator, :available) + available == 'true' if %w[true false].include?(available) + end + end + + def platform_authenticator_results_saved? + session[:platform_authenticator_analytics_saved] == true + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9bd9a780fd5..0904edeefcb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -274,24 +274,15 @@ def set_locale I18n.locale = LocaleChooser.new(params[:locale], request).locale end - def sp_session_ial - sp_session[:ial] - end - - def sp_session_ial_1_or_2 - return 1 if sp_session[:ial].blank? - sp_session[:ial] > 1 ? 2 : 1 - end - def increment_monthly_auth_count return unless current_user&.id issuer = sp_session[:issuer] - return if issuer.blank? || !first_auth_of_session?(issuer, sp_session_ial) - MonthlySpAuthCount.increment(current_user.id, issuer, sp_session_ial_1_or_2) + return if issuer.blank? || !first_auth_of_session?(issuer) + MonthlySpAuthCount.increment(current_user.id, issuer, sp_session[:ial2] ? 2 : 1) end - def first_auth_of_session?(issuer, ial) - authenticated_to_sp_token = "auth_counted_ial#{ial}_#{issuer}" + def first_auth_of_session?(issuer) + authenticated_to_sp_token = "auth-counted-#{issuer}" authenticated_to_sp = user_session[authenticated_to_sp_token] return if authenticated_to_sp user_session[authenticated_to_sp_token] = true @@ -353,7 +344,7 @@ def analytics_exception_info(exception) end def add_sp_cost(token) - Db::SpCost::AddSpCost.call(sp_session[:issuer].to_s, sp_session_ial_1_or_2, token) + Db::SpCost::AddSpCost.call(sp_session[:issuer].to_s, sp_session[:ial2] ? 2 : 1, token) end def mobile? diff --git a/app/controllers/concerns/fully_authenticatable.rb b/app/controllers/concerns/fully_authenticatable.rb index 3b746f8b833..bd0ce49dbcb 100644 --- a/app/controllers/concerns/fully_authenticatable.rb +++ b/app/controllers/concerns/fully_authenticatable.rb @@ -1,7 +1,6 @@ module FullyAuthenticatable def delete_branded_experience ServiceProviderRequestProxy.delete(request_id) - session.delete(:sp) end def request_id diff --git a/app/controllers/concerns/idv/document_capture_concern.rb b/app/controllers/concerns/idv/document_capture_concern.rb deleted file mode 100644 index 1902b85fc4d..00000000000 --- a/app/controllers/concerns/idv/document_capture_concern.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Idv - module DocumentCaptureConcern - def override_document_capture_step_csp - return if params[:step] != 'document_capture' - - SecureHeaders.append_content_security_policy_directives( - request, - # required to run wasm until wasm-eval is available - script_src: ['\'unsafe-eval\''], - # required for retrieving image dimensions from uploaded images - img_src: ['blob:'], - ) - end - end -end diff --git a/app/controllers/concerns/verify_sp_attributes_concern.rb b/app/controllers/concerns/verify_sp_attributes_concern.rb index 47b29173740..3a29feddc61 100644 --- a/app/controllers/concerns/verify_sp_attributes_concern.rb +++ b/app/controllers/concerns/verify_sp_attributes_concern.rb @@ -74,4 +74,8 @@ def requested_attributes_verified? sp_session[:requested_attributes] - @sp_session_identity.verified_attributes.to_a ).empty? end + + def sp_session_ial + sp_session[:ial2] ? 2 : 1 + end end diff --git a/app/controllers/idv/cac_controller.rb b/app/controllers/idv/cac_controller.rb index fc19cd3b819..ae005d2edde 100644 --- a/app/controllers/idv/cac_controller.rb +++ b/app/controllers/idv/cac_controller.rb @@ -2,6 +2,7 @@ module Idv class CacController < ApplicationController include PivCacConcern + before_action :render_404_if_disabled before_action :confirm_two_factor_authenticated before_action :cac_callback @@ -25,6 +26,10 @@ def redirect_to_piv_cac_service private + def render_404_if_disabled + render_not_found unless AppConfig.env.cac_proofing_enabled == 'true' + end + def cac_callback return unless request.path == idv_cac_step_path(:present_cac) && params[:token] diff --git a/app/controllers/idv/cancellations_controller.rb b/app/controllers/idv/cancellations_controller.rb index 95aa6690bd2..9c326347cfc 100644 --- a/app/controllers/idv/cancellations_controller.rb +++ b/app/controllers/idv/cancellations_controller.rb @@ -1,7 +1,6 @@ module Idv class CancellationsController < ApplicationController include IdvSession - include GoBackHelper before_action :confirm_two_factor_authenticated before_action :confirm_idv_needed @@ -9,7 +8,7 @@ class CancellationsController < ApplicationController def new properties = ParseControllerFromReferer.new(request.referer).call analytics.track_event(Analytics::IDV_CANCELLATION, properties) - @go_back_path = go_back_path || idv_path + @go_back_path = go_back_path end def destroy @@ -25,5 +24,22 @@ def reset_doc_auth user_session.delete('idv/doc_auth') user_session['idv'] = { params: {}, step_attempts: { phone: 0 } } end + + def go_back_path + referer_path || idv_path + end + + def referer_path + referer_string = request.env['HTTP_REFERER'] + return if referer_string.blank? + referer_uri = URI.parse(referer_string) + return if referer_uri.scheme == 'javascript' + return unless referer_uri.host == AppConfig.env.domain_name + extract_path_and_query_from_uri(referer_uri) + end + + def extract_path_and_query_from_uri(uri) + [uri.path, uri.query].compact.join('?') + end end end diff --git a/app/controllers/idv/capture_doc_controller.rb b/app/controllers/idv/capture_doc_controller.rb index 6d7ab0125a7..cff36b91832 100644 --- a/app/controllers/idv/capture_doc_controller.rb +++ b/app/controllers/idv/capture_doc_controller.rb @@ -1,11 +1,9 @@ module Idv class CaptureDocController < ApplicationController before_action :ensure_user_id_in_session + before_action :add_unsafe_eval_to_capture_steps include Flow::FlowStateMachine - include Idv::DocumentCaptureConcern - - before_action :override_document_capture_step_csp FSM_SETTINGS = { step_url: :idv_capture_doc_step_url, @@ -27,6 +25,16 @@ def ensure_user_id_in_session process_result(result) end + def add_unsafe_eval_to_capture_steps + return unless current_step == 'document_capture' + + # required to run wasm until wasm-eval is available + SecureHeaders.append_content_security_policy_directives( + request, + script_src: ['\'unsafe-eval\''], + ) + end + def process_result(result) if result.success? reset_session @@ -52,9 +60,5 @@ def token def document_capture_session_uuid params['document-capture-session'] end - - def analytics_user - user_id_from_token ? User.find(user_id_from_token) : super - end end end diff --git a/app/controllers/idv/capture_doc_status_controller.rb b/app/controllers/idv/capture_doc_status_controller.rb index 42cac305011..b73f4a71e97 100644 --- a/app/controllers/idv/capture_doc_status_controller.rb +++ b/app/controllers/idv/capture_doc_status_controller.rb @@ -16,9 +16,7 @@ def document_capture_session_poll_render_result document_capture_session = DocumentCaptureSession.find_by(uuid: session_uuid) return { plain: 'Unauthorized', status: :unauthorized } unless document_capture_session - result = document_capture_session.load_result || - document_capture_session.load_doc_auth_async_result - + result = document_capture_session.load_result return { plain: 'Pending', status: :accepted } if result.blank? return { plain: 'Unauthorized', status: :unauthorized } unless result.success? { plain: 'Complete', status: :ok } diff --git a/app/controllers/idv/doc_auth_controller.rb b/app/controllers/idv/doc_auth_controller.rb index c4a71aad335..1151587e570 100644 --- a/app/controllers/idv/doc_auth_controller.rb +++ b/app/controllers/idv/doc_auth_controller.rb @@ -4,12 +4,11 @@ class DocAuthController < ApplicationController before_action :redirect_if_mail_bounced before_action :redirect_if_pending_profile before_action :extend_timeout_using_meta_refresh_for_select_paths + before_action :add_unsafe_eval_to_capture_steps include IdvSession # remove if we retire the non docauth LOA3 flow include Flow::FlowStateMachine - include Idv::DocumentCaptureConcern - before_action :override_document_capture_step_csp before_action :update_if_skipping_upload FSM_SETTINGS = { @@ -50,5 +49,15 @@ def do_meta_refresh(meta_refresh_count) def flow_session user_session['idv/doc_auth'] end + + def add_unsafe_eval_to_capture_steps + return unless params[:step] == 'document_capture' + + # required to run wasm until wasm-eval is available + SecureHeaders.append_content_security_policy_directives( + request, + script_src: ['\'unsafe-eval\''], + ) + end end end diff --git a/app/controllers/idv/image_uploads_controller.rb b/app/controllers/idv/image_uploads_controller.rb index e452ee771f0..a6e06a76ae6 100644 --- a/app/controllers/idv/image_uploads_controller.rb +++ b/app/controllers/idv/image_uploads_controller.rb @@ -5,16 +5,10 @@ class ImageUploadsController < ApplicationController respond_to :json def create - image_form_response = image_form.submit - analytics.track_event( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM, - image_form_response.to_h, - ) - + form_response = image_form.submit client_response = nil - doc_pii_form_response = nil - if image_form_response.success? + if form_response.success? client_response = doc_auth_client.post_images( front_image: image_form.front.read, back_image: image_form.back.read, @@ -26,24 +20,16 @@ def create if client_response.success? doc_pii_form_response = Idv::DocPiiForm.new(client_response.pii_from_doc).submit - analytics.track_event( - Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - doc_pii_form_response.to_h.merge(user_id: user_uuid), - ) + form_response = form_response.merge(doc_pii_form_response) store_pii(client_response) if client_response.success? && doc_pii_form_response.success? - - # merge in the image_form_response to pick up the remaining_attempts - doc_pii_form_response = image_form_response.merge(doc_pii_form_response) end end + analytics.track_event(Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM, form_response.to_h) + presenter = ImageUploadResponsePresenter.new( form: image_form, - form_response: presenter_response( - image_form_response: image_form_response, - client_response: client_response, - doc_pii_form_response: doc_pii_form_response, - ), + form_response: presenter_response(form_response, client_response), url_options: url_options, ) @@ -72,7 +58,7 @@ def update_analytics(client_response) update_funnel(client_response) analytics.track_event( Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, - client_response.to_h.merge(user_id: user_uuid), + client_response.to_h.merge(user_id: image_form.document_capture_session.user.uuid), ) end @@ -94,20 +80,9 @@ def add_costs(client_response) call(client_response) end - def presenter_response(image_form_response:, client_response:, doc_pii_form_response:) - # image form wasn't valid - return image_form_response unless image_form_response.success? - - # doc_pii_form exists, but wasn't valid - if doc_pii_form_response.present? && !doc_pii_form_response.success? - return doc_pii_form_response - end - - client_response - end - - def user_uuid - image_form.document_capture_session.user.uuid + def presenter_response(form_response, client_response) + return client_response if form_response.success? && client_response.present? + form_response end end end diff --git a/app/controllers/idv_controller.rb b/app/controllers/idv_controller.rb index 0018113c4bd..421e3f325f6 100644 --- a/app/controllers/idv_controller.rb +++ b/app/controllers/idv_controller.rb @@ -61,7 +61,8 @@ def active_profile? end def proof_with_cac? - Db::EmailAddress::HasGovOrMil.call(current_user) || - current_user.piv_cac_configurations.any? + AppConfig.env.cac_proofing_enabled == 'true' && + (Db::EmailAddress::HasGovOrMil.call(current_user) || + current_user.piv_cac_configurations.any?) end end diff --git a/app/controllers/openid_connect/authorization_controller.rb b/app/controllers/openid_connect/authorization_controller.rb index 946501f888f..34cad9a6011 100644 --- a/app/controllers/openid_connect/authorization_controller.rb +++ b/app/controllers/openid_connect/authorization_controller.rb @@ -32,7 +32,7 @@ def check_sp_active end def check_sp_handoff_bounced - return unless SpHandoffBounce.is_bounced?(session) + return unless SpHandoffBounce::IsBounced.call(sp_session) analytics.track_event(Analytics::SP_HANDOFF_BOUNCED_DETECTED) redirect_to bounced_url true @@ -56,7 +56,7 @@ def link_identity_to_service_provider def handle_successful_handoff track_events - SpHandoffBounce.add_handoff_time_to_session(session) + SpHandoffBounce::AddHandoffTimeToSession.call(sp_session) redirect_to @authorize_form.success_redirect_uri delete_branded_experience end @@ -130,13 +130,15 @@ def store_request end def pii_requested_but_locked? - sp_session && sp_session_ial > 1 && + FeatureManagement.allow_piv_cac_login? && + sp_session && sp_session_ial > 1 && UserDecorator.new(current_user).identity_verified? && user_session[:decrypted_pii].blank? end def track_events - analytics.track_event(Analytics::SP_REDIRECT_INITIATED, ial: sp_session_ial) + ial = sp_session[:ial2] ? 2 : 1 + analytics.track_event(Analytics::SP_REDIRECT_INITIATED, ial: ial) Db::SpReturnLog::AddReturn.call(request_id, current_user.id) increment_monthly_auth_count add_sp_cost(:authentication) diff --git a/app/controllers/openid_connect/logout_controller.rb b/app/controllers/openid_connect/logout_controller.rb index f4cdc83e21e..fadd49041bc 100644 --- a/app/controllers/openid_connect/logout_controller.rb +++ b/app/controllers/openid_connect/logout_controller.rb @@ -1,5 +1,8 @@ module OpenidConnect class LogoutController < ApplicationController + include SecureHeadersConcern + + before_action :apply_secure_headers_override, only: [:index] def index @logout_form = OpenidConnectLogoutForm.new(params) diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index 9c519bcbcf5..c6818c74526 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -102,7 +102,8 @@ def render_template_for(message, action_url, type) end def track_events - analytics.track_event(Analytics::SP_REDIRECT_INITIATED, ial: sp_session_ial) + ial = sp_session[:ial2] ? 2 : 1 + analytics.track_event(Analytics::SP_REDIRECT_INITIATED, ial: ial) Db::SpReturnLog::AddReturn.call(request_id, current_user.id) increment_monthly_auth_count add_sp_cost(:authentication) diff --git a/app/controllers/users/piv_cac_login_controller.rb b/app/controllers/users/piv_cac_login_controller.rb index 8a6ea0d2c8b..ee04cabb181 100644 --- a/app/controllers/users/piv_cac_login_controller.rb +++ b/app/controllers/users/piv_cac_login_controller.rb @@ -85,7 +85,7 @@ def request_is_ial2? end def request_ial - sp_session ? sp_session_ial_1_or_2 : 1 + sp_session ? sp_session_ial : 1 end def process_invalid_submission diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index a52892c9bc2..29d42b344b6 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -21,7 +21,7 @@ def new ) @request_id = request_id_if_valid - @ial = sp_session ? sp_session_ial_1_or_2 : 1 + @ial = sp_session ? sp_session_ial : 1 session[:ial2_with_no_sp_campaign] = campaign if sp_session.blank? && params[:ial] == '2' super end @@ -173,6 +173,10 @@ def request_id params.fetch(:request_id, '') end + def sp_session_ial + sp_session[:ial2] ? 2 : 1 + end + def redirect_to_2fa_or_pending_reset if pending_account_reset_request.present? redirect_to account_reset_pending_url diff --git a/app/helpers/go_back_helper.rb b/app/helpers/go_back_helper.rb deleted file mode 100644 index bc8b149643f..00000000000 --- a/app/helpers/go_back_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -module GoBackHelper - def go_back_path - referer_string = request.referer - return if referer_string.blank? - referer_uri = URI.parse(referer_string) - return if referer_uri.scheme == 'javascript' - return unless referer_uri.host == app_host - extract_path_and_query_from_uri(referer_uri) - end - - private - - def extract_path_and_query_from_uri(uri) - [uri.path, uri.query].compact.join('?') - end - - def app_host - AppConfig.env.domain_name.split(':')[0] - end -end - -ActionView::Base.send :include, GoBackHelper diff --git a/app/helpers/session_timeout_warning_helper.rb b/app/helpers/session_timeout_warning_helper.rb index 395a20405e1..1dae0a8e7d0 100644 --- a/app/helpers/session_timeout_warning_helper.rb +++ b/app/helpers/session_timeout_warning_helper.rb @@ -1,13 +1,13 @@ module SessionTimeoutWarningHelper - def session_timeout_frequency + def frequency (AppConfig.env.session_check_frequency || 150).to_i end - def session_timeout_start + def start (AppConfig.env.session_check_delay || 30).to_i end - def session_timeout_warning + def warning (AppConfig.env.session_timeout_warning_seconds || 30).to_i end @@ -18,15 +18,44 @@ def timeout_refresh_path )&.html_safe # rubocop:disable Rails/OutputSafety end + def auto_session_timeout_js + nonced_javascript_tag do + render partial: 'session_timeout/ping', + formats: [:js], + locals: { + timeout_url: timeout_url, + warning: warning, + start: start, + frequency: frequency, + modal: modal, + } + end + end + + # rubocop:disable Rails/HelperInstanceVariable + def auto_session_expired_js + return if @skip_session_expiration + + session_timeout_in = Devise.timeout_in + nonced_javascript_tag do + render( + partial: 'session_timeout/expire_session', + formats: [:js], + locals: { session_timeout_in: session_timeout_in }, + ) + end + end + # rubocop:enable Rails/HelperInstanceVariable + def time_left_in_session distance_of_time_in_words( - session_timeout_warning, + warning, 0, two_words_connector: " #{I18n.t('datetime.dotiw.two_words_connector')} ", ) end - def session_modal + def modal if user_fully_authenticated? FullySignedInModalPresenter.new(time_left_in_session) else @@ -34,3 +63,5 @@ def session_modal end end end + +ActionView::Base.send :include, SessionTimeoutWarningHelper diff --git a/app/javascript/app/platform-authenticator.js b/app/javascript/app/platform-authenticator.js new file mode 100644 index 00000000000..5cf9657e364 --- /dev/null +++ b/app/javascript/app/platform-authenticator.js @@ -0,0 +1,20 @@ +function postPlatformAuthenticator(userIntent) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/analytics', true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(`platform_authenticator[available]=${userIntent}`); +} +function platformAuthenticator() { + if (document.querySelector('[data-platform-authenticator-enabled]')) { + if (!window.PublicKeyCredential) { + postPlatformAuthenticator(false); + return; + } + window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then(function ( + userIntent, + ) { + postPlatformAuthenticator(userIntent); + }); + } +} +document.addEventListener('DOMContentLoaded', platformAuthenticator); diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 1bf5badbf2e..698cbb9bf24 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -32,14 +32,14 @@ import './acuant-capture.scss'; */ /** - * @typedef {"acuant"|"upload"} ImageSource + * @typedef {"acuant"} ImageSource */ /** * @typedef ImageAnalyticsPayload * - * @prop {number?} width Image width, or null if unknown. - * @prop {number?} height Image height, or null if unknown. + * @prop {number} width + * @prop {number} height * @prop {string?} mimeType Mime type, or null if unknown. * @prop {ImageSource} source Method by which image was added. */ @@ -125,27 +125,6 @@ function getDocumentTypeLabel(documentType) { } } -/** - * @param {File} file Image file. - * - * @return {Promise<{width: number?, height: number?}>} - */ -function getImageDimensions(file) { - let objectURL; - return file.type.indexOf('image/') === 0 - ? new Promise((resolve) => { - objectURL = window.URL.createObjectURL(file); - const image = new window.Image(); - image.onload = () => resolve({ width: image.width, height: image.height }); - image.onerror = () => resolve({ width: null, height: null }); - image.src = objectURL; - }).then(({ width, height }) => { - window.URL.revokeObjectURL(objectURL); - return { width, height }; - }) - : Promise.resolve({ width: null, height: null }); -} - /** * Returns an element serving as an enhanced FileInput, supporting direct capture using Acuant SDK * in supported devices. @@ -166,7 +145,7 @@ function AcuantCapture( }, ref, ) { - const { isReady, isAcuantLoaded, isError, isCameraSupported } = useContext(AcuantContext); + const { isReady, isError, isCameraSupported } = useContext(AcuantContext); const { isMockClient } = useContext(UploadContext); const { addPageAction } = useContext(AnalyticsContext); const inputRef = useRef(/** @type {?HTMLInputElement} */ (null)); @@ -198,32 +177,6 @@ function AcuantCapture( onChange(nextValue); } - /** - * Handler for file input change events. - * - * @param {File?} nextValue Next value, if set. - */ - function onUpload(nextValue) { - if (nextValue) { - getImageDimensions(nextValue).then(({ width, height }) => { - /** @type {ImageAnalyticsPayload} */ - const analyticsPayload = { - width, - height, - mimeType: nextValue.type, - source: 'upload', - }; - - addPageAction({ - label: `IdV: ${analyticsPrefix} added`, - payload: analyticsPayload, - }); - }); - } - - onChangeAndResetError(nextValue); - } - /** * Responds to a click by starting capture if supported in the environment, or triggering the * default file picker prompt. The click event may originate from the file input itself, or @@ -235,8 +188,7 @@ function AcuantCapture( if (event.target === inputRef.current) { const shouldStartEnvironmentCapture = hasCapture && capture !== 'user' && !isForceUploading.current; - const shouldStartSelfieCapture = - isAcuantLoaded && capture === 'user' && !isForceUploading.current; + const shouldStartSelfieCapture = capture === 'user' && !isForceUploading.current; if (!allowUpload || shouldStartSelfieCapture || shouldStartEnvironmentCapture) { event.preventDefault(); @@ -357,7 +309,7 @@ function AcuantCapture( value={value} errorMessage={ownErrorMessage ?? errorMessage} onClick={startCaptureOrTriggerUpload} - onChange={onUpload} + onChange={onChangeAndResetError} onError={() => setOwnErrorMessage(null)} />
diff --git a/app/javascript/packages/document-capture/components/file-input.jsx b/app/javascript/packages/document-capture/components/file-input.jsx index b798f43db10..08c96cb5340 100644 --- a/app/javascript/packages/document-capture/components/file-input.jsx +++ b/app/javascript/packages/document-capture/components/file-input.jsx @@ -23,7 +23,7 @@ import usePrevious from '../hooks/use-previous'; * @prop {Blob|string|null|undefined} value Current value. * @prop {ReactNode=} errorMessage Error to show. * @prop {(event:ReactMouseEvent)=>void=} onClick Input click handler. - * @prop {(nextValue:File?)=>void=} onChange Input change handler. + * @prop {(nextValue:Blob?)=>void=} onChange Input change handler. * @prop {(message:ReactNode)=>void=} onError Callback to trigger if upload error occurs. */ diff --git a/app/javascript/packages/document-capture/context/acuant.jsx b/app/javascript/packages/document-capture/context/acuant.jsx index 94fc7a9dba7..c5932ed02d5 100644 --- a/app/javascript/packages/document-capture/context/acuant.jsx +++ b/app/javascript/packages/document-capture/context/acuant.jsx @@ -1,5 +1,4 @@ -import { createContext, useContext, useMemo, useEffect, useState } from 'react'; -import DeviceContext from './device'; +import { createContext, useMemo, useEffect, useState } from 'react'; /** @typedef {import('react').ReactNode} ReactNode */ @@ -49,7 +48,6 @@ import DeviceContext from './device'; const AcuantContext = createContext({ isReady: false, - isAcuantLoaded: false, isError: false, isCameraSupported: /** @type {boolean?} */ (null), credentials: /** @type {string?} */ (null), @@ -65,26 +63,18 @@ function AcuantContextProvider({ endpoint = null, children, }) { - const { isMobile } = useContext(DeviceContext); - // Only mobile devices should load the Acuant SDK. Consider immediately ready otherwise. - const [isReady, setIsReady] = useState(!isMobile); - const [isAcuantLoaded, setIsAcuantLoaded] = useState(false); + const [isReady, setIsReady] = useState(false); const [isError, setIsError] = useState(false); - // If the user is on a mobile device, it can't be known that the camera is supported until after - // Acuant SDK loads, so assign a value of `null` as representing this unknown state. Other device - // types should treat camera as unsupported, since it's not relevant for Acuant SDK usage. - const [isCameraSupported, setIsCameraSupported] = useState(isMobile ? null : false); - const value = useMemo( - () => ({ isReady, isAcuantLoaded, isError, isCameraSupported, endpoint, credentials }), - [isReady, isAcuantLoaded, isError, isCameraSupported, endpoint, credentials], - ); + const [isCameraSupported, setIsCameraSupported] = useState(/** @type {?boolean} */ (null)); + const value = useMemo(() => ({ isReady, isError, isCameraSupported, endpoint, credentials }), [ + isReady, + isError, + isCameraSupported, + endpoint, + credentials, + ]); useEffect(() => { - // If state is already ready (via consideration of device type), skip loading Acuant SDK. - if (isReady) { - return; - } - // Acuant SDK expects this global to be assigned at the time the script is // loaded, which is why the script element is manually appended to the DOM. const originalOnAcuantSdkLoaded = /** @type {AcuantGlobal} */ (window).onAcuantSdkLoaded; @@ -98,7 +88,6 @@ function AcuantContextProvider({ /** @type {AcuantGlobal} */ (window).AcuantCamera.isCameraSupported, ); setIsReady(true); - setIsAcuantLoaded(true); }, onFail: () => setIsError(true), }, diff --git a/app/javascript/packages/document-capture/context/analytics.jsx b/app/javascript/packages/document-capture/context/analytics.jsx index bdd3b446883..1f07f854f92 100644 --- a/app/javascript/packages/document-capture/context/analytics.jsx +++ b/app/javascript/packages/document-capture/context/analytics.jsx @@ -5,7 +5,7 @@ import { createContext } from 'react'; /** * @typedef PageAction * - * @property {string=} key Short, camel-cased, dot-namespaced key describing event. + * @property {string} key Short, camel-cased, dot-namespaced key describing event. * @property {string} label Long-form, human-readable label describing event action. * @property {Payload} payload Additional payload arguments to log with action. */ diff --git a/app/javascript/packages/document-capture/higher-order/with-background-encrypted-upload.jsx b/app/javascript/packages/document-capture/higher-order/with-background-encrypted-upload.jsx index ef8f4d2e6e3..a99869e1875 100644 --- a/app/javascript/packages/document-capture/higher-order/with-background-encrypted-upload.jsx +++ b/app/javascript/packages/document-capture/higher-order/with-background-encrypted-upload.jsx @@ -3,17 +3,17 @@ import UploadContext from '../context/upload'; import AnalyticsContext from '../context/analytics'; /** - * Returns a promise resolving to an ArrayBuffer representation of the given Blob object. + * Returns a promise resolving to an DataView representation of the given Blob object. * * @param {Blob} blob Blob object. * - * @return {Promise} + * @return {Promise} */ -export function blobToArrayBuffer(blob) { +export function blobToDataView(blob) { return new Promise((resolve, reject) => { const reader = new window.FileReader(); reader.onload = ({ target }) => { - resolve(/** @type {ArrayBuffer} */ (target?.result)); + resolve(new DataView(/** @type {ArrayBuffer} */ (target?.result))); }; reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(blob); @@ -31,15 +31,12 @@ export function blobToArrayBuffer(blob) { */ export async function encrypt(key, iv, value) { const data = - typeof value === 'string' ? new TextEncoder().encode(value) : await blobToArrayBuffer(value); + typeof value === 'string' ? new TextEncoder().encode(value) : await blobToDataView(value); return window.crypto.subtle.encrypt( /** @type {AesGcmParams} */ ({ name: 'AES-GCM', iv, - // Normally, it would not be expected to assign this value, since the property is optional and - // the default is 128. However, if not specified, Internet Explorer will throw an error. - tagLength: 128, }), key, data, diff --git a/app/javascript/packages/polyfill/index.js b/app/javascript/packages/polyfill/index.js index a7b97185da1..cb0e7d1241f 100644 --- a/app/javascript/packages/polyfill/index.js +++ b/app/javascript/packages/polyfill/index.js @@ -6,17 +6,21 @@ */ /** - * @typedef {"fetch"|"classlist"|"crypto"|"custom-event"} SupportedPolyfills + * @typedef {"fetch"|"element-closest"|"classlist"|"crypto"|"custom-event"} SupportedPolyfills */ /** - * @type {Record} + * @type {Record} */ const POLYFILLS = { fetch: { test: () => 'fetch' in window, load: () => import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch'), }, + 'element-closest': { + test: () => !!Element.prototype.closest, + load: () => import(/* webpackChunkName: "element-closest" */ 'element-closest'), + }, classlist: { test: () => 'classList' in Element.prototype, load: () => import(/* webpackChunkName: "classlist-polyfill" */ 'classlist-polyfill'), diff --git a/app/javascript/packages/polyfill/package.json b/app/javascript/packages/polyfill/package.json index 482a09301af..2058b332186 100644 --- a/app/javascript/packages/polyfill/package.json +++ b/app/javascript/packages/polyfill/package.json @@ -5,6 +5,7 @@ "dependencies": { "classlist-polyfill": "^1.2.0", "custom-event-polyfill": "^1.0.7", + "element-closest": "^3.0.2", "webcrypto-shim": "^0.1.6", "whatwg-fetch": "^3.4.0" } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c8e98e128a0..b3ecb14e07d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -6,5 +6,6 @@ require('../app/form-field-format'); require('../app/radio-btn'); require('../app/print-personal-key'); require('../app/i18n-dropdown'); +require('../app/platform-authenticator'); require('../app/accessible-forms'); require('../app/ssn-field'); diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index 97f23f90b2a..7057b1b03ec 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -98,10 +98,7 @@ const device = { /** @type {import('@18f/identity-document-capture/context/analytics').AddPageAction} */ function addPageAction(action) { - const { newrelic } = /** @type {DocumentCaptureGlobal} */ (window); - if (action.key && newrelic) { - newrelic.addPageAction(action.key, action.payload); - } + /** @type {DocumentCaptureGlobal} */ (window).newrelic?.addPageAction(action.key, action.payload); window.fetch(logEndpoint, { method: 'POST', @@ -140,36 +137,36 @@ loadPolyfills(['fetch', 'crypto']).then(async () => { window.fetch(keepAliveEndpoint, { method: 'POST', headers: { 'X-CSRF-Token': csrf } }); render( - - + - - - - - + + + + + - - - - - - - , + + + + + + + , appRoot, ); }); diff --git a/app/javascript/packs/ial2-consent-button.js b/app/javascript/packs/ial2-consent-button.js index b02bd76855c..d237407e2eb 100644 --- a/app/javascript/packs/ial2-consent-button.js +++ b/app/javascript/packs/ial2-consent-button.js @@ -1,9 +1,10 @@ function toggleButton() { - const continueButton = document.querySelector('button[type="submit"]'); + const continueButton = document.querySelector('input[value="Continue"]'); const checkbox = document.querySelector('input[name="ial2_consent_given"]'); function sync() { - continueButton.classList.toggle('btn-disabled', !checkbox.checked); + continueButton.disabled = !checkbox.checked; + continueButton.classList.toggle('btn-disabled', continueButton.disabled); } sync(); diff --git a/app/javascript/packs/session-expire-session.js b/app/javascript/packs/session-expire-session.js deleted file mode 100644 index e92090085cd..00000000000 --- a/app/javascript/packs/session-expire-session.js +++ /dev/null @@ -1,10 +0,0 @@ -const expireConfig = document.getElementById('js-expire-session'); - -if (expireConfig && expireConfig.dataset.sessionTimeoutIn) { - const sessionTimeoutIn = parseInt(expireConfig.dataset.sessionTimeoutIn, 10) * 1000; - const timeoutRefreshPath = expireConfig.dataset.timeoutRefreshPath || ''; - - setTimeout(() => { - document.location.href = timeoutRefreshPath; - }, sessionTimeoutIn); -} diff --git a/app/javascript/packs/session-timeout-ping.js b/app/javascript/packs/session-timeout-ping.js deleted file mode 100644 index fe705f4153c..00000000000 --- a/app/javascript/packs/session-timeout-ping.js +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @typedef NewRelicAgent - * - * @prop {(name:string,attributes:object)=>void} addPageAction Log page action to New Relic. - */ - -/** - * @typedef LoginGov - * - * @prop {(any)=>void} Modal - * @prop {(string)=>void} autoLogout - * @prop {(el:HTMLElement?,timeLeft:number,endTime:number,interval?:number)=>void} countdownTimer - */ - -/** - * @typedef NewRelicGlobals - * - * @prop {NewRelicAgent=} newrelic New Relic agent. - */ - -/** - * @typedef LoginGovGlobals - * - * @prop {LoginGov} LoginGov - */ - -/** - * @typedef {typeof window & NewRelicGlobals & LoginGovGlobals} LoginGovGlobal - */ - -const login = /** @type {LoginGovGlobal} */ (window).LoginGov; - -const warningEl = document.getElementById('session-timeout-cntnr'); - -const defaultTime = '60'; - -const frequency = parseInt(warningEl?.dataset.frequency || defaultTime, 10) * 1000; -const warning = parseInt(warningEl?.dataset.warning || defaultTime, 10) * 1000; -const start = parseInt(warningEl?.dataset.start || defaultTime, 10) * 1000; -const timeoutUrl = warningEl?.dataset.timeoutUrl; -const warningInfo = warningEl?.dataset.warningInfoHtml || ''; -warningEl?.insertAdjacentHTML('afterbegin', warningInfo); -const initialTime = new Date(); - -const modal = new login.Modal({ el: '#session-timeout-msg' }); -const keepaliveEl = document.getElementById('session-keepalive-btn'); -/** @type {HTMLMetaElement?} */ -const csrfEl = document.querySelector('meta[name="csrf-token"]'); - -let csrfToken = ''; -if (csrfEl) { - csrfToken = csrfEl.content; -} - -let countdownInterval; - -function notifyNewRelic(request, error, actionName) { - /** @type {LoginGovGlobal} */ (window).newrelic?.addPageAction('Session Ping Error', { - action_name: actionName, - request_status: request.status, - time_elapsed_ms: new Date().valueOf() - initialTime.valueOf(), - error: error.message, - }); -} - -function success(data) { - let timeRemaining = data.remaining * 1000; - const timeTimeout = new Date().getTime() + timeRemaining; - const showWarning = timeRemaining < warning; - - if (!data.live) { - login.autoLogout(timeoutUrl); - return; - } - - if (showWarning && !modal.shown) { - modal.show(); - - if (countdownInterval) { - clearInterval(countdownInterval); - } - countdownInterval = login.countdownTimer( - document.getElementById('countdown'), - timeRemaining, - timeTimeout, - ); - } - - if (!showWarning && modal.shown) { - modal.hide(); - } - - if (timeRemaining < frequency) { - timeRemaining = timeRemaining < 0 ? 0 : timeRemaining; - // Disable reason: circular dependency between ping and success - // eslint-disable-next-line no-use-before-define - setTimeout(ping, timeRemaining); - } -} - -function ping() { - const request = new XMLHttpRequest(); - request.open('GET', '/active', true); - - request.onload = function () { - try { - success(JSON.parse(request.responseText)); - } catch (error) { - notifyNewRelic(request, error, 'ping'); - } - }; - - request.send(); - setTimeout(ping, frequency); -} - -function keepalive() { - const request = new XMLHttpRequest(); - request.open('POST', '/sessions/keepalive', true); - request.setRequestHeader('X-CSRF-Token', csrfToken); - - request.onload = function () { - try { - success(JSON.parse(request.responseText)); - modal.hide(); - } catch (error) { - notifyNewRelic(request, error, 'keepalive'); - } - }; - - request.send(); -} - -keepaliveEl?.addEventListener('click', keepalive, false); -setTimeout(ping, start); diff --git a/app/javascript/packs/submit-with-spinner.js b/app/javascript/packs/submit-with-spinner.js new file mode 100644 index 00000000000..9eca956cc3f --- /dev/null +++ b/app/javascript/packs/submit-with-spinner.js @@ -0,0 +1,8 @@ +import { loadPolyfills } from '@18f/identity-polyfill'; + +loadPolyfills(['element-closest']).then(() => { + const spinner = /** @type {HTMLDivElement} */ (document.getElementById('submit-spinner')); + spinner.closest('form')?.addEventListener('submit', () => { + spinner.className = ''; + }); +}); diff --git a/app/models/agency.rb b/app/models/agency.rb index a8f67ad49bd..58f99e1ec9a 100644 --- a/app/models/agency.rb +++ b/app/models/agency.rb @@ -4,5 +4,4 @@ class Agency < ApplicationRecord has_many :service_providers, inverse_of: :agency # rubocop:enable Rails/HasManyOrHasOneDependent validates :name, presence: true - validates :abbreviation, uniqueness: { case_sensitive: false, allow_nil: true } end diff --git a/app/models/letter_requests_to_usps_ftp_log.rb b/app/models/letter_requests_to_usps_ftp_log.rb deleted file mode 100644 index 17a163e0d74..00000000000 --- a/app/models/letter_requests_to_usps_ftp_log.rb +++ /dev/null @@ -1,4 +0,0 @@ -class LetterRequestsToUspsFtpLog < ApplicationRecord - validates :ftp_at, presence: true - validates :letter_requests_count, presence: true -end diff --git a/app/models/null_service_provider.rb b/app/models/null_service_provider.rb index 2f96ee7eb19..1ed8e41613c 100644 --- a/app/models/null_service_provider.rb +++ b/app/models/null_service_provider.rb @@ -26,7 +26,6 @@ class NullServiceProvider iaa_start_date ial2_quota id - identities launch_date logo metadata_url @@ -73,10 +72,6 @@ def redirect_uris [] end - def identities - [] - end - def liveness_checking_required false end diff --git a/app/models/service_provider.rb b/app/models/service_provider.rb index a8fb68c0f8e..27700151df5 100644 --- a/app/models/service_provider.rb +++ b/app/models/service_provider.rb @@ -6,12 +6,6 @@ class ServiceProvider < ApplicationRecord belongs_to :agency - # rubocop:disable Rails/HasManyOrHasOneDependent - has_many :identities, inverse_of: :service_provider_record, - foreign_key: 'service_provider', - primary_key: 'issuer' - # rubocop:enable Rails/HasManyOrHasOneDependent - # Do not define validations in this model. # See https://github.com/18F/identity_validations include IdentityValidations::ServiceProviderValidation diff --git a/app/presenters/cancellation_presenter.rb b/app/presenters/cancellation_presenter.rb index 56fc839bbe3..5117f48adab 100644 --- a/app/presenters/cancellation_presenter.rb +++ b/app/presenters/cancellation_presenter.rb @@ -23,7 +23,6 @@ def cancellation_warnings t('users.delete.bullet_1', app: APP_NAME), t('users.delete.bullet_2_loa1'), t('users.delete.bullet_3', app: APP_NAME), - t('users.delete.bullet_4', app: APP_NAME), ] end diff --git a/app/presenters/idv/usps_presenter.rb b/app/presenters/idv/usps_presenter.rb index 8d7807adfd5..9cc3162cfc8 100644 --- a/app/presenters/idv/usps_presenter.rb +++ b/app/presenters/idv/usps_presenter.rb @@ -14,7 +14,7 @@ def title end def byline - if usps_mail_bounced? + if current_user.decorate.usps_mail_bounced? I18n.t('idv.messages.usps.new_address') else I18n.t('idv.messages.usps.address_on_file') @@ -25,8 +25,10 @@ def button letter_already_sent? ? I18n.t('idv.buttons.mail.resend') : I18n.t('idv.buttons.mail.send') end - def fallback_back_path - user_needs_address_otp_verification? ? verify_account_path : idv_phone_path + def cancel_path + return verify_account_path if user_needs_address_otp_verification? + + idv_cancel_path end def usps_mail_bounced? diff --git a/app/services/agency_seeder.rb b/app/services/agency_seeder.rb index 5c6259349f2..4dd5cf9e99a 100644 --- a/app/services/agency_seeder.rb +++ b/app/services/agency_seeder.rb @@ -13,6 +13,7 @@ def initialize( def run agencies.each do |agency_id, config| agency = Agency.find_by(id: agency_id) + config.delete('abbreviation') if agency agency.update!(config) else diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 4107e3b1b6b..5a5e2954c4d 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -84,6 +84,7 @@ def browser_attributes CAPTURE_DOC = 'Capture Doc'.freeze # visited or submitted is appended DOC_AUTH = 'Doc Auth'.freeze # visited or submitted is appended DOC_AUTH_ASYNC = 'Doc Auth Async'.freeze + IN_PERSON_PROOFING = 'In Person Proofing'.freeze # visited or submitted is appended EMAIL_AND_PASSWORD_AUTH = 'Email and Password Authentication'.freeze EMAIL_DELETION_REQUEST = 'Email Deletion Requested'.freeze EMAIL_LANGUAGE_VISITED = 'Email Language: Visited'.freeze @@ -95,6 +96,7 @@ def browser_attributes EXPIRED_LETTERS = 'Expired Letters'.freeze FORGET_ALL_BROWSERS_SUBMITTED = 'Forget All Browsers Submitted'.freeze FORGET_ALL_BROWSERS_VISITED = 'Forget All Browsers Visited'.freeze + FRONTEND_BROWSER_CAPABILITIES = 'Frontend: Browser capabilities'.freeze IAL2_RECOVERY = 'IAL2 Recovery'.freeze # visited or submitted is appended IAL2_RECOVERY_REQUEST = 'IAL2 Recovery Request'.freeze IAL2_RECOVERY_REQUEST_VISITED = 'IAL2 Recovery Request Visited'.freeze @@ -108,7 +110,6 @@ def browser_attributes IDV_COME_BACK_LATER_VISIT = 'IdV: come back later visited'.freeze IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM = 'IdV: doc auth image upload form submitted'.freeze IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR = 'IdV: doc auth image upload vendor submitted'.freeze - IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION = 'IdV: doc auth image upload vendor pii validation'.freeze IDV_MAX_ATTEMPTS_EXCEEDED = 'IdV: max attempts exceeded'.freeze IDV_FINAL = 'IdV: final resolution'.freeze IDV_FORGOT_PASSWORD = 'IdV: forgot password visited'.freeze @@ -135,7 +136,6 @@ def browser_attributes IDV_USPS_ADDRESS_VISITED = 'IdV: USPS address visited'.freeze IDV_VERIFICATION_ATTEMPT_CANCELLED = 'IdV: verification attempt cancelled'.freeze INVALID_AUTHENTICITY_TOKEN = 'Invalid Authenticity Token'.freeze - IN_PERSON_PROOFING = 'In Person Proofing'.freeze # visited or submitted is appended LAMBDA_RESULT_RESOLUTION_PROOF_RESULT = 'Lambda Resolution Proof Result Received'.freeze LAMBDA_RESULT_ADDRESS_PROOF_RESULT = 'Lambda Address Proof Result Received'.freeze LAMBDA_RESULT_DOCUMENT_PROOF_RESULT = 'Lambda Document Proof Result Received'.freeze diff --git a/app/services/data_requests/write_cloudwatch_logs.rb b/app/services/data_requests/write_cloudwatch_logs.rb index 801ea944f8e..486d77c8a58 100644 --- a/app/services/data_requests/write_cloudwatch_logs.rb +++ b/app/services/data_requests/write_cloudwatch_logs.rb @@ -5,7 +5,6 @@ class WriteCloudwatchLogs event_name success multi_factor_auth_method - multi_factor_id service_provider ip_address user_agent @@ -19,17 +18,23 @@ def initialize(cloudwatch_results, output_dir) end def call - CSV.open(File.join(output_dir, 'logs.csv'), 'w') do |csv| - csv << HEADERS - cloudwatch_results.each do |row| - csv << build_row(row) - end + output_file.puts(HEADERS.join(',')) + cloudwatch_results.each do |row| + write_row(row) end + output_file.close end private - def build_row(row) + def output_file + @output_file ||= begin + output_path = File.join(output_dir, 'logs.csv') + File.open(output_path, 'w') + end + end + + def write_row(row) data = JSON.parse(row.message) timestamp = data.dig('time') @@ -38,34 +43,18 @@ def build_row(row) multi_factor_auth_method = data.dig( 'properties', 'event_properties', 'multi_factor_auth_method' ) - - mfa_key = case multi_factor_auth_method - when 'sms', 'voice' - 'phone_configuration_id' - when 'piv_cac' - 'piv_cac_configuration_id' - when 'webauthn' - 'webauthn_configuration_id' - when 'totp' - 'auth_app_configuration_id' - end - - row_id = data.dig('properties', 'event_properties', mfa_key) - multi_factor_id = row_id && "#{mfa_key}:#{row_id}" service_provider = data.dig('properties', 'service_provider') ip_address = data.dig('properties', 'user_ip') user_agent = data.dig('properties', 'user_agent') - [ - timestamp, - event_name, - success, - multi_factor_auth_method, - multi_factor_id, - service_provider, - ip_address, - user_agent, - ] + output_file.puts( + CSV.generate_line( + [ + timestamp, event_name, success, multi_factor_auth_method, + service_provider, ip_address, user_agent + ], + ), + ) end end end diff --git a/app/services/db/sp_cost/add_sp_cost.rb b/app/services/db/sp_cost/add_sp_cost.rb index bb99808ece3..6d465231122 100644 --- a/app/services/db/sp_cost/add_sp_cost.rb +++ b/app/services/db/sp_cost/add_sp_cost.rb @@ -20,20 +20,14 @@ class SpCostTypeError < StandardError; end voice ].freeze - def self.call(issuer, ial, token, transaction_id: nil) + def self.call(issuer, ial, token) return if token.blank? unless TOKEN_WHITELIST.include?(token.to_sym) NewRelic::Agent.notice_error(SpCostTypeError.new(token.to_s)) return end agency_id = (issuer.present? && ServiceProvider.find_by(issuer: issuer)&.agency_id) || 0 - ::SpCost.create( - issuer: issuer.to_s, - ial: ial, - agency_id: agency_id, - cost_type: token, - transaction_id: transaction_id, - ) + ::SpCost.create(issuer: issuer.to_s, ial: ial, agency_id: agency_id, cost_type: token) end end end diff --git a/app/services/doc_auth_router.rb b/app/services/doc_auth_router.rb index 924ad647d96..b51d1e42c63 100644 --- a/app/services/doc_auth_router.rb +++ b/app/services/doc_auth_router.rb @@ -219,7 +219,17 @@ def self.notify_exception(exception, custom_params = nil) end end + # + # The `acuant_simulator` config is deprecated. The logic to switch vendors + # based on its value can be removed once FORCE_ACUANT_CONFIG_UPGRADE in + # acuant_simulator_config_validation.rb has been set to true for at least + # a deploy cycle. + # def self.doc_auth_vendor vendor_from_config = AppConfig.env.doc_auth_vendor + if vendor_from_config.blank? + return AppConfig.env.acuant_simulator == 'true' ? 'mock' : 'acuant' + end + vendor_from_config end end diff --git a/app/services/document_capture_session_async_result.rb b/app/services/document_capture_session_async_result.rb index c7e95648b9f..3f505d2e5d0 100644 --- a/app/services/document_capture_session_async_result.rb +++ b/app/services/document_capture_session_async_result.rb @@ -22,8 +22,6 @@ def done? status == DocumentCaptureSessionAsyncResult::DONE end - alias_method :success?, :done? - def in_progress? status == DocumentCaptureSessionAsyncResult::IN_PROGRESS end diff --git a/app/services/encryption/multi_region_kms_client.rb b/app/services/encryption/multi_region_kms_client.rb index 4cf87338bdb..a566c1b96ef 100644 --- a/app/services/encryption/multi_region_kms_client.rb +++ b/app/services/encryption/multi_region_kms_client.rb @@ -59,7 +59,7 @@ def encrypt_legacy(key_id, plaintext, encryption_context) def find_available_region(regions) regions.each do |region, cipher| region_client = @aws_clients[region] - return CipherData.new(region_client, Base64.strict_decode64(cipher)) if region_client + return CipherData.new(region_client, cipher) if region_client end raise EncryptionError, 'No supported region found in ciphertext' end diff --git a/app/services/flow/flow_state_machine.rb b/app/services/flow/flow_state_machine.rb index 49085955360..40e8ea93ac2 100644 --- a/app/services/flow/flow_state_machine.rb +++ b/app/services/flow/flow_state_machine.rb @@ -22,10 +22,7 @@ def update step = current_step result = flow.handle(step) if @analytics_id - increment_step_name_counts analytics.track_event(analytics_submitted, result.to_h.merge(analytics_properties)) - # keeping the old event names for backward compatibility - analytics.track_event(old_analytics_submitted, result.to_h.merge(analytics_properties)) end register_update_step(step, result) if flow.json @@ -47,12 +44,7 @@ def current_step end def track_step_visited - if @analytics_id - increment_step_name_counts - analytics.track_event(analytics_visited, analytics_properties) - # keeping the old event names for backward compatibility - analytics.track_event(old_analytics_visited, analytics_properties) - end + analytics.track_event(analytics_visited, analytics_properties) if @analytics_id Funnel::DocAuth::RegisterStep.new(user_id, issuer).call(current_step, :view, true) register_campaign end @@ -124,11 +116,7 @@ def call_optional_show_step(step) result = optional_show_step.new(@flow).base_call if @analytics_id - optional_properties = result.to_h.merge(step: optional_show_step) - - analytics.track_event(analytics_optional_step, optional_properties) - # keeping the old event names for backward compatibility - analytics.track_event(old_analytics_optional_step, optional_properties) + analytics.track_event(analytics_optional_step, result.to_h.merge(step: optional_show_step)) end if next_step.to_s != step @@ -156,50 +144,31 @@ def redirect_to_step(step) end def analytics_submitted - 'IdV: ' + "#{@analytics_id} #{current_step} submitted".downcase - end - - def analytics_visited - 'IdV: ' + "#{@analytics_id} #{current_step} visited".downcase - end - - def analytics_optional_step - 'IdV: ' + "#{@analytics_id} optional #{current_step} submitted".downcase - end - - def old_analytics_submitted @analytics_id + ' submitted' end - def old_analytics_visited + def analytics_visited @analytics_id + ' visited' end - def old_analytics_optional_step + def analytics_optional_step [@analytics_id, 'optional submitted'].join(' ') end def analytics_properties + current_step_name = "#{current_step.to_s}_#{action_name}" + current_flow_step_counts[current_step_name] ||= 0 { step: current_step, - step_count: current_flow_step_counts[current_step_name], + step_count: current_flow_step_counts[current_step_name] += 1, } end - def current_step_name - "#{current_step}_#{action_name}" - end - def current_flow_step_counts current_session["#{@name}_flow_step_counts"] ||= {} - current_session["#{@name}_flow_step_counts"].default = 0 current_session["#{@name}_flow_step_counts"] end - def increment_step_name_counts - current_flow_step_counts[current_step_name] += 1 - end - def next_step flow.next_step end diff --git a/app/services/identity_linker.rb b/app/services/identity_linker.rb index c95ba7b5dbb..13af39c4771 100644 --- a/app/services/identity_linker.rb +++ b/app/services/identity_linker.rb @@ -8,7 +8,6 @@ def initialize(user, provider) end def link_identity(**extra_attrs) - return unless user && provider.present? process_ial(extra_attrs) attributes = merged_attributes(extra_attrs) identity.update!(attributes) diff --git a/app/services/idv/actions/cancel_link_sent_action.rb b/app/services/idv/actions/cancel_link_sent_action.rb deleted file mode 100644 index b8ebc0b2e93..00000000000 --- a/app/services/idv/actions/cancel_link_sent_action.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Idv - module Actions - class CancelLinkSentAction < Idv::Steps::DocAuthBaseStep - def call - mark_step_incomplete(:send_link) - end - end - end -end diff --git a/app/services/idv/actions/cancel_send_link_action.rb b/app/services/idv/actions/cancel_send_link_action.rb deleted file mode 100644 index 8d9fe50ce29..00000000000 --- a/app/services/idv/actions/cancel_send_link_action.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Idv - module Actions - class CancelSendLinkAction < Idv::Steps::DocAuthBaseStep - def call - mark_step_incomplete(:upload) - end - end - end -end diff --git a/app/services/idv/actions/verify_document_action.rb b/app/services/idv/actions/verify_document_action.rb index 3bbdc4ccc77..5443ae189bd 100644 --- a/app/services/idv/actions/verify_document_action.rb +++ b/app/services/idv/actions/verify_document_action.rb @@ -30,13 +30,9 @@ def form end def enqueue_job - verify_document_capture_session = if hybrid_flow_mobile? - document_capture_session - else - create_document_capture_session( - verify_document_capture_session_uuid_key, - ) - end + verify_document_capture_session = create_document_capture_session( + verify_document_capture_session_uuid_key, + ) verify_document_capture_session.requested_at = Time.zone.now verify_document_capture_session.create_doc_auth_session diff --git a/app/services/idv/actions/verify_document_status_action.rb b/app/services/idv/actions/verify_document_status_action.rb index 69b209c0773..6f8cc8b0770 100644 --- a/app/services/idv/actions/verify_document_status_action.rb +++ b/app/services/idv/actions/verify_document_status_action.rb @@ -61,13 +61,9 @@ def process_result(result) def verify_document_capture_session return @verify_document_capture_session if defined?(@verify_document_capture_session) - @verify_document_capture_session = if hybrid_flow_mobile? - document_capture_session - else - DocumentCaptureSession.find_by( - uuid: flow_session[verify_document_capture_session_uuid_key], - ) - end + @verify_document_capture_session = DocumentCaptureSession.find_by( + uuid: flow_session[verify_document_capture_session_uuid_key], + ) end def async_state diff --git a/app/services/idv/flows/cac_flow.rb b/app/services/idv/flows/cac_flow.rb index 9a1a8217c13..f4eb6c15f2e 100644 --- a/app/services/idv/flows/cac_flow.rb +++ b/app/services/idv/flows/cac_flow.rb @@ -8,6 +8,7 @@ class CacFlow < Flow::BaseFlow enter_info: Idv::Steps::Cac::EnterInfoStep, verify: Idv::Steps::Cac::VerifyStep, verify_wait: Idv::Steps::Cac::VerifyWaitStep, + success: Idv::Steps::Cac::SuccessStep, }.freeze OPTIONAL_SHOW_STEPS = { diff --git a/app/services/idv/flows/capture_doc_flow.rb b/app/services/idv/flows/capture_doc_flow.rb index 680d4747a88..401946b0468 100644 --- a/app/services/idv/flows/capture_doc_flow.rb +++ b/app/services/idv/flows/capture_doc_flow.rb @@ -8,8 +8,6 @@ class CaptureDocFlow < Flow::BaseFlow ACTIONS = { reset: Idv::Actions::ResetAction, - verify_document: Idv::Actions::VerifyDocumentAction, - verify_document_status: Idv::Actions::VerifyDocumentStatusAction, }.freeze def initialize(controller, session, _name) diff --git a/app/services/idv/flows/doc_auth_flow.rb b/app/services/idv/flows/doc_auth_flow.rb index 8711b4e5784..82fce9e343a 100644 --- a/app/services/idv/flows/doc_auth_flow.rb +++ b/app/services/idv/flows/doc_auth_flow.rb @@ -17,8 +17,6 @@ class DocAuthFlow < Flow::BaseFlow }.freeze ACTIONS = { - cancel_send_link: Idv::Actions::CancelSendLinkAction, - cancel_link_sent: Idv::Actions::CancelLinkSentAction, reset: Idv::Actions::ResetAction, redo_ssn: Idv::Actions::RedoSsnAction, verify_document: Idv::Actions::VerifyDocumentAction, diff --git a/app/services/idv/steps/cac/success_step.rb b/app/services/idv/steps/cac/success_step.rb new file mode 100644 index 00000000000..5361d311a42 --- /dev/null +++ b/app/services/idv/steps/cac/success_step.rb @@ -0,0 +1,9 @@ +module Idv + module Steps + module Cac + class SuccessStep < DocAuthBaseStep + def call; end + end + end + end +end diff --git a/app/services/idv/steps/doc_auth_base_step.rb b/app/services/idv/steps/doc_auth_base_step.rb index c55a1e68c87..f21be9308d6 100644 --- a/app/services/idv/steps/doc_auth_base_step.rb +++ b/app/services/idv/steps/doc_auth_base_step.rb @@ -50,10 +50,6 @@ def user_id_from_token flow_session[:doc_capture_user_id] end - def hybrid_flow_mobile? - user_id_from_token.present? - end - def throttled_response redirect_to throttled_url IdentityDocAuth::Response.new( @@ -75,9 +71,9 @@ def user_id current_user ? current_user.id : user_id_from_token end - def add_cost(token, transaction_id: nil) + def add_cost(token) issuer = sp_session[:issuer].to_s - Db::SpCost::AddSpCost.call(issuer, 2, token, transaction_id: transaction_id) + Db::SpCost::AddSpCost.call(issuer, 2, token) Db::ProofingCost::AddUserProofingCost.call(user_id, token) end diff --git a/app/services/idv/steps/link_sent_step.rb b/app/services/idv/steps/link_sent_step.rb index 47ac432a929..06b607d1363 100644 --- a/app/services/idv/steps/link_sent_step.rb +++ b/app/services/idv/steps/link_sent_step.rb @@ -31,10 +31,7 @@ def take_photo_with_phone_successful? end def document_capture_session_result - @document_capture_session_result ||= ( - document_capture_session&.load_result || - document_capture_session&.load_doc_auth_async_result - ) + @document_capture_session_result ||= document_capture_session&.load_result end def mark_steps_complete diff --git a/app/services/idv/steps/verify_base_step.rb b/app/services/idv/steps/verify_base_step.rb index 29283fe78d2..8aa9f5fffa4 100644 --- a/app/services/idv/steps/verify_base_step.rb +++ b/app/services/idv/steps/verify_base_step.rb @@ -54,14 +54,8 @@ def idv_result_to_form_response(idv_result) def add_proofing_costs(results) vendors = results[:context][:stages] vendors.each do |hash| - if hash[:state_id] - # transaction_id comes from TransactionLocatorId - add_cost(:aamva, transaction_id: hash[:transaction_id]) - end - if hash[:resolution] - # transaction_id comes from ConversationId - add_cost(:lexis_nexis_resolution, transaction_id: hash[:transaction_id]) - end + add_cost(:aamva) if hash[:state_id] + add_cost(:lexis_nexis_resolution) if hash[:resolution] end end diff --git a/app/services/reports/monthly_usps_letter_requests_report.rb b/app/services/reports/monthly_usps_letter_requests_report.rb deleted file mode 100644 index 29acfe0cce4..00000000000 --- a/app/services/reports/monthly_usps_letter_requests_report.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'login_gov/hostdata' - -module Reports - class MonthlyUspsLetterRequestsReport < BaseReport - REPORT_NAME = 'monthly-usps-letter-requests-report'.freeze - - def call - daily_results = transaction_with_timeout do - ::LetterRequestsToUspsFtpLog.where(ftp_at: first_of_this_month..end_of_today) - end - totals = calculate_totals(daily_results) - save_report(REPORT_NAME, {total_letter_requests: totals, - daily_letter_requests: daily_results}.to_json) - end - - private - - def calculate_totals(daily_results) - daily_results.inject(0) {|sum, rec| sum + rec['letter_requests_count'].to_i } - end - end -end diff --git a/app/services/sp_handoff_bounce.rb b/app/services/sp_handoff_bounce.rb deleted file mode 100644 index ffcf919bab0..00000000000 --- a/app/services/sp_handoff_bounce.rb +++ /dev/null @@ -1,13 +0,0 @@ -class SpHandoffBounce - def self.is_bounced?(session) - start_time = session[:sp_handoff_start_time] - return if start_time.blank? - tz = Time.zone - start_time = tz.parse(start_time) if start_time.class == String - tz.now <= (start_time + AppConfig.env.sp_handoff_bounce_max_seconds.to_i.seconds) - end - - def self.add_handoff_time_to_session(session) - session[:sp_handoff_start_time] = Time.zone.now - end -end diff --git a/app/services/sp_handoff_bounce/add_handoff_time_to_session.rb b/app/services/sp_handoff_bounce/add_handoff_time_to_session.rb new file mode 100644 index 00000000000..feaa37377eb --- /dev/null +++ b/app/services/sp_handoff_bounce/add_handoff_time_to_session.rb @@ -0,0 +1,7 @@ +module SpHandoffBounce + class AddHandoffTimeToSession + def self.call(session) + session[:sp_handoff_start_time] = Time.zone.now + end + end +end diff --git a/app/services/sp_handoff_bounce/is_bounced.rb b/app/services/sp_handoff_bounce/is_bounced.rb new file mode 100644 index 00000000000..a0673c48804 --- /dev/null +++ b/app/services/sp_handoff_bounce/is_bounced.rb @@ -0,0 +1,11 @@ +module SpHandoffBounce + class IsBounced + def self.call(session) + start_time = session[:sp_handoff_start_time] + return if start_time.blank? + tz = Time.zone + start_time = tz.parse(start_time) if start_time.class == String + tz.now <= (start_time + AppConfig.env.sp_handoff_bounce_max_seconds.to_i.seconds) + end + end +end diff --git a/app/services/store_sp_metadata_in_session.rb b/app/services/store_sp_metadata_in_session.rb index e5f6865a673..38454c71a3a 100644 --- a/app/services/store_sp_metadata_in_session.rb +++ b/app/services/store_sp_metadata_in_session.rb @@ -35,7 +35,6 @@ def sp_request def update_session session[:sp] = { issuer: sp_request.issuer, - ial: ial_context.ial, ial2: ial_context.ial2_requested?, ial2_strict: ial_context.ial2_strict_requested?, ialmax: ial_context.ialmax_requested?, diff --git a/app/services/usps_confirmation_uploader.rb b/app/services/usps_confirmation_uploader.rb index c2e60709dec..2878bccce9f 100644 --- a/app/services/usps_confirmation_uploader.rb +++ b/app/services/usps_confirmation_uploader.rb @@ -1,14 +1,9 @@ class UspsConfirmationUploader - def initialize - @now = Time.zone.now - end - def run confirmations = UspsConfirmation.all export = generate_export(confirmations) upload_export(export) clear_confirmations(confirmations) - LetterRequestsToUspsFtpLog.create(ftp_at: @now, letter_requests_count: confirmations.count) rescue StandardError => error NewRelic::Agent.notice_error(error) end @@ -32,7 +27,7 @@ def clear_confirmations(confirmations) end def remote_path - timestamp = @now.strftime('%Y%m%d') + timestamp = Time.zone.now.strftime('%Y%m%d') File.join(env.usps_upload_sftp_directory, "batch#{timestamp}.psv") end diff --git a/app/views/account_reset/confirm_delete_account/show.html.erb b/app/views/account_reset/confirm_delete_account/show.html.erb index e1a255e8410..f57931ea938 100644 --- a/app/views/account_reset/confirm_delete_account/show.html.erb +++ b/app/views/account_reset/confirm_delete_account/show.html.erb @@ -5,7 +5,7 @@ alt: '', class: 'absolute top-n24 left-0 right-0 margin-x-auto') %>

<%= t('account_reset.confirm_delete_account.title') %>

-

<%= t('account_reset.confirm_delete_account.info_html', app: APP_NAME, email: email) %>

+

<%= t('account_reset.confirm_delete_account.info_html', email: email) %>

<%= t('account_reset.confirm_delete_account.cta_html', link: link_to(t('account_reset.confirm_delete_account.link_text'), sign_up_email_path)) %>

diff --git a/app/views/account_reset/delete_account/show.html.erb b/app/views/account_reset/delete_account/show.html.erb index 24119796876..eb1d6b24275 100644 --- a/app/views/account_reset/delete_account/show.html.erb +++ b/app/views/account_reset/delete_account/show.html.erb @@ -4,7 +4,7 @@ <%= t('account_reset.delete_account.title') %>

- <%= t('account_reset.delete_account.info', app: APP_NAME) %> + <%= t('account_reset.delete_account.info') %>


diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 02d433fb4b7..fef025acf80 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -39,7 +39,7 @@ ) %>

<% end %> -<% if @ial && desktop_device? %> +<% if FeatureManagement.allow_piv_cac_login? && @ial && desktop_device? %>
<%= link_to( t('account.login.piv_cac'), diff --git a/app/views/idv/address/new.html.erb b/app/views/idv/address/new.html.erb index 3f058c20891..acf16805944 100644 --- a/app/views/idv/address/new.html.erb +++ b/app/views/idv/address/new.html.erb @@ -37,4 +37,6 @@ <% end %>
-<%= render 'idv/doc_auth/back', step: 'verify' %> +
+ <%= link_to t('links.cancel'), idv_cancel_path, class: 'h5' %> +
diff --git a/app/views/idv/cac/success.html.erb b/app/views/idv/cac/success.html.erb new file mode 100644 index 00000000000..269df24d8d5 --- /dev/null +++ b/app/views/idv/cac/success.html.erb @@ -0,0 +1,22 @@ +<% title t('cac_proofing.titles.cac_proofing') %> +
+ <%= t('cac_proofing.step', step: 2) %> +
+ +<%= image_tag(asset_url('state-id-confirm@3x.png'), width: 210) %> + +

+ <%= t('cac_proofing.headings.success') %> +

+ +
+ +
+ <%= button_to(t('forms.buttons.continue'), url_for, method: :put, + class: 'btn btn-primary btn-wide sm-col-6 col-12') %> +
+ +<%= validated_form_for('', url: url_for, method: 'PUT', + html: { autocomplete: 'off', role: 'form', class: 'margin-top-2' }) do |f| %> +<% end %> +<%= render 'idv/cac/start_over_or_cancel' %> diff --git a/app/views/idv/doc_auth/_back.html.erb b/app/views/idv/doc_auth/_back.html.erb deleted file mode 100644 index 0bef687810e..00000000000 --- a/app/views/idv/doc_auth/_back.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<%# -Renders a "Back" link to return to a previous step, given by one of action or step local variables. -If neither are passed, redirects to the previous screen using HTTP referer. An optional fallback -path can be passed in case the HTTP header is not specified or is invalid. If none of the above -yield a useable URL, nothing will be rendered. - -locals: -* action: (Optional) Flow action to call to return to the previous step. -* step: (Optional) Name of step to which user should be returned. -* fallback_path: (Optional) Path to redirect absent action, step, and HTTP referer. -%> -<% -text = '‹ ' + t('forms.buttons.back') -step = local_assigns[:action] || local_assigns[:step] -path = step ? idv_doc_auth_step_path(step: step) : go_back_path -path ||= local_assigns[:fallback_path] -%> -<% if path %> -
- <% if local_assigns[:action] %> - <%= button_to( - text, - path, - method: :put, - class: 'btn btn-link' - ) %> - <% else %> - <%= link_to(text, path) %> - <% end %> -
-<% end %> diff --git a/app/views/idv/doc_auth/_spinner.html.erb b/app/views/idv/doc_auth/_spinner.html.erb new file mode 100644 index 00000000000..3e832ffd5dd --- /dev/null +++ b/app/views/idv/doc_auth/_spinner.html.erb @@ -0,0 +1,11 @@ +
+ +
diff --git a/app/views/idv/doc_auth/_submit_with_spinner.html.erb b/app/views/idv/doc_auth/_submit_with_spinner.html.erb new file mode 100644 index 00000000000..ff80876cf8e --- /dev/null +++ b/app/views/idv/doc_auth/_submit_with_spinner.html.erb @@ -0,0 +1,6 @@ + +<%= render 'idv/doc_auth/spinner' %> +
+<%= javascript_packs_tag_once 'submit-with-spinner' %> diff --git a/app/views/idv/doc_auth/link_sent.html.erb b/app/views/idv/doc_auth/link_sent.html.erb index 374357fc5df..2c5ae586e09 100644 --- a/app/views/idv/doc_auth/link_sent.html.erb +++ b/app/views/idv/doc_auth/link_sent.html.erb @@ -38,7 +38,7 @@ %> -<%= render 'idv/doc_auth/back', action: 'cancel_link_sent' %> +<%= render 'idv/doc_auth/start_over_or_cancel' %> <% if FeatureManagement.doc_capture_polling_enabled? %> <%= javascript_packs_tag_once 'doc_capture_polling' %> diff --git a/app/views/idv/doc_auth/send_link.html.erb b/app/views/idv/doc_auth/send_link.html.erb index 6fc5f489a0f..0fade994cb0 100644 --- a/app/views/idv/doc_auth/send_link.html.erb +++ b/app/views/idv/doc_auth/send_link.html.erb @@ -39,4 +39,4 @@ <% end %> -<%= render 'idv/doc_auth/back', action: 'cancel_send_link' %> +<%= render 'idv/doc_auth/start_over_or_cancel' %> diff --git a/app/views/idv/doc_auth/upload.html.erb b/app/views/idv/doc_auth/upload.html.erb index 4bf187bec2c..74976737b4b 100644 --- a/app/views/idv/doc_auth/upload.html.erb +++ b/app/views/idv/doc_auth/upload.html.erb @@ -36,7 +36,7 @@

<% end %> -<% if desktop_device? %> +<% if AppConfig.env.cac_proofing_enabled == 'true' && desktop_device? %>

<%= t('doc_auth.info.use_cac') %> <%= link_to(t('doc_auth.info.use_cac_link'), idv_cac_step_path(step: :choose_method)) %> diff --git a/app/views/idv/doc_auth/welcome.html.erb b/app/views/idv/doc_auth/welcome.html.erb index bd4a478c32f..d578aa2d5b8 100644 --- a/app/views/idv/doc_auth/welcome.html.erb +++ b/app/views/idv/doc_auth/welcome.html.erb @@ -102,8 +102,7 @@ <%= new_window_link_to t('doc_auth.instructions.learn_more'), 'https://login.gov/policy/' %> - <%= f.button :button, t('doc_auth.buttons.continue'), type: :submit, - class: 'btn btn-primary btn-wide sm-col-6 col-6' %> + <%= f.button :submit, t('doc_auth.buttons.continue'), class: 'btn btn-primary btn-wide sm-col-6 col-6' %> <% end %>
diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 0075f9c2ad7..1d7eb3f7291 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -18,10 +18,10 @@ mock_client: (DocAuthRouter.doc_auth_vendor == 'mock').presence, document_capture_session_uuid: flow_session[:document_capture_session_uuid], endpoint: FeatureManagement.document_capture_async_uploads_enabled? ? - send(@step_url, step: :verify_document) : + idv_doc_auth_step_path(step: :verify_document) : api_verify_images_url, status_endpoint: FeatureManagement.document_capture_async_uploads_enabled? ? - send(@step_url, step: :verify_document_status) : + idv_doc_auth_step_path(step: :verify_document_status) : nil, status_poll_interval_ms: AppConfig.env.poll_rate_for_verify_in_seconds.to_i * 1000, sp_name: sp_name, @@ -133,10 +133,7 @@ <%# ---- Submit ----- %>

- -
+ <%= render 'idv/doc_auth/submit_with_spinner' %>
<% end %> <%# end validated_form_for %> diff --git a/app/views/idv/usps/index.html.erb b/app/views/idv/usps/index.html.erb index 29c87f89157..3f3db6e1a30 100644 --- a/app/views/idv/usps/index.html.erb +++ b/app/views/idv/usps/index.html.erb @@ -16,4 +16,7 @@ <% end %> -<%= render 'idv/doc_auth/back', fallback_path: @presenter.fallback_back_path %> +
+ <%= button_to(t('idv.messages.clear_and_start_over'), idv_session_path, method: :delete, class: 'btn btn-link margin-bottom-1') %> + <%= link_to(t('links.cancel'), @presenter.cancel_path, class: 'h5') %> +
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 992ffb65a5b..d3c08c16388 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -73,26 +73,18 @@ <%= render 'shared/footer_lite' %> - <% if current_user # Render the JS snipped that collects platform authenticator analytics %> -
- <%= render partial: 'session_timeout/ping', - locals: { - timeout_url: timeout_url, - warning: session_timeout_warning, - start: session_timeout_start, - frequency: session_timeout_frequency, - modal: session_modal, - } %> - <% elsif !@skip_session_expiration %> - <%= render partial: 'session_timeout/expire_session', - locals: { - session_timeout_in: Devise.timeout_in, - } %> - <% end %> +
<%= javascript_packs_tag_once 'application', prepend: true %> <%= render_javascript_pack_once_tags %> + <% if current_user # Render the JS snipped that collects platform authenticator analytics %> +
+ <%= auto_session_timeout_js %> + <% else %> + <%= auto_session_expired_js %> + <% end %> + <%= render 'shared/dap_analytics' if AppConfig.env.participate_in_dap == 'true' && !session_with_trust? %> diff --git a/app/views/session_timeout/_expire_session.html.erb b/app/views/session_timeout/_expire_session.html.erb deleted file mode 100644 index 709ad682a8b..00000000000 --- a/app/views/session_timeout/_expire_session.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%= tag.div id: 'js-expire-session', - data: { - session_timeout_in: session_timeout_in, - timeout_refresh_path: timeout_refresh_path - } %> - -<%= javascript_packs_tag_once 'session-expire-session' %> diff --git a/app/views/session_timeout/_expire_session.js.erb b/app/views/session_timeout/_expire_session.js.erb new file mode 100644 index 00000000000..318143d1c37 --- /dev/null +++ b/app/views/session_timeout/_expire_session.js.erb @@ -0,0 +1,7 @@ +var sessionTimeoutIn = <%= session_timeout_in %> * 1000; + +function refreshPage() { + document.location = "<%= j timeout_refresh_path %>"; +} + +setTimeout(refreshPage, sessionTimeoutIn); diff --git a/app/views/session_timeout/_ping.html.erb b/app/views/session_timeout/_ping.html.erb deleted file mode 100644 index 4bb082c8808..00000000000 --- a/app/views/session_timeout/_ping.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%= tag.div id: 'session-timeout-cntnr', - data: { - timeout_url: timeout_url, - warning: warning, - start: start, - frequency: frequency, - warning_info_html: render(partial: 'session_timeout/warning', - locals: { modal: modal }), - } %> - -<%= javascript_packs_tag_once 'session-timeout-ping' %> diff --git a/app/views/session_timeout/_ping.js.erb b/app/views/session_timeout/_ping.js.erb new file mode 100644 index 00000000000..f81c9e2891c --- /dev/null +++ b/app/views/session_timeout/_ping.js.erb @@ -0,0 +1,84 @@ +var frequency = <%= frequency %> * 1000; +var warning = <%= warning %> * 1000; +var start = <%= start %> * 1000; +var timeoutUrl = "<%= j timeout_url %>"; +var warning_info = "<%= j render('session_timeout/warning', locals: { modal: modal }) %>"; +var warningEl = document.getElementById('session-timeout-cntnr'); +warningEl.insertAdjacentHTML('afterbegin', warning_info); + +var modal = new window.LoginGov.Modal({ el: '#session-timeout-msg' }); +var keepaliveEl = document.getElementById('session-keepalive-btn'); +var csrfEl = document.querySelector('meta[name="csrf-token"]') + +var csrfToken = ""; +if (csrfEl) { + csrfToken = csrfEl.content +} + +keepaliveEl.addEventListener('click', keepalive, false); + +var pingTimeout; +var countdownInterval; + +function ping() { + var request = new XMLHttpRequest(); + request.open('GET', '/active', true); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + success(JSON.parse(request.responseText)); + } + }; + + request.send(); + pingTimeout = setTimeout(ping, frequency) +} + +function success(data) { + var el = document.getElementById('session-timeout-msg'), + cntnr = document.getElementById('session-timeout-cntnr'); + + var time_remaining = data.remaining * 1000, + time_timeout = new Date().getTime() + time_remaining, + show_warning = time_remaining < warning + + if (!data.live) { + window.LoginGov.autoLogout(timeoutUrl); + return; + } + + if (show_warning && !modal.shown) { + modal.show(); + + if(countdownInterval) { + clearInterval(countdownInterval); + } + countdownInterval = window.LoginGov.countdownTimer( + document.getElementById('countdown'), time_remaining, time_timeout + ); + } + + if (!show_warning && modal.shown) modal.hide(); + + if (time_remaining < frequency){ + time_remaining = time_remaining < 0 ? 0 : time_remaining + ping_timeout = setTimeout(ping, time_remaining) + } +} + +function keepalive() { + var request = new XMLHttpRequest(); + request.open('POST', '/sessions/keepalive', true); + request.setRequestHeader('X-CSRF-Token', csrfToken); + + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + success(JSON.parse(request.responseText)); + modal.hide(); + } + }; + + request.send(); +} + +setTimeout(ping, start); diff --git a/app/views/user_mailer/account_reset_request.html.erb b/app/views/user_mailer/account_reset_request.html.erb index 182223c1b8e..fb779664275 100644 --- a/app/views/user_mailer/account_reset_request.html.erb +++ b/app/views/user_mailer/account_reset_request.html.erb @@ -1,5 +1,5 @@

- <%= t('user_mailer.account_reset_request.intro_html', app: link_to(APP_NAME, AppConfig.env.mailer_domain_name, class: 'gray')) %> + <%= t('user_mailer.account_reset_request.intro', app: link_to(APP_NAME, AppConfig.env.mailer_domain_name, class: 'gray')) %>

diff --git a/app/views/users/delete/show.html.erb b/app/views/users/delete/show.html.erb index 763228118cc..d206a482bfe 100644 --- a/app/views/users/delete/show.html.erb +++ b/app/views/users/delete/show.html.erb @@ -12,7 +12,6 @@

  • <%= t('users.delete.bullet_1', app: APP_NAME) %>
  • <%= current_user.decorate.delete_account_bullet_key %>
  • <%= t('users.delete.bullet_3', app: APP_NAME) %>
  • -
  • <%= t('users.delete.bullet_4', app: APP_NAME) %>
  • <%= validated_form_for(current_user, url: account_delete_path, diff --git a/bin/smoke_test b/bin/smoke_test index 0be0eff8bd4..c6e662ecca6 100755 --- a/bin/smoke_test +++ b/bin/smoke_test @@ -4,7 +4,7 @@ set -euo pipefail params="" spec_helper="rails_helper" should_source_env=1 -only_failures="" +retry_count=3 function help() { cat < { Reports::UspsReport.new.call }, ) - -# Send Monthly USPS Letter Requests Report to S3 -JobRunner::Runner.add_config JobRunner::JobConfiguration.new( - name: 'Monthly USPS letter requests report', - interval: 24 * 60 * 60, # 24 hours - timeout: 300, - callback: -> { Reports::MonthlyUspsLetterRequestsReport.new.call }, -) diff --git a/config/initializers/saml_idp.rb b/config/initializers/saml_idp.rb index 31c8d507736..702703be34c 100644 --- a/config/initializers/saml_idp.rb +++ b/config/initializers/saml_idp.rb @@ -13,8 +13,8 @@ # config.verify_authnrequest_sig = true # Organization contact information - config.organization_name = 'login.gov' - config.organization_url = 'https://login.gov' + config.organization_name = '18F' + config.organization_url = 'http://18f.gsa.gov' config.base_saml_location = "#{api_base}/saml" config.attribute_service_location = "#{api_base}/saml/attributes" config.single_service_post_location = "#{api_base}/saml/auth" diff --git a/config/locales/account_reset/en.yml b/config/locales/account_reset/en.yml index 7c64d798859..2817003b2dd 100644 --- a/config/locales/account_reset/en.yml +++ b/config/locales/account_reset/en.yml @@ -24,9 +24,8 @@ en: are_you_sure: Are you sure you want to delete your account? info: Deleting your account should be your last resort if you are locked out of your account. You will not be able to recover any information linked to - your account. We will notify the agencies you access with %{app} that you - no longer have an account. Once your account is deleted, you can create a - new one using the same email address. + your account. Once your account is deleted, you can create a new one using + the same email address. title: Deleting your account should be your last resort pending: cancel_request: Cancel request diff --git a/config/locales/account_reset/es.yml b/config/locales/account_reset/es.yml index 41069142bf6..d3ed6b40036 100644 --- a/config/locales/account_reset/es.yml +++ b/config/locales/account_reset/es.yml @@ -22,11 +22,10 @@ es: a su registro número de teléfono. delete_account: are_you_sure: "¿Seguro que quieres eliminar tu cuenta?" - info: Eliminar su cuenta debe ser su último recurso si su cuenta está bloqueada. - No podrá recuperar ninguna información vinculada a su cuenta. Notificaremos - a las agencias a las que acceda con %{app} que ya no tiene una cuenta. Cuando - su cuenta sea eliminada podrá crear una nueva usando la misma dirección de - correo electrónico. + info: Eliminar su cuenta debe ser su último recurso si está bloqueado de tu + cuenta No podrá recuperar ninguna información vinculada a su cuenta. Una vez + que se elimine su cuenta, puede crear una nueva usando la misma dirección + de correo electrónico. title: Eliminar tu cuenta debería ser tu último recurso pending: cancel_request: Cancelar petición diff --git a/config/locales/account_reset/fr.yml b/config/locales/account_reset/fr.yml index 8fa6288c9fe..5da3e2331aa 100644 --- a/config/locales/account_reset/fr.yml +++ b/config/locales/account_reset/fr.yml @@ -25,9 +25,8 @@ fr: are_you_sure: Êtes-vous sûr de vouloir supprimer votre compte? info: La suppression de votre compte devrait être votre dernier recours si vous êtes en lock-out de votre compte Vous ne pourrez pas récupérer les informations - liées à ton compte. Nous informerons les agences auxquelles vous accédez avec - %{app} que vous ne plus avoir un compte. Une fois votre compte supprimé, vous - pouvez en créer un nouveau en utilisant la même adresse e-mail. + liées à ton compte. Une fois votre compte supprimé, vous pouvez en créer un + nouveau en utilisant la même adresse e-mail. title: La suppression de votre compte devrait être votre dernier recours pending: cancel_request: Demande d'annulation diff --git a/config/locales/cac_proofing/en.yml b/config/locales/cac_proofing/en.yml index 0feaf335c80..7fa25e918f0 100644 --- a/config/locales/cac_proofing/en.yml +++ b/config/locales/cac_proofing/en.yml @@ -16,6 +16,7 @@ en: choose_method: How would you like to verify your identity? enter_info: Enter in your information present_cac: Present your PIV/CAC card to verify your account + success: We've verified your information and PIV/CAC card. verify: Verify your information welcome: We need to verify your identity info: diff --git a/config/locales/cac_proofing/es.yml b/config/locales/cac_proofing/es.yml index bec69df515d..e2d9bccdab0 100644 --- a/config/locales/cac_proofing/es.yml +++ b/config/locales/cac_proofing/es.yml @@ -15,6 +15,7 @@ es: choose_method: "¿Cómo le gustaría verificar su identidad?" enter_info: Ingrese su información present_cac: Presente su tarjeta PIV / CAC para verificar su cuenta + success: Hemos verificado su información y su tarjeta PIV / CAC. verify: Verifica tu información welcome: Nosotros necesitamos verificar tu identidad info: diff --git a/config/locales/cac_proofing/fr.yml b/config/locales/cac_proofing/fr.yml index a66dabdf057..35cd61e411a 100644 --- a/config/locales/cac_proofing/fr.yml +++ b/config/locales/cac_proofing/fr.yml @@ -17,6 +17,7 @@ fr: choose_method: Comment souhaitez-vous vérifier votre identité? enter_info: Entrez vos informations present_cac: Presente su tarjeta PIV / CAC para verificar su cuenta + success: Nous avons vérifié vos informations et votre carte PIV / CAC. verify: Vérifiez vos informations welcome: Nous devons vérifier votre identité info: diff --git a/config/locales/image_description/en.yml b/config/locales/image_description/en.yml index 937df2a9774..c90a078f1c6 100644 --- a/config/locales/image_description/en.yml +++ b/config/locales/image_description/en.yml @@ -2,5 +2,6 @@ en: image_description: camera_mobile_phone: Camera flashing on a mobile phone + spinner: Loading spinner totp_qrcode: QR code for authenticator app us_flag: US flag diff --git a/config/locales/image_description/es.yml b/config/locales/image_description/es.yml index 619c89d36fe..44a0413b389 100644 --- a/config/locales/image_description/es.yml +++ b/config/locales/image_description/es.yml @@ -2,5 +2,6 @@ es: image_description: camera_mobile_phone: Cámara parpadeando en un teléfono móvil + spinner: Indicador de carga totp_qrcode: Código QR para la aplicación de autenticación us_flag: Bandera de estados unidos diff --git a/config/locales/image_description/fr.yml b/config/locales/image_description/fr.yml index aa8cb9f7c4e..47487020658 100644 --- a/config/locales/image_description/fr.yml +++ b/config/locales/image_description/fr.yml @@ -2,5 +2,6 @@ fr: image_description: camera_mobile_phone: Appareil photo clignotant sur un téléphone mobile + spinner: Indicateur de chargement totp_qrcode: Code QR pour l'application d'authentification us_flag: Drapeau américain diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index 4a915635ea7..ba6bbaef8e8 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -19,25 +19,20 @@ en: button: Yes, continue deleting cancel_link_text: please cancel help_html: If you don’t want to delete your account, %{cancel_account_reset}. - intro_html: Your 24 hour waiting period has ended. Please complete step 2 of - the process.

    If you’ve been unable to locate your authentication methods, - select "confirm deletion" to delete your %{app} account.

    In the future, - if you need to access participating government websites who use %{app}, you - can create a new %{app} account using the same email address after your account - is deleted.

    - subject: Delete your login.gov account + intro_html: You are receiving this email because you requested to delete and + reset your login.gov account.

    Deleting your account should be your + last resort if you are locked out of your account. You will not be able to + recover any information linked to your account. Once your account is deleted, + you can create a new one using the same email address.

    Are you sure + you want to delete your account? + subject: Delete your account account_reset_request: cancel: Don't want to delete your account? Sign in to your login.gov account to cancel. header: Your account will be deleted in 24 hours - intro_html: 'As a security measure, %{app} requires a two-step process to delete - your account:

    Step One: There is a 24 hour waiting period if you have - lost access to your authentication methods and need to delete your account. - If you locate your authentication methods, you can sign in to your %{app} - account to cancel this request.

    Step Two: After your 24 hour waiting - period, you will receive an email that will ask you to confirm the deletion - of your %{app} account. Your account will not be deleted until you confirm.' - subject: How to delete your login.gov account + intro: To ensure quality and security, the account deletion process takes 24 + hours. You will receive a confirmation email once this process is completed. + subject: Delete your account add_email: footer: This link will expire in %{confirmation_period}. header: Thanks for adding an email. Please click the link below or copy and diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index 062be784fcd..d1c2340c868 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -20,26 +20,21 @@ es: button: Sí, continúa eliminando cancel_link_text: por favor cancele help_html: Si no desea eliminar su cuenta, %{cancel_account_reset}. - intro_html: Su período de espera de 24 horas ha finalizado. Complete el paso - 2 del proceso.

    Si no ha podido localizar sus métodos de autenticación, - seleccione "confirmar eliminación" para eliminar su cuenta de %{app}.

    - En el futuro, si necesita acceder a los sitios web gubernamentales participantes - que utilizan %{app}, puede crear una nueva cuenta %{app} con la misma dirección - de correo electrónico después de que se elimine su cuenta.

    - subject: Elimina tu cuenta login.gov + intro_html: Recibes este correo electrónico porque solicitaste eliminar y restablecer + su cuenta de login.gov

    Eliminar tu cuenta debería ser tu último recurso + si está bloqueado de su cuenta. No podrás recuperar cualquier información + vinculada a su cuenta. Una vez que su cuenta es eliminada, usted puede crear + uno nuevo usando la misma dirección de correo electrónico.

    ¿Estás seguro + de que ¿Desea eliminar su cuenta? + subject: Eliminar su cuenta account_reset_request: cancel: "¿No quieres eliminar tu cuenta? Inicie sesión en su cuenta login.gov para cancelar." header: Su cuenta será eliminada en 24 horas - intro_html: 'Como medida de seguridad, %{app} requiere un proceso de dos pasos - para eliminar su cuenta:

    Paso uno: hay un período de espera de 24 - horas si ha perdido el acceso a sus métodos de autenticación y necesita eliminar - su cuenta. Si encuentra sus métodos de autenticación, puede iniciar sesión - en su cuenta %{app} para cancelar esta solicitud.

    Paso dos: Después - de su período de espera de 24 horas, recibirá un correo electrónico que le - pedirá que confirme la eliminación de su cuenta %{app}. Su cuenta no se eliminará - hasta que confirme.' - subject: Cómo eliminar su cuenta de login.gov + intro: Para garantizar la calidad y la seguridad, el proceso de eliminación + de la cuenta lleva 24 horas. Recibirá un correo electrónico de confirmación + una vez que se complete este proceso. + subject: Eliminar su cuenta add_email: footer: Este enlace expira en %{confirmation_period}. header: Gracias por enviar su correo electrónico. Haga clic en el enlace debajo diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml index 109b8299cbf..355bd30f848 100644 --- a/config/locales/user_mailer/fr.yml +++ b/config/locales/user_mailer/fr.yml @@ -20,28 +20,22 @@ fr: button: Oui, continuez la suppression cancel_link_text: veuillez annuler help_html: Si vous ne souhaitez pas supprimer votre compte, %{cancel_account_reset}. - intro_html: Votre période d'attente de 24 heures est terminée. Veuillez terminer - l'étape 2 du processus.

    Si vous ne parvenez pas à localiser vos méthodes - d'authentification, sélectionnez "confirmer la suppression" pour supprimer - votre compte %{app}.

    À l'avenir, si vous devez accéder aux sites Web - gouvernementaux participants qui utilisent %{app}, vous pouvez créer un nouveau - compte %{app} en utilisant la même adresse e-mail après la suppression de - votre compte.

    - subject: Supprimer votre compte login.gov + intro_html: Vous recevez cet e-mail parce que vous avez demandé à supprimer + et à réinitialiser votre compte login.gov.

    Supprimer votre compte + devrait être votre dernier recours si vous êtes exclu de votre compte. Vous + ne serez pas en mesure de récupérer toute information liée à votre compte. + Une fois votre compte supprimé, vous pouvez en créer un nouveau en utilisant + la même adresse email.

    Es-tu sûr de toi voulez-vous supprimer votre + compte? + subject: Supprimer votre compte account_reset_request: cancel: Vous ne voulez pas supprimer votre compte? Connectez-vous à votre compte login.gov pour annuler. header: Votre compte sera supprimé dans 24 heures - intro_html: 'Par mesure de sécurité, %{app} nécessite un processus en deux étapes - pour supprimer votre compte:

    Étape 1: Il y a une période d''attente - de 24 heures si vous avez perdu l''accès à vos méthodes d''authentification - et devez supprimer votre compte. Si vous trouvez vos méthodes d''authentification, - vous pouvez vous connecter à votre compte %{app} pour annuler cette demande. -

    Deuxième étape: après votre période d''attente de 24 heures, vous - recevrez un e-mail qui vous demandera de confirmer la suppression de votre - compte %{app}. Votre compte ne sera pas supprimé tant que vous ne l''aurez - pas confirmé.' - subject: Comment supprimer votre compte login.gov + intro: Pour garantir la qualité et la sécurité, le processus de suppression + de compte prend 24 heures. Vous recevrez un e-mail de confirmation une fois + ce processus terminé. + subject: Supprimer votre compte add_email: footer: Ce lien expirera dans %{confirmation_period}. header: Merci d'avoir ajouté un email. S'il vous plaît cliquez sur le lien ci-dessous diff --git a/config/locales/users/en.yml b/config/locales/users/en.yml index 1ec8d9bd081..95640baeb6a 100644 --- a/config/locales/users/en.yml +++ b/config/locales/users/en.yml @@ -12,8 +12,6 @@ en: bullet_2_loa3: "%{app} will delete your email address, password, phone number, name, address, date of birth and Social Security number from our system." bullet_3: You won't be able to securely access your information using %{app}. - bullet_4: We will notify the agencies you access with %{app} that you no longer - have an account heading: Are you sure you want to delete your account? instructions: Enter your password to confirm that you want to delete your account. subheading: If you delete your account diff --git a/config/locales/users/es.yml b/config/locales/users/es.yml index 7fcddb0e817..6a1674cfefc 100644 --- a/config/locales/users/es.yml +++ b/config/locales/users/es.yml @@ -13,8 +13,6 @@ es: bullet_2_loa3: "%{app} borrará su email, contraseña, número de teléfono, nombre, dirección, fecha de nacimiento y número de Seguro Social de nuestro sistema." bullet_3: Usted no podrá tener acceso seguro a su información usando %{app} - bullet_4: Notificaremos a las agencias a las que acceda con %{app} que no ya - tengo una cuenta heading: "¿Está seguro que desea eliminar su cuenta?" instructions: Ingrese su contraseña para confirmar que desea eliminar su cuenta. subheading: Si elimina su cuenta diff --git a/config/locales/users/fr.yml b/config/locales/users/fr.yml index ecfed8eb471..53dbcd73a58 100644 --- a/config/locales/users/fr.yml +++ b/config/locales/users/fr.yml @@ -15,8 +15,6 @@ fr: et votre numéro de sécurité sociale de notre système." bullet_3: Vous ne pourrez pas accéder en toute sécurité à vos informations en utilisant %{app}. - bullet_4: Nous informerons les agences auxquelles vous accédez avec %{app} que - vous ne plus avoir un compte heading: Êtes-vous sûr de vouloir supprimer votre compte? instructions: Saisissez votre mot de passe pour confirmer que vous souhaitez supprimer votre compte. diff --git a/config/routes.rb b/config/routes.rb index 5a20bfbfbaa..1009f038607 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,13 +75,15 @@ get '/active' => 'users/sessions#active' post '/sessions/keepalive' => 'users/sessions#keepalive' - get '/login/piv_cac' => 'users/piv_cac_login#new' - get '/login/piv_cac_account_not_found' => 'users/piv_cac_login#account_not_found' - get '/login/piv_cac_did_not_work' => 'users/piv_cac_login#did_not_work' - get '/login/piv_cac_temporary_error' => 'users/piv_cac_login#temporary_error' - get '/login/present_piv_cac' => 'users/piv_cac_login#redirect_to_piv_cac_service' - get '/login/password' => 'password_capture#new', as: :capture_password - post '/login/password' => 'password_capture#create' + if FeatureManagement.allow_piv_cac_login? + get '/login/piv_cac' => 'users/piv_cac_login#new' + get '/login/piv_cac_account_not_found' => 'users/piv_cac_login#account_not_found' + get '/login/piv_cac_did_not_work' => 'users/piv_cac_login#did_not_work' + get '/login/piv_cac_temporary_error' => 'users/piv_cac_login#temporary_error' + get '/login/present_piv_cac' => 'users/piv_cac_login#redirect_to_piv_cac_service' + get '/login/password' => 'password_capture#new', as: :capture_password + post '/login/password' => 'password_capture#create' + end get '/account_reset/request' => 'account_reset/request#show' post '/account_reset/request' => 'account_reset/request#create' diff --git a/config/service_providers.localdev.yml b/config/service_providers.localdev.yml index 40d4351b4e4..7673db457c5 100644 --- a/config/service_providers.localdev.yml +++ b/config/service_providers.localdev.yml @@ -111,15 +111,6 @@ test: friendly_name: 'Test SP' allow_prompt_login: true - 'https://rp3.serviceprovider.com/auth/saml/metadata': - acs_url: 'http://example.com/test/saml/decode_assertion' - assertion_consumer_logout_service_url: 'http://example.com/test/saml/decode_slo_request' - block_encryption: 'aes256-cbc' - cert: 'saml_test_sp' - ial: 2 - friendly_name: 'Test SP' - allow_prompt_login: true - 'http://test.host': acs_url: 'http://test.host/test/saml/decode_assertion' block_encryption: 'aes256-cbc' @@ -183,18 +174,6 @@ test: ial: 2 allow_prompt_login: true - 'urn:gov:gsa:openidconnect:sp:server_two': - agency_id: 2 - redirect_uris: - - 'http://localhost:7654/auth/result' - - 'https://example.com' - - 'http://www.example.com/test/oidc' - cert: 'saml_test_sp' - friendly_name: 'Test SP' - assertion_consumer_logout_service_url: '' - ial: 2 - allow_prompt_login: true - 'urn:gov:gsa:openidconnect:sp:server_requiring_aal3': agency_id: 2 redirect_uris: diff --git a/db/migrate/20210218185311_remove_user_id_index_from_events.rb b/db/migrate/20210218185311_remove_user_id_index_from_events.rb deleted file mode 100644 index 58f4b19197d..00000000000 --- a/db/migrate/20210218185311_remove_user_id_index_from_events.rb +++ /dev/null @@ -1,5 +0,0 @@ -class RemoveUserIdIndexFromEvents < ActiveRecord::Migration[6.1] - def change - remove_index :events, name: "index_events_on_user_id" - end -end diff --git a/db/migrate/20210223011217_add_uniqueness_to_agency_abbreviation.rb b/db/migrate/20210223011217_add_uniqueness_to_agency_abbreviation.rb deleted file mode 100644 index b9b832f9457..00000000000 --- a/db/migrate/20210223011217_add_uniqueness_to_agency_abbreviation.rb +++ /dev/null @@ -1,7 +0,0 @@ -class AddUniquenessToAgencyAbbreviation < ActiveRecord::Migration[6.1] - disable_ddl_transaction! - - def change - add_index :agencies, :abbreviation, unique: true, algorithm: :concurrently - end -end diff --git a/db/migrate/20210223232534_add_transaction_id_to_sp_costs.rb b/db/migrate/20210223232534_add_transaction_id_to_sp_costs.rb deleted file mode 100644 index 46e1585e632..00000000000 --- a/db/migrate/20210223232534_add_transaction_id_to_sp_costs.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddTransactionIdToSpCosts < ActiveRecord::Migration[6.1] - def change - add_column :sp_costs, :transaction_id, :string, null: true - end -end diff --git a/db/migrate/202102245131_letter_requests_to_usps_ftp_logs.rb b/db/migrate/202102245131_letter_requests_to_usps_ftp_logs.rb deleted file mode 100644 index b284bbab2e9..00000000000 --- a/db/migrate/202102245131_letter_requests_to_usps_ftp_logs.rb +++ /dev/null @@ -1,9 +0,0 @@ -class LetterRequestsToUspsFtpLogs < ActiveRecord::Migration[6.1] - def change - create_table :letter_requests_to_usps_ftp_logs do |t| - t.timestamp :ftp_at, null: false - t.integer :letter_requests_count, null: false - end - add_index :letter_requests_to_usps_ftp_logs, %i[ftp_at] - end -end diff --git a/db/schema.rb b/db/schema.rb index 47d6865fd04..0a00fc39608 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.define(version: 2021_02_23_232534) do +ActiveRecord::Schema.define(version: 2021_02_03_002937) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,7 +44,6 @@ create_table "agencies", force: :cascade do |t| t.string "name", null: false t.string "abbreviation" - t.index ["abbreviation"], name: "index_agencies_on_abbreviation", unique: true t.index ["name"], name: "index_agencies_on_name", unique: true end @@ -168,12 +167,12 @@ t.integer "choose_method_view_count", default: 0 t.datetime "present_cac_view_at" t.integer "present_cac_view_count", default: 0 + t.integer "present_cac_submit_count", default: 0 + t.integer "present_cac_error_count", default: 0 t.datetime "enter_info_view_at" t.integer "enter_info_view_count", default: 0 t.datetime "success_view_at" t.integer "success_view_count", default: 0 - t.integer "present_cac_submit_count", default: 0 - t.integer "present_cac_error_count", default: 0 t.datetime "selfie_view_at" t.integer "selfie_view_count", default: 0 t.integer "selfie_submit_count", default: 0 @@ -241,6 +240,7 @@ t.index ["device_id", "created_at"], name: "index_events_on_device_id_and_created_at" t.index ["disavowal_token_fingerprint"], name: "index_events_on_disavowal_token_fingerprint" t.index ["user_id", "created_at"], name: "index_events_on_user_id_and_created_at" + t.index ["user_id"], name: "index_events_on_user_id" end create_table "iaa_gtcs", force: :cascade do |t| @@ -349,12 +349,6 @@ t.index ["job_name", "finish_time"], name: "index_job_runs_on_job_name_and_finish_time" end - create_table "letter_requests_to_usps_ftp_logs", force: :cascade do |t| - t.datetime "ftp_at", null: false - t.integer "letter_requests_count", null: false - t.index ["ftp_at"], name: "index_letter_requests_to_usps_ftp_logs_on_ftp_at" - end - create_table "monthly_auth_counts", force: :cascade do |t| t.string "issuer", null: false t.string "year_month", null: false @@ -574,7 +568,6 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "ial" - t.string "transaction_id" t.index ["created_at"], name: "index_sp_costs_on_created_at" end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index ed660856df9..d0ee513585a 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -16,6 +16,10 @@ def self.identity_pki_disabled? !env.piv_cac_verify_token_url end + def self.allow_piv_cac_login? + AppConfig.env.login_with_piv_cac == 'true' + end + def self.development_and_identity_pki_disabled? # This controls if we try to hop over to identity-pki or just throw up # a screen asking for a Subject or one of a list of error conditions. diff --git a/lib/lambda_jobs/git_ref.rb b/lib/lambda_jobs/git_ref.rb index 2b694db66d6..951a7925b96 100644 --- a/lib/lambda_jobs/git_ref.rb +++ b/lib/lambda_jobs/git_ref.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module LambdaJobs - GIT_REF = 'eb8aa1657173af64fd9fcad2ab4df2a5741eb51d' + GIT_REF = '8c16776e19b211d15bda7246d99ff95155d60c11' end diff --git a/mac-test-passphrase-prompt.png b/mac-test-passphrase-prompt.png new file mode 100644 index 00000000000..649e1bf0270 Binary files /dev/null and b/mac-test-passphrase-prompt.png differ diff --git a/package.json b/package.json index 8b57793cea5..53527122d73 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "@peculiar/webcrypto": "^1.1.6", + "@peculiar/webcrypto": "^1.1.4", "@testing-library/dom": "^7.29.0", "@testing-library/react": "^11.2.2", "@testing-library/react-hooks": "^3.7.0", diff --git a/spec/config/initializers/job_configurations.rb b/spec/config/initializers/job_configurations.rb index 90685433269..ec0105d68ad 100644 --- a/spec/config/initializers/job_configurations.rb +++ b/spec/config/initializers/job_configurations.rb @@ -255,19 +255,5 @@ expect(job.callback.call).to eq 'the report test worked' end - - it 'runs the Monthly USPS letter requests report' do - job = JobRunner::Runner.configurations.find do |c| - c.name == 'Monthly USPS letter requests report' - end - expect(job).to be_instance_of(JobRunner::JobConfiguration) - expect(job.interval).to eq 24 * 60 * 60 - - service = instance_double(Reports::MonthlyUspsLetterRequestsReport) - expect(Reports::MonthlyUspsLetterRequestsReport).to receive(:new).and_return(service) - expect(service).to receive(:call).and_return('the report test worked') - - expect(job.callback.call).to eq 'the report test worked' - end end end diff --git a/spec/controllers/concerns/idv/document_capture_concern_spec.rb b/spec/controllers/concerns/idv/document_capture_concern_spec.rb deleted file mode 100644 index c3ee2df2e02..00000000000 --- a/spec/controllers/concerns/idv/document_capture_concern_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'rails_helper' - -RSpec.describe Idv::DocumentCaptureConcern, type: :controller do - controller ApplicationController do - include Idv::DocumentCaptureConcern - - before_action :override_document_capture_step_csp - - def index; end - end - - describe '#override_document_capture_step_csp' do - it 'sets the headers for the document capture step' do - get :index, params: { step: 'document_capture' } - - csp = response.request.headers.env['secure_headers_request_config'].csp - expect(csp.script_src).to include("'unsafe-eval'") - expect(csp.img_src).to include('blob:') - end - - it 'does not set headers for any other step' do - get :index, params: { step: 'some_other_step' } - - secure_header_config = response.request.headers.env['secure_headers_request_config'] - expect(secure_header_config).to be_nil - end - end -end diff --git a/spec/controllers/idv/capture_doc_controller_spec.rb b/spec/controllers/idv/capture_doc_controller_spec.rb index fcf96ec46ae..10b3a9023d4 100644 --- a/spec/controllers/idv/capture_doc_controller_spec.rb +++ b/spec/controllers/idv/capture_doc_controller_spec.rb @@ -123,6 +123,27 @@ hash_including(step: 'capture_complete', step_count: 2), ) end + + it 'add unsafe-eval to the CSP for capture steps' do + steps = %i[document_capture] + steps.each do |step| + mock_next_step(step) + + get :show, params: { step: step } + + script_src = response.request.headers.env['secure_headers_request_config'].csp.script_src + expect(script_src).to include("'unsafe-eval'") + end + end + + it 'does not add unsafe-eval to the CSP for non-capture steps' do + mock_next_step(:capture_complete) + + get :show, params: { step: 'capture_complete' } + + secure_header_config = response.request.headers.env['secure_headers_request_config'] + expect(secure_header_config).to be_nil + end end end diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb index 33aedad31c9..03233c84e1c 100644 --- a/spec/controllers/idv/doc_auth_controller_spec.rb +++ b/spec/controllers/idv/doc_auth_controller_spec.rb @@ -66,41 +66,40 @@ get :show, params: { step: 'welcome' } - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} welcome visited".downcase, result - ) expect(@analytics).to have_received(:track_event).with( Analytics::DOC_AUTH + ' visited', result ) end - it 'tracks analytics for the optional step' do - mock_next_step(:verify_wait) - result = { errors: {}, step: Idv::Steps::VerifyWaitStepShow, success: true } - - get :show, params: { step: 'verify_wait' } - - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} optional verify_wait submitted".downcase, result - ) - expect(@analytics).to have_received(:track_event).with( - Analytics::DOC_AUTH + ' optional submitted', result - ) - end - it 'increments the analytics step counts on subsequent submissions' do get :show, params: { step: 'welcome' } get :show, params: { step: 'welcome' } expect(@analytics).to have_received(:track_event).ordered.with( - Analytics::DOC_AUTH + ' visited', - hash_including(step: 'welcome', step_count: 1), + Analytics::DOC_AUTH + ' visited', hash_including(step: 'welcome', step_count: 1) ) expect(@analytics).to have_received(:track_event).ordered.with( - Analytics::DOC_AUTH + ' visited', - hash_including(step: 'welcome', step_count: 2), + Analytics::DOC_AUTH + ' visited', hash_including(step: 'welcome', step_count: 2) ) end + + it 'add unsafe-eval to the CSP for the doucment capture step' do + mock_next_step(:document_capture) + + get :show, params: { step: :document_capture } + + script_src = response.request.headers.env['secure_headers_request_config'].csp.script_src + expect(script_src).to include("'unsafe-eval'") + end + + it 'does not add unsafe-eval to the CSP for non-capture steps' do + mock_next_step(:ssn) + + get :show, params: { step: 'ssn' } + + secure_header_config = response.request.headers.env['secure_headers_request_config'] + expect(secure_header_config).to be_nil + end end describe '#update' do @@ -112,9 +111,6 @@ put :update, params: {step: 'ssn', doc_auth: { step: 'ssn', ssn: '111-11-1111' } } - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} ssn submitted".downcase, result - ) expect(@analytics).to have_received(:track_event).with( Analytics::DOC_AUTH + ' submitted', result ) @@ -135,14 +131,6 @@ expect(@analytics).to have_received(:track_event).ordered.with( Analytics::DOC_AUTH + ' submitted', hash_including(step: 'ssn', step_count: 2) ) - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} ssn submitted".downcase, - hash_including(step: 'ssn', step_count: 1), - ) - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} ssn submitted".downcase, - hash_including(step: 'ssn', step_count: 2), - ) end it 'progresses from welcome to upload' do @@ -170,9 +158,6 @@ } expect(response).to redirect_to idv_doc_auth_errors_no_camera_url - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} welcome submitted".downcase, result - ) expect(@analytics).to have_received(:track_event).with( Analytics::DOC_AUTH + ' submitted', result ) @@ -368,15 +353,6 @@ message: I18n.t('doc_auth.errors.lexis_nexis.general_error_no_liveness') }], remaining_attempts: AppConfig.env.acuant_max_attempts.to_i, }.to_json) - expect(@analytics).to have_received(:track_event).with( - 'IdV: ' + "#{Analytics::DOC_AUTH} verify_document_status submitted".downcase, { - errors: { pii: [I18n.t('doc_auth.errors.lexis_nexis.general_error_no_liveness')] }, - success: false, - remaining_attempts: AppConfig.env.acuant_max_attempts.to_i, - step: 'verify_document_status', - step_count: 1, - } - ) expect(@analytics).to have_received(:track_event).with( Analytics::DOC_AUTH + ' submitted', { errors: { pii: [I18n.t('doc_auth.errors.lexis_nexis.general_error_no_liveness')] }, diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 6fba548a80b..04c7dde663e 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -14,7 +14,6 @@ document_capture_session_uuid: document_capture_session.uuid, } end - let(:json) { JSON.parse(response.body, symbolize_names: true) } before do Funnel::DocAuth::RegisterStep.new(user.id, '').call('welcome', :view, true) @@ -26,6 +25,7 @@ it 'returns error status when not provided image fields' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:success]).to eq(false) expect(json[:errors]).to eq [ @@ -63,6 +63,7 @@ it 'returns an error' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:errors]).to eq [ { field: 'front', message: I18n.t('doc_auth.errors.not_a_file') }, @@ -75,6 +76,7 @@ it 'translates errors using the locale param' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:errors]).to eq [ { field: 'front', message: I18n.t('doc_auth.errors.not_a_file', locale: 'es') }, @@ -111,6 +113,7 @@ params.delete(:document_capture_session_uuid) action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:success]).to eq(false) expect(json[:errors]).to eq [ @@ -122,6 +125,7 @@ params[:document_capture_session_uuid] = 'bad uuid' action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:success]).to eq(false) expect(json[:errors]).to eq [ @@ -137,6 +141,7 @@ action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json).to eq({ success: false, @@ -151,6 +156,7 @@ action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(429) expect(json).to eq({ success: false, @@ -189,6 +195,7 @@ it 'returns a successful response and modifies the session' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(200) expect(json[:success]).to eq(true) expect(document_capture_session.reload.load_result.success?).to eq(true) @@ -215,152 +222,10 @@ user_id: user.uuid, ) - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - success: true, - errors: {}, - user_id: user.uuid, - ) - action expect_funnel_update_counts(user, 1) end - - context 'but doc_pii validation fails' do - let(:first_name) { 'FAKEY' } - let(:last_name) { 'MCFAKERSON' } - let(:state) { 'ND' } - let(:dob) { '10/06/1938' } - - before do - IdentityDocAuth::Mock::DocAuthMockClient.mock_response!( - method: :get_results, - response: IdentityDocAuth::Response.new( - success: true, - errors: {}, - extra: { result: 'Passed', billed: true }, - pii_from_doc: { - first_name: first_name, - last_name: last_name, - state: state, - dob: dob, - }, - ), - ) - end - - context 'due to invalid Name' do - let(:first_name) { nil } - - it 'tracks name validation errors in analytics' do - stub_analytics - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM, - success: true, - errors: {}, - user_id: user.uuid, - remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, - success: true, - errors: {}, - billed: true, - exception: nil, - result: 'Passed', - user_id: user.uuid, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - success: false, - errors: { - pii: [I18n.t('doc_auth.errors.lexis_nexis.full_name_check')], - }, - user_id: user.uuid, - ) - - action - end - end - - context 'due to invalid State' do - let(:state) { 'Maryland' } - - it 'tracks state validation errors in analytics' do - stub_analytics - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM, - success: true, - errors: {}, - user_id: user.uuid, - remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, - success: true, - errors: {}, - billed: true, - exception: nil, - result: 'Passed', - user_id: user.uuid, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - success: false, - errors: { - pii: [I18n.t('doc_auth.errors.lexis_nexis.general_error_no_liveness')], - }, - user_id: user.uuid, - ) - - action - end - end - - context 'but doc_pii validation fails due to invalid DOB' do - let(:dob) { nil } - - it 'tracks dob validation errors in analytics' do - stub_analytics - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_FORM, - success: true, - errors: {}, - user_id: user.uuid, - remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, - success: true, - errors: {}, - billed: true, - exception: nil, - result: 'Passed', - user_id: user.uuid, - ) - - expect(@analytics).to receive(:track_event).with( - Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - success: false, - errors: { - pii: [I18n.t('doc_auth.errors.lexis_nexis.birth_date_checks')], - }, - user_id: user.uuid, - ) - - action - end - end - end end context 'when image upload fails' do @@ -377,6 +242,7 @@ it 'returns an error response' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:success]).to eq(false) expect(json[:remaining_attempts]).to be_a_kind_of(Numeric) @@ -419,6 +285,7 @@ it 'returns error from yaml file' do action + json = JSON.parse(response.body, symbolize_names: true) expect(json[:remaining_attempts]).to be_a_kind_of(Numeric) expect(json[:errors]).to eq [ { @@ -463,6 +330,7 @@ it 'returns error' do action + json = JSON.parse(response.body, symbolize_names: true) expect(response.status).to eq(400) expect(json[:success]).to eq(false) expect(json[:remaining_attempts]).to be_a_kind_of(Numeric) diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 27868dc0dc2..4c49de2fb7f 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -243,7 +243,6 @@ expect(session[:sp]).to eq( aal_level_requested: nil, piv_cac_requested: false, - ial: 1, ial2: false, ial2_strict: false, ialmax: false, diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 7bc918379a4..1460f8b8f1c 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -50,7 +50,7 @@ get :metadata end - let(:org_name) { 'login.gov' } + let(:org_name) { '18F' } let(:xmldoc) { SamlResponseDoc.new('controller', 'metadata', response) } it 'renders XML inline' do @@ -381,7 +381,6 @@ issuer: saml_settings.issuer, aal_level_requested: aal_level, piv_cac_requested: false, - ial: 1, ial2: false, ial2_strict: false, ialmax: false, @@ -405,7 +404,6 @@ issuer: saml_settings.issuer, aal_level_requested: aal_level, piv_cac_requested: false, - ial: 1, ial2: false, ial2_strict: false, ialmax: false, diff --git a/spec/controllers/sign_up/completions_controller_spec.rb b/spec/controllers/sign_up/completions_controller_spec.rb index f22117aedc6..fbf2d64815f 100644 --- a/spec/controllers/sign_up/completions_controller_spec.rb +++ b/spec/controllers/sign_up/completions_controller_spec.rb @@ -126,7 +126,7 @@ it 'updates verified attributes' do stub_sign_in subject.session[:sp] = { - ial: 1, + ial2: false, request_url: 'http://example.com', requested_attributes: ['email'], } @@ -166,7 +166,7 @@ user = create(:user, profiles: [create(:profile, :verified, :active)]) stub_sign_in(user) subject.session[:sp] = { - ial: 2, + ial2: true, request_url: 'http://example.com', requested_attributes: %w[email first_name verified_at], } diff --git a/spec/features/idv/actions/cancel_link_sent_action_spec.rb b/spec/features/idv/actions/cancel_link_sent_action_spec.rb deleted file mode 100644 index 8b6b1d80398..00000000000 --- a/spec/features/idv/actions/cancel_link_sent_action_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails_helper' - -feature 'doc auth cancel link sent action' do - include IdvStepHelper - include DocAuthHelper - - before do - sign_in_and_2fa_user - complete_doc_auth_steps_before_link_sent_step - end - - it 'returns to send link step' do - click_doc_auth_back_link - - expect(page).to have_current_path(idv_doc_auth_send_link_step) - end -end diff --git a/spec/features/idv/actions/cancel_send_link_action_spec.rb b/spec/features/idv/actions/cancel_send_link_action_spec.rb deleted file mode 100644 index 82bd3e79bf4..00000000000 --- a/spec/features/idv/actions/cancel_send_link_action_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rails_helper' - -feature 'doc auth cancel send link action' do - include IdvStepHelper - include DocAuthHelper - - before do - sign_in_and_2fa_user - complete_doc_auth_steps_before_send_link_step - end - - it 'returns to upload step' do - click_doc_auth_back_link - - expect(page).to have_current_path(idv_doc_auth_upload_step) - end -end diff --git a/spec/features/idv/cac/success_step_spec.rb b/spec/features/idv/cac/success_step_spec.rb new file mode 100644 index 00000000000..062758467da --- /dev/null +++ b/spec/features/idv/cac/success_step_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +feature 'cac proofing success step' do + include CacProofingHelper + + before do + sign_in_and_2fa_user + complete_cac_proofing_steps_before_success_step + end + + it 'is on the correct page' do + expect(page).to have_current_path(idv_cac_proofing_success_step) + end + + it 'proceeds to the next page' do + click_continue + + expect(page).to have_current_path(idv_phone_path) + end +end diff --git a/spec/features/idv/cac/use_cac_step_spec.rb b/spec/features/idv/cac/use_cac_step_spec.rb index c33097af8d6..4f92836dc01 100644 --- a/spec/features/idv/cac/use_cac_step_spec.rb +++ b/spec/features/idv/cac/use_cac_step_spec.rb @@ -8,7 +8,7 @@ strip_tags(t('doc_auth.info.use_cac', link: t('doc_auth.info.use_cac_link'))) end - it 'shows cac proofing option if cac proofing on desktop' do + it 'shows cac proofing option if cac proofing is enabled' do sign_in_and_2fa_user complete_doc_auth_steps_before_upload_step @@ -18,6 +18,15 @@ expect(page).to have_current_path(idv_cac_proofing_choose_method_step) end + it 'does not show cac proofing option if cac proofing is disabled' do + allow(AppConfig.env).to receive(:cac_proofing_enabled).and_return('false') + + sign_in_and_2fa_user + complete_doc_auth_steps_before_upload_step + + expect(page).to_not have_content use_cac_content + end + it 'does not show cac proofing option on mobile' do allow(DeviceDetector).to receive(:new).and_return(mobile_device) diff --git a/spec/features/idv/cac/verify_step_spec.rb b/spec/features/idv/cac/verify_step_spec.rb index 1ae799c20e9..ae76522b41a 100644 --- a/spec/features/idv/cac/verify_step_spec.rb +++ b/spec/features/idv/cac/verify_step_spec.rb @@ -15,7 +15,7 @@ expect(page).to have_current_path(idv_cac_proofing_verify_step) click_continue - expect(page).to have_current_path(idv_phone_path) + expect(page).to have_current_path(idv_cac_proofing_success_step) expect(SpCost.count).to eq(1) sp_cost = SpCost.first @@ -58,7 +58,7 @@ expect(page).to have_content t('idv.failure.timeout') allow(DocumentCaptureSession).to receive(:find_by).and_call_original click_continue - expect(page).to have_current_path(idv_phone_path) + expect(page).to have_current_path(idv_cac_proofing_success_step) end end @@ -78,7 +78,7 @@ it 'proceeds to the next page upon confirmation' do click_continue - expect(page).to have_current_path(idv_phone_path) + expect(page).to have_current_path(idv_cac_proofing_success_step) end context 'async timed out' do @@ -96,7 +96,7 @@ expect(page).to have_current_path(idv_cac_proofing_verify_step) allow(DocumentCaptureSession).to receive(:find_by).and_call_original click_continue - expect(page).to have_current_path(idv_phone_path) + expect(page).to have_current_path(idv_cac_proofing_success_step) end end end diff --git a/spec/features/idv/clearing_and_restarting_spec.rb b/spec/features/idv/clearing_and_restarting_spec.rb index 468ad0ec26f..5dbc84751ff 100644 --- a/spec/features/idv/clearing_and_restarting_spec.rb +++ b/spec/features/idv/clearing_and_restarting_spec.rb @@ -31,4 +31,30 @@ it_behaves_like 'clearing and restarting idv' end end + + context 'during USPS step' do + context 'sending a letter before signing out' do + before do + start_idv_from_sp + complete_idv_steps_before_usps_step(user) + end + + it_behaves_like 'clearing and restarting idv' + end + + context 're-sending a letter after signing out' do + before do + start_idv_from_sp + complete_idv_steps_with_usps_before_confirmation_step(user) + click_acknowledge_personal_key + visit account_path + first(:link, t('links.sign_out')).click + start_idv_from_sp + sign_in_live_with_2fa(user) + click_on t('idv.messages.usps.resend') + end + + it_behaves_like 'clearing and restarting idv' + end + end end diff --git a/spec/features/idv/doc_auth/address_step_spec.rb b/spec/features/idv/doc_auth/address_step_spec.rb index e4d5caa7456..9b5bdbf8e17 100644 --- a/spec/features/idv/doc_auth/address_step_spec.rb +++ b/spec/features/idv/doc_auth/address_step_spec.rb @@ -27,10 +27,4 @@ click_idv_continue expect(page).to have_current_path(idv_address_path) end - - it 'allows the user to click back to return to the verify step' do - click_doc_auth_back_link - - expect(page).to have_current_path(idv_doc_auth_verify_step) - end end diff --git a/spec/features/idv/doc_auth/document_capture_step_spec.rb b/spec/features/idv/doc_auth/document_capture_step_spec.rb index c314d92451b..e53a279d1ab 100644 --- a/spec/features/idv/doc_auth/document_capture_step_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb @@ -59,12 +59,6 @@ result: 'Passed', billed: true, ) - expect(fake_analytics).to have_logged_event( - 'IdV: ' + "#{Analytics::DOC_AUTH} document_capture submitted".downcase, - step: 'document_capture', - result: 'Passed', - billed: true, - ) expect_costing_for_document end @@ -92,13 +86,6 @@ billed: true, success: false, ) - expect(fake_analytics).to have_logged_event( - 'IdV: ' + "#{Analytics::DOC_AUTH} document_capture submitted".downcase, - step: 'document_capture', - result: 'Passed', - billed: true, - success: false, - ) end it 'offers in person option on failure' do diff --git a/spec/features/idv/doc_auth/link_sent_step_spec.rb b/spec/features/idv/doc_auth/link_sent_step_spec.rb index 2ba8ad99f41..9ed903524eb 100644 --- a/spec/features/idv/doc_auth/link_sent_step_spec.rb +++ b/spec/features/idv/doc_auth/link_sent_step_spec.rb @@ -70,15 +70,16 @@ visit current_path end - context 'clicks back link' do + context 'user cancels flow session' do before do - click_doc_auth_back_link + click_on t('links.cancel') + click_on t('forms.buttons.cancel') visit idv_doc_auth_link_sent_step end - it 'redirects to send link step' do - expect(page).to have_current_path(idv_doc_auth_send_link_step) + it 'redirects to welcome step' do + expect(page).to have_current_path(idv_doc_auth_welcome_step) end end @@ -89,8 +90,6 @@ end expect(page).to_not have_css 'meta[http-equiv="refresh"]', visible: false - click_doc_auth_back_link - click_doc_auth_back_link click_on t('doc_auth.buttons.start_over') complete_doc_auth_steps_before_link_sent_step expect(page).to have_css 'meta[http-equiv="refresh"]', visible: false diff --git a/spec/features/idv/doc_auth/send_link_step_spec.rb b/spec/features/idv/doc_auth/send_link_step_spec.rb index be182f1f50b..89a2635b0ca 100644 --- a/spec/features/idv/doc_auth/send_link_step_spec.rb +++ b/spec/features/idv/doc_auth/send_link_step_spec.rb @@ -62,17 +62,20 @@ it 'throttles sending the link' do user = sign_in_and_2fa_user - complete_doc_auth_steps_before_send_link_step idv_send_link_max_attempts.times do + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_send_link_step expect(page).to_not have_content I18n.t('errors.doc_auth.send_link_throttle') fill_in :doc_auth_phone, with: '415-555-0199' click_idv_continue expect(page).to have_current_path(idv_doc_auth_link_sent_step) - click_doc_auth_back_link + click_on t('doc_auth.buttons.start_over') end + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_send_link_step fill_in :doc_auth_phone, with: '415-555-0199' click_idv_continue expect(page).to have_current_path(idv_doc_auth_send_link_step) diff --git a/spec/features/idv/doc_auth/welcome_step_spec.rb b/spec/features/idv/doc_auth/welcome_step_spec.rb index b3f6a3613c8..01421dc4190 100644 --- a/spec/features/idv/doc_auth/welcome_step_spec.rb +++ b/spec/features/idv/doc_auth/welcome_step_spec.rb @@ -57,14 +57,6 @@ def expect_doc_auth_first_step expect(fake_analytics).to have_logged_event( Analytics::DOC_AUTH + ' submitted', step: 'upload', step_count: 2, success: true ) - expect(fake_analytics).to have_logged_event( - 'IdV: ' + "#{Analytics::DOC_AUTH} upload visited".downcase, step: 'upload', step_count: 1 - ) - expect(fake_analytics).to have_logged_event( - 'IdV: ' + "#{Analytics::DOC_AUTH} upload submitted".downcase, - step: 'upload', step_count: 2, success: true, - ) - end end diff --git a/spec/features/idv/doc_capture/document_capture_step_spec.rb b/spec/features/idv/doc_capture/document_capture_step_spec.rb index ff449fec2cc..41bccda6b76 100644 --- a/spec/features/idv/doc_capture/document_capture_step_spec.rb +++ b/spec/features/idv/doc_capture/document_capture_step_spec.rb @@ -20,52 +20,13 @@ else visit_idp_from_oidc_sp_with_ial2 end + complete_doc_capture_steps_before_first_step(user) allow_any_instance_of(DeviceDetector).to receive(:device_type).and_return('mobile') end - context 'invalid session' do - let!(:request_uri) { doc_capture_request_uri(user) } - - before do - Capybara.reset_session! - expired_minutes = (AppConfig.env.doc_capture_request_valid_for_minutes.to_i + 1).minutes - document_capture_session = user.document_capture_sessions.last - document_capture_session.requested_at -= expired_minutes - document_capture_session.save! - end - - it 'logs events as an anonymous user' do - allow(Analytics).to receive(:new).and_return(fake_analytics) - expect(Analytics).to receive(:new).with(hash_including(user: instance_of(AnonymousUser))) - visit request_uri - - expect(fake_analytics).to have_logged_event( - Analytics::CAPTURE_DOC, - success: false, - ) - end - end - - context 'valid session' do - it 'logs events as the inherited user' do - allow(Analytics).to receive(:new).and_return(fake_analytics) - expect(Analytics).to receive(:new).with(hash_including(user: user)) - complete_doc_capture_steps_before_first_step(user) - - expect(fake_analytics).to have_logged_event( - Analytics::CAPTURE_DOC + ' visited', - step: 'document_capture', - ) - end - end - context 'when liveness checking is enabled' do let(:liveness_enabled) { 'true' } - before do - complete_doc_capture_steps_before_first_step(user) - end - context 'when the SP does not request strict IAL2' do let(:sp_requests_ial2_strict) { false } @@ -130,8 +91,8 @@ end it 'proceeds to the next page with valid info and logs analytics info' do - allow(Analytics).to receive(:new).and_return(fake_analytics) - expect(Analytics).to receive(:new).with(hash_including(user: user)) + allow_any_instance_of(ApplicationController). + to receive(:analytics).and_return(fake_analytics) attach_and_submit_images @@ -198,10 +159,6 @@ context 'when liveness checking is not enabled' do let(:liveness_enabled) { 'false' } - before do - complete_doc_capture_steps_before_first_step(user) - end - it 'is on the correct_page and shows the document upload options' do expect(current_path).to eq(idv_capture_doc_document_capture_step) expect(page).to have_content(t('doc_auth.headings.document_capture_front')) @@ -278,10 +235,6 @@ end context 'when there is a stored result' do - before do - complete_doc_capture_steps_before_first_step(user) - end - it 'proceeds to the next step if the result was successful' do document_capture_session = user.document_capture_sessions.last response = IdentityDocAuth::Response.new(success: true) diff --git a/spec/features/idv/hybrid_flow_spec.rb b/spec/features/idv/hybrid_flow_spec.rb deleted file mode 100644 index 61d341d6240..00000000000 --- a/spec/features/idv/hybrid_flow_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'rails_helper' - -describe 'Hybrid Flow' do - include IdvHelper - include DocAuthHelper - - before do - allow(FeatureManagement).to receive(:doc_capture_polling_enabled?).and_return(true) - allow(AppConfig.env).to receive(:doc_auth_enable_presigned_s3_urls).and_return('true') - allow(AppConfig.env).to receive(:document_capture_async_uploads_enabled).and_return('true') - allow(LoginGov::Hostdata::EC2).to receive(:load). - and_return(OpenStruct.new(region: 'us-west-2', account_id: '123456789')) - end - - it 'proofs and hands off to mobile', js: true do - user = nil - sms_link = nil - - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - sms_link = config[:link] - impl.call(config) - end - - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_send_link_step - fill_in :doc_auth_phone, with: '415-555-0199' - click_idv_continue - - expect(page).to have_content(t('doc_auth.headings.text_message')) - end - - expect(sms_link).to be_present - - perform_in_browser(:mobile) do - visit sms_link - attach_and_submit_images - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - end - - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - - fill_out_ssn_form_ok - click_idv_continue - - expect(page).to have_content(t('doc_auth.headings.verify')) - click_idv_continue - - fill_out_phone_form_mfa_phone(user) - click_idv_continue - - fill_in :user_password, with: Features::SessionHelper::VALID_PASSWORD - click_idv_continue - - acknowledge_and_confirm_personal_key - - expect(page).to have_current_path(account_path) - expect(page).to have_content(t('headings.account.verified_account')) - end - end -end diff --git a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb index 81b0515c7ae..995520db634 100644 --- a/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb +++ b/spec/features/idv/steps/phone_otp_delivery_selection_step_spec.rb @@ -46,19 +46,6 @@ end end - context 'the user opts to verify by mail instead' do - it 'can return back to the OTP selection screen' do - start_idv_from_sp - complete_idv_steps_before_phone_otp_delivery_selection_step - click_on t('idv.form.activate_by_mail') - - expect(page).to have_content(t('idv.titles.mail.verify')) - - click_doc_auth_back_link - expect(current_path).to eq(idv_otp_delivery_method_path) - end - end - context 'with a non-US number' do let(:bahamas_phone) { '+12423270143' } diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 2a09acd2989..b18a76d9cec 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -157,21 +157,6 @@ context "when the user's information cannot be verified" do it_behaves_like 'fail to verify idv info', :phone - it 'links to verify by mail, from which user can return back to the warning screen' do - start_idv_from_sp - complete_idv_steps_before_phone_step - fill_out_phone_form_fail - click_idv_continue - - expect(page).to have_content(t('idv.failure.phone.warning')) - - click_on t('idv.form.activate_by_mail') - expect(page).to have_content(t('idv.titles.mail.verify')) - - click_doc_auth_back_link - expect(page).to have_content(t('idv.failure.phone.warning')) - end - it 'does not render the link to proof by mail if proofing by mail is disabled' do allow(FeatureManagement).to receive(:enable_usps_verification?).and_return(false) diff --git a/spec/features/idv/steps/usps_step_spec.rb b/spec/features/idv/steps/usps_step_spec.rb index 37042a63d7e..4f32e07b5c2 100644 --- a/spec/features/idv/steps/usps_step_spec.rb +++ b/spec/features/idv/steps/usps_step_spec.rb @@ -12,13 +12,14 @@ expect(page).to have_current_path(idv_review_path) end - it 'allows the user to go back' do + it 'allows the user to clear IdV and restart' do start_idv_from_sp complete_idv_steps_before_usps_step - click_doc_auth_back_link + click_on t('idv.messages.clear_and_start_over') - expect(page).to have_current_path(idv_phone_path) + expect(page).to have_content(t('doc_auth.headings.welcome')) + expect(page).to have_current_path(idv_doc_auth_step_path(step: :welcome)) end context 'the user has sent a letter but not verified an OTP' do @@ -34,9 +35,9 @@ expect(page).to have_current_path(idv_come_back_later_path) end - it 'allows the user to return to usps otp confirmation' do + it 'allows the user to cancel and return to usps otp confirmation' do complete_idv_and_return_to_usps_step - click_doc_auth_back_link + click_link t('links.cancel') expect(page).to have_content(t('forms.verify_profile.title')) expect(page).to have_current_path(verify_account_path) @@ -67,4 +68,10 @@ def expect_user_to_be_unverified(user) expect(profile.deactivation_reason).to eq 'verification_pending' end end + + context 'cancelling IdV' do + it_behaves_like 'cancel at idv step', :usps + it_behaves_like 'cancel at idv step', :usps, :oidc + it_behaves_like 'cancel at idv step', :usps, :saml + end end diff --git a/spec/features/openid_connect/openid_connect_spec.rb b/spec/features/openid_connect/openid_connect_spec.rb index 5f655101d65..c1cd2ca74e5 100644 --- a/spec/features/openid_connect/openid_connect_spec.rb +++ b/spec/features/openid_connect/openid_connect_spec.rb @@ -89,6 +89,7 @@ click_submit_default expect(current_url).to start_with('http://localhost:7654/auth/result') + expect(page.get_rack_session.keys).to include('sp') end it 'auto-allows and includes redirect_uris in CSP headers after an incorrect OTP' do @@ -115,6 +116,7 @@ click_submit_default expect(current_url).to start_with('http://localhost:7654/auth/result') + expect(page.get_rack_session.keys).to include('sp') end end @@ -135,6 +137,11 @@ "http://www.example.com/openid_connect/logout?id_token_hint=#{id_token}", ) + expect(page.response_headers['Content-Security-Policy']).to include( + 'form-action \'self\' gov.gsa.openidconnect.test://result '\ + 'gov.gsa.openidconnect.test://result/signout', + ) + visit account_path expect(page).to_not have_content(t('headings.account.login_info')) expect(page).to have_content(t('headings.sign_in_without_sp')) @@ -481,6 +488,7 @@ continue_as(email) redirect_uri = URI(current_url) expect(redirect_uri.to_s).to start_with('gov.gsa.openidconnect.test://result') + expect(page.get_rack_session.keys).to include('sp') end end end diff --git a/spec/features/reports/authorization_count_spec.rb b/spec/features/reports/authorization_count_spec.rb deleted file mode 100644 index b88c203cee7..00000000000 --- a/spec/features/reports/authorization_count_spec.rb +++ /dev/null @@ -1,203 +0,0 @@ -require 'rails_helper' - -describe 'OpenID Connect' do - include IdvFromSpHelper - include OidcAuthHelper - include DocAuthHelper - - let(:email) { 'test@test.com' } - let(:password) { RequestHelper::VALID_PASSWORD } - let(:today) { Time.zone.today } - let(:client_id_1) { 'urn:gov:gsa:openidconnect:sp:server' } - let(:client_id_2) { 'urn:gov:gsa:openidconnect:sp:server_two' } - let(:issuer_1) { 'https://rp1.serviceprovider.com/auth/saml/metadata' } - let(:issuer_2) { 'https://rp3.serviceprovider.com/auth/saml/metadata' } - - context 'an IAL1 user with an active session' do - before do - create_ial1_user_from_sp(email) - user = User.find_with_email(email) - end - - context 'using oidc' do - it 'does not count second IAL1 auth at same sp' do - visit_idp_from_ial1_oidc_sp(client_id: client_id_1) - click_continue - expect_ial1_count_only(client_id_1) - - visit_idp_from_ial1_oidc_sp(client_id: client_id_1) - click_continue - expect_ial1_count_only(client_id_1) - end - - it 'counts step up from IAL1 to IAL2 after proofing' do - visit_idp_from_ial1_oidc_sp(client_id: client_id_1) - click_continue - expect_ial1_count_only(client_id_1) - - visit_idp_from_ial2_oidc_sp(client_id: client_id_1) - complete_proofing_steps - expect_ial1_and_ial2_count(client_id_1) - end - end - - context 'using saml' do - it 'does not count second IAL1 auth at same sp' do - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_count_only(issuer_1) - - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_continue - expect_ial1_count_only(issuer_1) - end - - it 'counts step up from IAL1 to IAL2 after proofing' do - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_count_only(issuer_1) - - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - complete_proofing_steps - expect_ial1_and_ial2_count(issuer_1) - end - end - end - - - context 'an IAL2 user with an active session' do - before do - create_ial2_user_from_sp(email) - user = User.find_with_email(email) - end - - context 'using oidc' do - - it 'counts IAL1 auth at same sp' do - visit_idp_from_ial2_oidc_sp(client_id: client_id_1) - click_continue - expect_ial2_count_only(client_id_1) - - visit_idp_from_ial1_oidc_sp(client_id: client_id_1) - click_continue - expect_ial1_and_ial2_count(client_id_1) - end - - it 'does not count second IAL2 auth at same sp' do - visit_idp_from_ial2_oidc_sp(client_id: client_id_1) - click_continue - expect_ial2_count_only(client_id_1) - - visit_idp_from_ial2_oidc_sp(client_id: client_id_1) - click_continue - expect_ial2_count_only(client_id_1) - end - - it 'counts step up from IAL1 to IAL2 at another sp' do - visit_idp_from_ial1_oidc_sp(client_id: client_id_2) - click_agree_and_continue - expect_ial1_count_only(client_id_2) - - visit_idp_from_ial2_oidc_sp(client_id: client_id_2) - click_agree_and_continue - expect_ial1_and_ial2_count(client_id_2) - end - - it 'counts IAL2 auth at another sp' do - visit_idp_from_ial2_oidc_sp(client_id: client_id_1) - click_continue - expect_ial2_count_only(client_id_1) - - visit_idp_from_ial2_oidc_sp(client_id: client_id_2) - click_agree_and_continue - expect_ial2_count_only(client_id_2) - end - - it 'counts IAL1 auth at another sp' do - visit_idp_from_ial1_oidc_sp(client_id: client_id_1) - click_continue - expect_ial1_and_ial2_count(client_id_1) - - visit_idp_from_ial1_oidc_sp(client_id: client_id_2) - click_agree_and_continue - expect_ial1_count_only(client_id_2) - end - end - - context 'using saml' do - - it 'counts IAL1 auth at same sp' do - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial2_count_only(issuer_1) - - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_and_ial2_count(issuer_1) - end - - it 'does not count second IAL2 auth at same sp' do - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial2_count_only(issuer_1) - - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - click_continue - expect_ial2_count_only(issuer_1) - end - - it 'counts step up from IAL1 to IAL2 at same sp' do - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_count_only(issuer_1) - - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_and_ial2_count(issuer_1) - end - - it 'counts IAL1 auth at another sp' do - visit_idp_from_ial1_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial1_count_only(issuer_1) - - visit_idp_from_ial1_saml_sp(issuer: issuer_2) - click_agree_and_continue - expect_ial1_count_only(issuer_2) - end - - it 'counts IAL2 auth at another sp' do - visit_idp_from_ial2_saml_sp(issuer: issuer_1) - click_agree_and_continue - expect_ial2_count_only(issuer_1) - - visit_idp_from_ial2_saml_sp(issuer: issuer_2) - click_agree_and_continue - expect_ial2_count_only(issuer_2) - end - end - end - - def expect_ial1_count_only(issuer) - expect(ial1_monthly_auth_count(issuer)).to eq(1) - expect(ial2_monthly_auth_count(issuer)).to eq(0) - end - - def expect_ial2_count_only(issuer) - expect(ial1_monthly_auth_count(issuer)).to eq(0) - expect(ial2_monthly_auth_count(issuer)).to eq(1) - end - - def expect_ial1_and_ial2_count(issuer) - expect(ial1_monthly_auth_count(issuer)).to eq(1) - expect(ial2_monthly_auth_count(issuer)).to eq(1) - end - - def ial2_monthly_auth_count(client_id) - Db::MonthlySpAuthCount::SpMonthTotalAuthCounts.call(today, client_id, 2) - end - - def ial1_monthly_auth_count(client_id) - Db::MonthlySpAuthCount::SpMonthTotalAuthCounts.call(today, client_id, 1) - end -end diff --git a/spec/features/reports/monthly_usps_letter_requests_report_spec.rb b/spec/features/reports/monthly_usps_letter_requests_report_spec.rb deleted file mode 100644 index ffd344cffbb..00000000000 --- a/spec/features/reports/monthly_usps_letter_requests_report_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'rails_helper' - -feature 'Monthly usps letter requests report' do - it 'runs when there are not entries' do - results_hash = JSON.parse(Reports::MonthlyUspsLetterRequestsReport.new.call) - expect(results_hash['total_letter_requests']).to eq(0) - expect(results_hash['daily_letter_requests'].count).to eq(0) - end - - it 'runs when there is one ftp' do - LetterRequestsToUspsFtpLog.create(ftp_at: Time.zone.now, letter_requests_count: 3) - - results_hash = JSON.parse(Reports::MonthlyUspsLetterRequestsReport.new.call) - expect(results_hash['total_letter_requests']).to eq(3) - expect(results_hash['daily_letter_requests'].count).to eq(1) - end - - it 'totals correctly when there are two ftps' do - now = Time.zone.now - LetterRequestsToUspsFtpLog.create(ftp_at: now.yesterday, letter_requests_count: 3) - LetterRequestsToUspsFtpLog.create(ftp_at: now, letter_requests_count: 4) - - results_hash = JSON.parse(Reports::MonthlyUspsLetterRequestsReport.new.call) - expect(results_hash['total_letter_requests']).to eq(7) - expect(results_hash['daily_letter_requests'].count).to eq(2) - end - - it 'only reports on the current month' do - now = Time.zone.now - LetterRequestsToUspsFtpLog.create(ftp_at: now - 32.days, letter_requests_count: 3) - LetterRequestsToUspsFtpLog.create(ftp_at: now, letter_requests_count: 4) - - results_hash = JSON.parse(Reports::MonthlyUspsLetterRequestsReport.new.call) - expect(results_hash['total_letter_requests']).to eq(4) - expect(results_hash['daily_letter_requests'].count).to eq(1) - end -end diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index e9b3e3bd32a..e72d5b186df 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -32,6 +32,7 @@ continue_as(email) expect(current_url).to eq authn_request + expect(page.get_rack_session.keys).to include('sp') end end diff --git a/spec/features/sp_cost_tracking_spec.rb b/spec/features/sp_cost_tracking_spec.rb index 21ce030a414..c4a29f1845d 100644 --- a/spec/features/sp_cost_tracking_spec.rb +++ b/spec/features/sp_cost_tracking_spec.rb @@ -27,10 +27,8 @@ expect_sp_cost_type(1, 2, 'acuant_front_image') expect_sp_cost_type(2, 2, 'acuant_back_image') expect_sp_cost_type(3, 2, 'acuant_result') - expect_sp_cost_type(4, 2, 'lexis_nexis_resolution', - transaction_id: IdentityIdpFunctions::ResolutionMockClient::TRANSACTION_ID) - expect_sp_cost_type(5, 2, 'aamva', - transaction_id: IdentityIdpFunctions::StateIdMockClient::TRANSACTION_ID) + expect_sp_cost_type(4, 2, 'lexis_nexis_resolution') + expect_sp_cost_type(5, 2, 'aamva') expect_sp_cost_type(6, 2, 'lexis_nexis_address') expect_sp_cost_type(7, 2, 'user_added') expect_sp_cost_type(8, 2, 'authentication') @@ -122,13 +120,12 @@ expect_direct_cost_type(0, 'digest') end - def expect_sp_cost_type(sp_cost_index, ial, token, transaction_id: nil) + def expect_sp_cost_type(sp_cost_index, ial, token) sp_cost = sp_costs(sp_cost_index) expect(sp_cost.ial).to eq(ial) expect(sp_cost.issuer).to eq(issuer) expect(sp_cost.agency_id).to eq(agency_id) expect(sp_cost.cost_type).to eq(token) - expect(sp_cost.transaction_id).to(eq(transaction_id)) if transaction_id end def expect_direct_cost_type(sp_cost_index, token) diff --git a/spec/features/two_factor_authentication/multiple_tabs_spec.rb b/spec/features/two_factor_authentication/multiple_tabs_spec.rb new file mode 100644 index 00000000000..0593d891557 --- /dev/null +++ b/spec/features/two_factor_authentication/multiple_tabs_spec.rb @@ -0,0 +1,9 @@ +require 'rails_helper' + +feature 'user interacts with 2FA across multiple browser tabs' do + include SpAuthHelper + include SamlAuthHelper + + it_behaves_like 'visiting 2fa when fully authenticated', :oidc + it_behaves_like 'visiting 2fa when fully authenticated', :saml +end diff --git a/spec/features/users/sign_in_spec.rb b/spec/features/users/sign_in_spec.rb index 97676ea2c3b..c99a4416974 100644 --- a/spec/features/users/sign_in_spec.rb +++ b/spec/features/users/sign_in_spec.rb @@ -360,11 +360,11 @@ describe 'session timeout configuration' do it 'uses delay and warning settings whose sum is a multiple of 60' do - expect((session_timeout_start + session_timeout_warning) % 60).to eq 0 + expect((start + warning) % 60).to eq 0 end it 'uses frequency and warning settings whose sum is a multiple of 60' do - expect((session_timeout_frequency + session_timeout_warning) % 60).to eq 0 + expect((frequency + warning) % 60).to eq 0 end end @@ -479,25 +479,6 @@ end end - context 'adds phone number after IAL1 sign in' do - it 'redirects to account page and not the SP' do - user = create(:user, :signed_up) - visit_idp_from_oidc_sp_with_loa1_prompt_login - fill_in_credentials_and_submit(user.email, user.password) - fill_in_code_with_last_phone_otp - click_submit_default - click_agree_and_continue - - visit account_path - click_on "+ #{t('account.index.phone_add')}" - fill_in :new_phone_form_phone, with: '415-555-0199' - click_continue - fill_in_code_with_last_phone_otp - click_submit_default - expect(current_path).to eq account_path - end - end - context 'invalid request_id' do it 'allows the user to sign in and does not try to redirect to any SP' do user = create(:user, :signed_up) diff --git a/spec/helpers/go_back_helper_spec.rb b/spec/helpers/go_back_helper_spec.rb deleted file mode 100644 index 627d97cd317..00000000000 --- a/spec/helpers/go_back_helper_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'rails_helper' - -RSpec.describe GoBackHelper do - include GoBackHelper - - describe '#go_back_path' do - let(:referer) { nil } - let(:request) { double('request', referer: referer) } - - before do - allow(helper).to receive(:request).and_return(request) - end - - subject { go_back_path } - - context 'no referer' do - let(:referer) { nil } - - it 'is nil' do - expect(subject).to be_nil - end - end - - context 'referer is invalid scheme' do - let(:referer) { 'javascript:alert()' } - - it 'is nil' do - expect(subject).to be_nil - end - end - - context 'referer from different domain' do - let(:referer) { 'https://gsa.gov/' } - - it 'is nil' do - expect(subject).to be_nil - end - end - - context 'referer from same domain' do - let(:referer) { 'https://gsa.gov/' } - - before do - allow(AppConfig.env).to receive(:domain_name).and_return('gsa.gov') - end - - it 'is path from referer' do - expect(subject).to eq('/') - end - end - end - - describe '#extract_path_and_query_from_uri' do - it 'preserves query parameter and path from uri' do - uri = URI.parse('https://gsa.gov/path/to/?with_params=true') - extracted = extract_path_and_query_from_uri(uri) - - expect(extracted).to eq('/path/to/?with_params=true') - end - end - - describe '#app_host' do - let(:domain_name) { nil } - - before do - allow(AppConfig.env).to receive(:domain_name).and_return(domain_name) - end - - subject { app_host } - - context 'without port' do - let(:domain_name) { 'gsa.gov' } - - it 'returns host' do - expect(subject).to eq('gsa.gov') - end - end - - context 'with port' do - let(:domain_name) { 'localhost:8000' } - - it 'returns host' do - expect(subject).to eq('localhost') - end - end - end -end diff --git a/spec/javascripts/packages/document-capture/components/acuant-capture-canvas-spec.jsx b/spec/javascripts/packages/document-capture/components/acuant-capture-canvas-spec.jsx index ab4a7e17ba7..c38856946f6 100644 --- a/spec/javascripts/packages/document-capture/components/acuant-capture-canvas-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/acuant-capture-canvas-spec.jsx @@ -1,4 +1,4 @@ -import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; +import { Provider as AcuantContextProvider } from '@18f/identity-document-capture/context/acuant'; import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas'; import { render, useAcuant } from '../../../support/document-capture'; @@ -7,11 +7,9 @@ describe('document-capture/components/acuant-capture-canvas', () => { it('waits for initialization', () => { render( - - - - - , + + + , ); // At this point, it's assumed `window.AcuantCameraUI.start` has not been called. This can't be @@ -34,11 +32,9 @@ describe('document-capture/components/acuant-capture-canvas', () => { it('ends on unmount', () => { const { unmount } = render( - - - - - , + + + , ); initialize(); diff --git a/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx index f85bcf5580c..61aa5891a7f 100644 --- a/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/acuant-capture-spec.jsx @@ -10,7 +10,6 @@ import { AcuantContextProvider, AnalyticsContext } from '@18f/identity-document- import DeviceContext from '@18f/identity-document-capture/context/device'; import I18nContext from '@18f/identity-document-capture/context/i18n'; import { render, useAcuant } from '../../../support/document-capture'; -import { getFixtureFile } from '../../../support/file'; const ACUANT_CAPTURE_SUCCESS_RESULT = { image: { @@ -37,11 +36,6 @@ const ACUANT_CAPTURE_BLURRY_RESULT = { describe('document-capture/components/acuant-capture', () => { const { initialize } = useAcuant(); - let validUpload; - before(async () => { - validUpload = await getFixtureFile('doc_auth_images/id-back.jpg'); - }); - context('mobile', () => { it('renders with assumed capture button support while acuant is not ready and on mobile', () => { const { getByText } = render( @@ -221,12 +215,14 @@ describe('document-capture/components/acuant-capture', () => { expect(window.AcuantCameraUI.end.calledOnce).to.be.true(); }); - it('renders retry button when value and capture supported', async () => { - const selfie = await getFixtureFile('doc_auth_images/selfie.jpg'); + it('renders retry button when value and capture supported', () => { const { getByText } = render( - + , ); @@ -241,28 +237,23 @@ describe('document-capture/components/acuant-capture', () => { }); it('renders upload button when value and capture not supported', () => { - const onChange = sinon.spy(); - const onClick = sinon.spy(); - const { getByText, getByLabelText } = render( + const { getByText } = render( - + , ); initialize({ isCameraSupported: false }); - const input = getByLabelText('Image'); - - // Since file input prompt occurs by button click proxy to input, we must fire upload event - // directly at the input. At least ensure that clicking button does "click" input. - input.addEventListener('click', onClick); - userEvent.click(getByText('doc_auth.buttons.upload_picture')); - expect(onClick).to.have.been.calledOnce(); + const button = getByText('doc_auth.buttons.upload_picture'); + expect(button).to.be.ok(); - userEvent.upload(input, validUpload); - expect(onChange).to.have.been.calledWith(validUpload); + userEvent.click(button); }); it('renders error message if capture succeeds but photo glare exceeds threshold', async () => { @@ -505,11 +496,11 @@ describe('document-capture/components/acuant-capture', () => { Upload' }} > - - + + - - + + , ); @@ -526,11 +517,9 @@ describe('document-capture/components/acuant-capture', () => { it('still captures selfie value when upload disallowed', () => { const { getByLabelText } = render( - - - - - , + + + , ); initialize(); @@ -542,108 +531,106 @@ describe('document-capture/components/acuant-capture', () => { expect(window.AcuantCameraUI.start.called).to.be.false(); expect(window.AcuantPassiveLiveness.startSelfieCapture.called).to.be.true(); }); + }); - it('does not show hint if capture is supported', () => { + context('desktop', () => { + it('renders without capture button while acuant is not ready and on desktop', () => { const { getByText } = render( - + , ); - initialize(); - - expect(() => getByText('doc_auth.tips.document_capture_hint')).to.throw(); + expect(() => getByText('doc_auth.buttons.take_picture')).to.throw(); }); - it('shows hint if capture is not supported', () => { + it('optionally disallows upload', () => { const { getByText } = render( - - - - - , + + + + + , ); - initialize({ isSuccess: false }); - - const hint = getByText('doc_auth.tips.document_capture_hint'); + expect(() => getByText('doc_auth.tips.document_capture_hint')).to.throw(); - expect(hint).to.be.ok(); + initialize(); }); + }); - it('captures selfie', async () => { - const onChange = sinon.stub(); - const { getByLabelText } = render( - - - - - , - ); - - initialize({ - startSelfieCapture: sinon.stub().callsArgWithAsync(0, ''), - }); - - const button = getByLabelText('Image'); - const defaultPrevented = !fireEvent.click(button); + it('renders with custom className', () => { + const { container } = render(); - expect(defaultPrevented).to.be.true(); - expect(window.AcuantCameraUI.start.called).to.be.false(); - expect(window.AcuantPassiveLiveness.startSelfieCapture.called).to.be.true(); - await waitFor(() => expect(onChange.calledOnce).to.be.true()); - }); + expect(container.firstChild.classList.contains('my-custom-class')).to.be.true(); }); - context('desktop', () => { - it('does not render acuant capture canvas for environmental capture', () => { - const { getByLabelText } = render( - - - - - , - ); + it('clears a selected value', () => { + const onChange = sinon.spy(); + const { getByLabelText } = render( + + + , + ); - userEvent.click(getByLabelText('Image')); + const input = getByLabelText('Image'); + fireEvent.change(input, { target: { files: [] } }); - // It would be expected that if AcuantCaptureCanvas was rendered, an error would be thrown at - // this point, since it references Acuant globals not loaded. - }); + expect(onChange.getCall(0).args).to.have.lengthOf(1); + expect(onChange.getCall(0).args).to.deep.equal([null]); }); - it('optionally disallows upload', () => { + it('does not show hint if capture is supported', () => { const { getByText } = render( - + , ); + initialize(); + expect(() => getByText('doc_auth.tips.document_capture_hint')).to.throw(); }); - it('renders with custom className', () => { - const { container } = render(); + it('shows hint if capture is not supported', () => { + const { getByText } = render( + + + , + ); - expect(container.firstChild.classList.contains('my-custom-class')).to.be.true(); + initialize({ isSuccess: false }); + + const hint = getByText('doc_auth.tips.document_capture_hint'); + + expect(hint).to.be.ok(); }); - it('clears a selected value', async () => { - const image = await getFixtureFile('doc_auth_images/id-front.jpg'); - const onChange = sinon.spy(); + it('captures selfie', async () => { + const onChange = sinon.stub(); const { getByLabelText } = render( - + , ); - const input = getByLabelText('Image'); - fireEvent.change(input, { target: { files: [] } }); + initialize({ + startSelfieCapture: sinon.stub().callsArgWithAsync(0, ''), + }); - expect(onChange.getCall(0).args).to.have.lengthOf(1); - expect(onChange.getCall(0).args).to.deep.equal([null]); + const button = getByLabelText('Image'); + const defaultPrevented = !fireEvent.click(button); + + expect(defaultPrevented).to.be.true(); + expect(window.AcuantCameraUI.start.called).to.be.false(); + expect(window.AcuantPassiveLiveness.startSelfieCapture.called).to.be.true(); + await waitFor(() => expect(onChange.calledOnce).to.be.true()); }); it('restricts accepted file types', () => { @@ -658,29 +645,4 @@ describe('document-capture/components/acuant-capture', () => { expect(input.getAttribute('accept')).to.equal('image/jpeg,image/png,image/bmp,image/tiff'); }); - - it('logs metrics for manual upload', async () => { - const addPageAction = sinon.mock(); - const { getByLabelText } = render( - - - - - , - ); - - const input = getByLabelText('Image'); - userEvent.upload(input, validUpload); - - await new Promise((resolve) => addPageAction.callsFake(resolve)); - expect(addPageAction).to.have.been.calledWith({ - label: 'IdV: image added', - payload: { - height: sinon.match.number, - mimeType: 'image/jpeg', - source: 'upload', - width: sinon.match.number, - }, - }); - }); }); diff --git a/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx index 0778e186bd9..7feaba81381 100644 --- a/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx @@ -18,7 +18,6 @@ import DocumentCapture, { import { expect } from 'chai'; import { render, useAcuant, useDocumentCaptureForm } from '../../../support/document-capture'; import { useSandbox } from '../../../support/sinon'; -import { getFixture, getFixtureFile } from '../../../support/file'; describe('document-capture/components/document-capture', () => { const onSubmit = useDocumentCaptureForm(); @@ -30,13 +29,6 @@ describe('document-capture/components/document-capture', () => { } let originalHash; - let validUpload; - let validSelfieBase64; - - before(async () => { - validUpload = await getFixtureFile('doc_auth_images/id-front.jpg'); - validSelfieBase64 = await getFixture('doc_auth_images/selfie.jpg', 'base64'); - }); beforeEach(() => { originalHash = window.location.hash; @@ -92,11 +84,9 @@ describe('document-capture/components/document-capture', () => { it('progresses through steps to completion', async () => { const { getByLabelText, getByText, getAllByText, findAllByText } = render( - - - - - , + + + , ); initialize(); @@ -108,11 +98,11 @@ describe('document-capture/components/document-capture', () => { glare: 70, sharpness: 70, image: { - data: validSelfieBase64, + data: 'data:image/png;base64,', }, }); }); - window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, validSelfieBase64); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, ''); // Continue is enabled (but grayed out).Attempting to proceed without providing values will // trigger error messages. @@ -128,7 +118,7 @@ describe('document-capture/components/document-capture', () => { // Providing values should remove errors progressively. fireEvent.change(getByLabelText('doc_auth.headings.document_capture_front'), { target: { - files: [validUpload], + files: [new window.File([''], 'upload.png', { type: 'image/png' })], }, }); await waitFor(() => expect(getAllByText('simple_form.required.text')).to.have.lengthOf(1)); @@ -188,11 +178,20 @@ describe('document-capture/components/document-capture', () => { }, ); + initialize({ isCameraSupported: false }); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, ''); + const continueButton = getByText('forms.buttons.continue'); userEvent.click(continueButton); await findAllByText('simple_form.required.text'); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(continueButton); @@ -200,7 +199,7 @@ describe('document-capture/components/document-capture', () => { userEvent.click(submitButton); await findAllByText('simple_form.required.text'); const selfieInput = getByLabelText('doc_auth.headings.document_capture_selfie'); - userEvent.upload(selfieInput, validUpload); + fireEvent.click(selfieInput); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(submitButton); @@ -251,11 +250,20 @@ describe('document-capture/components/document-capture', () => { }, ); + initialize({ isCameraSupported: false }); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, ''); + const continueButton = getByText('forms.buttons.continue'); userEvent.click(continueButton); await findAllByText('simple_form.required.text'); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(continueButton); @@ -263,7 +271,7 @@ describe('document-capture/components/document-capture', () => { userEvent.click(submitButton); await findAllByText('simple_form.required.text'); const selfieInput = getByLabelText('doc_auth.headings.document_capture_selfie'); - userEvent.upload(selfieInput, validUpload); + fireEvent.click(selfieInput); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(submitButton); @@ -286,8 +294,14 @@ describe('document-capture/components/document-capture', () => { // Submit button should be disabled until field errors are resolved. submitButton = getByText('forms.buttons.submit.default'); expect(submitButton.classList.contains('btn-disabled')).to.be.true(); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); // Once fields are changed, their notices should be cleared. If all field-specific errors are // addressed, submit should be enabled once more. @@ -334,8 +348,15 @@ describe('document-capture/components/document-capture', () => { }), }); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + initialize({ isCameraSupported: false }); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); userEvent.click(getByText('forms.buttons.submit.default')); await waitFor(() => window.location.hash === '#teapot'); @@ -401,11 +422,20 @@ describe('document-capture/components/document-capture', () => { , ); + initialize({ isCameraSupported: false }); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, ''); + const continueButton = getByText('forms.buttons.continue'); userEvent.click(continueButton); await findAllByText('simple_form.required.text'); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(continueButton); @@ -413,7 +443,7 @@ describe('document-capture/components/document-capture', () => { userEvent.click(submitButton); await findAllByText('simple_form.required.text'); const selfieInput = getByLabelText('doc_auth.headings.document_capture_selfie'); - userEvent.upload(selfieInput, validUpload); + fireEvent.click(selfieInput); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(submitButton); @@ -447,11 +477,20 @@ describe('document-capture/components/document-capture', () => { { uploadError }, ); + initialize({ isCameraSupported: false }); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, ''); + const continueButton = getByText('forms.buttons.continue'); userEvent.click(continueButton); await findAllByText('simple_form.required.text'); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_front'), validUpload); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_back'), validUpload); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_front'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); + userEvent.upload( + getByLabelText('doc_auth.headings.document_capture_back'), + new window.File([''], 'upload.png', { type: 'image/png' }), + ); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(continueButton); expect(onStepChange.callCount).to.equal(1); @@ -461,7 +500,7 @@ describe('document-capture/components/document-capture', () => { expect(onStepChange.callCount).to.equal(1); await findAllByText('simple_form.required.text'); const selfieInput = getByLabelText('doc_auth.headings.document_capture_selfie'); - userEvent.upload(selfieInput, validUpload); + fireEvent.click(selfieInput); await waitFor(() => expect(() => getAllByText('simple_form.required.text')).to.throw()); userEvent.click(submitButton); expect(onStepChange.callCount).to.equal(1); diff --git a/spec/javascripts/packages/document-capture/components/file-image-spec.jsx b/spec/javascripts/packages/document-capture/components/file-image-spec.jsx index 960330bac45..6644f1de82d 100644 --- a/spec/javascripts/packages/document-capture/components/file-image-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/file-image-spec.jsx @@ -1,15 +1,11 @@ import FileImage from '@18f/identity-document-capture/components/file-image'; import { render } from '../../../support/document-capture'; -import { getFixtureFile } from '../../../support/file'; describe('document-capture/components/file-image', () => { - let file; - before(async () => { - file = await getFixtureFile('doc_auth_images/id-back.jpg'); - }); - it('renders span prior to load', () => { - const { container } = render(); + const { container } = render( + , + ); expect(container.childNodes).to.have.lengthOf(1); const loader = container.childNodes[0]; @@ -21,28 +17,40 @@ describe('document-capture/components/file-image', () => { }); it('renders a given file object as an image', async () => { - const { findByAltText } = render(); + const { findByAltText } = render( + , + ); const image = await findByAltText('image'); - expect(image.getAttribute('src')).to.match(/^data:image\/jpeg;base64,/); + expect(image.getAttribute('src')).to.match(/^data:image\/png;base64,/); expect(Array.from(image.classList.values())).to.have.members(['document-capture-file-image']); }); it('renders a a changed file object as an image', async () => { - const { findByAltText, rerender } = render(); + const { findByAltText, rerender } = render( + , + ); await findByAltText('first image'); - rerender(); + rerender( + , + ); const image = await findByAltText('second image'); - expect(image.getAttribute('src')).to.match(/^data:image\/jpeg;base64,/); + expect(image.getAttribute('src')).to.match(/^data:image\/png;base64,/); }); it('renders with className', async () => { - const { findByAltText } = render(); + const { findByAltText } = render( + , + ); const image = await findByAltText('image'); diff --git a/spec/javascripts/packages/document-capture/components/file-input-spec.jsx b/spec/javascripts/packages/document-capture/components/file-input-spec.jsx index f15971746b6..285a12292da 100644 --- a/spec/javascripts/packages/document-capture/components/file-input-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/file-input-spec.jsx @@ -10,14 +10,8 @@ import FileInput, { } from '@18f/identity-document-capture/components/file-input'; import DeviceContext from '@18f/identity-document-capture/context/device'; import { render } from '../../../support/document-capture'; -import { getFixtureFile } from '../../../support/file'; describe('document-capture/components/file-input', () => { - let file; - before(async () => { - file = await getFixtureFile('doc_auth_images/id-front.jpg'); - }); - describe('getAcceptPattern', () => { it('returns a pattern for audio matching', () => { const accept = 'audio/*'; @@ -147,14 +141,14 @@ describe('document-capture/components/file-input', () => { it('renders a value preview for a blob', async () => { const { container, findByRole, getByLabelText } = render( - , + , ); const preview = await findByRole('img', { hidden: true }); const input = getByLabelText('File'); expect(input).to.be.ok(); - expect(preview.getAttribute('src')).to.match(/^data:image\/jpeg;base64,/); + expect(preview.getAttribute('src')).to.match(/^data:image\/png;base64,/); expect(container.querySelector('.usa-file-input__preview-heading').textContent).to.equal( 'doc_auth.forms.change_file', ); @@ -162,32 +156,29 @@ describe('document-capture/components/file-input', () => { it('renders a value preview for a file', async () => { const { container, findByRole, getByLabelText } = render( - , + , ); const preview = await findByRole('img', { hidden: true }); const input = getByLabelText('File'); expect(input).to.be.ok(); - expect(preview.getAttribute('src')).to.match(/^data:image\/jpeg;base64,/); + expect(preview.getAttribute('src')).to.match(/^data:image\/png;base64,/); expect(container.querySelector('.usa-file-input__preview-heading').textContent).to.equal( - 'doc_auth.forms.selected_file: id-front.jpg doc_auth.forms.change_file', + 'doc_auth.forms.selected_file: demo.png doc_auth.forms.change_file', ); }); it('renders a value preview for a data URL', async () => { const { container, findByRole, getByLabelText } = render( - , + , ); const preview = await findByRole('img', { hidden: true }); const input = getByLabelText('File'); expect(input).to.be.ok(); - expect(preview.getAttribute('src')).to.match(/^data:image\/png;base64,/); + expect(preview.getAttribute('src')).to.match(/^data:image\/jpeg;base64,/); expect(container.querySelector('.usa-file-input__preview-heading').textContent).to.equal( 'doc_auth.forms.change_file', ); @@ -210,6 +201,7 @@ describe('document-capture/components/file-input', () => { }); it('calls onChange with next value', () => { + const file = new window.File([''], 'upload.png', { type: 'image/png' }); const onChange = sinon.stub(); const { getByLabelText } = render(); @@ -220,19 +212,21 @@ describe('document-capture/components/file-input', () => { }); it('allows changing the selected value', () => { - const file2 = new window.File([file], 'file2.jpg'); + const file1 = new window.File([''], 'upload1.png', { type: 'image/png' }); + const file2 = new window.File([''], 'upload2.png', { type: 'image/png' }); const onChange = sinon.stub(); const { getByLabelText } = render(); const input = getByLabelText('File'); - userEvent.upload(input, file); + userEvent.upload(input, file1); userEvent.upload(input, file2); - expect(onChange.getCall(0).args[0]).to.equal(file); + expect(onChange.getCall(0).args[0]).to.equal(file1); expect(onChange.getCall(1).args[0]).to.equal(file2); }); it('allows clearing the selected value', () => { + const file = new window.File([''], 'upload1.png', { type: 'image/png' }); const onChange = sinon.stub(); const { getByLabelText } = render(); @@ -266,7 +260,7 @@ describe('document-capture/components/file-input', () => { , ); @@ -295,6 +289,7 @@ describe('document-capture/components/file-input', () => { }); it('shows an error state', () => { + const file = new window.File([''], 'upload.png', { type: 'image/png' }); const onChange = sinon.stub(); const onError = sinon.stub(); const { getByLabelText, getByText } = render( @@ -309,6 +304,7 @@ describe('document-capture/components/file-input', () => { }); it('allows customization of invalid file type error message', () => { + const file = new window.File([''], 'upload.png', { type: 'image/png' }); const onChange = sinon.stub(); const onError = sinon.stub(); const { getByLabelText, getByText } = render( @@ -329,6 +325,7 @@ describe('document-capture/components/file-input', () => { }); it('shows an error from rendering parent', () => { + const file = new window.File([''], 'upload.png', { type: 'image/png' }); const onChange = sinon.stub(); const onError = sinon.stub(); const props = { label: 'File', accept: ['text/*'], onChange, onError }; @@ -348,17 +345,18 @@ describe('document-capture/components/file-input', () => { }); it('shows an updated state', () => { - const file2 = new window.File([file], 'file2.jpg'); + const file1 = new window.File([''], 'upload.png', { type: 'image/png' }); + const file2 = new window.File([''], 'upload.png', { type: 'image/png' }); const { getByText, rerender } = render(); expect(() => getByText('forms.file_input.file_updated')).to.throw(); - rerender(); + rerender(); expect(() => getByText('forms.file_input.file_updated')).to.throw(); - rerender(); + rerender(); expect(() => getByText('forms.file_input.file_updated')).to.throw(); @@ -372,13 +370,14 @@ describe('document-capture/components/file-input', () => { }); it('allows customization of updated file text', () => { - const file2 = new window.File([file], 'file2.jpg'); + const file1 = new window.File([''], 'upload.png', { type: 'image/png' }); + const file2 = new window.File([''], 'upload.png', { type: 'image/png' }); const { getByText, rerender } = render(); expect(() => getByText('Updated')).to.throw(); - rerender(); + rerender(); expect(() => getByText('Updated')).to.throw(); diff --git a/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx index be0e12b2c4f..38b77d7c832 100644 --- a/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/selfie-capture-spec.jsx @@ -5,7 +5,6 @@ import { I18nContext } from '@18f/identity-document-capture'; import SelfieCapture from '@18f/identity-document-capture/components/selfie-capture'; import { render } from '../../../support/document-capture'; import { useSandbox } from '../../../support/sinon'; -import { getFixtureFile } from '../../../support/file'; describe('document-capture/components/selfie-capture', () => { // Since DOM globals are stubbed with sandbox, ensure that cleanup is the first task, as otherwise @@ -26,10 +25,7 @@ describe('document-capture/components/selfie-capture', () => { ); const track = { stop: sinon.stub() }; - let value; - before(async () => { - value = await getFixtureFile('doc_auth_images/selfie.jpg'); - }); + const value = new window.File([], 'image.png', { type: 'image/png' }); let originalMediaDevices; let originalMediaStream; diff --git a/spec/javascripts/packages/document-capture/components/selfie-step-spec.jsx b/spec/javascripts/packages/document-capture/components/selfie-step-spec.jsx index 270afc6c469..3963c8ff7ae 100644 --- a/spec/javascripts/packages/document-capture/components/selfie-step-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/selfie-step-spec.jsx @@ -1,49 +1,27 @@ import userEvent from '@testing-library/user-event'; import { waitFor } from '@testing-library/dom'; import sinon from 'sinon'; -import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; +import { AcuantContextProvider } from '@18f/identity-document-capture'; import SelfieStep from '@18f/identity-document-capture/components/selfie-step'; import { render, useAcuant } from '../../../support/document-capture'; describe('document-capture/components/selfie-step', () => { - context('mobile', () => { - const { initialize } = useAcuant(); + const { initialize } = useAcuant(); - it('calls onChange callback with uploaded image', async () => { - const onChange = sinon.stub(); - const { getByLabelText } = render( - - - - - , - ); - initialize(); - window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, '8J+Riw=='); + it('calls onChange callback with uploaded image', async () => { + const onChange = sinon.stub(); + const { getByLabelText } = render( + + + , + ); + initialize(); + window.AcuantPassiveLiveness.startSelfieCapture.callsArgWithAsync(0, '8J+Riw=='); - userEvent.click(getByLabelText('doc_auth.headings.document_capture_selfie')); + userEvent.click(getByLabelText('doc_auth.headings.document_capture_selfie')); - await waitFor(() => - expect(onChange.getCall(0).args[0].selfie).to.equal('data:image/jpeg;base64,8J+Riw=='), - ); - }); - }); - - context('desktop', () => { - it('calls onChange callback with uploaded image', async () => { - const onChange = sinon.stub(); - const { getByLabelText } = render( - - - - - , - ); - - const file = new window.File([], 'image.png', { type: 'image/png' }); - userEvent.upload(getByLabelText('doc_auth.headings.document_capture_selfie'), file); - - await waitFor(() => expect(onChange.getCall(0).args[0].selfie).to.equal(file)); - }); + await waitFor(() => + expect(onChange.getCall(0).args[0].selfie).to.equal('data:image/jpeg;base64,8J+Riw=='), + ); }); }); diff --git a/spec/javascripts/packages/document-capture/context/acuant-spec.jsx b/spec/javascripts/packages/document-capture/context/acuant-spec.jsx index 769eff53aea..e4f8d9765ee 100644 --- a/spec/javascripts/packages/document-capture/context/acuant-spec.jsx +++ b/spec/javascripts/packages/document-capture/context/acuant-spec.jsx @@ -1,6 +1,5 @@ import { useContext } from 'react'; import { renderHook } from '@testing-library/react-hooks'; -import { DeviceContext } from '@18f/identity-document-capture'; import AcuantContext, { Provider as AcuantContextProvider, } from '@18f/identity-document-capture/context/acuant'; @@ -17,7 +16,6 @@ describe('document-capture/context/acuant', () => { expect(result.current).to.eql({ isReady: false, - isAcuantLoaded: false, isError: false, isCameraSupported: null, credentials: null, @@ -25,153 +23,99 @@ describe('document-capture/context/acuant', () => { }); }); - context('desktop', () => { - it('does not append script element', () => { - render( - - - , - ); + it('appends script element', () => { + render(); - const script = document.querySelector('script[src="about:blank"]'); + const script = document.querySelector('script[src="about:blank"]'); - expect(script).to.not.be.ok(); + expect(script).to.be.ok(); + }); + + it('provides context from provider crendentials', () => { + const { result } = renderHook(() => useContext(AcuantContext), { + wrapper: ({ children }) => ( + + {children} + + ), }); - it('provides context as ready, unsupported', () => { - const { result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - - expect(result.current).to.eql({ - isReady: true, - isAcuantLoaded: false, - isError: false, - isCameraSupported: false, - credentials: null, - endpoint: null, - }); + expect(result.current).to.eql({ + isReady: false, + isError: false, + isCameraSupported: null, + credentials: 'a', + endpoint: 'b', }); }); - context('mobile', () => { - it('appends script element', () => { - render( - - - , - ); + it('provides ready context when successfully loaded', () => { + const { result } = renderHook(() => useContext(AcuantContext), { + wrapper: ({ children }) => ( + {children} + ), + }); - const script = document.querySelector('script[src="about:blank"]'); + window.AcuantJavascriptWebSdk = { + initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), + }; + window.AcuantCamera = { isCameraSupported: true }; + window.onAcuantSdkLoaded(); - expect(script).to.be.ok(); + expect(result.current).to.eql({ + isReady: true, + isError: false, + isCameraSupported: true, + credentials: null, + endpoint: null, }); + }); - it('provides context from provider crendentials', () => { - const { result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( - - - {children} - - - ), - }); - - expect(result.current).to.eql({ - isReady: false, - isAcuantLoaded: false, - isError: false, - isCameraSupported: null, - credentials: 'a', - endpoint: 'b', - }); + it('has camera availability at time of ready', () => { + const { result } = renderHook(() => useContext(AcuantContext), { + wrapper: ({ children }) => ( + {children} + ), }); - it('provides ready context when successfully loaded', () => { - const { result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - - window.AcuantJavascriptWebSdk = { - initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), - }; - window.AcuantCamera = { isCameraSupported: true }; - window.onAcuantSdkLoaded(); - - expect(result.current).to.eql({ - isReady: true, - isAcuantLoaded: true, - isError: false, - isCameraSupported: true, - credentials: null, - endpoint: null, - }); - }); + window.AcuantJavascriptWebSdk = { + initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), + }; + window.AcuantCamera = { isCameraSupported: true }; + window.onAcuantSdkLoaded(); + + expect(result.current.isCameraSupported).to.be.true(); + }); - it('has camera availability at time of ready', () => { - const { result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - - window.AcuantJavascriptWebSdk = { - initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(), - }; - window.AcuantCamera = { isCameraSupported: true }; - window.onAcuantSdkLoaded(); - - expect(result.current.isCameraSupported).to.be.true(); + it('provides error context when failed to loaded', () => { + const { result } = renderHook(() => useContext(AcuantContext), { + wrapper: ({ children }) => ( + {children} + ), }); - it('provides error context when failed to loaded', () => { - const { result } = renderHook(() => useContext(AcuantContext), { - wrapper: ({ children }) => ( - - {children} - - ), - }); - - window.AcuantJavascriptWebSdk = { - initialize: (_credentials, _endpoint, { onFail }) => onFail(), - }; - window.onAcuantSdkLoaded(); - - expect(result.current).to.eql({ - isReady: false, - isAcuantLoaded: false, - isError: true, - isCameraSupported: null, - credentials: null, - endpoint: null, - }); + window.AcuantJavascriptWebSdk = { + initialize: (_credentials, _endpoint, { onFail }) => onFail(), + }; + window.onAcuantSdkLoaded(); + + expect(result.current).to.eql({ + isReady: false, + isError: true, + isCameraSupported: null, + credentials: null, + endpoint: null, }); + }); - it('cleans up after itself on unmount', () => { - const { unmount } = render( - - - , - ); + it('cleans up after itself on unmount', () => { + const { unmount } = render(); - unmount(); + unmount(); - const script = document.querySelector('script[src="about:blank"]'); + const script = document.querySelector('script[src="about:blank"]'); - expect(script).not.to.be.ok(); - expect(window.AcuantJavascriptWebSdk).to.be.undefined(); - }); + expect(script).not.to.be.ok(); + expect(window.AcuantJavascriptWebSdk).to.be.undefined(); }); }); diff --git a/spec/javascripts/packages/document-capture/higher-order/with-background-encrypted-upload-spec.jsx b/spec/javascripts/packages/document-capture/higher-order/with-background-encrypted-upload-spec.jsx index 609797b4753..833de9bf34a 100644 --- a/spec/javascripts/packages/document-capture/higher-order/with-background-encrypted-upload-spec.jsx +++ b/spec/javascripts/packages/document-capture/higher-order/with-background-encrypted-upload-spec.jsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import sinon from 'sinon'; import { UploadContextProvider, AnalyticsContext } from '@18f/identity-document-capture'; import withBackgroundEncryptedUpload, { - blobToArrayBuffer, + blobToDataView, encrypt, } from '@18f/identity-document-capture/higher-order/with-background-encrypted-upload'; import { useSandbox } from '../../../support/sinon'; @@ -33,13 +33,13 @@ function isArrayBufferEqual(a, b) { describe('document-capture/higher-order/with-background-encrypted-upload', () => { const sandbox = useSandbox(); - describe('blobToArrayBuffer', () => { + describe('blobToDataView', () => { it('converts blob to data view', async () => { const data = new window.File(['Hello world'], 'demo.text', { type: 'text/plain' }); const expected = new Uint8Array([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]).buffer; - const actual = await blobToArrayBuffer(data); - expect(isArrayBufferEqual(actual, expected)).to.be.true(); + const dataView = await blobToDataView(data); + expect(isArrayBufferEqual(dataView.buffer, expected)).to.be.true(); }); it('rejects on filereader error', async () => { @@ -50,9 +50,7 @@ describe('document-capture/higher-order/with-background-encrypted-upload', () => }); try { - await blobToArrayBuffer( - new window.File(['Hello world'], 'demo.text', { type: 'text/plain' }), - ); + await blobToDataView(new window.File(['Hello world'], 'demo.text', { type: 'text/plain' })); } catch (actualError) { expect(actualError).to.equal(error); } diff --git a/spec/javascripts/packs/submit-with-spinner-spec.js b/spec/javascripts/packs/submit-with-spinner-spec.js new file mode 100644 index 00000000000..4d403bf3ce8 --- /dev/null +++ b/spec/javascripts/packs/submit-with-spinner-spec.js @@ -0,0 +1,41 @@ +import { screen } from '@testing-library/dom'; + +describe('submit-with-spinner', () => { + async function initialize({ withForm = true } = {}) { + const parent = withForm + ? document.body.appendChild(document.createElement('form')) + : document.body; + + parent.innerHTML = ` + +
    + +
    + `; + + delete require.cache[require.resolve('../../../app/javascript/packs/submit-with-spinner')]; + await import('../../../app/javascript/packs/submit-with-spinner'); + } + + it('gracefully handles absence of form', async () => { + await initialize({ withForm: false }); + }); + + it('should show spinner on form submit', async () => { + await initialize(); + + // JSDOM doesn't support submitting a form natively. + // See: https://github.com/jsdom/jsdom/issues/123 + const form = document.querySelector('form'); + const event = new window.Event('submit', { target: form }); + form.dispatchEvent(event); + + const spinner = screen.getByAltText('Loading spinner'); + + expect(spinner.parentNode.classList.contains('hidden')).to.be.false(); + }); +}); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index bce98e3f6d8..b4cbe3e2ac9 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -4,8 +4,6 @@ import dirtyChai from 'dirty-chai'; import sinonChai from 'sinon-chai'; import { createDOM, useCleanDOM } from './support/dom'; import { chaiConsoleSpy, useConsoleLogSpy } from './support/console'; -import { createObjectURLAsDataURL } from './support/file'; -import { useBrowserCompatibleEncrypt } from './support/crypto'; chai.use(dirtyChai); chai.use(sinonChai); @@ -19,13 +17,6 @@ const dom = createDOM(); global.window = dom.window; global.window.fetch = () => Promise.reject(new Error('Fetch must be stubbed')); global.window.crypto = new Crypto(); // In the future (Node >=15), use native webcrypto: https://nodejs.org/api/webcrypto.html -global.window.URL.createObjectURL = createObjectURLAsDataURL; -global.window.URL.revokeObjectURL = () => {}; -Object.defineProperty(global.window.Image.prototype, 'src', { - set() { - this.onload(); - }, -}); global.navigator = window.navigator; global.document = window.document; global.Document = window.Document; @@ -35,4 +26,3 @@ global.self = window; useCleanDOM(); useConsoleLogSpy(); -useBrowserCompatibleEncrypt(); diff --git a/spec/javascripts/support/crypto.js b/spec/javascripts/support/crypto.js deleted file mode 100644 index b091a4dee86..00000000000 --- a/spec/javascripts/support/crypto.js +++ /dev/null @@ -1,33 +0,0 @@ -import sinon from 'sinon'; - -/** - * Test lifecycle hook which ensures that any call to `crypto.subtle.encrypt` using the AES-GCM - * algorithm should always include an explicit `tagLength`, despite specification allowing for its - * omission, due to browser-specific incompatibilities. - * - * This may be removed in the future if the upstream polyfill handles this incompatibility. - * - * @see https://github.com/vibornoff/webcrypto-shim/pull/44 - */ -export function useBrowserCompatibleEncrypt() { - let originalEncrypt; - - beforeEach(() => { - originalEncrypt = window.crypto.subtle.encrypt; - const stub = sinon.stub().callsFake(originalEncrypt); - stub - .withArgs( - sinon.match({ - name: 'AES-GCM', - tagLength: undefined, - }), - ) - .throws(new TypeError('Always pass numeric `tagLength`, even if default of `128`.')); - - window.crypto.subtle.encrypt = stub; - }); - - afterEach(() => { - window.crypto.subtle.encrypt = originalEncrypt; - }); -} diff --git a/spec/javascripts/support/dom.js b/spec/javascripts/support/dom.js index 2c549d1a50d..6bdb87c35d9 100644 --- a/spec/javascripts/support/dom.js +++ b/spec/javascripts/support/dom.js @@ -10,18 +10,8 @@ export function createDOM() { const dom = new JSDOM('', { url: 'http://example.test', resources: new (class extends ResourceLoader { - /** - * @param {string} url - * @param {import('jsdom').FetchOptions} options - */ // eslint-disable-next-line class-methods-use-this - fetch(url, options) { - if (url.startsWith('data:') && options.element instanceof window.HTMLImageElement) { - const [header, content] = url.split(','); - const isBase64 = header.endsWith(';base64'); - return Promise.resolve(Buffer.from(content, isBase64 ? 'base64' : 'utf-8')); - } - + fetch(url) { return url === 'about:blank' ? Promise.resolve(Buffer.from('')) : Promise.reject(new Error('Failed to load')); diff --git a/spec/javascripts/support/file.js b/spec/javascripts/support/file.js deleted file mode 100644 index 963296500ab..00000000000 --- a/spec/javascripts/support/file.js +++ /dev/null @@ -1,51 +0,0 @@ -import { join, basename, extname } from 'path'; -import { promises as fs } from 'fs'; - -/** - * Rough approximation of assumed mime type by file extension. This is very incomplete, and assumes - * files which would be used as fixtures. For a more complete implementation, consider pulling in a - * package like `mime-types`. - * - * @see https://www.npmjs.com/package/mime-types - * - * @type {Record} - */ -const MIME_TYPES_BY_EXTENSION = { - '.jpg': 'image/jpeg', -}; - -/** - * @typedef {File & {rawBuffer: Buffer}} LoginGovTestFile - */ - -/** - * @param {string} fixturePath - * @param {BufferEncoding=} encoding - * - * @return {Promise} - */ -export function getFixture(fixturePath, encoding) { - const path = join(__dirname, '../../fixtures', fixturePath); - return fs.readFile(path, encoding); -} - -/** - * @param {string} fixturePath Path relative fixtures directory. - * - * @return {Promise} - */ -export async function getFixtureFile(fixturePath) { - const rawBuffer = /** @type {Buffer} */ (await getFixture(fixturePath)); - const type = MIME_TYPES_BY_EXTENSION[extname(fixturePath)]; - const file = new window.File([rawBuffer], basename(fixturePath), { type }); - return Object.assign(file, { rawBuffer }); -} - -/** - * @param {LoginGovTestFile} file - * - * @return {string} Data URL - */ -export function createObjectURLAsDataURL(file) { - return `data:${file.type};base64,${file.rawBuffer.toString('base64')}`; -} diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 38bfef46931..14c27187fef 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -297,8 +297,7 @@ def expect_email_body_to_have_help_and_contact_links reset_text = t('user_mailer.account_reset_granted.cancel_link_text') expect(mail.html_part.body).to have_content( strip_tags( - t('user_mailer.account_reset_request.intro_html', app: APP_NAME, - cancel_account_reset: reset_text), + t('user_mailer.account_reset_request.intro', cancel_account_reset: reset_text), ), ) end @@ -336,7 +335,7 @@ def expect_email_body_to_have_help_and_contact_links it 'renders the body' do expect(mail.html_part.body).to \ - have_content(strip_tags(t('user_mailer.account_reset_granted.intro_html', app: APP_NAME))) + have_content(strip_tags(t('user_mailer.account_reset_granted.intro_html'))) end end diff --git a/spec/models/agency_spec.rb b/spec/models/agency_spec.rb index ff7dfe68979..fadef7c5765 100644 --- a/spec/models/agency_spec.rb +++ b/spec/models/agency_spec.rb @@ -8,6 +8,5 @@ let(:agency) { build_stubbed(:agency) } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:abbreviation).case_insensitive.allow_nil } end end diff --git a/spec/models/null_service_provider_spec.rb b/spec/models/null_service_provider_spec.rb index 828b992df2b..7b67d825092 100644 --- a/spec/models/null_service_provider_spec.rb +++ b/spec/models/null_service_provider_spec.rb @@ -76,20 +76,12 @@ end end - describe '#identities' do - it 'returns empty array' do - expect(subject.identities).to eq([]) - end - end - context 'matching methods on ServiceProvider' do it 'has all the methods that ServiceProvider has' do sp_methods = ServiceProvider.instance_methods(false) ignored_methods = %i[ autosave_associated_records_for_agency - autosave_associated_records_for_identities belongs_to_counter_cache_after_update - validate_associated_records_for_identities ] null_sp_methods = NullServiceProvider.instance_methods diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index 5274129e0d6..5b05c056ac9 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -136,18 +136,6 @@ end end - describe 'associations' do - subject { service_provider } - - it { is_expected.to belong_to(:agency) } - it do - is_expected.to have_many(:identities). - inverse_of(:service_provider_record). - with_foreign_key('service_provider'). - with_primary_key('issuer') - end - end - describe '#issuer' do it 'returns the constructor value' do expect(service_provider.issuer).to eq 'http://localhost:3000' diff --git a/spec/monitor_spec_helper.rb b/spec/monitor_spec_helper.rb index 2cecf767b19..90e97e78674 100644 --- a/spec/monitor_spec_helper.rb +++ b/spec/monitor_spec_helper.rb @@ -3,7 +3,6 @@ require 'capybara/rspec' require 'webdrivers/chromedriver' require 'active_support/all' -require 'rspec/retry' Time.zone ||= ActiveSupport::TimeZone['UTC'] @@ -28,42 +27,12 @@ config.color = true config.order = :random - # show retry status in spec process - config.verbose_retry = true - # show exception that triggers a retry if verbose_retry is set to true - config.display_try_failure_messages = true - # config.infer_spec_type_from_file_location is a Rails-only feature, # so we do it ourselves. config.define_derived_metadata(file_path: %r{/spec/features/monitor}) do |metadata| metadata[:type] = :feature metadata[:js] = true - - # Can be overridden with RSPEC_RETRY_RETRY_COUNT - metadata[:retry] = 3 end config.example_status_persistence_file_path = './tmp/rspec-examples.txt' - - count = 1 - config.after do |example| - next if !example.exception - - spec_name = example.description.strip.tr(' ', '_').dasherize.downcase - - dirname = "tmp/capybara/#{count}-#{spec_name}" - FileUtils.mkdir_p(dirname) - - page.driver.browser.save_screenshot(File.join(dirname, 'screenshot.png')) - File.open(File.join(dirname, 'page.html'), 'w') { |f| f.puts page.html } - File.open(File.join(dirname, 'info.txt'), 'w') do |info| - info.puts "example name: #{example.description}" - info.puts "example location: #{example.location}" - info.puts - info.puts "current path: #{page.current_path}" - info.puts "exception: #{example.exception.class} #{example.exception.message}" - end - - count += 1 - end end diff --git a/spec/presenters/idv/usps_presenter_spec.rb b/spec/presenters/idv/usps_presenter_spec.rb index 87834f2451d..8f4ddca8b51 100644 --- a/spec/presenters/idv/usps_presenter_spec.rb +++ b/spec/presenters/idv/usps_presenter_spec.rb @@ -46,17 +46,17 @@ end end - describe '#fallback_back_path' do + describe '#cancel_path' do context 'when the user has a pending profile' do it 'returns the verify account path' do create(:profile, user: user, deactivation_reason: :verification_pending) - expect(subject.fallback_back_path).to eq('/account/verify') + expect(subject.cancel_path).to eq('/account/verify') end end context 'when the user does not have a pending profile' do - it 'returns the idv phone path' do - expect(subject.fallback_back_path).to eq('/verify/phone') + it 'returns the idv cancel path' do + expect(subject.cancel_path).to eq('/verify/cancel') end end end diff --git a/spec/requests/frontend_analytics_spec.rb b/spec/requests/frontend_analytics_spec.rb new file mode 100644 index 00000000000..aa07ab93663 --- /dev/null +++ b/spec/requests/frontend_analytics_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +describe 'frontend analytics requests' do + describe 'platform authenticators' do + let(:analytics) { FakeAnalytics.new } + + before do + allow(analytics).to receive(:track_event) + allow(Analytics).to receive(:new).and_return(analytics) + end + + it 'does not log anything if the user is not authed' do + expect(analytics).to_not receive(:track_event). + with(Analytics::FRONTEND_BROWSER_CAPABILITIES, any_args) + + post analytics_path, params: { platform_authenticator: { available: true } } + end + + it 'logs true if the platform authenticator is available' do + sign_in_user + + post analytics_path, params: { platform_authenticator: { available: true } } + + expect(analytics).to have_received(:track_event). + with(Analytics::FRONTEND_BROWSER_CAPABILITIES, hash_including(platform_authenticator: true)) + end + + it 'logs false if the platform authenticator is not available' do + sign_in_user + + post analytics_path, params: { platform_authenticator: { available: false } } + + expect(analytics).to have_received(:track_event). + with( + Analytics::FRONTEND_BROWSER_CAPABILITIES, + hash_including(platform_authenticator: false), + ) + end + + it 'only logs 1 platform authenticator event per session' do + sign_in_user + + post analytics_path, params: { platform_authenticator: { available: true } } + post analytics_path, params: { platform_authenticator: { available: true } } + + expect(analytics).to have_received(:track_event). + with( + Analytics::FRONTEND_BROWSER_CAPABILITIES, + hash_including(platform_authenticator: true), + ). + once + end + + it 'logs ignores garbage values' do + sign_in_user + + post analytics_path, params: { platform_authenticator: { available: 'blah blah blah' } } + + expect(analytics).to_not have_received(:track_event). + with(Analytics::FRONTEND_BROWSER_CAPABILITIES, any_args) + end + end +end diff --git a/spec/services/agency_seeder_spec.rb b/spec/services/agency_seeder_spec.rb index 798b3bc3671..62692b066af 100644 --- a/spec/services/agency_seeder_spec.rb +++ b/spec/services/agency_seeder_spec.rb @@ -16,6 +16,8 @@ subject(:run) { instance.run } + # This implictly validates that the `abbreviation` attribute in the YAML is + # ignored it 'inserts agencies into the database from agencies.yml' do expect { run }.to change(Agency, :count) end diff --git a/spec/services/data_requests/write_cloudwatch_logs_spec.rb b/spec/services/data_requests/write_cloudwatch_logs_spec.rb deleted file mode 100644 index 8ecdddf2edb..00000000000 --- a/spec/services/data_requests/write_cloudwatch_logs_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -require 'rails_helper' - -RSpec.describe DataRequests::WriteCloudwatchLogs do - let(:now) { Time.zone.now } - - def build_result_row(event_properties = {}) - DataRequests::FetchCloudwatchLogs::ResultRow.new( - Time.zone.now, - { - time: now.iso8601, - name: 'Some Log: Event', - properties: { - event_properties: { - success: true, - multi_factor_auth_method: 'sms', - phone_configuration_id: '12345', - }.merge(event_properties), - service_provider: 'some:service:provider', - user_ip: '0.0.0.0', - user_agent: 'Chrome', - }, - }.to_json, - ) - end - - let(:cloudwatch_results) do - [ - build_result_row, - ] - end - - around do |ex| - Dir.mktmpdir do |dir| - @output_dir = dir - ex.run - end - end - - subject(:writer) do - DataRequests::WriteCloudwatchLogs.new(cloudwatch_results, @output_dir) - end - - describe '#call' do - it 'writes the logs to output_dir/logs.csv' do - writer.call - - row = CSV.read(File.join(@output_dir, 'logs.csv'), headers: true).first - - expect(row['timestamp']).to eq(now.iso8601) - expect(row['event_name']).to eq('Some Log: Event') - expect(row['success']).to eq('true') - expect(row['multi_factor_auth_method']).to eq('sms') - expect(row['multi_factor_id']).to eq('phone_configuration_id:12345') - expect(row['service_provider']).to eq('some:service:provider') - expect(row['ip_address']).to eq('0.0.0.0') - expect(row['user_agent']).to eq('Chrome') - end - - context 'missing data' do - let(:cloudwatch_results) do - [ - DataRequests::FetchCloudwatchLogs::ResultRow.new(now, {}.to_json), - ] - end - - it 'does not blow up' do - expect { writer.call }.to_not raise_error - end - end - - context 'various multi factor ids' do - let(:cloudwatch_results) do - [ - build_result_row(multi_factor_auth_method: 'sms', phone_configuration_id: '1111'), - build_result_row(multi_factor_auth_method: 'voice', phone_configuration_id: '2222'), - build_result_row(multi_factor_auth_method: 'piv_cac', piv_cac_configuration_id: '3333'), - build_result_row(multi_factor_auth_method: 'webauthn', webauthn_configuration_id: '4444'), - build_result_row(multi_factor_auth_method: 'totp', auth_app_configuration_id: '5555'), - ] - end - - it 'unpacks all multi factor ids' do - writer.call - - csv = CSV.read(File.join(@output_dir, 'logs.csv'), headers: true) - - expect(csv.map { |row| [row['multi_factor_auth_method'], row['multi_factor_id']] }). - to eq([%w[sms phone_configuration_id:1111], - %w[voice phone_configuration_id:2222], - %w[piv_cac piv_cac_configuration_id:3333], - %w[webauthn webauthn_configuration_id:4444], - %w[totp auth_app_configuration_id:5555], - ]) - end - end - end -end diff --git a/spec/services/doc_auth_router_spec.rb b/spec/services/doc_auth_router_spec.rb index 2a62d167399..089cc20f3b7 100644 --- a/spec/services/doc_auth_router_spec.rb +++ b/spec/services/doc_auth_router_spec.rb @@ -4,10 +4,21 @@ describe '.client' do before do allow(AppConfig.env).to receive(:doc_auth_vendor).and_return(doc_auth_vendor) + allow(AppConfig.env).to receive(:acuant_simulator).and_return(acuant_simulator) + end + + context 'legacy mock configuration' do + let(:doc_auth_vendor) { '' } + let(:acuant_simulator) { 'true' } + + it 'is the mock client' do + expect(DocAuthRouter.client).to be_a(IdentityDocAuth::Mock::DocAuthMockClient) + end end context 'for acuant' do let(:doc_auth_vendor) { 'acuant' } + let(:acuant_simulator) { '' } it 'is a translation-proxied acuant client' do expect(DocAuthRouter.client).to be_a(DocAuthRouter::AcuantErrorTranslatorProxy) @@ -17,6 +28,7 @@ context 'for lexisnexis' do let(:doc_auth_vendor) { 'lexisnexis' } + let(:acuant_simulator) { '' } it 'is a translation-proxied lexisnexis client' do expect(DocAuthRouter.client).to be_a(DocAuthRouter::LexisNexisTranslatorProxy) @@ -26,6 +38,7 @@ context 'other config' do let(:doc_auth_vendor) { 'unknown' } + let(:acuant_simulator) { '' } it 'errors' do expect { DocAuthRouter.client }.to raise_error(RuntimeError) diff --git a/spec/services/encryption/contextless_kms_client_spec.rb b/spec/services/encryption/contextless_kms_client_spec.rb index 7d8fd4c475e..3abca21aa1a 100644 --- a/spec/services/encryption/contextless_kms_client_spec.rb +++ b/spec/services/encryption/contextless_kms_client_spec.rb @@ -103,20 +103,8 @@ allow(Encryption::Encryptors::AesEncryptor).to receive(:new).and_return(encryptor) stub_mapped_aws_kms_client( - [ - { - plaintext: long_kms_plaintext[0..long_kms_plaintext_chunksize - 1], - ciphertext: 'chunk1', - key_id: AppConfig.env.aws_kms_key_id, - region: AppConfig.env.aws_region, - }, - { - plaintext: long_kms_plaintext[long_kms_plaintext_chunksize..-1], - ciphertext: 'chunk2', - key_id: AppConfig.env.aws_kms_key_id, - region: AppConfig.env.aws_region, - }, - ], + long_kms_plaintext[0..long_kms_plaintext_chunksize - 1] => 'chunk1', + long_kms_plaintext[long_kms_plaintext_chunksize..-1] => 'chunk2', ) allow(FeatureManagement).to receive(:use_kms?).and_return(kms_enabled) end diff --git a/spec/services/encryption/kms_client_spec.rb b/spec/services/encryption/kms_client_spec.rb index 2e1d8ec803b..cc42ccee314 100644 --- a/spec/services/encryption/kms_client_spec.rb +++ b/spec/services/encryption/kms_client_spec.rb @@ -2,20 +2,11 @@ describe Encryption::KmsClient do before do - # rubocop:disable Layout/LineLength stub_mapped_aws_kms_client( - [ - { plaintext: 'a' * 3000, ciphertext: 'us-north-1:kms1', key_id: key_id, region: 'us-north-1' }, - { plaintext: 'a' * 3000, ciphertext: 'us-south-1:kms1', key_id: key_id, region: 'us-south-1' }, - - { plaintext: 'b' * 3000, ciphertext: 'us-north-1:kms2', key_id: key_id, region: 'us-north-1' }, - { plaintext: 'b' * 3000, ciphertext: 'us-south-1:kms2', key_id: key_id, region: 'us-south-1' }, - - { plaintext: 'c' * 3000, ciphertext: 'us-north-1:kms3', key_id: key_id, region: 'us-north-1' }, - { plaintext: 'c' * 3000, ciphertext: 'us-south-1:kms3', key_id: key_id, region: 'us-south-1' }, - ], + 'a' * 3000 => 'kms1', + 'b' * 3000 => 'kms2', + 'c' * 3000 => 'kms3', ) - # rubocop:enable Layout/LineLength encryptor = Encryption::Encryptors::AesEncryptor.new { @@ -33,12 +24,8 @@ allow(Encryption::Encryptors::AesEncryptor).to receive(:new).and_return(encryptor) allow(FeatureManagement).to receive(:kms_multi_region_enabled?).and_return(kms_multi_region_enabled) # rubocop:disable Layout/LineLength allow(FeatureManagement).to receive(:use_kms?).and_return(kms_enabled) - allow(AppConfig.env).to receive(:aws_kms_regions).and_return(aws_kms_regions.to_json) - allow(AppConfig.env).to receive(:aws_region).and_return(aws_region) - allow(AppConfig.env).to receive(:aws_kms_key_id).and_return(key_id) end - let(:key_id) { 'key1' } let(:plaintext) { 'a' * 3000 + 'b' * 3000 + 'c' * 3000 } let(:encryption_context) { { 'context' => 'attribute-bundle', 'user_id' => '123-abc-456-def' } } @@ -50,29 +37,21 @@ ) end - let(:aws_region) { 'us-north-1' } - let(:aws_kms_regions) { %w[us-north-1 us-south-1] } - + let(:kms_regions) { %w[us-west-2 us-east-1] } let(:kms_multi_region_enabled) { true } let(:kms_regionalized_ciphertext) do - 'KMSc' + %w[kms1 kms2 kms3].map do |kms| - payload = { - regions: { - 'us-north-1' => Base64.strict_encode64("us-north-1:#{kms}"), - 'us-south-1' => Base64.strict_encode64("us-south-1:#{kms}"), - }, - } - Base64.strict_encode64(payload.to_json) + 'KMSc' + %w[kms1 kms2 kms3].map do |c| + region_hash = {} + kms_regions.each do |r| + region_hash[r] = Base64.strict_encode64(c) + end + Base64.strict_encode64({ regions: region_hash }.to_json) end.to_json end let(:kms_legacy_ciphertext) do - 'KMSc' + %w[ - us-north-1:kms1 - us-north-1:kms2 - us-north-1:kms3 - ].map { |c| Base64.strict_encode64(c) }.to_json + 'KMSc' + %w[kms1 kms2 kms3].map { |c| Base64.strict_encode64(c) }.to_json end let(:local_ciphertext) do @@ -91,7 +70,6 @@ end context 'with multi region disabled' do let(:kms_multi_region_enabled) { false } - it 'encrypts with KMS legacy single region' do result = subject.encrypt(plaintext, encryption_context) expect(result).to eq(kms_legacy_ciphertext) diff --git a/spec/services/encryption/multi_region_kms_client_spec.rb b/spec/services/encryption/multi_region_kms_client_spec.rb index a4387f83a4d..c46cd26c620 100644 --- a/spec/services/encryption/multi_region_kms_client_spec.rb +++ b/spec/services/encryption/multi_region_kms_client_spec.rb @@ -1,53 +1,49 @@ require 'rails_helper' describe Encryption::MultiRegionKmsClient do - let(:kms_enabled) { true } - let(:kms_multi_region_enabled) { true } - let(:aws_kms_regions) { %w[us-north-1 us-south-1] } - let(:aws_region) { 'us-north-1' } - before do - allow(FeatureManagement).to receive(:use_kms?).and_return(kms_enabled) - allow(FeatureManagement).to receive(:kms_multi_region_enabled?). - and_return(kms_multi_region_enabled) - allow(AppConfig.env).to receive(:aws_kms_regions).and_return(aws_kms_regions.to_json) - allow(AppConfig.env).to receive(:aws_region).and_return(aws_region) - stub_mapped_aws_kms_client( - [ - { plaintext: plaintext, ciphertext: 'k1:us-north-1', key_id: 'key1', region: 'us-north-1' }, - { plaintext: plaintext, ciphertext: 'k1:us-south-1', key_id: 'key1', region: 'us-south-1' }, - ], + 'a' * 3000 => 'kms1', + 'b' * 3000 => 'kms2', ) + + allow(FeatureManagement).to receive(:use_kms?).and_return(kms_enabled) + allow(FeatureManagement).to receive(:kms_multi_region_enabled?).and_return(kms_multi_region_enabled) # rubocop:disable Layout/LineLength end - let(:plaintext) { 'a' * 3000 } + let(:first_plaintext) { 'a' * 3000 } + let(:second_plaintext) { 'b' * 3000 } let(:encryption_context) { { 'context' => 'attribute-bundle', 'user_id' => '123-abc-456-def' } } + let(:kms_regions) { %w[us-west-2 us-east-1] } + let(:current_aws_region) { 'us-east-1' } + let(:regionalized_kms_ciphertext) do - { - regions: { - 'us-north-1' => Base64.strict_encode64('k1:us-north-1'), - 'us-south-1' => Base64.strict_encode64('k1:us-south-1'), - }, - }.to_json + region_hash = {} + kms_regions.each do |r| + region_hash[r] = Base64.strict_encode64('kms1') + end + { regions: region_hash }.to_json end - let(:legacy_kms_ciphertext) { 'k1:us-north-1' } + let(:legacy_kms_ciphertext) { 'kms1' } - describe '#encrypt' do - let(:aws_key_id) { 'key1' } + let(:kms_enabled) { true } + let(:kms_multi_region_enabled) { true } + + let(:aws_key_id) { AppConfig.env.aws_kms_key_id } + describe '#encrypt' do context 'with multi region enabled' do it 'encrypts with KMS' do - result = subject.encrypt(aws_key_id, plaintext, encryption_context) + result = subject.encrypt(aws_key_id, first_plaintext, encryption_context) expect(result).to eq(regionalized_kms_ciphertext) end end context 'with multi region disabled' do let(:kms_multi_region_enabled) { false } it 'encrypts with KMS' do - result = subject.encrypt(aws_key_id, plaintext, encryption_context) + result = subject.encrypt(aws_key_id, first_plaintext, encryption_context) expect(result).to eq(legacy_kms_ciphertext) end end @@ -57,23 +53,23 @@ context 'with a multi region ciphertext' do it 'decrypts the ciphertext with KMS' do result = subject.decrypt(regionalized_kms_ciphertext, encryption_context) - expect(result).to eq(plaintext) + expect(result).to eq(first_plaintext) end end context 'with a legacy ciphertext' do it 'decrypts the ciphertext with KMS' do result = subject.decrypt(legacy_kms_ciphertext, encryption_context) - expect(result).to eq(plaintext) + expect(result).to eq(first_plaintext) end end it 'decrypts successfully if the default region is not present' do non_default_ciphertext = { - regions: { 'us-north-1' => Base64.strict_encode64('k1:us-north-1') }, + regions: { 'us-east-1' => Base64.strict_encode64('kms1') }, }.to_json result = subject.decrypt(non_default_ciphertext, encryption_context) - expect(result).to eq(plaintext) + expect(result).to eq(first_plaintext) end it 'errors if none of the encryption regions are present' do @@ -92,22 +88,22 @@ partially_valid_ciphertext = { regions: { foo: 'kms1', - 'us-south-1': Base64.strict_encode64('k1:us-south-1'), + 'us-west-2': 'kms2', }, }.to_json result = subject.decrypt(partially_valid_ciphertext, encryption_context) - expect(result).to eq(plaintext) + expect(result).to eq(second_plaintext) end it 'decrypts in default region where multiple regions present' do multi_region_ciphertext = { regions: { - 'us-north-1': Base64.strict_encode64('k1:us-north-1'), - 'us-south-1': Base64.strict_encode64('k1:us-south-1'), + 'us-east-1': Base64.strict_encode64('kms1'), + 'us-west-2': Base64.strict_encode64('kms2'), }, }.to_json result = subject.decrypt(multi_region_ciphertext, encryption_context) - expect(result).to eq(plaintext) + expect(result).to eq(first_plaintext) end end end diff --git a/spec/services/identity_linker_spec.rb b/spec/services/identity_linker_spec.rb index 6d33c12c304..fcbff021a1d 100644 --- a/spec/services/identity_linker_spec.rb +++ b/spec/services/identity_linker_spec.rb @@ -113,9 +113,9 @@ to raise_error(ArgumentError) end - it 'does not link to an identity record if the provider is nil' do + it 'fails when given a nil provider' do linker = IdentityLinker.new(user, nil) - expect(linker.link_identity).to eq(nil) + expect { linker.link_identity }.to raise_error(ActiveRecord::RecordInvalid) end it 'can link two different clients to the same rails_session_id' do diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 8094fd30cdd..46a25fd6c8a 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -24,29 +24,17 @@ ) result = document_capture_session.load_proofing_result.result expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] - expect(result[:context][:stages]).to_not include( - state_id: 'StateIdMock', - transaction_id: IdentityIdpFunctions::StateIdMockClient::TRANSACTION_ID, - ) + expect(result[:context][:stages]).to_not include({ state_id: 'StateIdMock' }) end it 'does proof state_id if resolution succeeds' do - agent = Idv::Agent.new( - ssn: '444-55-8888', - first_name: Faker::Name.first_name, - zipcode: '11111', - state_id_number: '123456789', - state_id_type: 'drivers_license', - state_id_jurisdiction: 'MD', - ) + agent = Idv::Agent.new({ ssn: '444-55-8888', first_name: Faker::Name.first_name, + zipcode: '11111' }) agent.proof_resolution( document_capture_session, should_proof_state_id: true, trace_id: trace_id ) result = document_capture_session.load_proofing_result.result - expect(result[:context][:stages]).to include( - state_id: 'StateIdMock', - transaction_id: IdentityIdpFunctions::StateIdMockClient::TRANSACTION_ID, - ) + expect(result[:context][:stages]).to include({ state_id: 'StateIdMock' }) end context 'proofing partial date of birth' do @@ -75,10 +63,7 @@ ) result = document_capture_session.load_proofing_result.result expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] - expect(result[:context][:stages]).to_not include( - state_id: 'StateIdMock', - transaction_id: IdentityIdpFunctions::StateIdMockClient::TRANSACTION_ID, - ) + expect(result[:context][:stages]).to_not include({ state_id: 'StateIdMock' }) end it 'does not proof state_id if resolution succeeds' do @@ -88,10 +73,7 @@ document_capture_session, should_proof_state_id: false, trace_id: trace_id ) result = document_capture_session.load_proofing_result.result - expect(result[:context][:stages]).to_not include( - state_id: 'StateIdMock', - transaction_id: IdentityIdpFunctions::StateIdMockClient::TRANSACTION_ID, - ) + expect(result[:context][:stages]).to_not include({ state_id: 'StateIdMock' }) end end diff --git a/spec/services/store_sp_metadata_in_session_spec.rb b/spec/services/store_sp_metadata_in_session_spec.rb index 529de5ab622..86e06465b86 100644 --- a/spec/services/store_sp_metadata_in_session_spec.rb +++ b/spec/services/store_sp_metadata_in_session_spec.rb @@ -42,7 +42,6 @@ issuer: 'issuer', aal_level_requested: nil, piv_cac_requested: false, - ial: 1, ial2: false, ial2_strict: false, ialmax: false, @@ -82,7 +81,6 @@ issuer: 'issuer', aal_level_requested: 3, piv_cac_requested: false, - ial: 2, ial2: true, ial2_strict: false, ialmax: false, diff --git a/spec/services/usps_confirmation_uploader_spec.rb b/spec/services/usps_confirmation_uploader_spec.rb index 8e11e86d6ca..b1317c14bd0 100644 --- a/spec/services/usps_confirmation_uploader_spec.rb +++ b/spec/services/usps_confirmation_uploader_spec.rb @@ -75,18 +75,12 @@ subject { uploader.run } context 'when successful' do - it 'uploads the psv, creates a file, uploads it via SFTP, and deletes and logs it after' do + it 'uploads the psv created by creates a file, uploads it via SFTP, and deletes it after' do expect(uploader).to receive(:generate_export).with(confirmations).and_return(export) expect(uploader).to receive(:upload_export).with(export) expect(uploader).to receive(:clear_confirmations).with(confirmations) subject - - logs = LetterRequestsToUspsFtpLog.all - expect(logs.count).to eq(1) - log = logs.first - expect(log.ftp_at).to be_present - expect(log.letter_requests_count).to eq(1) end end diff --git a/spec/support/aws_kms_client.rb b/spec/support/aws_kms_client.rb index da8ddf43349..ce500e3b405 100644 --- a/spec/support/aws_kms_client.rb +++ b/spec/support/aws_kms_client.rb @@ -10,28 +10,17 @@ def stub_aws_kms_client(random_key = random_str, ciphered_key = random_str) [random_key, ciphered_key] end - # Configs is an array of: - # [{ ciphertext:, plaintext:, key_id:, region: }] - def stub_mapped_aws_kms_client(configs) - encryptor = proc do |context| - config = configs.find do |c| - c.slice(:key_id, :plaintext) == context.params.slice(:key_id, :plaintext) && - c[:region] == context.client.config.region - end - { ciphertext_blob: config[:ciphertext], key_id: config[:key_id] } - end - - decryptor = proc do |context| - config = configs.find do |c| - c[:ciphertext] == context.params[:ciphertext_blob] - end - { plaintext: config[:plaintext], key_id: config[:key_id] } - end - + def stub_mapped_aws_kms_client(forward = {}) + reverse = forward.invert + aws_key_id = AppConfig.env.aws_kms_key_id Aws.config[:kms] = { stub_responses: { - encrypt: encryptor, - decrypt: decryptor, + encrypt: lambda { |context| + { ciphertext_blob: forward[context.params[:plaintext]], key_id: aws_key_id } + }, + decrypt: lambda { |context| + { plaintext: reverse[context.params[:ciphertext_blob]], key_id: aws_key_id } + }, }, } end diff --git a/spec/support/fake_analytics.rb b/spec/support/fake_analytics.rb index 10d56f1e4e9..22a1bd98b4d 100644 --- a/spec/support/fake_analytics.rb +++ b/spec/support/fake_analytics.rb @@ -2,11 +2,10 @@ class FakeAnalytics attr_reader :events def initialize - @events = Hash.new + @events = Hash.new { |hash, key| hash[key] = [] } end def track_event(event, attributes = {}) - events[event] ||= [] events[event] << attributes nil end diff --git a/spec/support/features/cac_proofing_helper.rb b/spec/support/features/cac_proofing_helper.rb index c8222f04115..1b9839ad763 100644 --- a/spec/support/features/cac_proofing_helper.rb +++ b/spec/support/features/cac_proofing_helper.rb @@ -23,6 +23,10 @@ def idv_cac_proofing_verify_wait_step idv_cac_step_path(step: :verify_wait) end + def idv_cac_proofing_success_step + idv_cac_step_path(step: :success) + end + def complete_cac_proofing_steps_before_choose_method_step visit idv_cac_proofing_choose_method_step end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index ed878e263e8..8f77506f535 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -34,10 +34,6 @@ def fill_out_ssn_form_fail fill_in 'doc_auth_ssn', with: '' end - def click_doc_auth_back_link - click_on '‹ ' + t('forms.buttons.back') - end - def idv_doc_auth_welcome_step idv_doc_auth_step_path(step: :welcome) end @@ -137,15 +133,6 @@ def complete_all_doc_auth_steps(expect_accessible: false) click_idv_continue end - def complete_proofing_steps - complete_all_doc_auth_steps - click_continue - fill_in 'Password', with: RequestHelper::VALID_PASSWORD - click_continue - click_acknowledge_personal_key - click_agree_and_continue - end - def mock_doc_auth_no_name_pii(method) pii_with_no_name = IdentityDocAuth::Mock::ResultResponseBuilder::DEFAULT_PII_FROM_DOC.dup pii_with_no_name[:last_name] = nil diff --git a/spec/support/saml_auth_helper.rb b/spec/support/saml_auth_helper.rb index 3ae46ba51f7..42aff59a6d7 100644 --- a/spec/support/saml_auth_helper.rb +++ b/spec/support/saml_auth_helper.rb @@ -308,26 +308,6 @@ def visit_saml_auth_path ) end - def visit_idp_from_ial2_saml_sp(**args) - settings = ial2_with_bundle_saml_settings - settings.security[:embed_sign] = false - settings.issuer = args[:issuer] if args[:issuer] - settings.name_identifier_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT - saml_authn_request = auth_request.create(settings) - visit saml_authn_request - saml_authn_request - end - - def visit_idp_from_ial1_saml_sp(**args) - settings = ial1_with_verified_at_saml_settings - settings.security[:embed_sign] = false - settings.issuer = args[:issuer] if args[:issuer] - settings.name_identifier_format = Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT - saml_authn_request = auth_request.create(settings) - visit saml_authn_request - saml_authn_request - end - private def link_user_to_identity(user, link, settings) diff --git a/spec/support/shared_examples/account_creation.rb b/spec/support/shared_examples/account_creation.rb index 556cddcfabd..dff1d2258a6 100644 --- a/spec/support/shared_examples/account_creation.rb +++ b/spec/support/shared_examples/account_creation.rb @@ -150,6 +150,7 @@ expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') end + expect(page.get_rack_session.keys).to include('sp') end perform_in_browser(:one) do @@ -170,6 +171,7 @@ expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') end + expect(page.get_rack_session.keys).to include('sp') end end end diff --git a/spec/support/shared_examples/ial2_consent.rb b/spec/support/shared_examples/ial2_consent.rb index 4566c49a81f..46cb6ef50fa 100644 --- a/spec/support/shared_examples/ial2_consent.rb +++ b/spec/support/shared_examples/ial2_consent.rb @@ -1,14 +1,11 @@ shared_examples 'ial2 consent with js' do - it 'shows the notice if the user clicks continue without giving consent' do - expect(page).to have_button('Continue') - click_continue - - expect(page).to have_content(t('errors.doc_auth.consent_form')) + it 'does not allow the user to continue without checking the checkbox' do + expect(page).to have_button('Continue', disabled: true) end it 'allows the user to continue after checking the checkbox' do find('span[class="indicator"]').set(true) - expect(page).to have_button('Continue') + expect(page).to have_button('Continue', disabled: false) click_continue expect_doc_auth_upload_step diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index 57f4f966254..7f1d5cf76b4 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -41,6 +41,27 @@ end end +shared_examples 'visiting 2fa when fully authenticated' do |sp| + before { Timecop.freeze Time.zone.now } + after { Timecop.return } + + it 'redirects to SP after visiting a 2fa screen when fully authenticated', email: true do + ial1_sign_in_with_personal_key_goes_to_sp(sp) + + visit login_two_factor_options_path + + click_continue + continue_as + expect(current_url).to eq @saml_authn_request if sp == :saml + + if sp == :oidc + redirect_uri = URI(current_url) + + expect(redirect_uri.to_s).to start_with('http://localhost:7654/auth/result') + end + end +end + shared_examples 'signing in as IAL2 with personal key' do |sp| before { Timecop.freeze Time.zone.now } after { Timecop.return } diff --git a/spec/views/idv/doc_auth/_back.html.erb_spec.rb b/spec/views/idv/doc_auth/_back.html.erb_spec.rb deleted file mode 100644 index 8a0a99b18a5..00000000000 --- a/spec/views/idv/doc_auth/_back.html.erb_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'rails_helper' - -describe 'idv/doc_auth/_back.html.erb' do - it 'renders with action' do - render 'idv/doc_auth/back', action: 'redo_ssn' - - expect(rendered).to have_selector("form[action='#{idv_doc_auth_step_path(step: 'redo_ssn')}']") - expect(rendered).to have_selector('input[name="_method"][value="put"]', visible: false) - expect(rendered).to have_selector("[type='submit'][value='#{'‹ ' + t('forms.buttons.back')}']") - end - - it 'renders with step' do - render 'idv/doc_auth/back', step: 'verify' - - expect(rendered).to have_selector("a[href='#{idv_doc_auth_step_path(step: 'verify')}']") - expect(rendered).to have_content('‹ ' + t('forms.buttons.back')) - end - - it 'renders with back path' do - allow(view).to receive(:go_back_path).and_return('/example') - - render 'idv/doc_auth/back' - - expect(rendered).to have_selector('a[href="/example"]') - expect(rendered).to have_content('‹ ' + t('forms.buttons.back')) - end - - it 'renders fallback path' do - render 'idv/doc_auth/back', fallback_path: '/example' - - expect(rendered).to have_selector('a[href="/example"]') - expect(rendered).to have_content('‹ ' + t('forms.buttons.back')) - end - - it 'renders nothing if there is no back path' do - render 'idv/doc_auth/back' - - expect(rendered).to be_empty - end -end diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index c4fac83d728..7fe5c10fe7e 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -11,29 +11,17 @@ let(:selfie_image_upload_url) { nil } before do + allow(view).to receive(:flow_session).and_return(flow_session) + allow(view).to receive(:sp_name).and_return(sp_name) + allow(view).to receive(:failure_to_proof_url).and_return(failure_to_proof_url) + allow(view).to receive(:front_image_upload_url).and_return(front_image_upload_url) + allow(view).to receive(:back_image_upload_url).and_return(back_image_upload_url) + allow(view).to receive(:selfie_image_upload_url).and_return(selfie_image_upload_url) allow(view).to receive(:url_for).and_return('https://example.com/') - - allow(FeatureManagement).to receive(:document_capture_async_uploads_enabled?). - and_return(async_uploads_enabled) - - assign(:step_url, :idv_doc_auth_step_url) - end - - subject(:render_partial) do - render partial: 'idv/shared/document_capture', locals: { - flow_session: flow_session, - sp_name: sp_name, - failure_to_proof_url: failure_to_proof_url, - front_image_upload_url: front_image_upload_url, - back_image_upload_url: back_image_upload_url, - selfie_image_upload_url: selfie_image_upload_url, - } end describe 'async upload urls' do context 'when async upload is disabled' do - let(:async_uploads_enabled) { false } - it 'does not modify CSP connect_src headers' do allow(SecureHeaders).to receive(:append_content_security_policy_directives).with(any_args) expect(SecureHeaders).to receive(:append_content_security_policy_directives).with( @@ -41,12 +29,11 @@ connect_src: [], ) - render_partial + render end end - context 'when async upload are enabled' do - let(:async_uploads_enabled) { true } + context 'when async upload is enabled' do let(:front_image_upload_url) { 'https://s3.example.com/bucket/a?X-Amz-Security-Token=UAOL2' } let(:back_image_upload_url) { 'https://s3.example.com/bucket/b?X-Amz-Security-Token=UAOL2' } let(:selfie_image_upload_url) { 'https://s3.example.com/bucket/c?X-Amz-Security-Token=UAOL2' } @@ -62,7 +49,7 @@ ], ) - render_partial + render end end end diff --git a/spec/views/idv/usps/index.html.erb_spec.rb b/spec/views/idv/usps/index.html.erb_spec.rb index 80bf24f813a..bf03775ee15 100644 --- a/spec/views/idv/usps/index.html.erb_spec.rb +++ b/spec/views/idv/usps/index.html.erb_spec.rb @@ -1,68 +1,21 @@ require 'rails_helper' describe 'idv/usps/index.html.erb' do - let(:usps_mail_bounced) { false } - let(:letter_already_sent) { false } - let(:user_needs_address_otp_verification) { false } - let(:go_back_path) { nil } - let(:presenter) do + it 'calls UspsPresenter#title, #button, and #cancel_path' do user = build_stubbed(:user, :signed_up) - Idv::UspsPresenter.new(user, {}) - end + usps_mail_service = Idv::UspsMail.new(user) - before do - allow(view).to receive(:go_back_path).and_return(go_back_path) + usps_presenter = instance_double(Idv::UspsPresenter) + allow(Idv::UspsPresenter).to receive(:new).with(usps_mail_service). + and_return(usps_presenter) + @presenter = usps_presenter - allow(presenter).to receive(:usps_mail_bounced?).and_return(usps_mail_bounced) - allow(presenter).to receive(:letter_already_sent?).and_return(letter_already_sent) - allow(presenter).to receive(:user_needs_address_otp_verification?). - and_return(user_needs_address_otp_verification) + expect(usps_presenter).to receive(:title) + expect(usps_presenter).to receive(:button) + expect(usps_presenter).to receive(:cancel_path) + expect(usps_presenter).to receive(:byline) + expect(usps_presenter).to receive(:usps_mail_bounced?) - @presenter = presenter render end - - it 'prompts to send letter' do - expect(rendered).to have_content(I18n.t('idv.titles.mail.verify')) - expect(rendered).to have_button(I18n.t('idv.buttons.mail.send')) - end - - it 'renders fallback link to return to phone verify path' do - expect(rendered).to have_link('‹ ' + t('forms.buttons.back'), href: idv_phone_path) - end - - context 'has page to go back to' do - let(:go_back_path) { idv_otp_verification_path } - - it 'renders back link to return to previous path' do - expect(rendered).to have_link('‹ ' + t('forms.buttons.back'), href: go_back_path) - end - end - - context 'usps mail bounced' do - let(:usps_mail_bounced) { true } - - it 'renders address form to resend letter' do - expect(rendered).to have_content(I18n.t('idv.messages.usps.new_address')) - expect(rendered).to have_field(t('idv.form.address1')) - expect(rendered).to have_button(I18n.t('idv.buttons.mail.resend')) - end - end - - context 'letter already sent' do - let(:letter_already_sent) { true } - - it 'prompts to send another letter' do - expect(rendered).to have_content(I18n.t('idv.titles.mail.resend')) - expect(rendered).to have_button(I18n.t('idv.buttons.mail.resend')) - end - end - - context 'user needs address otp verification' do - let(:user_needs_address_otp_verification) { true } - - it 'renders fallback link to return to verify path' do - expect(rendered).to have_link('‹ ' + t('forms.buttons.back'), href: verify_account_path) - end - end end diff --git a/spec/views/users/delete/show.html.erb_spec.rb b/spec/views/users/delete/show.html.erb_spec.rb index 89e00f3bbe4..46f8f24d671 100644 --- a/spec/views/users/delete/show.html.erb_spec.rb +++ b/spec/views/users/delete/show.html.erb_spec.rb @@ -22,7 +22,6 @@ expect(rendered).to have_content(t('users.delete.bullet_1', app: APP_NAME)) expect(rendered).to have_content(user.decorate.delete_account_bullet_key) expect(rendered).to have_content(t('users.delete.bullet_3', app: APP_NAME)) - expect(rendered).to have_content(t('users.delete.bullet_4', app: APP_NAME)) end it 'displays bullets for loa1' do diff --git a/tsconfig.json b/tsconfig.json index 6b8ef0be529..2ad5010b8cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,7 @@ "app/javascript/packs/form-validation.js", "app/javascript/packs/intl-tel-input.js", "app/javascript/packs/spinner-button.js", - "app/javascript/packs/session-expire-session.js", - "app/javascript/packs/session-timeout-ping.js" + "app/javascript/packs/submit-with-spinner.js" ], "exclude": ["**/fixtures", "**/*.spec.js"] } diff --git a/yarn.lock b/yarn.lock index 8e8bf1e9a64..f267b3722b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -997,7 +997,7 @@ dependencies: mkdirp "^1.0.4" -"@peculiar/asn1-schema@^2.0.27": +"@peculiar/asn1-schema@^2.0.12", "@peculiar/asn1-schema@^2.0.26": version "2.0.27" resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.0.27.tgz#1ee3b2b869ff3200bcc8ec60e6c87bd5a6f03fe0" integrity sha512-1tIx7iL3Ma3HtnNS93nB7nhyI0soUJypElj9owd4tpMrRDmeJ8eZubsdq1sb0KSaCs5RqZNoABCP6m5WtnlVhQ== @@ -1014,16 +1014,16 @@ dependencies: tslib "^2.0.0" -"@peculiar/webcrypto@^1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.6.tgz#484bb58be07149e19e873861b585b0d5e4f83b7b" - integrity sha512-xcTjouis4Y117mcsJslWAGypwhxtXslkVdRp7e3tHwtuw0/xCp1te8RuMMv/ia5TsvxomcyX/T+qTbRZGLLvyA== +"@peculiar/webcrypto@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.1.4.tgz#cbbe2195e5e6f879780bdac9a66bcbaca75c483c" + integrity sha512-gEVxfbseFDV0Za3AmjTrRB+wigEMOejHDzoo571e8/YWD33Ejmk0XPF3+G+VaN8+5C5IWZx4CPvxQZ7mF2dvNA== dependencies: - "@peculiar/asn1-schema" "^2.0.27" + "@peculiar/asn1-schema" "^2.0.26" "@peculiar/json-schema" "^1.1.12" - pvtsutils "^1.1.2" - tslib "^2.1.0" - webcrypto-core "^1.2.0" + pvtsutils "^1.1.1" + tslib "^2.0.3" + webcrypto-core "^1.1.8" "@rails/webpacker@^5.2.1": version "5.2.1" @@ -3524,6 +3524,11 @@ element-closest@^2.0.1: resolved "https://registry.yarnpkg.com/element-closest/-/element-closest-2.0.2.tgz#72a740a107453382e28df9ce5dbb5a8df0f966ec" integrity sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw= +element-closest@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/element-closest/-/element-closest-3.0.2.tgz#3814a69a84f30e48e63eaf57341f4dbf4227d2aa" + integrity sha512-JxKQiJKX0Zr5Q2/bCaTx8P+UbfyMET1OQd61qu5xQFeWr1km3fGaxelSJtnfT27XQ5Uoztn2yIyeamAc/VX13g== + elliptic@^6.5.3: version "6.5.3" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" @@ -4951,9 +4956,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== internal-ip@^4.3.0: version "4.3.0" @@ -6085,9 +6090,9 @@ multicast-dns@^6.0.1: thunky "^1.0.2" nan@^2.12.1, nan@^2.13.2: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== nanoid@3.1.12: version "3.1.12" @@ -7670,12 +7675,12 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pvtsutils@^1.1.1, pvtsutils@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.1.2.tgz#483d72f4baa5e354466e68ff783ce8a9e2810030" - integrity sha512-Yfm9Dsk1zfEpOWCaJaHfqtNXAFWNNHMFSCLN6jTnhuCCBCC2nqge4sAgo7UrkRBoAAYIL8TN/6LlLoNfZD/b5A== +pvtsutils@^1.0.11, pvtsutils@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.1.1.tgz#22c2d7689139d2c36d7ef3ac3d5e29bcd818d38a" + integrity sha512-Evbhe6L4Sxwu4SPLQ4LQZhgfWDQO3qa1lju9jM5cxsQp8vE10VipcSmo7hiJW48TmiHgVLgDtC2TL6/+ND+IVg== dependencies: - tslib "^2.1.0" + tslib "^2.0.3" pvutils@latest: version "1.0.17" @@ -9172,10 +9177,10 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== tty-browserify@0.0.0: version "0.0.0" @@ -9478,16 +9483,16 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webcrypto-core@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.2.0.tgz#44fda3f9315ed6effe9a1e47466e0935327733b5" - integrity sha512-p76Z/YLuE4CHCRdc49FB/ETaM4bzM3roqWNJeGs+QNY1fOTzKTOVnhmudW1fuO+5EZg6/4LG9NJ6gaAyxTk9XQ== +webcrypto-core@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.1.8.tgz#91720c07f4f2edd181111b436647ea5a282af0a9" + integrity sha512-hKnFXsqh0VloojNeTfrwFoRM4MnaWzH6vtXcaFcGjPEu+8HmBdQZnps3/2ikOFqS8bJN1RYr6mI2P/FJzyZnXg== dependencies: - "@peculiar/asn1-schema" "^2.0.27" + "@peculiar/asn1-schema" "^2.0.12" "@peculiar/json-schema" "^1.1.12" asn1js "^2.0.26" - pvtsutils "^1.1.2" - tslib "^2.1.0" + pvtsutils "^1.0.11" + tslib "^2.0.1" webcrypto-shim@^0.1.6: version "0.1.6"