<% if @presenter.active_profile_for_authn_context? %> - <% if @presenter.identity_verified_with_biometric_comparison? %> + <% if @presenter.identity_verified_with_facial_match? %> <%= t('account.index.verification.you_verified_your_biometric_identity', app_name: APP_NAME) %> <% else %> <%= t('account.index.verification.you_verified_your_identity_html', sp_name: @presenter.user.active_profile.initiating_service_provider&.friendly_name || APP_NAME) %> <% end %> <% elsif @presenter.active_profile? %> - <%= t('account.index.verification.nonbiometric_verified_html', app_name: APP_NAME, date: @presenter.formatted_nonbiometric_idv_date) %> + <%= t('account.index.verification.nonbiometric_verified_html', app_name: APP_NAME, date: @presenter.formatted_legacy_idv_date) %> <% elsif @presenter.sp_name || @presenter.user.pending_profile.initiating_service_provider %> <%= t('account.index.verification.finish_verifying_html', sp_name: @presenter.sp_name || @presenter.user.pending_profile.initiating_service_provider.friendly_name) %> <% else %> diff --git a/bin/query-cloudwatch b/bin/query-cloudwatch index 4fd91877640..42ecfb40b42 100755 --- a/bin/query-cloudwatch +++ b/bin/query-cloudwatch @@ -9,6 +9,7 @@ require 'aws-sdk-cloudwatchlogs' require 'concurrent-ruby' require 'csv' require 'json' +require 'sqlite3' require 'optparse' require 'optparse/time' @@ -33,41 +34,173 @@ class QueryCloudwatch :num_threads, :wait_duration, :count_distinct, + :sqlite_database_file, keyword_init: true, ) + class JsonOutput + attr_reader :stdout + + def initialize(stdout:) + @stdout = stdout + end + + def start + yield + end + + def handle_row(row) + stdout.puts row.to_json + end + end + + class CsvOutput + attr_reader :stdout + + def initialize(stdout:) + @stdout = stdout + end + + def start + yield + end + + def handle_row(row) + stdout.puts row.values.to_csv + end + end + + class SqliteOutput + attr_reader :filename, :invalid_message_json_count, :stderr + + def initialize(filename:, stderr:) + @filename = filename + @stderr = stderr + @invalid_message_json_count = 0 + end + + def start + create_tables_if_needed + db.transaction do + yield + end + rescue + db&.interrupt + raise + ensure + if db + if invalid_message_json_count > 0 + stderr.puts <<~END + WARNING: For #{invalid_message_json_count} event#{invalid_message_json_count == 1 ? '' : 's'}, @message did not contain valid JSON + END + end + stderr.puts <<~END + Wrote #{db.total_changes.inspect} rows to the 'events' table in #{filename} + END + end + close_database + end + + def handle_row(row) + message = row['@message'] + raise "Query must include @message in output when using --sqlite" unless message + + timestamp = row['@timestamp'] + raise "Query must include @timestamp in output when using --sqlite" unless timestamp + + json = begin + JSON.parse(message) + rescue + @invalid_message_json_count += 1 + nil + end + + success = json&.dig('properties', 'event_properties', 'success') + success = success ? 1 : (success.nil? ? nil : 0) + + # NOTE: Order matters here + row = { + id: json&.dig('id') || generate_id(timestamp:,message:), + timestamp:, + name:json&.dig('name'), + user_id: json&.dig('properties', 'user_id'), + success:, + message:, + log_stream: row.dig('@logStream'), + log: row.dig('@log'), + } + + insert_statement.execute(row.values) + end + + def insert_statement + @insert_statement ||= db.prepare( + <<~SQL + INSERT INTO + events (id, timestamp, name, user_id, success, message, log_stream, log) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (id) DO NOTHING + SQL + ) + end + + def db + @db ||= SQLite3::Database.new(filename) + end + + def close_database + db&.close + @db = nil + end + + def create_tables_if_needed + db.execute_batch( + <<~SQL + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY NOT NULL, + timestamp TEXT NOT NULL, + name TEXT NULL, + user_id TEXT NULL, + success INTEGER NULL, + message TEXT NOT NULL, + log_stream TEXT NULL, + log TEXT NULL + ); + SQL + ) + end + + def generate_id(timestamp:,message:) + hash = Digest::SHA256.hexdigest("#{timestamp}|#{message}") + "NOID-#{hash}" + end + end + attr_reader :config def initialize(config) @config = config end - def run(stdout: STDOUT) + def run(stdout: STDOUT, stderr: STDERR) if config.count_distinct values = {} fetch { |row| values[row[config.count_distinct]] = true } stdout.puts values.size - else - fetch { |row| stdout.puts format_response(row) } + return end + + output = create_output(stdout:, stderr:) + + output.start do + fetch { |row| output.handle_row(row) } + end + rescue Interrupt # catch interrupts (ctrl-c) and directly exit, this skips printing the backtrace exit 1 end - # Relies on the fact that hashes preserve insertion order - # @param [Hash] row - # @return [String] - def format_response(row) - case config.format - when :csv - row.values.to_csv - when :json - row.to_json - else - raise "unknown format #{config.format}" - end - end def fetch(&block) cloudwatch_client.fetch( @@ -90,6 +223,19 @@ class QueryCloudwatch ) end + def create_output(stdout:, stderr:) + case config.format + when :csv + CsvOutput.new(stdout:) + when :json + JsonOutput.new(stdout:) + when :sqlite + SqliteOutput.new(filename: config.sqlite_database_file, stderr:) + else + raise "unknown format #{config.format}" + end + end + # @return [Config] def self.parse!(argv:, stdin:, stdout:, now: Time.now) config = Config.new( @@ -235,6 +381,11 @@ class QueryCloudwatch config.format = :csv end + opts.on('--sqlite [FILENAME]', 'load output into the given Sqlite database (events.db by default). Output MUST include @message and @timestamp') do |filename| + config.format = :sqlite + config.sqlite_database_file = filename || 'events.db' + end + opts.on( '--[no-]complete', 'whether or not to split query slices if exactly 10k rows are returned, defaults to off', @@ -280,6 +431,10 @@ class QueryCloudwatch end end + if config.format == :sqlite + errors << "ERROR: can't do --count-distinct with --sqlite" if config.count_distinct + end + if config.count_distinct config.complete = true config.query = [ diff --git a/lib/saml_idp_constants.rb b/lib/saml_idp_constants.rb index d8f4902fcd5..67a738998a8 100644 --- a/lib/saml_idp_constants.rb +++ b/lib/saml_idp_constants.rb @@ -50,20 +50,20 @@ module Constants IdentityConfig.store.valid_authn_contexts end).freeze - BIOMETRIC_IAL_CONTEXTS = [ + FACIAL_MATCH_IAL_CONTEXTS = [ IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR, IAL_VERIFIED_FACIAL_MATCH_PREFERRED_ACR, IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF, ].freeze - BIOMETRIC_REQUIRED_IAL_CONTEXTS = [ + FACIAL_MATCH_REQUIRED_IAL_CONTEXTS = [ IAL_VERIFIED_FACIAL_MATCH_REQUIRED_ACR, IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF, ].freeze IAL2_AUTHN_CONTEXTS = [ - *BIOMETRIC_IAL_CONTEXTS, + *FACIAL_MATCH_IAL_CONTEXTS, IAL_VERIFIED_ACR, IAL2_AUTHN_CONTEXT_CLASSREF, LOA3_AUTHN_CONTEXT_CLASSREF, diff --git a/spec/bin/query-cloudwatch_spec.rb b/spec/bin/query-cloudwatch_spec.rb index 1e44c3e69db..c17ac2e9d57 100644 --- a/spec/bin/query-cloudwatch_spec.rb +++ b/spec/bin/query-cloudwatch_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'sqlite3' load Rails.root.join('bin/query-cloudwatch') RSpec.describe QueryCloudwatch do @@ -165,6 +166,32 @@ end end + context 'with --sqlite' do + let(:argv) { required_parameters + %w[--sqlite] } + + it 'sets sqlite_database_file to events.db by default' do + config = parse! + expect(config.sqlite_database_file).to eql('events.db') + end + + context 'with a database file' do + let(:argv) { required_parameters + %w[--sqlite foo.db] } + it 'sets sqlite_database_file appropriately' do + config = parse! + expect(config.sqlite_database_file).to eql('foo.db') + end + end + + context 'with --count-distinct' do + let(:argv) { super() + %w[--count-distinct foo] } + it 'errors out' do + expect(QueryCloudwatch).to receive(:exit).with(1) + parse! + expect(stdout.string).to include("can't do --count-distinct with --sqlite") + end + end + end + context 'with --slice' do let(:argv) { required_parameters + %w[--slice 3mon] } @@ -239,13 +266,28 @@ def build_stdin_with_query(query) wait_duration: 0, query: 'fields @timestamp, @message', format: format, + sqlite_database_file: 'events.db', count_distinct: count_distinct, num_threads: Reporting::CloudwatchClient::DEFAULT_NUM_THREADS, ) end let(:query_cloudwatch) { QueryCloudwatch.new(config) } let(:stdout) { StringIO.new } - subject(:run) { query_cloudwatch.run(stdout:) } + let(:stderr) { StringIO.new } + let(:query_results) do + [ + [ + { field: '@timestamp', value: 'timestamp-1' }, + { field: '@message', value: 'message-1' }, + ], + [ + { field: '@timestamp', value: 'timestamp-2' }, + { field: '@message', value: 'message-2' }, + ], + ] + end + + subject(:run) { query_cloudwatch.run(stdout:, stderr:) } before do Aws.config[:cloudwatchlogs] = { @@ -256,16 +298,7 @@ def build_stdin_with_query(query) get_query_results: [ { status: 'Complete', - results: [ - [ - { field: '@timestamp', value: 'timestamp-1' }, - { field: '@message', value: 'message-1' }, - ], - [ - { field: '@timestamp', value: 'timestamp-2' }, - { field: '@message', value: 'message-2' }, - ], - ], + results: query_results, }, ], }, @@ -292,6 +325,237 @@ def build_stdin_with_query(query) end end + context 'with sqlite format' do + let(:format) { :sqlite } + + let(:db) do + SQLite3::Database.new(':memory:') + end + + before do + allow_any_instance_of(QueryCloudwatch::SqliteOutput).to receive(:db). + and_return(db) + allow_any_instance_of(QueryCloudwatch::SqliteOutput).to receive(:close_database) + end + + it 'does not output on stdout' do + run + expect(stdout.string).to eql('') + end + + context 'with invalid json in @message' do + let(:message_1) { 'message 1 here, not at all json' } + let(:message_2) { 'message 2 here, not at all json' } + + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: message_1 }, + ], + [ + { field: '@timestamp', value: '"2024-01-02 03:42:50.451",' }, + { field: '@message', value: message_2 }, + ], + ] + end + + it 'generates ids for events that start with NOID-' do + run + expect(db.get_first_value('SELECT COUNT(*) FROM events')).to eql(2) + + actual_ids = db.query('SELECT id FROM events') do |results| + results.map do |row| + row.first + end + end + + expect(actual_ids).to all(start_with('NOID-')) + end + + it 'outputs warnings + summary on stderr' do + run + expect(stderr.string).to eql <<~STR + WARNING: For 2 events, @message did not contain valid JSON + Wrote 2 rows to the 'events' table in events.db + STR + end + + context 'two messages are identitical' do + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: message_1 }, + ], + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: message_1 }, + ], + ] + end + it 'only inserts 1 record' do + run + expect(db.get_first_value('SELECT COUNT(*) FROM events')).to eql(1) + end + end + end + + context 'with valid JSON in @message' do + let(:message_1) do + JSON.parse(<<~JSON) + { + "id": "message_1", + "name": "IdV: doc auth image upload vendor submitted", + "properties": { + "event_properties": { + "success": true, + "errors": {}, + "exception": null + }, + "user_id": "user_1" + } + } + JSON + end + + let(:message_2) do + JSON.parse(<<~JSON) + { + "id": "message_2", + "name": "IdV: doc auth image upload vendor submitted", + "properties": { + "event_properties": { + "success": false, + "errors": {}, + "exception": null + }, + "user_id": "user_2" + } + } + JSON + end + + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: JSON.generate(message_1) }, + ], + [ + { field: '@timestamp', value: '"2024-01-02 03:42:50.451",' }, + { field: '@message', value: JSON.generate(message_2) }, + ], + ] + end + + it 'inserts 2 rows in events table' do + run + expect(db.get_first_value('SELECT COUNT(*) FROM events')).to eql(2) + end + + context 'when two messages have same id' do + let(:message_2) { message_1 } + + it 'only inserts 1 row' do + run + expect(db.get_first_value('SELECT COUNT(*) FROM events')).to eql(1) + end + end + end + + context 'when query does not return @timestamp' do + let(:query_results) do + [ + [ + { field: '@message', value: '{}' }, + ], + [ + { field: '@message', value: '{}' }, + ], + ] + end + + it 'errors out' do + expect do + run + end.to raise_error 'Query must include @timestamp in output when using --sqlite' + end + end + + context 'when query does not return @message' do + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + ], + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + ], + ] + end + + it 'errors out' do + expect do + run + end.to raise_error 'Query must include @message in output when using --sqlite' + end + end + + context 'when query returns @log' do + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: '{}' }, + { field: '@log', value: 'my log' }, + ], + [ + { field: '@timestamp', value: '"2024-01-02 03:42:50.451",' }, + { field: '@message', value: '{}' }, + { field: '@log', value: 'my other log' }, + ], + ] + end + + it 'adds them to the log column' do + run + actual_log_values = db.query('SELECT log FROM events') do |results| + results.map(&:first) + end.sort + + expect(actual_log_values).to eql(['my log', 'my other log']) + end + end + + context 'when query returns @logStream' do + let(:query_results) do + [ + [ + { field: '@timestamp', value: '2024-01-11 22:26:50.336' }, + { field: '@message', value: '{}' }, + { field: '@logStream', value: 'my log stream' }, + ], + [ + { field: '@timestamp', value: '"2024-01-02 03:42:50.451",' }, + { field: '@message', value: '{}' }, + { field: '@logStream', value: 'my other log stream' }, + ], + ] + end + + it 'adds them to the log_stream column' do + run + + actual_log_stream_values = db.query('SELECT log_stream FROM events') do |results| + results.map(&:first) + end.sort + + expect(actual_log_stream_values).to eql(['my log stream', 'my other log stream']) + end + end + end + context 'with count distinct' do let(:count_distinct) { '@message' } diff --git a/spec/controllers/account_reset/delete_account_controller_spec.rb b/spec/controllers/account_reset/delete_account_controller_spec.rb index 90b86b874db..2d5702daf03 100644 --- a/spec/controllers/account_reset/delete_account_controller_spec.rb +++ b/spec/controllers/account_reset/delete_account_controller_spec.rb @@ -126,7 +126,7 @@ ) end - it 'logs info about user biometrically verified account' do + it 'logs info about user facial matched verified account' do user = create( :user, :proofed_with_selfie, :with_phone ) diff --git a/spec/controllers/concerns/idv/document_capture_concern_spec.rb b/spec/controllers/concerns/idv/document_capture_concern_spec.rb index 6010aa4511f..d8e6ddae2dc 100644 --- a/spec/controllers/concerns/idv/document_capture_concern_spec.rb +++ b/spec/controllers/concerns/idv/document_capture_concern_spec.rb @@ -27,7 +27,7 @@ def show allow(controller).to receive(:resolved_authn_context_result).and_return(resolution_result) end - context 'SP requires biometric_comparison' do + context 'SP requires facial_match' do let(:vot) { 'Pb' } context 'selfie check performed' do @@ -47,7 +47,7 @@ def show end end - context 'SP does not require biometric_comparison' do + context 'SP does not require facial_match' do let(:vot) { 'P1' } context 'selfie check performed' do diff --git a/spec/controllers/idv_controller_spec.rb b/spec/controllers/idv_controller_spec.rb index 2d8b55609f4..031d560253c 100644 --- a/spec/controllers/idv_controller_spec.rb +++ b/spec/controllers/idv_controller_spec.rb @@ -56,12 +56,12 @@ expect(response).to redirect_to idv_activated_url end - context 'but user needs to redo idv with biometric' do + context 'but user needs to redo idv with facial match' do let(:current_sp) { create(:service_provider) } before do session[:sp] = - { issuer: current_sp.issuer, vtr: ['C2.Pb'], biometric_comparison_required: true } + { issuer: current_sp.issuer, vtr: ['C2.Pb'] } end it 'redirects to welcome' do diff --git a/spec/controllers/openid_connect/authorization_controller_spec.rb b/spec/controllers/openid_connect/authorization_controller_spec.rb index 5e5049bd526..16686795bc6 100644 --- a/spec/controllers/openid_connect/authorization_controller_spec.rb +++ b/spec/controllers/openid_connect/authorization_controller_spec.rb @@ -403,7 +403,7 @@ ) end - context 'SP requests biometric_comparison_required' do + context 'SP requests required facial match' do let(:vtr) { ['Pb'].to_json } before do @@ -433,7 +433,7 @@ end end - context 'selfie capture not enabled, biometric comparison not required' do + context 'selfie capture not enabled, facial match comparison not required' do let(:vtr) { ['P1'].to_json } it 'redirects to the service provider' do @@ -443,7 +443,7 @@ end end - context 'SP has a vector of trust that includes a biometric comparison' do + context 'SP has a vector of trust that includes a facial match comparison' do let(:acr_values) { nil } let(:vtr) { ['Pb'].to_json } @@ -475,7 +475,7 @@ end end - context 'biometric comparison was performed in-person' do + context 'facial match comparison was performed in-person' do it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do user.active_profile.idv_level = :in_person @@ -487,7 +487,7 @@ end end - context 'verified non-biometric profile with pending biometric profile' do + context 'verified non-facial match profile with pending facial match profile' do before do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('server_side') @@ -498,7 +498,7 @@ allow(controller).to receive(:pii_requested_but_locked?).and_return(false) end - context 'sp does not request biometrics' do + context 'sp does not request facial match' do let(:user) { create(:profile, :active, :verified).user } it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do @@ -520,7 +520,7 @@ end end - context 'sp requests biometrics' do + context 'sp requests facial match' do let(:user) { create(:profile, :active, :verified).user } let(:vtr) { ['C1.C2.P1.Pb'].to_json } @@ -1325,7 +1325,7 @@ ) end - context 'SP requests biometric_comparison_required' do + context 'SP requests required facial match' do let(:vtr) { ['Pb'].to_json } before do @@ -1355,7 +1355,7 @@ end end - context 'selfie capture not enabled, biometric comparison not required' do + context 'selfie capture not enabled, facial match comparison not required' do let(:vtr) { ['P1'].to_json } it 'redirects to the service provider' do @@ -1365,7 +1365,7 @@ end end - context 'SP has a vector of trust that includes a biometric comparison' do + context 'SP has a vector of trust that includes a facial match comparison' do let(:acr_values) { nil } let(:vtr) { ['Pb'].to_json } @@ -1397,7 +1397,7 @@ end end - context 'biometric comparison was performed in-person' do + context 'facial match comparison was performed in-person' do it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do user.active_profile.idv_level = :in_person @@ -1409,7 +1409,7 @@ end end - context 'verified non-biometric profile with pending biometric profile' do + context 'verified non-facial match profile with pending facial match profile' do before do allow(IdentityConfig.store).to receive(:openid_connect_redirect). and_return('server_side') @@ -1420,7 +1420,7 @@ allow(controller).to receive(:pii_requested_but_locked?).and_return(false) end - context 'sp does not request biometrics' do + context 'sp does not request facial match' do let(:user) { create(:profile, :active, :verified).user } it 'redirects to the redirect_uri immediately when pii is unlocked if client-side redirect is disabled' do @@ -1442,7 +1442,7 @@ end end - context 'sp requests biometrics' do + context 'sp requests facial match' do let(:user) { create(:profile, :active, :verified).user } let(:vtr) { ['C1.C2.P1.Pb'].to_json } diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index dba27b7a034..5a19104dccb 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -570,7 +570,7 @@ def name_id_version(format_urn) end end - context 'the request requires identity proofing with a biometric' do + context 'the request requires identity proofing with a facial match' do let(:vtr_settings) do saml_settings( overrides: { @@ -595,18 +595,18 @@ def name_id_version(format_urn) ) end - context 'the user has proofed without a biometric check' do + context 'the user has proofed without a facial match check' do before do user.active_profile.update!(idv_level: :legacy_unsupervised) end - it 'redirects to identity proofing for a user who is verified without a biometric' do + it 'redirects to identity proofing for a user who is verified without a facial match' do saml_get_auth(vtr_settings) expect(response).to redirect_to(idv_url) expect(controller.session[:sp][:vtr]).to eq(['C1.C2.P1.Pb']) end - context 'user has a pending biometric profile' do + context 'user has a pending facial match profile' do let(:vtr_settings) do saml_settings( overrides: { @@ -616,7 +616,7 @@ def name_id_version(format_urn) ) end - it 'does not redirect to proofing if sp does not request biometrics' do + it 'does not redirect to proofing if sp does not request facial match' do create( :profile, :verify_by_mail_pending, @@ -645,7 +645,7 @@ def name_id_version(format_urn) end end - context 'the user has proofed with a biometric check remotely' do + context 'the user has proofed with a facial match check remotely' do before do user.active_profile.update!(idv_level: :unsupervised_with_selfie) end @@ -657,7 +657,7 @@ def name_id_version(format_urn) end end - context 'the user has proofed with a biometric check in-person' do + context 'the user has proofed with a facial match check in-person' do before do user.active_profile.update!(idv_level: :in_person) end diff --git a/spec/factories/profiles.rb b/spec/factories/profiles.rb index f39daca07a9..66eead40d4c 100644 --- a/spec/factories/profiles.rb +++ b/spec/factories/profiles.rb @@ -79,7 +79,7 @@ end end - trait :biometric_proof do + trait :facial_match_proof do idv_level { :in_person } initiating_service_provider_issuer { 'urn:gov:gsa:openidconnect:inactive:sp:test' } end diff --git a/spec/factories/sp_upgraded_biometric_profiles.rb b/spec/factories/sp_upgraded_facial_match_profiles.rb similarity index 56% rename from spec/factories/sp_upgraded_biometric_profiles.rb rename to spec/factories/sp_upgraded_facial_match_profiles.rb index faa18072397..b115f9a933a 100644 --- a/spec/factories/sp_upgraded_biometric_profiles.rb +++ b/spec/factories/sp_upgraded_facial_match_profiles.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :sp_upgraded_biometric_profile do + factory :sp_upgraded_facial_match_profile do idv_level { :in_person } end end diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 5e3f1f61b70..4c82b87b2c8 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -1055,7 +1055,7 @@ def wait_for_event(event, wait) perform_in_browser(:desktop) do sign_in_and_2fa_user(user) - visit_idp_from_sp_with_ial2(:oidc, biometric_comparison_required: true) + visit_idp_from_sp_with_ial2(:oidc, facial_match_required: true) complete_doc_auth_steps_before_document_capture_step attach_images attach_selfie @@ -1106,66 +1106,290 @@ def wait_for_event(event, wait) end end end - context 'Happy split doc auth path' do + context 'doc_auth_separate_pages_enabled is true' do before do allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true) - allow_any_instance_of(FederatedProtocols::Oidc). - to receive(:biometric_comparison_required?). - and_return(true) - allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success) + end + context 'Hybrid flow' do + context 'facial comparison not required - Happy path' do + before do + sign_in_and_2fa_user(user) + visit_idp_from_sp_with_ial2(:oidc) + complete_welcome_step + complete_agreement_step + complete_hybrid_handoff_step + complete_document_capture_step + complete_ssn_step + complete_verify_step + complete_phone_step(user) + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key + end - perform_in_browser(:desktop) do - sign_in_and_2fa_user(user) - visit_idp_from_sp_with_ial2(:oidc, biometric_comparison_required: true) - complete_doc_auth_steps_before_document_capture_step - attach_images - continue_doc_auth_form - attach_selfie - submit_images + it 'records all of the events' do + aggregate_failures 'analytics events' do + happy_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end - click_idv_continue - visit idv_ssn_url - complete_ssn_step - complete_verify_step - fill_out_phone_form_ok('202-555-1212') - verify_phone_otp - complete_enter_password_step(user) - acknowledge_and_confirm_personal_key + aggregate_failures 'populates data for each step of the Daily Dropoff Report' do + row = CSV.parse( + Reports::DailyDropoffsReport.new.tap do |r| + r.report_date = Time.zone.now + end.report_body, + headers: true, + ).first + + Reports::DailyDropoffsReport::STEPS.each do |step| + expect(row[step].to_i).to(be > 0, "step #{step} was counted") + end + end + end + + context 'proofing_device_profiling disabled' do + let(:proofing_device_profiling) { :disabled } + let(:threatmetrix) { false } + let(:threatmetrix_response_body) { nil } + let(:threatmetrix_response) do + { + client: 'tmx_disabled', + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: nil, + review_status: 'pass', + account_lex_id: nil, + session_id: nil, + response_body: threatmetrix_response_body, + } + end + + it 'records all of the events', allow_browser_log: true do + aggregate_failures 'analytics events' do + happy_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + end + end end - end + context 'facial comparison required - Happy path' do + before do + allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:success) - it 'records all of the events' do - happy_mobile_selfie_path_events.each do |event, attributes| - expect(fake_analytics).to have_logged_event(event, attributes) + perform_in_browser(:mobile) do + sign_in_and_2fa_user(user) + visit_idp_from_sp_with_ial2(:oidc, facial_match_required: true) + complete_doc_auth_steps_before_document_capture_step + attach_images + click_continue + attach_selfie + submit_images + + click_idv_continue + visit idv_ssn_url + complete_ssn_step + complete_verify_step + fill_out_phone_form_ok('202-555-1212') + verify_phone_otp + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key + end + end + + it 'records all of the events' do + happy_mobile_selfie_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + + context 'proofing_device_profiling disabled' do + let(:proofing_device_profiling) { :disabled } + let(:threatmetrix) { false } + let(:threatmetrix_response_body) { nil } + let(:threatmetrix_response) do + { + client: 'tmx_disabled', + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: nil, + review_status: 'pass', + account_lex_id: nil, + session_id: nil, + response_body: threatmetrix_response_body, + } + end + + it 'records all of the events' do + aggregate_failures 'analytics events' do + happy_mobile_selfie_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + end + end end end + context 'facial comparison not required - Happy path' do + before do + allow(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + @sms_link = config[:link] + impl.call(**config) + end.at_least(1).times - context 'proofing_device_profiling disabled' do - let(:proofing_device_profiling) { :disabled } - let(:threatmetrix) { false } - let(:threatmetrix_response_body) { nil } - let(:threatmetrix_response) do - { - client: 'tmx_disabled', - success: true, - errors: {}, - exception: nil, - timed_out: false, - transaction_id: nil, - review_status: 'pass', - account_lex_id: nil, - session_id: nil, - response_body: threatmetrix_response_body, - } + perform_in_browser(:desktop) do + sign_in_and_2fa_user(user) + visit_idp_from_sp_with_ial2(:oidc) + complete_welcome_step + complete_agreement_step + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + attach_and_submit_images + visit idv_hybrid_mobile_document_capture_url + end + + perform_in_browser(:desktop) do + click_idv_continue + visit idv_ssn_url + complete_ssn_step + complete_verify_step + fill_out_phone_form_ok('202-555-1212') + verify_phone_otp + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key + end end it 'records all of the events' do aggregate_failures 'analytics events' do - happy_mobile_selfie_path_events.each do |event, attributes| + happy_hybrid_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + + aggregate_failures 'populates data for each step of the Daily Dropoff Report' do + row = CSV.parse( + Reports::DailyDropoffsReport.new.tap { |r| r.report_date = Time.zone.now }.report_body, + headers: true, + ).first + + Reports::DailyDropoffsReport::STEPS.each do |step| + expect(row[step].to_i).to(be > 0, "step #{step} was counted") + end + end + end + + context 'proofing_device_profiling disabled' do + let(:proofing_device_profiling) { :disabled } + let(:threatmetrix) { false } + let(:threatmetrix_response_body) { nil } + let(:threatmetrix_response) do + { + client: 'tmx_disabled', + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: nil, + review_status: 'pass', + account_lex_id: nil, + session_id: nil, + response_body: threatmetrix_response_body, + } + end + + it 'records all of the events' do + aggregate_failures 'analytics events' do + happy_hybrid_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + end + end + end + context 'in person path' do + let(:return_sp_url) { 'https://example.com/some/idv/ipp/url' } + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive(:in_person_proofing_opt_in_enabled).and_return(false) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).and_return(true) + allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter). + to receive(:service_provider_homepage_url).and_return(return_sp_url) + allow_any_instance_of(Idv::InPerson::ReadyToVerifyPresenter). + to receive(:sp_name).and_return(sp_friendly_name) + allow(IdentityConfig.store).to receive(:in_person_proofing_enforce_tmx). + and_return(true) + + start_idv_from_sp(:saml) + sign_in_and_2fa_user(user) + begin_in_person_proofing(user) + complete_all_in_person_proofing_steps(user, same_address_as_id: false) + complete_phone_step(user) + complete_enter_password_step(user) + acknowledge_and_confirm_personal_key + visit_help_center + visit_sp_from_in_person_ready_to_verify + end + + it 'records all of the events', allow_browser_log: true do + max_wait = Time.zone.now + 5.seconds + wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait) + wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait) + in_person_path_events.each do |event, attributes| + expect(fake_analytics).to have_logged_event(event, attributes) + end + end + + context 'proofing_device_profiling disabled' do + let(:proofing_device_profiling) { :disabled } + let(:idv_level) { 'legacy_in_person' } + let(:threatmetrix) { false } + let(:threatmetrix_response_body) { nil } + let(:threatmetrix_response) do + { + client: 'tmx_disabled', + success: true, + errors: {}, + exception: nil, + timed_out: false, + transaction_id: nil, + review_status: 'pass', + account_lex_id: nil, + session_id: nil, + response_body: threatmetrix_response_body, + } + end + + it 'records all of the events', allow_browser_log: true do + max_wait = Time.zone.now + 5.seconds + wait_for_event('IdV: user clicked what to bring link on ready to verify page', max_wait) + wait_for_event('IdV: user clicked sp link on ready to verify page', max_wait) + in_person_path_events.each do |event, attributes| expect(fake_analytics).to have_logged_event(event, attributes) end end end + + # wait for event to happen + def wait_for_event(event, wait) + frequency = 0.1.seconds + loop do + expect(fake_analytics).to have_logged_event(event) + return + rescue RSpec::Expectations::ExpectationNotMetError => err + raise err if wait - Time.zone.now < frequency + sleep frequency + next + end + end end end end diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 71e0b3a1299..d15b6b217cc 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -132,8 +132,6 @@ end context 'selfie check' do - let(:selfie_check_enabled) { true } - before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) end @@ -179,7 +177,7 @@ context 'with a passing selfie' do it 'proceeds to the next page with valid info, including a selfie image' do perform_in_browser(:mobile) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_document_capture_step @@ -326,11 +324,11 @@ visit_idp_from_sp_with_ial2( :oidc, **{ client_id: ipp_service_provider.issuer, - biometric_comparison_required: true }, + facial_match_required: true }, ) sign_in_and_2fa_user(@user) complete_up_to_how_to_verify_step_for_opt_in_ipp( - biometric_comparison_required: true, + facial_match_required: true, ) complete_verify_step end @@ -343,7 +341,7 @@ before do allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) perform_in_browser(:mobile) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_document_capture_step end @@ -354,8 +352,6 @@ end context 'when selfie check is not enabled (flag off, and/or in production)' do - let(:selfie_check_enabled) { false } - it 'proceeds to the next page with valid info, excluding a selfie image' do perform_in_browser(:mobile) do visit_idp_from_oidc_sp_with_ial2 @@ -399,7 +395,7 @@ describe 'when desktop selfie not allowed' do it 'can only proceed to link sent page' do perform_in_browser(:desktop) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_hybrid_handoff_step # we still have option to continue @@ -418,7 +414,7 @@ it 'proceed to the next page with valid info, including a selfie image' do perform_in_browser(:desktop) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_hybrid_handoff_step # we still have option to continue on handoff, since it's desktop no skip_hand_off @@ -461,7 +457,7 @@ describe 'when ipp is selected' do it 'proceed to the next page and start ipp' do perform_in_browser(:desktop) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_hybrid_handoff_step # we still have option to continue on handoff, since it's desktop no skip_hand_off @@ -489,7 +485,7 @@ allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_selfie_capture_enabled).and_return(true) allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true) - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) sign_in_and_2fa_user(@user) complete_doc_auth_steps_before_document_capture_step end @@ -497,7 +493,7 @@ expect(page).to have_current_path(idv_document_capture_url) expect(page).not_to have_content(t('doc_auth.tips.document_capture_selfie_text1')) attach_images - continue_doc_auth_form + click_continue expect(page).to have_title(t('doc_auth.headings.selfie_capture')) expect(page).to have_content(t('doc_auth.tips.document_capture_selfie_text1')) attach_selfie @@ -511,7 +507,7 @@ 'ial2_test_credential_multiple_doc_auth_failures_both_sides.yml' ), ) - continue_doc_auth_form + click_continue attach_selfie( Rails.root.join( 'spec', 'fixtures', @@ -522,10 +518,484 @@ expect(page).to have_content(t('doc_auth.errors.rate_limited_heading')) click_try_again expect(page).to have_content(t('doc_auth.headings.review_issues')) - attach_liveness_images + attach_images + attach_selfie submit_images expect(page).to have_content(t('doc_auth.headings.capture_complete')) end + context 'standard desktop flow' do + before do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + end + + context 'rate limits calls to backend docauth vendor', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + + (max_attempts - 1).times do + attach_and_submit_images + click_on t('idv.failure.button.warning') + end + end + + it 'redirects to the rate limited error page' do + freeze_time do + attach_and_submit_images + timeout = distance_of_time_in_words( + RateLimiter.attempt_window_in_minutes(:idv_doc_auth).minutes, + ) + message = strip_tags(t('doc_auth.errors.rate_limited_text_html', timeout: timeout)) + expect(page).to have_content(message) + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end + + it 'logs the rate limited analytics event for doc_auth' do + attach_and_submit_images + expect(fake_analytics).to have_logged_event( + 'Rate Limit Reached', + limiter_type: :idv_doc_auth, + ) + end + + context 'successfully processes image on last attempt' do + before { DocAuth::Mock::DocAuthMockClient.reset! } + + it 'proceeds to the next page with valid info' do + expect(page).to have_current_path(idv_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + attach_and_submit_images + expect(page).to have_current_path(idv_ssn_url) + + visit idv_document_capture_path + + expect(page).to have_current_path(idv_session_errors_rate_limited_path) + end + end + end + + it 'catches network connection errors on post_front_image', allow_browser_log: true do + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + + attach_and_submit_images + + expect(page).to have_current_path(idv_document_capture_url) + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + end + + it 'does not track state if state tracking is disabled' do + allow(IdentityConfig.store).to receive(:state_tracking_enabled).and_return(false) + attach_and_submit_images + + expect(DocAuthLog.find_by(user_id: @user.id).state).to be_nil + end + end + + context 'standard mobile flow' do + it 'proceeds to the next page with valid info' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + + # doc auth is successful while liveness is not req'd + use_id_image('ial2_test_credential_no_liveness.yml') + submit_images + + expect(page).to have_current_path(idv_ssn_url) + expect_costing_for_document + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('NY') + + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + end + context 'selfie check' do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + end + + context 'when a selfie is not requested by SP' do + it 'proceeds to the next page with valid info, excluding a selfie image' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + + attach_images + submit_images + + expect(page).to have_current_path(idv_ssn_url) + expect_costing_for_document + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT') + + expect(page).to have_current_path(idv_ssn_url) + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + # expect(page).to have_content(t('doc_auth.headings.document_capture_selfie')) + expect(page).to have_current_path(idv_phone_url) + end + end + end + + context 'when a selfie is required by the SP' do + context 'on mobile platform', allow_browser_log: true do + before do + # mock mobile device as cameraCapable, this allows us to process + allow_any_instance_of(ActionController::Parameters). + to receive(:[]).and_wrap_original do |impl, param_name| + param_name.to_sym == :skip_hybrid_handoff ? '' : impl.call(param_name) + end + end + + context 'with a passing selfie' do + it 'proceeds to the next page with valid info, including a selfie image' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_document_capture_url) + expect(max_capture_attempts_before_native_camera.to_i). + to eq(ActiveSupport::Duration::SECONDS_PER_HOUR) + expect(max_submission_attempts_before_native_camera.to_i). + to eq(ActiveSupport::Duration::SECONDS_PER_HOUR) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect_doc_capture_page_header(t('doc_auth.headings.document_capture_with_selfie')) + expect_doc_capture_id_subheader + attach_liveness_images + submit_images + + expect(page).to have_current_path(idv_ssn_url) + expect_costing_for_document + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT') + + expect(page).to have_current_path(idv_ssn_url) + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + end + + context 'documents or selfie with error is uploaded' do + shared_examples 'it has correct error displays' do + # when there are multiple doc auth errors on front and back + it 'shows the correct error message for the given error' do + perform_in_browser(:mobile) do + click_continue + use_id_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml') + click_continue + click_idv_submit_default + expect(page).not_to have_content(t('doc_auth.headings.capture_complete')) + expect(page).not_to have_content(t('doc_auth.errors.rate_limited_heading')) + expect(page).to have_title(t('doc_auth.headings.selfie_capture')) + + use_selfie_image('ial2_test_credential_multiple_doc_auth_failures_both_sides.yml') + submit_images + + expect_rate_limited_header(true) + + expect_try_taking_new_pictures + expect_review_issues_body_message('doc_auth.errors.general.no_liveness') + expect_rate_limit_warning(max_attempts - 1) + + expect_to_try_again + expect_resubmit_page_h1_copy + + expect_resubmit_page_body_copy('doc_auth.errors.general.no_liveness') + expect_resubmit_page_inline_error_messages(2) + expect_resubmit_page_inline_selfie_error_message(false) + + # Wrong doc type is uploaded + use_id_image('ial2_test_credential_wrong_doc_type.yml') + use_selfie_image('ial2_test_portrait_match_success.yml') + submit_images + + expect_rate_limited_header(false) + expect_try_taking_new_pictures(false) + # eslint-disable-next-line + expect_review_issues_body_message( + 'doc_auth.errors.doc_type_not_supported_heading', + ) + expect_review_issues_body_message('doc_auth.errors.doc.doc_type_check') + expect_rate_limit_warning(max_attempts - 2) + + expect_to_try_again + expect_resubmit_page_h1_copy + + expect_review_issues_body_message('doc_auth.errors.card_type') + expect_resubmit_page_inline_selfie_error_message(false) + + # when there are multiple front doc auth errors + use_id_image( + 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml', + ) + use_selfie_image( + 'ial2_test_credential_multiple_doc_auth_failures_front_side_only.yml', + ) + submit_images + + expect_rate_limited_header(true) + expect_try_taking_new_pictures(false) + expect_review_issues_body_message( + 'doc_auth.errors.general.multiple_front_id_failures', + ) + expect_rate_limit_warning(max_attempts - 3) + + expect_to_try_again + expect_resubmit_page_h1_copy + + expect_resubmit_page_body_copy( + 'doc_auth.errors.general.multiple_front_id_failures', + ) + expect_resubmit_page_inline_error_messages(1) + expect_resubmit_page_inline_selfie_error_message(false) + + # when there are multiple back doc auth errors + use_id_image( + 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml', + ) + use_selfie_image( + 'ial2_test_credential_multiple_doc_auth_failures_back_side_only.yml', + ) + submit_images + + expect_rate_limited_header(true) + expect_try_taking_new_pictures(false) + expect_review_issues_body_message( + 'doc_auth.errors.general.multiple_back_id_failures', + ) + expect_rate_limit_warning(max_attempts - 4) + + expect_to_try_again + expect_resubmit_page_h1_copy + + expect_resubmit_page_body_copy( + 'doc_auth.errors.general.multiple_back_id_failures', + ) + expect_resubmit_page_inline_error_messages(1) + expect_resubmit_page_inline_selfie_error_message(false) + + # attention barcode with invalid pii is uploaded + use_id_image('ial2_test_credential_barcode_attention_no_address.yml') + use_selfie_image('ial2_test_portrait_match_success.yml') + submit_images + + expect(page).to have_content(t('doc_auth.errors.alerts.address_check')) + expect(page).to have_current_path(idv_document_capture_path) + + click_try_again + + # And finally, after lots of errors, we can still succeed + attach_images + submit_images + + expect(page).to have_current_path(idv_ssn_path) + end + end + end + + context 'IPP enabled' do + let(:ipp_service_provider) do + create(:service_provider, :active, :in_person_proofing_enabled) + end + + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to receive( + :in_person_proofing_opt_in_enabled, + ).and_return(true) + allow_any_instance_of(ServiceProvider).to receive( + :in_person_proofing_enabled, + ).and_return(true) + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) + perform_in_browser(:mobile) do + visit_idp_from_sp_with_ial2( + :oidc, + **{ client_id: ipp_service_provider.issuer, + facial_match_required: true }, + ) + sign_in_and_2fa_user(@user) + complete_up_to_how_to_verify_step_for_opt_in_ipp( + facial_match_required: true, + ) + end + end + + it_should_behave_like 'it has correct error displays' + end + + context 'IPP not enabled' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(99) + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + end + end + + it_should_behave_like 'it has correct error displays' + end + end + + context 'when selfie check is not enabled (flag off, and/or in production)' do + it 'proceeds to the next page with valid info, excluding a selfie image' do + perform_in_browser(:mobile) do + visit_idp_from_oidc_sp_with_ial2 + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_document_capture_step + + expect(page).to have_current_path(idv_document_capture_url) + expect(max_capture_attempts_before_native_camera).to eq( + IdentityConfig.store.doc_auth_max_capture_attempts_before_native_camera.to_s, + ) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + attach_images + submit_images + + expect(page).to have_current_path(idv_ssn_url) + expect_costing_for_document + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT') + + expect(page).to have_current_path(idv_ssn_url) + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + end + end + + context 'on desktop' do + let(:desktop_selfie_mode) { false } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_selfie_desktop_test_mode). + and_return(desktop_selfie_mode) + end + + describe 'when desktop selfie not allowed' do + it 'can only proceed to link sent page' do + perform_in_browser(:desktop) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_hybrid_handoff_step + # we still have option to continue + expect(page).to have_current_path(idv_hybrid_handoff_path) + expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie')) + expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff')) + expect(page).not_to have_content(t('doc_auth.info.upload_from_computer')) + click_on t('forms.buttons.send_link') + expect(page).to have_current_path(idv_link_sent_path) + end + end + end + + describe 'when desktop selfie is allowed' do + let(:desktop_selfie_mode) { true } + + it 'proceed to the next page with valid info, including a selfie image' do + perform_in_browser(:desktop) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_hybrid_handoff_step + # we still have option to continue on handoff, since it's desktop no skip_hand_off + expect(page).to have_current_path(idv_hybrid_handoff_path) + expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie')) + expect(page).not_to have_content(t('doc_auth.headings.hybrid_handoff')) + expect(page).to have_content(t('doc_auth.info.upload_from_computer')) + click_on t('forms.buttons.upload_photos') + expect(page).to have_current_path(idv_document_capture_url) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect_doc_capture_page_header(t('doc_auth.headings.document_capture_with_selfie')) + expect_doc_capture_id_subheader + attach_liveness_images + submit_images + + expect(page).to have_current_path(idv_ssn_url) + expect_costing_for_document + expect(DocAuthLog.find_by(user_id: @user.id).state).to eq('MT') + + expect(page).to have_current_path(idv_ssn_url) + fill_out_ssn_form_ok + click_idv_continue + complete_verify_step + expect(page).to have_current_path(idv_phone_url) + end + end + + context 'when ipp is enabled' do + let(:in_person_doc_auth_button_enabled) { true } + let(:sp_ipp_enabled) { true } + + before do + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled). + and_return(in_person_doc_auth_button_enabled) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything). + and_return(sp_ipp_enabled) + end + + describe 'when ipp is selected' do + it 'proceed to the next page and start ipp' do + perform_in_browser(:desktop) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) + sign_in_and_2fa_user(@user) + complete_doc_auth_steps_before_hybrid_handoff_step + # still have option to continue handoff, since it's desktop no skip_hand_off + expect(page).to have_current_path(idv_hybrid_handoff_path) + expect(page).to have_content(t('doc_auth.headings.hybrid_handoff_selfie')) + click_on t('in_person_proofing.headings.prepare') + expect(page).to have_current_path( + idv_document_capture_path({ step: 'hybrid_handoff' }), + ) + expect_step_indicator_current_step( + t('step_indicator.flows.idv.find_a_post_office'), + ) + expect_doc_capture_page_header(t('in_person_proofing.headings.prepare')) + end + end + end + end + end + end + end + end end def expect_rate_limited_header(expected_to_be_present) @@ -597,6 +1067,7 @@ def expect_to_try_again end def use_id_image(filename) + expect(page).to have_content('Front of your ID') attach_images Rails.root.join('spec', 'fixtures', filename) end @@ -622,7 +1093,7 @@ def costing_for(cost_type) context 'before handoff page' do let(:sp_ipp_enabled) { true } let(:in_person_proofing_opt_in_enabled) { true } - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } let(:user) { user_with_2fa } before do @@ -637,7 +1108,7 @@ def costing_for(cost_type) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: service_provider.issuer, - biometric_comparison_required: biometric_comparison_required }, + facial_match_required: facial_match_required }, ) sign_in_via_branded_page(user) complete_doc_auth_steps_before_agreement_step @@ -652,7 +1123,7 @@ def costing_for(cost_type) end context 'when selfie is disabled' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'redirects back to agreement page' do expect(page).to have_current_path(idv_agreement_path) diff --git a/spec/features/idv/doc_auth/how_to_verify_spec.rb b/spec/features/idv/doc_auth/how_to_verify_spec.rb index 5de62644584..24a5fb5778a 100644 --- a/spec/features/idv/doc_auth/how_to_verify_spec.rb +++ b/spec/features/idv/doc_auth/how_to_verify_spec.rb @@ -10,7 +10,7 @@ let(:in_person_proofing_enabled) { true } let(:in_person_proofing_opt_in_enabled) { false } let(:service_provider_in_person_proofing_enabled) { true } - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } before do allow(IdentityConfig.store).to receive(:in_person_proofing_enabled) { in_person_proofing_enabled @@ -22,7 +22,7 @@ and_return(service_provider_in_person_proofing_enabled) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: ipp_service_provider.issuer, - biometric_comparison_required: biometric_comparison_required } + facial_match_required: facial_match_required } ) sign_in_via_branded_page(user) complete_doc_auth_steps_before_agreement_step @@ -105,7 +105,7 @@ context 'when selfie is enabled' do include InPersonHelper - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'goes to direct IPP if selected and can come back' do expect(page).to have_current_path(idv_how_to_verify_path) diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 075ede1f608..2a33550fb23 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'hybrid_handoff step send link and errors', allowed_extra_analytics: [:*] do +RSpec.feature 'hybrid_handoff step send link and errors', :js do include IdvStepHelper include DocAuthHelper include ActionView::Helpers::DateHelper @@ -10,250 +10,254 @@ let(:idv_send_link_attempt_window_in_minutes) do IdentityConfig.store.idv_send_link_attempt_window_in_minutes end - let(:biometric_comparison_required) { false } - - before do - if biometric_comparison_required - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: biometric_comparison_required) - end - sign_in_and_2fa_user - allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) - end - - context 'on a desktop device send link' do + let(:facial_match_required) { false } + context 'split doc auth', allow_browser_log: true do before do - complete_doc_auth_steps_before_hybrid_handoff_step + allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true) + if facial_match_required + visit_idp_from_oidc_sp_with_ial2( + facial_match_required: facial_match_required, + ) + end + sign_in_and_2fa_user + allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) end - it 'has the forms with the expected aria attributes' do - mobile_form = find('#form-to-submit-photos-through-mobile') - desktop_form = find('#form-to-submit-photos-through-desktop') - - expect(mobile_form).to have_name(t('forms.buttons.send_link')) - expect(desktop_form).to have_name(t('forms.buttons.upload_photos')) - end + context 'on a desktop device send link' do + before do + complete_doc_auth_steps_before_hybrid_handoff_step + end - it 'proceeds to link sent page when user chooses to use phone' do - click_send_link + it 'has the forms with the expected aria attributes' do + mobile_form = find('#form-to-submit-photos-through-mobile') + desktop_form = find('#form-to-submit-photos-through-desktop') - expect(page).to have_current_path(idv_link_sent_path) - expect(fake_analytics).to have_logged_event( - 'IdV: doc auth hybrid handoff submitted', - hash_including(step: 'hybrid_handoff', destination: :link_sent), - ) - end + expect(mobile_form).to have_name(t('forms.buttons.send_link')) + expect(desktop_form).to have_name(t('forms.buttons.upload_photos')) + end - it 'proceeds to the next page with valid info', :js do - expect(Telephony).to receive(:send_doc_auth_link). - with(hash_including(to: '+1 415-555-0199')). - and_call_original + it 'proceeds to link sent page when user chooses to use phone' do + click_send_link - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) + expect(page).to have_current_path(idv_link_sent_path) + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth hybrid handoff submitted', + hash_including(step: 'hybrid_handoff', destination: :link_sent), + ) + end - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link + it 'proceeds to the next page with valid info', :js do + expect(Telephony).to receive(:send_doc_auth_link). + with(hash_including(to: '+1 415-555-0199')). + and_call_original - expect(page).to have_current_path(idv_link_sent_path) - end + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - it 'does not proceed to the next page with invalid info', :js do - fill_in :doc_auth_phone, with: '' - click_send_link + fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link - expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) - end + expect(page).to have_current_path(idv_link_sent_path) + end - it 'sends a link that does not contain any underscores' do - # because URLs with underscores sometimes get messed up by carriers - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - expect(config[:link]).to_not include('_') + it 'does not proceed to the next page with invalid info', :js do + fill_in :doc_auth_phone, with: '' + click_send_link - impl.call(**config) + expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) end - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link + it 'sends a link that does not contain any underscores' do + # because URLs with underscores sometimes get messed up by carriers + expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + expect(config[:link]).to_not include('_') - expect(page).to have_current_path(idv_link_sent_path) - end + impl.call(**config) + end - it 'does not proceed if Telephony raises an error' do - fill_in :doc_auth_phone, with: '225-555-1000' + fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link + + expect(page).to have_current_path(idv_link_sent_path) + end - click_send_link + it 'does not proceed if Telephony raises an error' do + fill_in :doc_auth_phone, with: '225-555-1000' - expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) - expect(page).to have_content I18n.t('telephony.error.friendly_message.generic') - end + click_send_link - it 'displays error if user selects a country to which we cannot send SMS', js: true do - click_on t('components.phone_input.country_code_label') - within(page.find('.iti__country-container', visible: :all)) do - find('span', text: 'Sri Lanka').click + expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) + expect(page).to have_content I18n.t('telephony.error.friendly_message.generic') end - focused_input = page.find('.phone-input__number:focus') - error_message_id = focused_input[:'aria-describedby']&.split(' ')&.find do |id| - page.has_css?(".usa-error-message##{id}") + it 'displays error if user selects a country to which we cannot send SMS', js: true do + click_on t('components.phone_input.country_code_label') + within(page.find('.iti__country-container', visible: :all)) do + find('span', text: 'Sri Lanka').click + end + focused_input = page.find('.phone-input__number:focus') + + error_message_id = focused_input[:'aria-describedby']&.split(' ')&.find do |id| + page.has_css?(".usa-error-message##{id}") + end + expect(error_message_id).to_not be_empty + + error_message = page.find_by_id(error_message_id) + expect(error_message).to have_content( + t( + 'two_factor_authentication.otp_delivery_preference.sms_unsupported', + location: 'Sri Lanka', + ), + ) + click_send_link + expect(page.find(':focus')).to match_css('.phone-input__number') end - expect(error_message_id).to_not be_empty - - error_message = page.find_by_id(error_message_id) - expect(error_message).to have_content( - t( - 'two_factor_authentication.otp_delivery_preference.sms_unsupported', - location: 'Sri Lanka', - ), - ) - click_send_link - expect(page.find(':focus')).to match_css('.phone-input__number') - end - it 'rate limits sending the link' do - user = user_with_2fa - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_hybrid_handoff_step - timeout = distance_of_time_in_words( - RateLimiter.attempt_window_in_minutes(:idv_send_link).minutes, - ) - allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts). - and_return(idv_send_link_max_attempts) + it 'rate limits sending the link' do + user = user_with_2fa + sign_in_and_2fa_user(user) + complete_doc_auth_steps_before_hybrid_handoff_step + timeout = distance_of_time_in_words( + RateLimiter.attempt_window_in_minutes(:idv_send_link).minutes, + ) + allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts). + and_return(idv_send_link_max_attempts) - freeze_time do - idv_send_link_max_attempts.times do - expect(page).to_not have_content( - I18n.t('doc_auth.errors.send_link_limited', timeout: timeout), - ) + freeze_time do + idv_send_link_max_attempts.times do + expect(page).to_not have_content( + I18n.t('doc_auth.errors.send_link_limited', timeout: timeout), + ) + + fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link + + expect(page).to have_current_path(idv_link_sent_path) + + click_doc_auth_back_link + end fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link + expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) + expect(page).to have_content( + I18n.t( + 'doc_auth.errors.send_link_limited', + timeout: timeout, + ), + ) + expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff')) + expect(page).to have_selector('h2', text: t('doc_auth.headings.upload_from_phone')) + end + expect(fake_analytics).to have_logged_event( + 'Rate Limit Reached', + limiter_type: :idv_send_link, + ) + # Manual expiration is needed for now since the RateLimiter uses + # Redis ttl instead of expiretime + RateLimiter.new(rate_limit_type: :idv_send_link, user: user).reset! + travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do + fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link expect(page).to have_current_path(idv_link_sent_path) - - click_doc_auth_back_link end + end - fill_in :doc_auth_phone, with: '415-555-0199' + it 'includes expected URL parameters' do + expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + params = Rack::Utils.parse_nested_query URI(config[:link]).query - click_send_link - expect(page).to have_current_path(idv_hybrid_handoff_path, ignore_query: true) - expect(page).to have_content( - I18n.t( - 'doc_auth.errors.send_link_limited', - timeout: timeout, - ), - ) - expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff')) - expect(page).to have_selector('h2', text: t('doc_auth.headings.upload_from_phone')) - end - expect(fake_analytics).to have_logged_event( - 'Rate Limit Reached', - limiter_type: :idv_send_link, - ) + expect(params['document-capture-session']).to be_a_kind_of(String) + + impl.call(**config) + end - # Manual expiration is needed for now since the RateLimiter uses - # Redis ttl instead of expiretime - RateLimiter.new(rate_limit_type: :idv_send_link, user: user).reset! - travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do fill_in :doc_auth_phone, with: '415-555-0199' click_send_link - expect(page).to have_current_path(idv_link_sent_path) end - end - it 'includes expected URL parameters' do - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - params = Rack::Utils.parse_nested_query URI(config[:link]).query + it 'sets requested_at on the capture session' do + doc_capture_session_uuid = nil - expect(params['document-capture-session']).to be_a_kind_of(String) - - impl.call(**config) - end - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - end + expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| + params = Rack::Utils.parse_nested_query URI(config[:link]).query + doc_capture_session_uuid = params['document-capture-session'] + impl.call(**config) + end - it 'sets requested_at on the capture session' do - document_capture_session_uuid = nil + fill_in :doc_auth_phone, with: '415-555-0199' + click_send_link - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - params = Rack::Utils.parse_nested_query URI(config[:link]).query - document_capture_session_uuid = params['document-capture-session'] - impl.call(**config) + document_capture_session = DocumentCaptureSession.find_by(uuid: doc_capture_session_uuid) + expect(document_capture_session).to be + expect(document_capture_session).to have_attributes(requested_at: a_kind_of(Time)) end - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - - document_capture_session = DocumentCaptureSession.find_by(uuid: document_capture_session_uuid) - expect(document_capture_session).to be - expect(document_capture_session).to have_attributes(requested_at: a_kind_of(Time)) - end - end - - context 'on a desktop device and selfie is allowed' do - before do - complete_doc_auth_steps_before_hybrid_handoff_step end - describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } - it 'has expected UI elements' do - mobile_form = find('#form-to-submit-photos-through-mobile') - expect(mobile_form).to have_name(t('forms.buttons.send_link')) - expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie')) + context 'on a desktop device and selfie is allowed' do + before do + complete_doc_auth_steps_before_hybrid_handoff_step end - context 'on a desktop choose ipp', js: true do - let(:in_person_doc_auth_button_enabled) { true } - let(:sp_ipp_enabled) { true } - before do - allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled). - and_return(in_person_doc_auth_button_enabled) - allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything). - and_return(sp_ipp_enabled) - complete_doc_auth_steps_before_hybrid_handoff_step + + describe 'when selfie is required by sp' do + let(:facial_match_required) { true } + it 'has expected UI elements' do + mobile_form = find('#form-to-submit-photos-through-mobile') + expect(mobile_form).to have_name(t('forms.buttons.send_link')) + expect(page).to have_selector('h1', text: t('doc_auth.headings.hybrid_handoff_selfie')) end + context 'on a desktop choose ipp', js: true do + let(:in_person_doc_auth_button_enabled) { true } + let(:sp_ipp_enabled) { true } + before do + allow(IdentityConfig.store).to receive(:in_person_doc_auth_button_enabled). + and_return(in_person_doc_auth_button_enabled) + allow(Idv::InPersonConfig).to receive(:enabled_for_issuer?).with(anything). + and_return(sp_ipp_enabled) + complete_doc_auth_steps_before_hybrid_handoff_step + end - context 'when ipp is enabled' do - it 'proceeds to ipp if selected and can go back' do - expect(page).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) - click_on t('in_person_proofing.headings.prepare') - expect(page).to have_current_path(idv_document_capture_path({ step: 'hybrid_handoff' })) - click_on t('forms.buttons.back') - expect(page).to have_current_path(idv_hybrid_handoff_path) + context 'when ipp is enabled' do + it 'proceeds to ipp if selected and can go back' do + expect(page).to have_content(strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html'))) + click_on t('in_person_proofing.headings.prepare') + hybrid_step = { step: 'hybrid_handoff' } + expect(page).to have_current_path(idv_document_capture_path(hybrid_step)) + click_on t('forms.buttons.back') + expect(page).to have_current_path(idv_hybrid_handoff_path) + end end - end - context 'when ipp is disabled' do - let(:in_person_doc_auth_button_enabled) { false } - let(:sp_ipp_enabled) { false } - it 'has no ipp option can be selected' do - expect(page).to_not have_content( - strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')), - ) - expect(page).to_not have_content( - t('in_person_proofing.headings.prepare'), - ) + context 'when ipp is disabled' do + let(:in_person_doc_auth_button_enabled) { false } + let(:sp_ipp_enabled) { false } + it 'has no ipp option can be selected' do + expect(page).to_not have_content( + strip_tags(t('doc_auth.info.hybrid_handoff_ipp_html')), + ) + expect(page).to_not have_content( + t('in_person_proofing.headings.prepare'), + ) + end end end end - end - describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } - it 'has expected UI elements' do - mobile_form = find('#form-to-submit-photos-through-mobile') - desktop_form = find('#form-to-submit-photos-through-desktop') + describe 'when selfie is not required by sp' do + let(:facial_match_required) { false } + it 'has expected UI elements' do + mobile_form = find('#form-to-submit-photos-through-mobile') + desktop_form = find('#form-to-submit-photos-through-desktop') - expect(mobile_form).to have_name(t('forms.buttons.send_link')) - expect(desktop_form).to have_name(t('forms.buttons.upload_photos')) + expect(mobile_form).to have_name(t('forms.buttons.send_link')) + expect(desktop_form).to have_name(t('forms.buttons.upload_photos')) + end end end end end -RSpec.feature 'hybrid_handoff step for ipp, selfie variances', js: true, - allowed_extra_analytics: [:*] do +RSpec.feature 'hybrid_handoff step for ipp, selfie variances', js: true do include IdvStepHelper include DocAuthHelper include InPersonHelper @@ -319,7 +323,7 @@ def verify_no_upload_photos_section_and_link(page) let(:in_person_proofing_enabled) { true } let(:sp_ipp_enabled) { true } let(:in_person_proofing_opt_in_enabled) { true } - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } let(:user) { user_with_2fa } before do @@ -340,7 +344,7 @@ def verify_no_upload_photos_section_and_link(page) visit_idp_from_sp_with_ial2( :oidc, **{ client_id: service_provider.issuer, - biometric_comparison_required: biometric_comparison_required }, + facial_match_required: facial_match_required }, ) sign_in_via_branded_page(user) complete_doc_auth_steps_before_agreement_step @@ -363,7 +367,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } before do click_on t('forms.buttons.continue_remote') end @@ -378,7 +382,7 @@ def verify_no_upload_photos_section_and_link(page) context 'when sp ipp is not available' do let(:sp_ipp_enabled) { false } describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no ipp option section, no upload section' do verify_handoff_page_selfie_version_content(page) @@ -387,7 +391,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -404,7 +408,7 @@ def verify_no_upload_photos_section_and_link(page) let(:sp_ipp_enabled) { false } describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no upload section, no ipp option section' do verify_handoff_page_selfie_version_content(page) @@ -413,7 +417,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -426,7 +430,7 @@ def verify_no_upload_photos_section_and_link(page) let(:sp_ipp_enabled) { true } context 'when selfie is disabled system wide' do describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -446,7 +450,7 @@ def verify_no_upload_photos_section_and_link(page) context 'when sp ipp is available' do let(:sp_ipp_enabled) { true } describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no upload section, no ipp option section' do verify_handoff_page_selfie_version_content(page) @@ -455,7 +459,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -467,7 +471,7 @@ def verify_no_upload_photos_section_and_link(page) context 'when sp ipp is not available' do let(:sp_ipp_enabled) { false } describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no upload section, no ipp option section' do verify_handoff_page_selfie_version_content(page) @@ -476,7 +480,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -492,7 +496,7 @@ def verify_no_upload_photos_section_and_link(page) context 'when sp ipp is enabled' do let(:sp_ipp_enabled) { true } describe 'when selfie is required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no upload section, no ipp option section' do verify_handoff_page_selfie_version_content(page) @@ -501,7 +505,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie is not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) @@ -513,7 +517,7 @@ def verify_no_upload_photos_section_and_link(page) context 'when sp ipp is not enabled' do let(:sp_ipp_enabled) { false } describe 'when selfie required by sp' do - let(:biometric_comparison_required) { true } + let(:facial_match_required) { true } it 'shows selfie version of top content, no upload section, no ipp option section' do verify_handoff_page_selfie_version_content(page) @@ -522,7 +526,7 @@ def verify_no_upload_photos_section_and_link(page) end end describe 'when selfie not required by sp' do - let(:biometric_comparison_required) { false } + let(:facial_match_required) { false } it 'shows non selfie version of top content and upload section, no ipp option section' do verify_handoff_page_non_selfie_version_content(page) diff --git a/spec/features/idv/doc_auth/redo_document_capture_spec.rb b/spec/features/idv/doc_auth/redo_document_capture_spec.rb index f31492795d8..a80aeaab178 100644 --- a/spec/features/idv/doc_auth/redo_document_capture_spec.rb +++ b/spec/features/idv/doc_auth/redo_document_capture_spec.rb @@ -239,7 +239,7 @@ before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_success_face_match_fail @@ -261,7 +261,7 @@ before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_pass_and_portrait_match_not_live @@ -304,7 +304,7 @@ before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_failure_face_match_pass @@ -346,7 +346,7 @@ before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_fail_face_match_fail @@ -367,7 +367,7 @@ pii[:address1] = nil allow_any_instance_of(DocAuth::LexisNexis::Responses::TrueIdResponse). to receive(:pii_from_doc).and_return(Pii::StateId.new(**pii)) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_and_2fa_user complete_doc_auth_steps_before_document_capture_step mock_doc_auth_pass_face_match_pass_no_address1 @@ -418,4 +418,422 @@ end end end + + context 'split doc auth flow', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true) + end + context 'when barcode scan returns a warning', allow_browser_log: true do + let(:use_bad_ssn) { false } + + before do + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + + if use_bad_ssn + fill_out_ssn_form_with_ssn_that_fails_resolution + else + fill_out_ssn_form_ok + end + + click_idv_continue + end + + it 'shows a warning message to allow the user to return to upload new images' do + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + + expect(page).to have_css( + '[role="status"]', + text: strip_nbsp( + t( + 'doc_auth.headings.capture_scan_warning_html', + link_html: warning_link_text, + ), + ), + ) + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth hybrid handoff visited', + hash_including(redo_document_capture: true), + ) + complete_hybrid_handoff_step + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth document_capture visited', + hash_including(redo_document_capture: true), + ) + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(current_path).to eq(idv_ssn_path) + expect(page).to have_css('[role="status"]') # We verified your ID + complete_ssn_step + + expect(current_path).to eq(idv_verify_info_path) + check t('forms.ssn.show') + expect(page).to have_content(DocAuthHelper::GOOD_SSN) + end + + context 'with a bad SSN' do + let(:use_bad_ssn) { true } + + it 'shows a troubleshooting option to allow the user to cancel and return to SP' do + complete_verify_step + expect(page).to have_link( + t('links.cancel'), + href: idv_cancel_path(step: :invalid_session), + ) + + click_link t('links.cancel') + + expect(current_path).to eq(idv_cancel_path) + end + end + + context 'on mobile', driver: :headless_chrome_mobile do + it 'shows a warning message to allow the user to return to upload new images' do + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + + expect(page).to have_css( + '[role="status"]', + text: strip_nbsp( + t( + 'doc_auth.headings.capture_scan_warning_html', + link_html: warning_link_text, + ), + ), + ) + click_link warning_link_text + + expect(current_path).to eq(idv_document_capture_path) + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth document_capture visited', + hash_including(redo_document_capture: true), + ) + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(current_path).to eq(idv_ssn_path) + expect(page).to have_css('[role="status"]') # We verified your ID + complete_ssn_step + + expect(current_path).to eq(idv_verify_info_path) + check t('forms.ssn.show') + expect(page).to have_content(DocAuthHelper::GOOD_SSN) + end + end + end + + shared_examples_for 'image re-upload allowed' do + it 'allows user to submit the same image again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3), + ) + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 2), + ) + expect(current_path).to eq(idv_ssn_path) + check t('forms.ssn.show') + end + end + + shared_examples_for 'image re-upload not allowed' do + it 'stops user submitting the same image again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3, submit_attempts: 1), + ) + DocAuth::Mock::DocAuthMockClient.reset! + attach_images + # Error message without submit + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + end + end + + shared_examples_for 'document and selfie images re-upload not allowed' do + it 'stops user submitting the same images again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3, submit_attempts: 1), + ) + DocAuth::Mock::DocAuthMockClient.reset! + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + attach_selfie + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 1, + ) + + attach_images + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 3, + ) + end + end + + shared_examples_for 'inline error for 4xx status shown' do |status| + it "shows inline error for status #{status}" do + error = case status + when 438 + t('doc_auth.errors.http.image_load.failed_short') + when 439 + t('doc_auth.errors.http.pixel_depth.failed_short') + when 440 + t('doc_auth.errors.http.image_size.failed_short') + end + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: error, + ) + end + end + context 'error due to data issue with 2xx status code', allow_browser_log: true do + before do + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_general_doc_auth_client_error(:get_results) + attach_and_submit_images + click_try_again + end + it_behaves_like 'image re-upload not allowed' + end + + context 'error due to data issue with 4xx status code with trueid', allow_browser_log: true do + before do + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_trueid_http_non2xx_status(438) + attach_and_submit_images + # verify it's a network error + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + click_try_again + end + + it_behaves_like 'image re-upload allowed' + end + + context 'error due to http status error but non 4xx status code with trueid', + allow_browser_log: true do + before do + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_trueid_http_non2xx_status(500) + attach_and_submit_images + # verify it's a network error + expect(page).to have_content(I18n.t('doc_auth.errors.general.network_error')) + click_try_again + end + it_behaves_like 'image re-upload allowed' + end + + context 'when selfie is enabled' do + context 'when doc auth is success and face match fails (2xx)', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail) + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_success_face_match_fail + attach_images + click_continue + attach_selfie + submit_images + click_try_again + sleep(10) + end + + it_behaves_like 'document and selfie images re-upload not allowed' + + it 'shows current existing header' do + expect_doc_capture_page_header(t('doc_auth.headings.review_issues')) + end + end + + context 'when doc auth passes and portrait match is not live', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_pass_and_portrait_match_not_live + attach_images + click_continue + attach_selfie + submit_images + click_try_again + sleep(10) + end + + it 'stops user submitting the same images again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3, submit_attempts: 1), + ) + DocAuth::Mock::DocAuthMockClient.reset! + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + + attach_selfie + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 1, + ) + + attach_images + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 1, + ) + end + end + + context 'when doc auth fails and portrait match pass', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_failure_face_match_pass + attach_images + click_continue + attach_selfie + submit_images + click_try_again + sleep(10) + end + + it 'stops user submitting the same images again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3, submit_attempts: 1), + ) + DocAuth::Mock::DocAuthMockClient.reset! + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + + attach_selfie + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + + attach_images + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 2, + ) + end + end + + context 'when doc auth and portrait match fail', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + allow_any_instance_of(DocAuth::Response).to receive(:selfie_status).and_return(:fail) + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_fail_face_match_fail + attach_images + click_continue + attach_selfie + submit_images + click_try_again + sleep(10) + end + + it_behaves_like 'document and selfie images re-upload not allowed' + end + + context 'when pii validation fails', allow_browser_log: true do + before do + allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) + pii = Idp::Constants::MOCK_IDV_APPLICANT.dup + pii[:address1] = nil + allow_any_instance_of(DocAuth::LexisNexis::Responses::TrueIdResponse). + to receive(:pii_from_doc).and_return(Pii::StateId.new(**pii)) + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_pass_face_match_pass_no_address1 + attach_images + click_continue + attach_selfie + submit_images + click_try_again + sleep(10) + end + + it 'shows selfie inline error messages for both front and back' do + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.general.multiple_front_id_failures'), + count: 1, + ) + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.general.multiple_back_id_failures'), + count: 1, + ) + end + + it 'stops user submitting the same images again' do + expect(fake_analytics).to have_logged_event('IdV: doc auth document_capture visited') + expect(fake_analytics).to have_logged_event( + 'IdV: doc auth image upload form submitted', + hash_including(remaining_submit_attempts: 3, submit_attempts: 1), + ) + DocAuth::Mock::DocAuthMockClient.reset! + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + + attach_selfie + expect(page).not_to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + ) + + attach_images + expect(page).to have_css( + '.usa-error-message[role="alert"]', + text: t('doc_auth.errors.doc.resubmit_failed_image'), + count: 2, + ) + end + end + end + end end diff --git a/spec/features/idv/doc_auth/test_credentials_spec.rb b/spec/features/idv/doc_auth/test_credentials_spec.rb index 0a23423ab07..849236666e8 100644 --- a/spec/features/idv/doc_auth/test_credentials_spec.rb +++ b/spec/features/idv/doc_auth/test_credentials_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.feature 'doc auth test credentials', :js, allowed_extra_analytics: [:*] do +RSpec.feature 'doc auth test credentials', :js do include IdvStepHelper include DocAuthHelper diff --git a/spec/features/idv/gpo_disabled_spec.rb b/spec/features/idv/gpo_disabled_spec.rb index 859ded499fc..69cfcf3b379 100644 --- a/spec/features/idv/gpo_disabled_spec.rb +++ b/spec/features/idv/gpo_disabled_spec.rb @@ -37,14 +37,14 @@ end end - context 'GPO address verification disallowed for biometric comparison' do + context 'GPO address verification disallowed for facial match comparison' do before do allow(IdentityConfig.store).to receive(:use_vot_in_sp_requests).and_return(true) end - it 'does not allow verify by mail with biometric comparison', :js do + it 'does not allow verify by mail with facial match comparison', :js do user = user_with_2fa - start_idv_from_sp(:oidc, biometric_comparison_required: true) + start_idv_from_sp(:oidc, facial_match_required: true) sign_in_and_2fa_user(user) complete_all_doc_auth_steps(with_selfie: true) @@ -56,9 +56,9 @@ expect(page).to have_current_path(idv_phone_path) end - it 'does allow verify by mail without biometric comparison', :js do + it 'does allow verify by mail without facial match comparison', :js do user = user_with_2fa - start_idv_from_sp(:oidc, biometric_comparison_required: false) + start_idv_from_sp(:oidc, facial_match_required: false) sign_in_and_2fa_user(user) complete_all_doc_auth_steps(with_selfie: false) click_on t('idv.troubleshooting.options.verify_by_mail') diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index e1b04ea52ed..28e4281e3ec 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -20,103 +20,12 @@ end.at_least(1).times end - it 'proofs and hands off to mobile', js: true do - user = nil - - perform_in_browser(:desktop) do - visit_idp_from_sp_with_ial2(sp) - user = sign_up_and_2fa_ial1_user - - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - - expect(page).to have_content(t('doc_auth.headings.text_message')) - expect(page).to have_content(t('doc_auth.info.you_entered')) - expect(page).to have_content('+1 415-555-0199') - - # Confirm that Continue button is not shown when polling is enabled - expect(page).not_to have_content(t('doc_auth.buttons.continue')) - end - - expect(@sms_link).to be_present - - perform_in_browser(:mobile) do - visit @sms_link - - # Confirm that jumping to LinkSent page does not cause errors - visit idv_link_sent_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_document_capture_url - - # Confirm that clicking cancel and then coming back doesn't cause errors - click_link 'Cancel' - visit idv_hybrid_mobile_document_capture_url - - # Confirm that jumping to Phone page does not cause errors - visit idv_phone_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_document_capture_url - - # Confirm that jumping to Welcome page does not cause errors - visit idv_welcome_url - expect(page).to have_current_path(root_url) - visit idv_hybrid_mobile_document_capture_url - - expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - attach_and_submit_images - - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - - # Confirm app disallows jumping back to DocumentCapture page - visit idv_hybrid_mobile_document_capture_url - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - end - - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - expect(page).to have_current_path(idv_ssn_path) - - fill_out_ssn_form_ok - click_idv_continue - - expect(page).to have_content(t('headings.verify')) - complete_verify_step - - prefilled_phone = page.find(id: 'idv_phone_form_phone').value - - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(user.default_phone_configuration.phone), - ) - - fill_out_phone_form_ok - verify_phone_otp - - fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD - click_idv_continue - - acknowledge_and_confirm_personal_key - - validate_idv_completed_page(user) - click_agree_and_continue - - validate_return_to_sp - end - end - - context 'when biometric confirmation is requested' do + shared_examples_for 'hybrid flow doc auth' do it 'proofs and hands off to mobile', js: true do user = nil perform_in_browser(:desktop) do - visit_idp_from_oidc_sp_with_ial2(biometric_comparison_required: true) - + visit_idp_from_sp_with_ial2(sp) user = sign_up_and_2fa_ial1_user complete_doc_auth_steps_before_hybrid_handoff_step @@ -156,9 +65,8 @@ visit idv_hybrid_mobile_document_capture_url expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) - attach_images - attach_selfie - submit_images + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + attach_and_submit_images expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) @@ -202,91 +110,101 @@ validate_return_to_sp end end - end - it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do - user = nil + context 'when facial confirmation is requested' do + it 'proofs and hands off to mobile', js: true do + user = nil - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + perform_in_browser(:desktop) do + visit_idp_from_oidc_sp_with_ial2(facial_match_required: true) - expect(page).to have_content(t('doc_auth.headings.text_message')) - end + user = sign_up_and_2fa_ial1_user - expect(@sms_link).to be_present + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - perform_in_browser(:mobile) do - visit @sms_link - expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - click_on t('links.cancel') - click_on t('forms.buttons.cancel') # Yes, cancel - end + expect(page).to have_content(t('doc_auth.headings.text_message')) + expect(page).to have_content(t('doc_auth.info.you_entered')) + expect(page).to have_content('+1 415-555-0199') - perform_in_browser(:desktop) do - expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + # Confirm that Continue button is not shown when polling is enabled + expect(page).not_to have_content(t('doc_auth.buttons.continue')) + end - expect(page).to have_content(t('doc_auth.headings.text_message')) - end - end + expect(@sms_link).to be_present - context 'user is rate limited on mobile' do - let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + perform_in_browser(:mobile) do + visit @sms_link - before do - allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) - DocAuth::Mock::DocAuthMockClient.mock_response!( - method: :post_front_image, - response: DocAuth::Response.new( - success: false, - errors: { network: I18n.t('doc_auth.errors.general.network_error') }, - ), - ) - end + # Confirm that jumping to LinkSent page does not cause errors + visit idv_link_sent_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_document_capture_url - it 'shows capture complete on mobile and error page on desktop', js: true do - user = nil + # Confirm that clicking cancel and then coming back doesn't cause errors + click_link 'Cancel' + visit idv_hybrid_mobile_document_capture_url - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link + # Confirm that jumping to Phone page does not cause errors + visit idv_phone_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_document_capture_url - expect(page).to have_content(t('doc_auth.headings.text_message')) - end + # Confirm that jumping to Welcome page does not cause errors + visit idv_welcome_url + expect(page).to have_current_path(root_url) + visit idv_hybrid_mobile_document_capture_url - expect(@sms_link).to be_present + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) + attach_liveness_images + submit_images - perform_in_browser(:mobile) do - visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - (max_attempts - 1).times do - attach_and_submit_images - click_on t('idv.failure.button.warning') + # Confirm app disallows jumping back to DocumentCapture page + visit idv_hybrid_mobile_document_capture_url + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) end - # final failure - attach_and_submit_images + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + expect(page).to have_current_path(idv_ssn_path) - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).not_to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - end + fill_out_ssn_form_ok + click_idv_continue - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + expect(page).to have_content(t('headings.verify')) + complete_verify_step + + prefilled_phone = page.find(id: 'idv_phone_form_phone').value + + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(user.default_phone_configuration.phone), + ) + + fill_out_phone_form_ok + verify_phone_otp + + fill_in t('idv.form.password'), with: Features::SessionHelper::VALID_PASSWORD + click_idv_continue + + acknowledge_and_confirm_personal_key + + validate_idv_completed_page(user) + click_agree_and_continue + + validate_return_to_sp + end end end - end - context 'barcode read error on mobile (redo document capture)' do - it 'continues to ssn on desktop when user selects Continue', js: true do + it 'shows the waiting screen correctly after cancelling from mobile and restarting', js: true do user = nil perform_in_browser(:desktop) do @@ -302,173 +220,266 @@ perform_in_browser(:mobile) do visit @sms_link + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + click_on t('links.cancel') + click_on t('forms.buttons.cancel') # Yes, cancel + end - mock_doc_auth_attention_with_barcode - attach_and_submit_images - click_idv_continue + perform_in_browser(:desktop) do + expect(page).to_not have_content(t('doc_auth.headings.text_message'), wait: 10) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) + expect(page).to have_content(t('doc_auth.headings.text_message')) end + end - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) + context 'user is rate limited on mobile' do + let(:max_attempts) { IdentityConfig.store.doc_auth_max_attempts } + + before do + allow(IdentityConfig.store).to receive(:doc_auth_max_attempts).and_return(max_attempts) + DocAuth::Mock::DocAuthMockClient.mock_response!( + method: :post_front_image, + response: DocAuth::Response.new( + success: false, + errors: { network: I18n.t('doc_auth.errors.general.network_error') }, + ), + ) + end - fill_out_ssn_form_ok - click_idv_continue + it 'shows capture complete on mobile and error page on desktop', js: true do + user = nil - expect(page).to have_current_path(idv_verify_info_path, wait: 10) + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link - # verify pii is displayed - expect(page).to have_text('DAVID') - expect(page).to have_text('SAMPLE') - expect(page).to have_text('123 ABC AVE') + expect(page).to have_content(t('doc_auth.headings.text_message')) + end - warning_link_text = t('doc_auth.headings.capture_scan_warning_link') - click_link warning_link_text + expect(@sms_link).to be_present - expect(current_path).to eq(idv_hybrid_handoff_path) - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - end + perform_in_browser(:mobile) do + visit @sms_link - perform_in_browser(:mobile) do - visit @sms_link + (max_attempts - 1).times do + attach_and_submit_images + click_on t('idv.failure.button.warning') + end - DocAuth::Mock::DocAuthMockClient.reset! - attach_and_submit_images + # final failure + attach_and_submit_images - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).not_to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_session_errors_rate_limited_path, wait: 10) + end end + end - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) - complete_ssn_step - expect(page).to have_current_path(idv_verify_info_path) - - # verify orig pii no longer displayed - expect(page).not_to have_text('DAVID') - expect(page).not_to have_text('SAMPLE') - expect(page).not_to have_text('123 ABC AVE') - # verify new pii from redo is displayed - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + context 'barcode read error on mobile (redo document capture)' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil - complete_verify_step + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + + 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 + + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_content(strip_nbsp(t('doc_auth.headings.capture_complete'))) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + + fill_out_ssn_form_ok + click_idv_continue + + expect(page).to have_current_path(idv_verify_info_path, wait: 10) + + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') + + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text + + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end + + perform_in_browser(:mobile) do + visit @sms_link + + DocAuth::Mock::DocAuthMockClient.reset! + attach_and_submit_images + + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_verify_info_path) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + complete_verify_step + end end end - end - context 'barcode read error on desktop, redo document capture on mobile' do - it 'continues to ssn on desktop when user selects Continue', js: true do - user = nil + context 'barcode read error on desktop, redo document capture on mobile' do + it 'continues to ssn on desktop when user selects Continue', js: true do + user = nil - perform_in_browser(:desktop) do - user = sign_in_and_2fa_user - complete_doc_auth_steps_before_document_capture_step - mock_doc_auth_attention_with_barcode - attach_and_submit_images - click_idv_continue - expect(page).to have_current_path(idv_ssn_path, wait: 10) + perform_in_browser(:desktop) do + user = sign_in_and_2fa_user + complete_doc_auth_steps_before_document_capture_step + mock_doc_auth_attention_with_barcode + attach_and_submit_images + click_idv_continue + expect(page).to have_current_path(idv_ssn_path, wait: 10) - fill_out_ssn_form_ok - click_idv_continue + fill_out_ssn_form_ok + click_idv_continue - expect(page).to have_current_path(idv_verify_info_path, wait: 10) + expect(page).to have_current_path(idv_verify_info_path, wait: 10) - # verify pii is displayed - expect(page).to have_text('DAVID') - expect(page).to have_text('SAMPLE') - expect(page).to have_text('123 ABC AVE') + # verify pii is displayed + expect(page).to have_text('DAVID') + expect(page).to have_text('SAMPLE') + expect(page).to have_text('123 ABC AVE') - warning_link_text = t('doc_auth.headings.capture_scan_warning_link') - click_link warning_link_text + warning_link_text = t('doc_auth.headings.capture_scan_warning_link') + click_link warning_link_text - expect(current_path).to eq(idv_hybrid_handoff_path) - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - end + expect(current_path).to eq(idv_hybrid_handoff_path) + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link + end - perform_in_browser(:mobile) do - visit @sms_link + perform_in_browser(:mobile) do + visit @sms_link - DocAuth::Mock::DocAuthMockClient.reset! + DocAuth::Mock::DocAuthMockClient.reset! - expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - visit(idv_hybrid_mobile_document_capture_url(selfie: true)) - expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url(selfie: true)) - expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) + visit(idv_hybrid_mobile_document_capture_url(selfie: true)) + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url(selfie: true)) + expect(page).not_to have_content(t('doc_auth.headings.document_capture_selfie')) - attach_and_submit_images + attach_and_submit_images - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + end + + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) + complete_ssn_step + expect(page).to have_current_path(idv_verify_info_path) + + # verify orig pii no longer displayed + expect(page).not_to have_text('DAVID') + expect(page).not_to have_text('SAMPLE') + expect(page).not_to have_text('123 ABC AVE') + # verify new pii from redo is displayed + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) + expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + + complete_verify_step + end end + end + + it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do + user = create(:user, :with_authentication_app) perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) - complete_ssn_step - expect(page).to have_current_path(idv_verify_info_path) - - # verify orig pii no longer displayed - expect(page).not_to have_text('DAVID') - expect(page).not_to have_text('SAMPLE') - expect(page).not_to have_text('123 ABC AVE') - # verify new pii from redo is displayed - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:first_name]) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:last_name]) - expect(page).to have_text(Idp::Constants::MOCK_IDV_APPLICANT[:address1]) + start_idv_from_sp(facial_match_required: true) + sign_in_and_2fa_user(user) - complete_verify_step + complete_doc_auth_steps_before_hybrid_handoff_step + clear_and_fill_in(:doc_auth_phone, phone_number) + click_send_link end - end - end - - it 'prefills the phone number used on the phone step if the user has no MFA phone', :js do - user = create(:user, :with_authentication_app) - perform_in_browser(:desktop) do - start_idv_from_sp(biometric_comparison_required: true) - sign_in_and_2fa_user(user) + expect(@sms_link).to be_present - complete_doc_auth_steps_before_hybrid_handoff_step - clear_and_fill_in(:doc_auth_phone, phone_number) - click_send_link - end + perform_in_browser(:mobile) do + visit @sms_link - expect(@sms_link).to be_present + expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) - perform_in_browser(:mobile) do - visit @sms_link + attach_liveness_images + submit_images - expect(page).to have_current_path(idv_hybrid_mobile_document_capture_url) + expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) + expect(page).to have_text(t('doc_auth.instructions.switch_back')) + end - attach_liveness_images - submit_images + perform_in_browser(:desktop) do + expect(page).to have_current_path(idv_ssn_path, wait: 10) - expect(page).to have_current_path(idv_hybrid_mobile_capture_complete_url) - expect(page).to have_text(t('doc_auth.instructions.switch_back')) - end + fill_out_ssn_form_ok + click_idv_continue - perform_in_browser(:desktop) do - expect(page).to have_current_path(idv_ssn_path, wait: 10) + expect(page).to have_content(t('headings.verify')) + complete_verify_step - fill_out_ssn_form_ok - click_idv_continue + prefilled_phone = page.find(id: 'idv_phone_form_phone').value - expect(page).to have_content(t('headings.verify')) - complete_verify_step + expect( + PhoneFormatter.format(prefilled_phone), + ).to eq( + PhoneFormatter.format(phone_number), + ) + end + end + end - prefilled_phone = page.find(id: 'idv_phone_form_phone').value + it_behaves_like 'hybrid flow doc auth' - expect( - PhoneFormatter.format(prefilled_phone), - ).to eq( - PhoneFormatter.format(phone_number), - ) + context 'split doc auth flow' do + before do + allow(IdentityConfig.store).to receive(:doc_auth_separate_pages_enabled).and_return(true) end + + it_behaves_like 'hybrid flow doc auth' end end diff --git a/spec/features/idv/step_up_spec.rb b/spec/features/idv/step_up_spec.rb index e00732f1a8b..5a18c286802 100644 --- a/spec/features/idv/step_up_spec.rb +++ b/spec/features/idv/step_up_spec.rb @@ -12,7 +12,7 @@ end scenario 'User with active profile can redo idv when selfie required', js: true do - visit_idp_from_sp_with_ial2(sp, biometric_comparison_required: true) + visit_idp_from_sp_with_ial2(sp, facial_match_required: true) sign_in_live_with_2fa(user) expect(page).to have_current_path(idv_welcome_path) diff --git a/spec/features/idv/verify_by_mail_pending_spec.rb b/spec/features/idv/verify_by_mail_pending_spec.rb index b8251533908..c49aeb45775 100644 --- a/spec/features/idv/verify_by_mail_pending_spec.rb +++ b/spec/features/idv/verify_by_mail_pending_spec.rb @@ -8,7 +8,7 @@ profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user) create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago) - start_idv_from_sp(biometric_comparison_required: false) + start_idv_from_sp(facial_match_required: false) sign_in_live_with_2fa(user) expect(current_path).to eq(idv_verify_by_mail_enter_code_path) @@ -24,16 +24,16 @@ expect(current_path).to eq(idv_welcome_path) end - it 'does not require them to enter their code if they are upgrading to biometric' do + it 'does not require them to enter their code if they are upgrading to facial match' do user = create(:user, :fully_registered) profile = create(:profile, :with_pii, :verify_by_mail_pending, user: user) create(:gpo_confirmation_code, profile: profile, created_at: 2.days.ago, updated_at: 2.days.ago) - start_idv_from_sp(biometric_comparison_required: true) + start_idv_from_sp(facial_match_required: true) sign_in_live_with_2fa(user) # The user is redirected to proofing since their pending profile does not meet - # the biometric comparison requirement + # the facial match comparison requirement expect(current_path).to eq(idv_welcome_path) end end diff --git a/spec/features/openid_connect/vtr_spec.rb b/spec/features/openid_connect/vtr_spec.rb index d4d4c740b18..06683ad5da2 100644 --- a/spec/features/openid_connect/vtr_spec.rb +++ b/spec/features/openid_connect/vtr_spec.rb @@ -110,7 +110,8 @@ expect(current_path).to eq(idv_welcome_path) end - scenario 'sign in with VTR request for idv with biometric requires idv with biometric', :js do + scenario 'sign in with VTR request for idv with facial match requires idv with facial match', + :js do user = create(:user, :fully_registered) visit_idp_from_oidc_sp_with_vtr(vtr: ['Pb']) diff --git a/spec/features/saml/vtr_spec.rb b/spec/features/saml/vtr_spec.rb index ede0ab39f5f..7321a806b07 100644 --- a/spec/features/saml/vtr_spec.rb +++ b/spec/features/saml/vtr_spec.rb @@ -189,7 +189,7 @@ expect(ssn).to eq(pii[:ssn]) end - scenario 'sign in with VTR request for idv with biometric requires idv with biometric', + scenario 'sign in with VTR request for idv with facial match requires idv with facial match', :js, allowed_extra_analytics: [:*] do user = create(:user, :proofed) diff --git a/spec/features/sign_in/multiple_vot_spec.rb b/spec/features/sign_in/multiple_vot_spec.rb index d7715eb1bc4..8145eee78cb 100644 --- a/spec/features/sign_in/multiple_vot_spec.rb +++ b/spec/features/sign_in/multiple_vot_spec.rb @@ -7,8 +7,8 @@ include DocAuthHelper context 'with OIDC' do - context 'biometric and non-biometric proofing is acceptable' do - scenario 'identity proofing is not required if user is proofed with biometric' do + context 'facial match and non-facial match proofing is acceptable' do + scenario 'identity proofing is not required if user is proofed with facial match' do user = create(:user, :proofed_with_selfie) visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) @@ -23,7 +23,7 @@ expect(user_info[:vot]).to eq('C1.C2.P1.Pb') end - scenario 'identity proofing is not required if user is proofed without biometric' do + scenario 'identity proofing is not required if user is proofed without facial match' do user = create(:user, :proofed) visit_idp_from_oidc_sp_with_vtr(vtr: ['C1.C2.P1.Pb', 'C1.C2.P1']) @@ -38,7 +38,7 @@ expect(user_info[:vot]).to eq('C1.C2.P1') end - scenario 'identity proofing with biometric is required if user is not proofed', + scenario 'identity proofing with facial match is required if user is not proofed', :js, allowed_extra_analytics: [:*] do user = create(:user, :fully_registered) @@ -133,8 +133,8 @@ end end - context 'biometric and non-biometric proofing is acceptable' do - scenario 'identity proofing is not required if user is proofed with biometric' do + context 'facial match and non-facial match proofing is acceptable' do + scenario 'identity proofing is not required if user is proofed with facial match' do user = create(:user, :proofed_with_selfie) visit_saml_authn_request_url( @@ -155,7 +155,7 @@ expect(first_name).to_not be_blank end - scenario 'identity proofing is not required if user is proofed without biometric' do + scenario 'identity proofing is not required if user is proofed without facial match' do user = create(:user, :proofed) visit_saml_authn_request_url( @@ -176,7 +176,7 @@ expect(first_name).to_not be_blank end - scenario 'identity proofing with biometric is required if user is not proofed', + scenario 'identity proofing with facial match is required if user is not proofed', :js, allowed_extra_analytics: [:*] do user = create(:user, :fully_registered) diff --git a/spec/forms/openid_connect_authorize_form_spec.rb b/spec/forms/openid_connect_authorize_form_spec.rb index 580983f27d7..6fb26fda4d7 100644 --- a/spec/forms/openid_connect_authorize_form_spec.rb +++ b/spec/forms/openid_connect_authorize_form_spec.rb @@ -211,13 +211,13 @@ end end - shared_examples 'allows biometric IAL only if sp is authorized' do |biometric_ial| - let(:acr_values) { biometric_ial } + shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| + let(:acr_values) { facial_match_ial } - context "when the IAL requested is #{biometric_ial}" do - context 'when the service provider is allowed to use biometric ials' do + context "when the IAL requested is #{facial_match_ial}" do + context 'when the service provider is allowed to use facial match ials' do before do - allow_any_instance_of(ServiceProvider).to receive(:biometric_ial_allowed?). + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(true) end @@ -226,9 +226,9 @@ end end - context 'when the service provider is not allowed to use biometric ials' do + context 'when the service provider is not allowed to use facial match ials' do before do - allow_any_instance_of(ServiceProvider).to receive(:biometric_ial_allowed?). + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(false) end @@ -241,10 +241,10 @@ end end - it_behaves_like 'allows biometric IAL only if sp is authorized', + it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF - it_behaves_like 'allows biometric IAL only if sp is authorized', + it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF context 'with aal but not ial requested via acr_values' do diff --git a/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx b/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx index 0b78ae39936..a22d1deaa24 100644 --- a/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx +++ b/spec/javascript/packages/document-capture/components/document-side-acuant-capture-spec.tsx @@ -10,6 +10,7 @@ describe('DocumentSideAcuantCapture', () => { value: '', onChange: () => undefined, onError: () => undefined, + isReviewStep: false, }; context('when selfie is _not_ enabled', () => { diff --git a/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb index dad90904785..aba220a8154 100644 --- a/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb +++ b/spec/jobs/reports/idv_legacy_conversion_supplement_report_spec.rb @@ -56,7 +56,7 @@ ) iaa_order1.save create( - :sp_upgraded_biometric_profile, + :sp_upgraded_facial_match_profile, issuer: iaa1_sp.issuer, user_id: user1.id, upgraded_at: inside_iaa1, ) end @@ -136,12 +136,12 @@ ) create( - :sp_upgraded_biometric_profile, + :sp_upgraded_facial_match_profile, issuer: iaa2_sp1.issuer, user_id: user1.id, upgraded_at: inside_iaa2, ) create( - :sp_upgraded_biometric_profile, + :sp_upgraded_facial_match_profile, issuer: iaa2_sp2.issuer, user_id: user2.id, upgraded_at: inside_iaa2, ) end @@ -234,11 +234,11 @@ iaa_order3.integrations << integration_1 create( - :sp_upgraded_biometric_profile, + :sp_upgraded_facial_match_profile, issuer: iaa3_sp1.issuer, user_id: user1.id, upgraded_at: iaa3_range.begin + 1.day, ) create( - :sp_upgraded_biometric_profile, + :sp_upgraded_facial_match_profile, issuer: iaa3_sp1.issuer, user_id: user2.id, upgraded_at: iaa3_range.begin + 1.month, ) end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index cf4b1aa3004..7e709aba761 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -347,44 +347,48 @@ expect(active_profile.verified_at).to be_present end - context 'when a user creates a biometric comparision profile' do + context 'when a user creates a facial match comparision profile' do context 'when the user has an active profile' do - it 'creates a biometric upgrade record' do + it 'creates a facial match upgrade record' do profile.activate - biometric_profile = create( + facial_match_profile = create( :profile, - :biometric_proof, + :facial_match_proof, user: user, ) - expect { biometric_profile.activate }.to( + expect { facial_match_profile.activate }.to( change do - SpUpgradedBiometricProfile.count + SpUpgradedFacialMatchProfile.count end.by(1), ) end end - context 'when the user has an active biometric profile' do - it 'does not create a biometric conversion record' do - create(:profile, :active, :biometric_proof, user: user) + context 'when the user has an active facial match profile' do + it 'does not create a facial match conversion record' do + create(:profile, :active, :facial_match_proof, user: user) - biometric_reproof = create(:profile, :biometric_proof, user: user) - expect { biometric_reproof.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + facial_match_reproof = create(:profile, :facial_match_proof, user: user) + expect { facial_match_reproof.activate }.to_not( + change do + SpUpgradedFacialMatchProfile.count + end, + ) end end context 'when the user does not have an active profile' do - it 'does not create a biometric conversion record' do - profile = create(:profile, :biometric_proof, user: user) + it 'does not create a facial match conversion record' do + profile = create(:profile, :facial_match_proof, user: user) - expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + expect { profile.activate }.to_not(change { SpUpgradedFacialMatchProfile.count }) end end end - it 'does not create a biometric upgrade record for a non-biometric profile' do - expect { profile.activate }.to_not(change { SpUpgradedBiometricProfile.count }) + it 'does not create a facial match upgrade record for a non-facial match profile' do + expect { profile.activate }.to_not(change { SpUpgradedFacialMatchProfile.count }) end it 'sends a reproof completed push event' do diff --git a/spec/models/service_provider_spec.rb b/spec/models/service_provider_spec.rb index ba79ea601f9..532b8e24ca2 100644 --- a/spec/models/service_provider_spec.rb +++ b/spec/models/service_provider_spec.rb @@ -81,8 +81,8 @@ end end - describe '#biometric_ial_allowed?' do - context 'when the biometric ial feature is enabled' do + describe '#facial_match_ial_allowed?' do + context 'when the facial match ial feature is enabled' do before do allow(IdentityConfig.store).to receive(:biometric_ial_enabled). and_return(true) @@ -94,8 +94,8 @@ and_return([service_provider.issuer]) end - it 'allows the service provider to use biometric IALs' do - expect(service_provider.biometric_ial_allowed?).to be(true) + it 'allows the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(true) end end @@ -105,13 +105,13 @@ and_return([]) end - it 'does not allow the service provider to use biometric IALs' do - expect(service_provider.biometric_ial_allowed?).to be(false) + it 'does not allow the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(false) end end end - context 'when the biometric ial feature is disabled' do + context 'when the facial match ial feature is disabled' do before do allow(IdentityConfig.store).to receive(:biometric_ial_enabled). and_return(false) @@ -123,8 +123,8 @@ and_return([service_provider.issuer]) end - it 'does not allow the service provider to use biometric IALs' do - expect(service_provider.biometric_ial_allowed?).to be(false) + it 'does not allow the service provider to use facial match IALs' do + expect(service_provider.facial_match_ial_allowed?).to be(false) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 046e855586b..387260dace3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1433,7 +1433,7 @@ def it_should_not_send_survey end end - describe '#identity_verified_with_biometric_comparison?' do + describe '#identity_verified_with_facial_match?' do let(:user) { create(:user) } let(:active_profile) do create( @@ -1446,23 +1446,23 @@ def it_should_not_send_survey it 'returns true if user has an active profile with selfie' do active_profile.idv_level = :unsupervised_with_selfie active_profile.save - expect(user.identity_verified_with_biometric_comparison?).to eq true + expect(user.identity_verified_with_facial_match?).to eq true end it 'returns false if user has an active profile without selfie' do - expect(user.identity_verified_with_biometric_comparison?).to eq false + expect(user.identity_verified_with_facial_match?).to eq false end it 'return true if user has an active in-person profile' do active_profile.idv_level = :in_person active_profile.save - expect(user.identity_verified_with_biometric_comparison?).to eq true + expect(user.identity_verified_with_facial_match?).to eq true end context 'user does not have active profile' do let(:active_profile) { nil } it 'returns false' do - expect(user.identity_verified_with_biometric_comparison?).to eq false + expect(user.identity_verified_with_facial_match?).to eq false end end end diff --git a/spec/policies/pending_profile_policy_spec.rb b/spec/policies/pending_profile_policy_spec.rb index a4f927542b8..a6518e4c652 100644 --- a/spec/policies/pending_profile_policy_spec.rb +++ b/spec/policies/pending_profile_policy_spec.rb @@ -21,7 +21,7 @@ end describe '#user_has_pending_profile?' do - context 'has an active non-biometric profile and biometric comparison is requested' do + context 'has an active non-facial match profile and facial match comparison is requested' do let(:idv_level) { :unsupervised_with_selfie } before do create(:profile, :active, :verified, idv_level: :legacy_unsupervised, user: user) @@ -36,7 +36,7 @@ end end - context 'with biometric comparison requested ACR value' do + context 'with facial match comparison requested ACR value' do let(:acr_values) { Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF } it 'has a usable pending profile' do @@ -45,7 +45,7 @@ end end - context 'no biometric comparison is requested' do + context 'no facial match comparison is requested' do let(:idv_level) { :legacy_unsupervised } let(:vtr) { ['C2'] } context 'user has pending profile' do @@ -64,7 +64,7 @@ it { expect(policy.user_has_pending_profile?).to eq(false) } end - context 'user has active legacy profile with a pending fraud biometric profile' do + context 'user has active legacy profile with a pending fraud facial match profile' do before do create(:profile, :active, :verified, idv_level: idv_level, user: user) create(:profile, :fraud_review_pending, idv_level: :unsupervised_with_selfie, user: user) diff --git a/spec/policies/service_provider_mfa_policy_spec.rb b/spec/policies/service_provider_mfa_policy_spec.rb index 181aeff9c99..2c126dc827e 100644 --- a/spec/policies/service_provider_mfa_policy_spec.rb +++ b/spec/policies/service_provider_mfa_policy_spec.rb @@ -14,7 +14,7 @@ hspd12?: hspd12, phishing_resistant?: phishing_resistant, identity_proofing?: false, - biometric_comparison?: false, + facial_match?: false, two_pieces_of_fair_evidence?: false, ialmax?: false, enhanced_ipp?: false, diff --git a/spec/presenters/account_show_presenter_spec.rb b/spec/presenters/account_show_presenter_spec.rb index 977cc522123..fd2e3bd0997 100644 --- a/spec/presenters/account_show_presenter_spec.rb +++ b/spec/presenters/account_show_presenter_spec.rb @@ -26,14 +26,14 @@ ) end - describe 'identity_verified_with_biometric_comparison?' do - subject(:identity_verified_with_biometric_comparison?) do - presenter.identity_verified_with_biometric_comparison? + describe 'identity_verified_with_facial_match?' do + subject(:identity_verified_with_facial_match?) do + presenter.identity_verified_with_facial_match? end it 'delegates to user' do - expect(identity_verified_with_biometric_comparison?).to eq( - user.identity_verified_with_biometric_comparison?, + expect(identity_verified_with_facial_match?).to eq( + user.identity_verified_with_facial_match?, ) end end @@ -80,30 +80,30 @@ it { is_expected.to eq(false) } - context 'with non-biometric proofed user' do + context 'with non-facial match proofed user' do let(:user) { build(:user, :proofed) } it { is_expected.to eq(true) } - context 'with sp request for non-biometric' do + context 'with sp request for non-facial match' do let(:vtr) { ['C2.P1'] } it { is_expected.to eq(true) } end - context 'with sp request for biometric' do + context 'with sp request for facial match' do let(:vtr) { ['C2.Pb'] } it { is_expected.to eq(false) } end end - context 'with biometric proofed user' do + context 'with facial match proofed user' do let(:user) { build(:user, :proofed_with_selfie) } it { is_expected.to eq(true) } - context 'with sp request for biometric' do + context 'with sp request for facial match' do let(:vtr) { ['C2.Pb'] } it { is_expected.to eq(true) } @@ -116,30 +116,30 @@ it { is_expected.to eq(false) } - context 'with sp request for non-biometric' do + context 'with sp request for non-facial match' do let(:vtr) { ['C2.P1'] } it { is_expected.to eq(true) } - context 'with non-biometric proofed user' do + context 'with non-facial match proofed user' do let(:user) { build(:user, :proofed) } it { is_expected.to eq(false) } end end - context 'with sp request for biometric' do + context 'with sp request for facial match' do let(:vtr) { ['C2.Pb'] } it { is_expected.to eq(true) } - context 'with non-biometric proofed user' do + context 'with non-facial match proofed user' do let(:user) { build(:user, :proofed) } it { is_expected.to eq(true) } end - context 'with biometric proofed user' do + context 'with facial match proofed user' do let(:user) { build(:user, :proofed_with_selfie) } it { is_expected.to eq(false) } @@ -254,13 +254,13 @@ end end - describe '#formatted_nonbiometric_idv_date' do + describe '#formatted_legacy_idv_date' do let(:user) { build(:user, :proofed_with_selfie) } - subject(:formatted_nonbiometric_idv_date) { presenter.formatted_nonbiometric_idv_date } + subject(:formatted_legacy_idv_date) { presenter.formatted_legacy_idv_date } it 'formats a date string' do - expect { Date.parse(formatted_nonbiometric_idv_date) }.not_to raise_error + expect { Date.parse(formatted_legacy_idv_date) }.not_to raise_error end end diff --git a/spec/presenters/openid_connect_user_info_presenter_spec.rb b/spec/presenters/openid_connect_user_info_presenter_spec.rb index 8aa2bfecce4..b0f5d213e2c 100644 --- a/spec/presenters/openid_connect_user_info_presenter_spec.rb +++ b/spec/presenters/openid_connect_user_info_presenter_spec.rb @@ -184,7 +184,7 @@ end end - context 'with biometric comparison' do + context 'with facial match comparison' do let(:acr_values) do [ Saml::Idp::Constants::AAL2_AUTHN_CONTEXT_CLASSREF, diff --git a/spec/services/analytics_spec.rb b/spec/services/analytics_spec.rb index 402e75cda4a..76b797e27d4 100644 --- a/spec/services/analytics_spec.rb +++ b/spec/services/analytics_spec.rb @@ -236,7 +236,7 @@ end end - context 'phishing resistant and requiring biometric comparison' do + context 'phishing resistant and requiring facial match comparison' do let(:session) { { sp: { vtr: ['Ca.Pb'] } } } let(:component_values) do { @@ -252,7 +252,7 @@ { sp_request: { aal2: true, - biometric_comparison: true, + facial_match: true, two_pieces_of_fair_evidence: true, component_values:, identity_proofing: true, @@ -312,7 +312,7 @@ end end - context 'IAL2 with biometric' do + context 'IAL2 with facial match' do let(:session) do { sp: { acr_values: Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF } } end @@ -320,7 +320,7 @@ { sp_request: { aal2: true, - biometric_comparison: true, + facial_match: true, two_pieces_of_fair_evidence: true, component_values: { 'ial/2?bio=required' => true }, identity_proofing: true, diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index aa13f7f3885..7b6eb7a7f94 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -4,7 +4,9 @@ include SamlAuthHelper let(:user) { create(:profile, :active, :verified).user } - let(:biometric_verified_user) { create(:profile, :active, :verified, idv_level: :in_person).user } + let(:facial_match_verified_user) do + create(:profile, :active, :verified, idv_level: :in_person).user + end let(:user_session) { {} } let(:identity) do build( @@ -76,7 +78,7 @@ ] end - context 'when the user has been proofed without biometric' do + context 'when the user has been proofed without facial match' do context 'custom bundle includes email, phone, and first_name' do before do user.identities << identity @@ -240,7 +242,7 @@ end end - context 'when the user has been proofed with biometric' do + context 'when the user has been proofed with facial match' do let(:user) { create(:profile, :active, :verified, idv_level: :in_person).user } before do @@ -275,7 +277,7 @@ end context 'when an IAL1 request is made' do - context 'when the user has been proofed without biometric comparison' do + context 'when the user has been proofed without facial match comparison' do context 'custom bundle includes email, phone, and first_name' do before do user.identities << identity @@ -444,7 +446,7 @@ end end - context 'when the user has been proofed with biometric comparison' do + context 'when the user has been proofed with facial match comparison' do let(:user) { create(:profile, :active, :verified, idv_level: :in_person).user } before do @@ -593,7 +595,7 @@ end end - context 'when biometric IAL preferred is requested' do + context 'when facial match IAL preferred is requested' do let(:options) do { authn_context: [ @@ -602,34 +604,34 @@ } end - context 'when the user has been proofed with biometric' do - let(:user) { biometric_verified_user } + context 'when the user has been proofed with facial match' do + let(:user) { facial_match_verified_user } before do user.identities << identity subject.build end - it 'asserts IAL2 with biometric comparison' do + it 'asserts IAL2 with facial match comparison' do expected_ial = Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF expect(get_asserted_attribute(user, :ial)).to eq expected_ial end end - context 'when the user has been proofed without biometric' do + context 'when the user has been proofed without facial match' do before do user.identities << identity subject.build end - it 'asserts IAL2 (without biometric comparison)' do + it 'asserts IAL2 (without facial match comparison)' do expected_ial = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF expect(get_asserted_attribute(user, :ial)).to eq expected_ial end end end - context 'when biometric IAL required is requested' do + context 'when facial match IAL required is requested' do let(:options) do { authn_context: [ @@ -638,15 +640,15 @@ } end - context 'when the user has been proofed with biometric comparison' do - let(:user) { biometric_verified_user } + context 'when the user has been proofed with facial match comparison' do + let(:user) { facial_match_verified_user } before do user.identities << identity subject.build end - it 'asserts IAL2 with biometric comparison' do + it 'asserts IAL2 with facial match comparison' do expected_ial = Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF expect(get_asserted_attribute(user, :ial)).to eq expected_ial end diff --git a/spec/services/authn_context_resolver_spec.rb b/spec/services/authn_context_resolver_spec.rb index 59ae6de9ed0..60e4f4f6417 100644 --- a/spec/services/authn_context_resolver_spec.rb +++ b/spec/services/authn_context_resolver_spec.rb @@ -19,7 +19,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(true) - expect(result.biometric_comparison?).to eq(true) + expect(result.facial_match?).to eq(true) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -39,7 +39,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(true) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(true) end @@ -64,8 +64,8 @@ end context 'when the user uses a vtr param with multiple vectors' do - context 'a biometric proofing vector and non-biometric proofing vector is present' do - it 'returns a biometric requirement if the user can satisfy it' do + context 'a facial match proofing vector and non-facial match proofing vector is present' do + it 'returns a facial match requirement if the user can satisfy it' do user = create(:user, :proofed) user.active_profile.update!(idv_level: 'unsupervised_with_selfie') vtr = ['C2.Pb', 'C2.P1'] @@ -78,11 +78,11 @@ ).result expect(result.expanded_component_values).to eq('C1.C2.P1.Pb') - expect(result.biometric_comparison?).to eq(true) + expect(result.facial_match?).to eq(true) expect(result.identity_proofing?).to eq(true) end - it 'returns the non-biometric vector if the user has identity-proofed without biometric' do + it 'returns non-facial match vector if user has identity-proofed without facial match' do user = create(:user, :proofed) vtr = ['C2.Pb', 'C2.P1'] @@ -94,7 +94,7 @@ ).result expect(result.expanded_component_values).to eq('C1.C2.P1') - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.identity_proofing?).to eq(true) end @@ -110,12 +110,12 @@ ).result expect(result.expanded_component_values).to eq('C1.C2.P1.Pb') - expect(result.biometric_comparison?).to eq(true) + expect(result.facial_match?).to eq(true) expect(result.identity_proofing?).to eq(true) end end - context 'a non-biometric identity proofing vector is present' do + context 'a non-facial match identity proofing vector is present' do it 'returns the identity-proofing requirement if the user can satisfy it' do user = create(:user, :proofed) vtr = ['C2.P1', 'C2'] @@ -168,7 +168,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -190,7 +190,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -212,7 +212,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -346,7 +346,7 @@ end end - context 'if requesting biometric comparison' do + context 'if requesting facial match comparison' do let(:bio_value) { 'required' } let(:acr_values) do [ @@ -355,34 +355,34 @@ ].join(' ') end - context 'with biometric comparison is required' do + context 'with facial match comparison is required' do context 'when user is not verified' do - it 'sets biometric_comparison to true' do + it 'sets facial_match to true' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true expect(result.two_pieces_of_fair_evidence?).to be true end end context 'when the user is already verified' do - context 'without biometric comparison' do + context 'without facial match comparison' do let(:user) { build(:user, :proofed) } - it 'asserts biometric_comparison as true' do + it 'asserts facial_match as true' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true expect(result.two_pieces_of_fair_evidence?).to be true end end - context 'with biometric comparison' do + context 'with facial match comparison' do let(:user) { build(:user, :proofed_with_selfie) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.aal2?).to be true end @@ -390,27 +390,27 @@ end end - context 'with biometric comparison is preferred' do + context 'with facial match comparison is preferred' do let(:bio_value) { 'preferred' } context 'when the user is already verified' do - context 'without biometric comparison' do + context 'without facial match comparison' do let(:user) { build(:user, :proofed) } - it 'falls back on proofing without biometric comparison' do + it 'falls back on proofing without facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be false + expect(result.facial_match?).to be false expect(result.two_pieces_of_fair_evidence?).to be false expect(result.aal2?).to be true end end - context 'with biometric comparison' do + context 'with facial match comparison' do let(:user) { build(:user, :proofed_with_selfie) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true end end @@ -419,9 +419,9 @@ context 'when the user has not yet been verified' do let(:user) { build(:user) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true end end @@ -465,7 +465,7 @@ expect(result.component_names).to eq(acr_values) expect(result.to_h).to include( aal2?: false, - biometric_comparison?: false, + facial_match?: false, enhanced_ipp?: false, hspd12?: false, ialmax?: false, @@ -495,7 +495,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -518,7 +518,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -539,7 +539,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -615,7 +615,7 @@ end end - context 'if requesting biometric comparison' do + context 'if requesting facial match comparison' do let(:bio_value) { 'required' } let(:acr_values) do [ @@ -626,20 +626,20 @@ before do allow_any_instance_of(ServiceProvider). - to receive(:biometric_ial_allowed?). + to receive(:facial_match_ial_allowed?). and_return(true) end - context 'with biometric comparison is required' do + context 'with facial match comparison is required' do context 'when user is not verified' do it "asserts the resolved IAL as #{Saml::Idp::Constants::IAL_AUTH_ONLY_ACR}" do expect(subject.asserted_ial_acr). to eq(Saml::Idp::Constants::IAL_AUTH_ONLY_ACR) end - it 'sets biometric_comparison to true' do + it 'sets facial_match to true' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.ialmax?).to be false @@ -647,24 +647,24 @@ end context 'when the user is already verified' do - context 'without biometric comparison' do + context 'without facial match comparison' do let(:user) { build(:user, :proofed) } - it 'asserts biometric_comparison as true' do + it 'asserts facial_match as true' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.aal2?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.ialmax?).to be false end end - context 'with biometric comparison' do + context 'with facial match comparison' do let(:user) { build(:user, :proofed_with_selfie) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.aal2?).to be true expect(result.ialmax?).to be false @@ -673,28 +673,28 @@ end end - context 'with biometric comparison is preferred' do + context 'with facial match comparison is preferred' do let(:bio_value) { 'preferred' } context 'when the user is already verified' do - context 'without biometric comparison' do + context 'without facial match comparison' do let(:user) { build(:user, :proofed) } - it 'falls back on proofing without biometric comparison' do + it 'falls back on proofing without facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be false + expect(result.facial_match?).to be false expect(result.two_pieces_of_fair_evidence?).to be false expect(result.aal2?).to be true expect(result.ialmax?).to be false end end - context 'with biometric comparison' do + context 'with facial match comparison' do let(:user) { build(:user, :proofed_with_selfie) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.aal2?).to be true expect(result.ialmax?).to be false @@ -705,9 +705,9 @@ context 'when the user has not yet been verified' do let(:user) { build(:user) } - it 'asserts biometric comparison' do + it 'asserts facial match comparison' do expect(result.identity_proofing?).to be true - expect(result.biometric_comparison?).to be true + expect(result.facial_match?).to be true expect(result.two_pieces_of_fair_evidence?).to be true expect(result.aal2?).to be true expect(result.ialmax?).to be false diff --git a/spec/services/id_token_builder_spec.rb b/spec/services/id_token_builder_spec.rb index 2d24daa0856..97baa674602 100644 --- a/spec/services/id_token_builder_spec.rb +++ b/spec/services/id_token_builder_spec.rb @@ -108,7 +108,7 @@ end end - context 'ial2 with biometric comparison required' do + context 'ial2 with facial match comparison required' do before do identity.ial = 2 identity.acr_values = Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF diff --git a/spec/services/saml_request_validator_spec.rb b/spec/services/saml_request_validator_spec.rb index 427827ae380..c675f3a0b43 100644 --- a/spec/services/saml_request_validator_spec.rb +++ b/spec/services/saml_request_validator_spec.rb @@ -232,15 +232,15 @@ end end - shared_examples 'allows biometric IAL only if sp is authorized' do |biometric_ial| - let(:authn_context) { [biometric_ial] } + shared_examples 'allows facial match IAL only if sp is authorized' do |facial_match_ial| + let(:authn_context) { [facial_match_ial] } - context "when the IAL requested is #{biometric_ial}" do - context 'when the service provider is allowed to use biometric ials' do + context "when the IAL requested is #{facial_match_ial}" do + context 'when the service provider is allowed to use facial match ials' do let(:sp) { create(:service_provider, :idv) } before do - allow_any_instance_of(ServiceProvider).to receive(:biometric_ial_allowed?). + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(true) end @@ -253,9 +253,9 @@ end end - context 'when the service provider is not allowed to use biometric ials' do + context 'when the service provider is not allowed to use facial match ials' do before do - allow_any_instance_of(ServiceProvider).to receive(:biometric_ial_allowed?). + allow_any_instance_of(ServiceProvider).to receive(:facial_match_ial_allowed?). and_return(false) end @@ -275,10 +275,10 @@ end end - it_behaves_like 'allows biometric IAL only if sp is authorized', + it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_REQUIRED_AUTHN_CONTEXT_CLASSREF - it_behaves_like 'allows biometric IAL only if sp is authorized', + it_behaves_like 'allows facial match IAL only if sp is authorized', Saml::Idp::Constants::IAL2_BIO_PREFERRED_AUTHN_CONTEXT_CLASSREF shared_examples 'allows semantic IAL only if sp is authorized' do |semantic_ial| diff --git a/spec/services/vot/parser_spec.rb b/spec/services/vot/parser_spec.rb index 3ea2f312878..07bbbd761c1 100644 --- a/spec/services/vot/parser_spec.rb +++ b/spec/services/vot/parser_spec.rb @@ -20,7 +20,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(true) expect(result.identity_proofing?).to eq(false) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -37,7 +37,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(false) expect(result.identity_proofing?).to eq(true) - expect(result.biometric_comparison?).to eq(true) + expect(result.facial_match?).to eq(true) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end @@ -94,7 +94,7 @@ expect(result.phishing_resistant?).to eq(false) expect(result.hspd12?).to eq(true) expect(result.identity_proofing?).to eq(true) - expect(result.biometric_comparison?).to eq(false) + expect(result.facial_match?).to eq(false) expect(result.ialmax?).to eq(false) expect(result.enhanced_ipp?).to eq(false) end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 96b850afec1..264cee35488 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -97,12 +97,12 @@ def complete_doc_auth_steps_before_document_capture_step(expect_accessible: fals end def complete_up_to_how_to_verify_step_for_opt_in_ipp(remote: true, - biometric_comparison_required: false) + facial_match_required: false) complete_doc_auth_steps_before_welcome_step complete_welcome_step complete_agreement_step if remote - if biometric_comparison_required + if facial_match_required click_on t('forms.buttons.continue_remote_selfie') else click_on t('forms.buttons.continue_remote') diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index 62ed86bde8c..7a69f19be41 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -8,15 +8,6 @@ def submit_images end end - def continue_doc_auth_form - click_on 'Continue' - - # Wait for the the loading interstitial to disappear before continuing - wait_for_content_to_disappear do - expect(page).not_to have_content(t('doc_auth.headings.interstitial'), wait: 10) - end - end - def attach_and_submit_images attach_images submit_images @@ -34,6 +25,9 @@ def attach_liveness_images( ) ) attach_images(file) + if IdentityConfig.store.doc_auth_separate_pages_enabled + click_continue + end attach_selfie end diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb index 5aeb79925e0..9be0e693533 100644 --- a/spec/support/features/idv_helper.rb +++ b/spec/support/features/idv_helper.rb @@ -142,7 +142,7 @@ def visit_idp_from_oidc_sp_with_ial2( state: SecureRandom.hex, nonce: SecureRandom.hex, verified_within: nil, - biometric_comparison_required: nil + facial_match_required: nil ) params = { client_id:, @@ -155,7 +155,7 @@ def visit_idp_from_oidc_sp_with_ial2( verified_within:, } - if biometric_comparison_required + if facial_match_required params[:vtr] = ['C1.P1.Pb'].to_json else params[:acr_values] = Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF diff --git a/spec/support/features/idv_step_helper.rb b/spec/support/features/idv_step_helper.rb index e6a6011b541..51266c3b647 100644 --- a/spec/support/features/idv_step_helper.rb +++ b/spec/support/features/idv_step_helper.rb @@ -13,9 +13,9 @@ def self.included(base) end end - def start_idv_from_sp(sp = :oidc, biometric_comparison_required: nil) + def start_idv_from_sp(sp = :oidc, facial_match_required: nil) if sp.present? - visit_idp_from_sp_with_ial2(sp, biometric_comparison_required:) + visit_idp_from_sp_with_ial2(sp, facial_match_required:) else visit root_path end diff --git a/spec/support/oidc_auth_helper.rb b/spec/support/oidc_auth_helper.rb index 76e0b626b35..e8aaa703e2b 100644 --- a/spec/support/oidc_auth_helper.rb +++ b/spec/support/oidc_auth_helper.rb @@ -87,7 +87,7 @@ def ial2_params( nonce: SecureRandom.hex, client_id: OIDC_ISSUER, acr_values: Saml::Idp::Constants::IAL2_AUTHN_CONTEXT_CLASSREF, - biometric_comparison_required: false + facial_match_required: false ) ial2_params = { client_id: client_id, @@ -99,7 +99,7 @@ def ial2_params( } ial2_params[:prompt] = prompt if prompt - if biometric_comparison_required + if facial_match_required ial2_params[:vtr] = ['C1.P1.Pb'].to_json else ial2_params[:acr_values] = acr_values diff --git a/spec/views/accounts/_identity_verification.html.erb_spec.rb b/spec/views/accounts/_identity_verification.html.erb_spec.rb index 7a925ef168d..436621dae2b 100644 --- a/spec/views/accounts/_identity_verification.html.erb_spec.rb +++ b/spec/views/accounts/_identity_verification.html.erb_spec.rb @@ -61,7 +61,7 @@ end end - context 'with partner requesting non-biometric verification' do + context 'with partner requesting non-facial match verification' do let(:sp_name) { 'Example SP' } let(:vtr) { ['C2.P1'] } @@ -152,7 +152,7 @@ end end - context 'with non-biometric proofed user' do + context 'with non-facial match proofed user' do let(:user) { build(:user, :proofed) } it 'shows verified badge' do @@ -201,7 +201,7 @@ end end - context 'with partner requesting biometric verification' do + context 'with partner requesting facial match verification' do let(:sp_name) { 'Example SP' } let(:vtr) { ['C2.Pb'] } @@ -221,7 +221,7 @@ end end - context 'with non-biometric proofed user' do + context 'with non-facial match proofed user' do let(:user) { build(:user, :proofed) } it 'shows unverified badge' do @@ -234,7 +234,7 @@ t( 'account.index.verification.nonbiometric_verified_html', app_name: APP_NAME, - date: @presenter.formatted_nonbiometric_idv_date, + date: @presenter.formatted_legacy_idv_date, ), ), ) @@ -261,7 +261,7 @@ end end - context 'with biometric proofed user' do + context 'with facial match proofed user' do let(:user) { build(:user, :proofed_with_selfie) } it 'shows verified badge' do