From 4b15516c4e86ca243e6da06b4119b6a54a07577e Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Thu, 13 Jun 2024 09:45:22 -0400 Subject: [PATCH 01/15] LG-13387 Remove SP cost tracking logic from `VerifyInfoConcern` (#10786) In #10767 we changed the `ProgressiveProofer` class to begin tracking SP costs closer to where costly events occur. The logic for tracking costs was left in the `VerifyInfoConcern` to deal with the 50/50 state. When the changes in #10767 are fully deployed this can be merged to clean up dead code. [skip changelog] --- .../concerns/idv/verify_info_concern.rb | 30 ----- .../in_person/verify_info_controller_spec.rb | 111 ------------------ .../idv/verify_info_controller_spec.rb | 61 +--------- 3 files changed, 3 insertions(+), 199 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 9cf3f52c3e4..694592356a2 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -164,8 +164,6 @@ def process_async_state(current_async_state) end def async_state_done(current_async_state) - add_proofing_costs(current_async_state.result) - create_fraud_review_request_if_needed(current_async_state.result) form_response = idv_result_to_form_response( @@ -295,34 +293,6 @@ def move_applicant_to_idv_session idv_session.applicant['uuid'] = current_user.uuid end - def add_proofing_costs(results) - return if results[:context][:sp_costs_added] - - results[:context][:stages].each do |stage, hash| - if stage == :resolution - # transaction_id comes from ConversationId - add_cost(:lexis_nexis_resolution, transaction_id: hash[:transaction_id]) - elsif stage == :residential_address - next if pii[:same_address_as_id] == 'true' - next if hash[:vendor_name] == 'ResidentialAddressNotRequired' - add_cost(:lexis_nexis_resolution, transaction_id: hash[:transaction_id]) - elsif stage == :state_id - next if hash[:exception].present? - next if hash[:vendor_name] == 'UnsupportedJurisdiction' - # transaction_id comes from TransactionLocatorId - add_cost(:aamva, transaction_id: hash[:transaction_id]) - elsif stage == :threatmetrix - # transaction_id comes from request_id - if hash[:transaction_id] - add_cost( - :threatmetrix, - transaction_id: hash[:transaction_id], - ) - end - end - end - end - def add_cost(token, transaction_id: nil) Db::SpCost::AddSpCost.call(current_sp, token, transaction_id: transaction_id) end diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index 7b090f67b9a..b0bae4bfe0e 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -115,117 +115,6 @@ ) end end - - context 'tracks costs' do - let(:review_status) { 'pass' } - let(:async_state) { instance_double(ProofingSessionAsyncResult) } - let(:adjudicated_result) do - { - context: { - stages: { - threatmetrix: { - transaction_id: 1, - }, - resolution: { - transaction_id: 'resolution-mock-transaction-id-123', - vendor_name: 'ResolutionMock', - }, - residential_address: { - transaction_id: 'resolution-mock-transaction-id-123', - vendor_name: 'ResolutionMock', - }, - state_id: { - transaction_id: 'state-id-mock-transaction-id-456', - vendor_name: 'StateIdMock', - }, - }, - }, - } - end - - before do - allow(controller).to receive(:load_async_state).and_return(async_state) - allow(async_state).to receive(:done?).and_return(true) - end - - context 'when same address as id is true and in aamva jurisdiction' do - it 'adds costs to database' do - allow(async_state).to receive(:result).and_return(adjudicated_result) - - get :show - - lexis_nexis_costs = SpCost.where(cost_type: 'lexis_nexis_resolution') - expect(lexis_nexis_costs.count).to eq(1) - - aamva_costs = SpCost.where(cost_type: 'aamva') - expect(aamva_costs.count).to eq(1) - - threatmetrix_costs = SpCost.where(cost_type: 'threatmetrix') - expect(threatmetrix_costs.count).to eq(1) - end - end - - context 'when same address as id is true and not in aamva jurisdiction' do - it 'adds costs to database' do - adjudicated_result[:context][:stages][:state_id][:vendor_name] = 'UnsupportedJurisdiction' - allow(async_state).to receive(:result).and_return(adjudicated_result) - - get :show - - lexis_nexis_costs = SpCost.where(cost_type: 'lexis_nexis_resolution') - expect(lexis_nexis_costs.count).to eq(1) - - aamva_costs = SpCost.where(cost_type: 'aamva') - expect(aamva_costs.count).to eq(0) - - threatmetrix_costs = SpCost.where(cost_type: 'threatmetrix') - expect(threatmetrix_costs.count).to eq(1) - end - end - - context 'when same address as id is false and in aamva jurisdiction' do - let(:pii_from_user) do - { same_address_as_id: 'false' } - end - - it 'adds costs to database' do - allow(async_state).to receive(:result).and_return(adjudicated_result) - - get :show - - lexis_nexis_costs = SpCost.where(cost_type: 'lexis_nexis_resolution') - expect(lexis_nexis_costs.count).to eq(2) - - aamva_costs = SpCost.where(cost_type: 'aamva') - expect(aamva_costs.count).to eq(1) - - threatmetrix_costs = SpCost.where(cost_type: 'threatmetrix') - expect(threatmetrix_costs.count).to eq(1) - end - end - - context 'when same address as id is false and not in aamva jurisdiction' do - let(:pii_from_user) do - { same_address_as_id: 'false' } - end - - it 'adds costs to database' do - adjudicated_result[:context][:stages][:state_id][:vendor_name] = 'UnsupportedJurisdiction' - allow(async_state).to receive(:result).and_return(adjudicated_result) - - get :show - - lexis_nexis_costs = SpCost.where(cost_type: 'lexis_nexis_resolution') - expect(lexis_nexis_costs.count).to eq(2) - - aamva_costs = SpCost.where(cost_type: 'aamva') - expect(aamva_costs.count).to eq(0) - - threatmetrix_costs = SpCost.where(cost_type: 'threatmetrix') - expect(threatmetrix_costs.count).to eq(1) - end - end - end end describe '#update' do diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index 98eb9ff05d2..d19bd415338 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -296,26 +296,14 @@ hash_including(**analytics_args, success: true, analytics_id: 'Doc Auth'), ) end - - it 'records the cost as billable' do - expect { put :show }.to change { SpCost.where(cost_type: 'aamva').count }.by(1) - end end context 'when aamva returns success: false but no exception' do let(:success) { false } - it 'logs a cost' do - expect { put :show }.to change { SpCost.where(cost_type: 'aamva').count }.by(1) - end - end - - context 'when the jurisdiction is unsupported' do - let(:success) { true } - let(:vendor_name) { 'UnsupportedJurisdiction' } - - it 'does not consider the request billable' do - expect { put :show }.to_not change { SpCost.where(cost_type: 'aamva').count } + it 'redirects to the warning URL' do + put :show + expect(response).to redirect_to(idv_session_errors_warning_url) end end @@ -337,10 +325,6 @@ remaining_submit_attempts: kind_of(Numeric), ) end - - it 'does not log a cost' do - expect { put :show }.to change { SpCost.where(cost_type: 'aamva').count }.by(0) - end end end end @@ -423,43 +407,4 @@ end end end - - describe '#add_proofing_costs' do - let(:sp_costs_added) { nil } - let(:result) do - { - context: { - sp_costs_added:, - stages: { - resolution: { - transaction_id: 'ABCD1234', - }, - residential_address: { - vendor_name: 'ResidentialAddressNotRequired', - }, - state_id: { - transaction_id: 'EFGH5678', - }, - threatmetrix: { - transaction_id: 'IJKL9012', - }, - }, - }, - } - end - - it 'adds proofing costs' do - expect(subject).to receive(:add_cost).exactly(3).times - subject.send(:add_proofing_costs, result) - end - - context 'when proofing costs have already been added' do - let(:sp_costs_added) { true } - - it 'does not add proofing costs' do - expect(subject).not_to receive(:add_cost) - subject.send(:add_proofing_costs, result) - end - end - end end From 41d854a59879389318933ab22974f257713a6a69 Mon Sep 17 00:00:00 2001 From: Matt Wagner Date: Thu, 13 Jun 2024 11:37:50 -0400 Subject: [PATCH 02/15] Refactor proofing_components in AnalyticsSpec (#10810) changelog: Internal, Tests, Small refactor to AnalyticsSpec --- spec/features/idv/analytics_spec.rb | 102 ++++++++++++++++------------ 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 0492d5580d0..a87a9792f7f 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -28,6 +28,22 @@ timed_out: false, transaction_id: 'ddp-mock-transaction-id-123' } end + let(:base_proofing_components) do + { + document_check: 'mock', + document_type: 'state_id', + source_check: 'aamva', + resolution_check: 'lexis_nexis', + threatmetrix: threatmetrix, + threatmetrix_review_status: 'pass', + } + end + let(:lexis_nexis_address_proofing_components) do + base_proofing_components.merge(address_check: 'lexis_nexis_address') + end + let(:gpo_letter_proofing_components) do + base_proofing_components.merge(address_check: 'gpo_letter') + end # rubocop:disable Layout/LineLength # rubocop:disable Layout/MultilineHashKeyLineBreaks @@ -102,62 +118,62 @@ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation form' => { success: true, errors: {}, error_details: nil, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, error_details: nil, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, error_details: nil, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp visited' => { active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, otp_delivery_preference: :sms, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, error_details: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, profile_history: match_array(kind_of(Idv::ProfileLogging)), - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key visited' => { address_verification_method: 'phone', encrypted_profiles_missing: false, in_person_verification_pending: false, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key acknowledgment toggled' => { checked: true, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, } end @@ -228,62 +244,62 @@ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation form' => { success: true, errors: {}, error_details: nil, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, otp_delivery_preference: 'sms', active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, error_details: nil, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: true, area_code: '202', country_code: 'US', phone_fingerprint: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, error_details: nil, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp visited' => { active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp submitted' => { success: true, code_expired: false, code_matches: true, otp_delivery_preference: :sms, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, error_details: nil, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, profile_history: match_array(kind_of(Idv::ProfileLogging)), - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key visited' => { address_verification_method: 'phone', encrypted_profiles_missing: false, in_person_verification_pending: false, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key acknowledgment toggled' => { checked: true, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'legacy_unsupervised', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, } end @@ -351,12 +367,12 @@ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: USPS address letter requested' => { resend: false, phone_step_attempts: 0, first_letter_requested_at: nil, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: request letter visited' => { letter_already_sent: false, @@ -364,28 +380,28 @@ :idv_enter_password_visited => { address_verification_method: 'gpo', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } + proofing_components: gpo_letter_proofing_components }, 'IdV: USPS address letter enqueued' => { enqueued_at: Time.zone.now.utc, resend: false, phone_step_attempts: 0, first_letter_requested_at: Time.zone.now.utc, hours_since_first_letter: 0, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } + proofing_components: gpo_letter_proofing_components }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } + proofing_components: gpo_letter_proofing_components }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: nil, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: true, in_person_verification_pending: false, deactivation_reason: nil, # NOTE: pending_profile_idv_level should be set here, a nil value is cached for current_user.pending_profile. active_profile_idv_level: nil, pending_profile_idv_level: nil, profile_history: match_array(kind_of(Idv::ProfileLogging)), - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } + proofing_components: gpo_letter_proofing_components }, 'IdV: letter enqueued visited' => { active_profile_idv_level: nil, pending_profile_idv_level: 'legacy_unsupervised', - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'gpo_letter' } + proofing_components: gpo_letter_proofing_components }, } end @@ -602,62 +618,62 @@ 'IdV: phone of record visited' => { acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation form' => { success: true, errors: {}, error_details: nil, phone_type: :mobile, types: [:fixed_or_mobile], carrier: 'Test Mobile Carrier', country_code: 'US', area_code: '202', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, otp_delivery_preference: 'sms', active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass' } + proofing_components: base_proofing_components }, 'IdV: phone confirmation vendor' => { success: true, errors: {}, error_details: nil, vendor: { exception: nil, vendor_name: 'AddressMock', transaction_id: 'address-mock-transaction-id-123', timed_out: false, reference: '' }, new_phone_added: false, hybrid_handoff_phone_used: false, area_code: '202', country_code: 'US', phone_fingerprint: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp sent' => { success: true, otp_delivery_preference: :sms, country_code: 'US', area_code: '202', adapter: :test, errors: {}, error_details: nil, phone_fingerprint: anything, rate_limit_exceeded: false, telephony_response: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp visited' => { active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: phone confirmation otp submitted' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, code_expired: false, code_matches: true, otp_delivery_preference: :sms, second_factor_attempts_count: 0, second_factor_locked_at: nil, errors: {}, error_details: nil, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_visited => { address_verification_method: 'phone', acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, active_profile_idv_level: nil, pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, :idv_enter_password_submitted => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: final resolution' => { success: true, acuant_sdk_upgrade_ab_test_bucket: :default, skip_hybrid_handoff: anything, fraud_review_pending: false, fraud_rejection: false, gpo_verification_pending: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, profile_history: match_array(kind_of(Idv::ProfileLogging)), - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key visited' => { address_verification_method: 'phone', in_person_verification_pending: false, encrypted_profiles_missing: false, active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key acknowledgment toggled' => { checked: true, active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, 'IdV: personal key submitted' => { address_verification_method: 'phone', fraud_review_pending: false, fraud_rejection: false, in_person_verification_pending: false, deactivation_reason: nil, active_profile_idv_level: 'unsupervised_with_selfie', pending_profile_idv_level: nil, - proofing_components: { document_check: 'mock', document_type: 'state_id', source_check: 'aamva', resolution_check: 'lexis_nexis', threatmetrix: threatmetrix, threatmetrix_review_status: 'pass', address_check: 'lexis_nexis_address' } + proofing_components: lexis_nexis_address_proofing_components }, } end From c6ed1a6737af2b4e8fd487b96a44a19b3de093e7 Mon Sep 17 00:00:00 2001 From: Doug Price Date: Thu, 13 Jun 2024 13:30:15 -0400 Subject: [PATCH 03/15] LG-11285: Reset the callback property descriptor on cleanup of useEffect. (#10762) * Extract defineObservableProperty into its own file * Add stopObservingProperty * Use stopObservingProperty in acuant-capture-canvas changelog: Bug Fixes, Acuant SDK, Clear out a callback when done with it. --------- Co-authored-by: John Maxwell Co-authored-by: Alex Bradley Co-authored-by: Amir Reavis-Bey Co-authored-by: Zach Margolis --- .../components/acuant-capture-canvas.jsx | 31 ++++--------- .../higher-order/observable-property.tsx | 39 ++++++++++++++++ .../components/acuant-capture-canvas-spec.jsx | 23 +--------- .../higher-order/observable-property-spec.tsx | 44 +++++++++++++++++++ 4 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 app/javascript/packages/document-capture/higher-order/observable-property.tsx create mode 100644 spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx diff --git a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx index f21eb626abb..78b560e8d92 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx @@ -3,28 +3,10 @@ import { useContext, useEffect, useRef } from 'react'; import { getAssetPath } from '@18f/identity-assets'; import { useI18n } from '@18f/identity-react-i18n'; import AcuantContext from '../context/acuant'; - -/** - * Defines a property on the given object, calling the change callback when that property is set to - * a new value. - * - * @param {any} object Object on which to define property. - * @param {string} property Property name to observe. - * @param {(nextValue: any) => void} onChangeCallback Callback to trigger on change. - */ -export function defineObservableProperty(object, property, onChangeCallback) { - let currentValue; - - Object.defineProperty(object, property, { - get() { - return currentValue; - }, - set(nextValue) { - currentValue = nextValue; - onChangeCallback(nextValue); - }, - }); -} +import { + defineObservableProperty, + stopObservingProperty, +} from '../higher-order/observable-property'; function AcuantCaptureCanvas() { const { isReady, acuantCaptureMode, setAcuantCaptureMode } = useContext(AcuantContext); @@ -43,6 +25,11 @@ function AcuantCaptureCanvas() { cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated); return () => { + const canvas = document.getElementById('acuant-ui-canvas'); + if (canvas) { + stopObservingProperty(canvas, 'callback'); + } + cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated); }; }, []); diff --git a/app/javascript/packages/document-capture/higher-order/observable-property.tsx b/app/javascript/packages/document-capture/higher-order/observable-property.tsx new file mode 100644 index 00000000000..c6ed08b349f --- /dev/null +++ b/app/javascript/packages/document-capture/higher-order/observable-property.tsx @@ -0,0 +1,39 @@ +/** + * Defines a property on the given object, calling the change callback when that property is set to + * a new value. + * + * @param object Object on which to define property. + * @param property Property name to observe. + * @param onChangeCallback Callback to trigger on change. + */ +export function defineObservableProperty( + object: any, + property: string, + onChangeCallback: (nextValue: any) => void, +) { + let currentValue: any; + + Object.defineProperty(object, property, { + get() { + return currentValue; + }, + set(nextValue) { + currentValue = nextValue; + onChangeCallback(nextValue); + }, + configurable: true, + }); +} + +/** + * Removes an observable property by removing the defined getter/setter methods + * and replaces the value with the most recent value. + * + * @param object Object on which to remove defined property. + * @param property Property name to remove observer for + */ +export function stopObservingProperty(object: any, property: string) { + const currentValue = object[property]; + + Object.defineProperty(object, property, { value: currentValue, writable: true }); +} diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx index 5267a6af135..3eaa8b4f8e9 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx @@ -1,33 +1,12 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; -import AcuantCaptureCanvas, { - defineObservableProperty, -} from '@18f/identity-document-capture/components/acuant-capture-canvas'; +import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas'; import { render, useAcuant } from '../../../support/document-capture'; describe('document-capture/components/acuant-capture-canvas', () => { const { initialize } = useAcuant(); - describe('defineObservableProperty', () => { - it('behaves like an object', () => { - const object = {}; - defineObservableProperty(object, 'key', () => {}); - object.key = 'value'; - - expect(object.key).to.equal('value'); - }); - - it('calls the callback on changes, with the changed value', () => { - const callback = sinon.spy(); - const object = {}; - defineObservableProperty(object, 'key', callback); - object.key = 'value'; - - expect(callback).to.have.been.calledOnceWithExactly('value'); - }); - }); - it('renders a "take photo" button', async () => { const { getByRole, container } = render( diff --git a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx b/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx new file mode 100644 index 00000000000..0e0f66a56e6 --- /dev/null +++ b/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx @@ -0,0 +1,44 @@ +import sinon from 'sinon'; +import { + defineObservableProperty, + stopObservingProperty, +} from '@18f/identity-document-capture/higher-order/observable-property'; + +describe('document-capture/higher-order/observable-property', () => { + describe('defineObservableProperty', () => { + it('behaves like an object', () => { + const object = {} as { key?: string }; + defineObservableProperty(object, 'key', () => {}); + object.key = 'value'; + + expect(object.key).to.equal('value'); + }); + + it('calls the callback on changes, with the changed value', () => { + const callback = sinon.spy(); + const object = {} as { key?: string }; + defineObservableProperty(object, 'key', callback); + object.key = 'value'; + + expect(callback).to.have.been.calledOnceWithExactly('value'); + }); + }); + + describe('stopObservingProperty', () => { + it('removes the defined property and set the last value as a plain value', () => { + const object = {} as { key?: string }; + const callback = sinon.spy(); + defineObservableProperty(object, 'key', callback); + + object.key = 'value'; + + stopObservingProperty(object, 'key'); + expect(object.key).to.equal('value'); + + object.key = 'second_value'; + expect(object.key).to.equal('second_value'); + + expect(callback).to.have.been.calledOnceWithExactly('value'); + }); + }); +}); From d57f98c012f836a05ee51c5658ae86a17249f252 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Thu, 13 Jun 2024 14:25:59 -0400 Subject: [PATCH 04/15] Stop passing UUID and UUID prefix to `Idv::Agent` (#10753) In #10728 we started passing the service provider ID to the resolution proofing job. Since the job has the user ID this allows us to compute UUID and UUID prefix in the job instead of having to pass them through the applicant. Once that commit is deployed we can stop passing the UUID and UUID prefix via the applicant. This commit removes the code that looks up the UUID prefix and UUID and merges them into the applicant. This should not be merged until #10728 is fully deployed to production. [skip changelog] --- .../concerns/idv/verify_info_concern.rb | 8 +------ .../idv/in_person/verify_info_controller.rb | 2 +- app/controllers/idv/verify_info_controller.rb | 3 ++- .../in_person/verify_info_controller_spec.rb | 19 +++++++++-------- .../idv/verify_info_controller_spec.rb | 4 ++-- spec/services/idv/agent_spec.rb | 21 +++++++------------ 6 files changed, 24 insertions(+), 33 deletions(-) diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index 694592356a2..7cd09f84114 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -24,13 +24,7 @@ def shared_update idv_session.verify_info_step_document_capture_session_uuid = document_capture_session.uuid - # proof_resolution job expects these values - agent_pii = pii.merge( - uuid: current_user.uuid, - uuid_prefix: ServiceProvider.find_by(issuer: sp_session[:issuer])&.app_id, - ssn: idv_session.ssn, - ) - Idv::Agent.new(agent_pii).proof_resolution( + Idv::Agent.new(pii).proof_resolution( document_capture_session, should_proof_state_id: aamva_state?, trace_id: amzn_trace_id, diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index 3cc2d49ec90..50e06f35e39 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -74,7 +74,7 @@ def prev_url end def pii - user_session.dig('idv/in_person', :pii_from_user) + user_session.dig('idv/in_person', :pii_from_user).merge(ssn: idv_session.ssn) end # override IdvSessionConcern diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index b9990d401d5..10b2221c345 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -79,7 +79,8 @@ def analytics_arguments def pii idv_session.pii_from_doc.to_h.merge( - idv_session.updated_user_address.to_h, + ssn: idv_session.ssn, + **idv_session.updated_user_address.to_h, ).with_indifferent_access end end diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index b0bae4bfe0e..2db02ec4455 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -130,17 +130,21 @@ allow(user).to receive(:establishing_in_person_enrollment).and_return(enrollment) end - it 'sets uuid_prefix and state_id_type on pii_from_user' do - expect(Idv::Agent).to receive(:new). - with(hash_including(uuid_prefix: service_provider.app_id)).and_call_original + it 'sets ssn and state_id_type on pii_from_user' do + expect(Idv::Agent).to receive(:new).with( + hash_including( + state_id_type: 'drivers_license', + ssn: Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID[:ssn], + ), + ).and_call_original + # our test data already has the expected value by default subject.user_session['idv/in_person'][:pii_from_user].delete(:state_id_type) + put :update expect(subject.user_session['idv/in_person'][:pii_from_user][:state_id_type]). to eq 'drivers_license' - expect(subject.user_session['idv/in_person'][:pii_from_user][:uuid_prefix]). - to eq service_provider.app_id end context 'a user does not have an establishing in person enrollment associated with them' do @@ -181,10 +185,7 @@ it 'captures state id address fields in the pii' do expect(Idv::Agent).to receive(:new).with( - Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS.merge( - uuid_prefix: nil, - uuid: user.uuid, - ), + Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS, ).and_call_original put :update end diff --git a/spec/controllers/idv/verify_info_controller_spec.rb b/spec/controllers/idv/verify_info_controller_spec.rb index d19bd415338..93ee6516bf1 100644 --- a/spec/controllers/idv/verify_info_controller_spec.rb +++ b/spec/controllers/idv/verify_info_controller_spec.rb @@ -359,8 +359,8 @@ expect(Idv::Agent).to receive(:new).with( hash_including( - uuid_prefix: app_id, - uuid: user.uuid, + ssn: Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn], + **Idp::Constants::MOCK_IDV_APPLICANT, ), ).and_call_original diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 2c042563c51..ed062547aba 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -33,7 +33,7 @@ context 'proofing state_id enabled' do it 'does not proof state_id if resolution fails' do agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '444-55-6666'), + Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '444-55-6666'), ) agent.proof_resolution( document_capture_session, @@ -51,7 +51,7 @@ end it 'does proof state_id if resolution succeeds' do - agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(uuid: user.uuid)) + agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN) agent.proof_resolution( document_capture_session, should_proof_state_id: true, @@ -76,7 +76,7 @@ context 'proofing state_id disabled' do it 'does not proof state_id if resolution fails' do agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '444-55-6666'), + Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '444-55-6666'), ) agent.proof_resolution( document_capture_session, @@ -93,7 +93,7 @@ end it 'does not proof state_id if resolution succeeds' do - agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(uuid: user.uuid)) + agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN) agent.proof_resolution( document_capture_session, should_proof_state_id: false, @@ -113,7 +113,7 @@ it 'returns a successful result if SSN does not start with 900 but is in SSN allowlist' do agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '999-99-9999'), + Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '999-99-9999'), ) agent.proof_resolution( @@ -137,7 +137,7 @@ issuer = 'https://rp1.serviceprovider.com/auth/saml/metadata' document_capture_session.update!(issuer: issuer) agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT.merge(uuid: user.uuid, ssn: '999-99-9999'), + Idp::Constants::MOCK_IDV_APPLICANT.merge(ssn: '999-99-9999'), ) expect(ResolutionProofingJob).to receive(:perform_later).with( @@ -159,10 +159,7 @@ it 'returns an unsuccessful result and notifies exception trackers if an exception occurs' do agent = Idv::Agent.new( - Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge( - uuid: user.uuid, - first_name: 'Time Exception', - ), + Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN.merge(first_name: 'Time Exception'), ) agent.proof_resolution( @@ -187,8 +184,7 @@ let(:ipp_enrollment_in_progress) { true } it 'returns a successful result if resolution passes' do - addr = Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS - agent = Idv::Agent.new(addr.merge(uuid: user.uuid)) + agent = Idv::Agent.new(Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS) agent.proof_resolution( document_capture_session, should_proof_state_id: true, @@ -217,7 +213,6 @@ it 'proofs addresses successfully with valid information' do agent = Idv::Agent.new( - uuid: SecureRandom.uuid, first_name: 'Fakey', last_name: 'Fakersgerald', dob: 50.years.ago.to_date.to_s, From 869e5ff6cd481fb9f0aa467886d6bd334a80e254 Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Thu, 13 Jun 2024 14:26:10 -0400 Subject: [PATCH 05/15] Remove dead code in the `Idv::AddressController` (#10811) I found some code referencing `address_edited` on the `AddressForm` response. We removed that so there is not reason to inspect it. This commit also does some touch-ups to the form and the controller to clean up the logic. [skip changelog] --- app/controllers/idv/address_controller.rb | 13 +++++-------- app/forms/idv/address_form.rb | 2 ++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/controllers/idv/address_controller.rb b/app/controllers/idv/address_controller.rb index 4b13fc9ea2d..b8b1428fa03 100644 --- a/app/controllers/idv/address_controller.rb +++ b/app/controllers/idv/address_controller.rb @@ -20,7 +20,6 @@ def update @address_form = build_address_form form_result = @address_form.submit(profile_params) track_submit_event(form_result) - capture_address_edited(form_result) if form_result.success? success else @@ -69,8 +68,11 @@ def failure end def track_submit_event(form_result) - address_edited = form_result.success? && address_edited? - analytics.idv_address_submitted(**form_result.to_h.merge(address_edited:)) + analytics.idv_address_submitted( + **form_result.to_h.merge( + address_edited: address_edited?, + ), + ) end def address_edited? @@ -80,10 +82,5 @@ def address_edited? def profile_params params.require(:idv_form).permit(Idv::AddressForm::ATTRIBUTES) end - - def capture_address_edited(result) - address_edited = result.to_h[:address_edited] - idv_session.address_edited = true if address_edited - end end end diff --git a/app/forms/idv/address_form.rb b/app/forms/idv/address_form.rb index f0fc685c993..db960b5571d 100644 --- a/app/forms/idv/address_form.rb +++ b/app/forms/idv/address_form.rb @@ -30,6 +30,8 @@ def submit(params) end def updated_user_address + return nil unless valid? + Pii::Address.new( address1: address1, address2: address2, From 2e0294cb51b2a9b13739fccfb07d3cce0bcc53fb Mon Sep 17 00:00:00 2001 From: Zach Margolis Date: Thu, 13 Jun 2024 13:45:56 -0700 Subject: [PATCH 06/15] Move dependency audit checks into own task in Gitlab CI (#10814) **Why**: This is prep work that enables running these checks on a schedule on main branch, instead of on feature branches changelog: Internal, Tests, Reorganize dependency audit checks Co-authored-by: Mitchell Henke * Add "needs" clause for pinpoint-check job too --------- Co-authored-by: Mitchell Henke --- .gitlab-ci.yml | 14 ++++++++++++++ Makefile | 11 +++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dba17ec41ac..79c20ef4ce2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -319,6 +319,8 @@ js_tests: - yarn test pinpoint-check: + needs: + - job: install stage: test cache: - <<: *ruby_cache @@ -328,6 +330,18 @@ pinpoint-check: - *yarn_install - make lint_country_dialing_codes +audit_packages: + needs: + - job: install + stage: test + cache: + - <<: *ruby_cache + - <<: *yarn_cache + script: + - *bundle_install + - *yarn_install + - make audit + prepare_deploy: # Runs in parallel with tests so we can deploy more quickly after passing stage: test diff --git a/Makefile b/Makefile index dd275d8babb..04087052960 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz .PHONY: \ analytics_events \ + audit \ brakeman \ build_artifact \ check \ @@ -74,11 +75,7 @@ endif make lint_analytics_events_sorted @echo "--- brakeman ---" make brakeman - @echo "--- bundler-audit ---" - bundle exec bundler-audit check --update # JavaScript - @echo "--- yarn audit ---" - yarn audit --groups dependencies; test $$? -le 7 @echo "--- eslint ---" yarn run lint @echo "--- typescript ---" @@ -105,6 +102,12 @@ endif @echo "--- lint migrations ---" make lint_migrations +audit: ## Checks packages for vulnerabilities + @echo "--- bundler-audit ---" + bundle exec bundler-audit check --update + @echo "--- yarn audit ---" + yarn audit --groups dependencies; test $$? -le 7 + lint_erb: ## Lints ERB files bundle exec erblint app/views app/components From 4452ab34a606fbbc4958f300d199c22f67599acf Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Thu, 13 Jun 2024 15:51:11 -0500 Subject: [PATCH 07/15] Update db/schema.rb to match migrations (#10815) [skip changelog] --- db/schema.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 700e9562644..c99936d6691 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -14,7 +14,6 @@ # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" - enable_extension "pgcrypto" enable_extension "plpgsql" create_table "account_reset_requests", force: :cascade do |t| @@ -318,7 +317,7 @@ t.boolean "ready_for_status_check", default: false t.datetime "notification_sent_at", comment: "The time a notification was sent" t.datetime "last_batch_claimed_at" - t.string "sponsor_id", comment: "The identification number for USPS to recognize us and our flow (ex: Enhanced IPP)" + t.string "sponsor_id" t.index ["profile_id"], name: "index_in_person_enrollments_on_profile_id" t.index ["ready_for_status_check"], name: "index_in_person_enrollments_on_ready_for_status_check", where: "(ready_for_status_check = true)" t.index ["status_check_attempted_at"], name: "index_in_person_enrollments_on_status_check_attempted_at", where: "(status = 1)" @@ -657,7 +656,7 @@ add_foreign_key "iaa_gtcs", "partner_accounts" add_foreign_key "iaa_orders", "iaa_gtcs" add_foreign_key "in_person_enrollments", "profiles" - add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer" + add_foreign_key "in_person_enrollments", "service_providers", column: "issuer", primary_key: "issuer", validate: false add_foreign_key "in_person_enrollments", "users" add_foreign_key "integration_usages", "iaa_orders" add_foreign_key "integration_usages", "integrations" From de06afd7906755b1c06cd874b199a62246f2b866 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Thu, 13 Jun 2024 16:14:02 -0500 Subject: [PATCH 08/15] Add test to ensure schema files are consistent with migrations (#10813) * Add test to ensure schema files are consistent with migrations changelog: Internal, Continuous Integration, Add test to ensure schema files are consistent with migrations * move linting into Makefile --- .gitlab-ci.yml | 23 +++++++++++++++++++++++ Makefile | 5 +++++ 2 files changed, 28 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 79c20ef4ce2..5f2b52fae84 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -214,6 +214,29 @@ check_changelog: exit 0 fi +migrate: + stage: test + needs: + - job: install + cache: + - <<: *ruby_cache + variables: + DOCKER_DB_HOST: db-postgres + POSTGRES_DB: identity_idp_test + POSTGRES_USER: postgres_user + POSTGRES_PASSWORD: postgres_password + POSTGRES_HOST_AUTH_METHOD: trust + RAILS_ENV: test + services: + - name: postgres:13.9 + alias: db-postgres + command: ['--fsync=false', '--synchronous_commit=false', '--full_page_writes=false'] + script: + - *bundle_install + - bundle exec rake db:create db:migrate --trace + - git diff db/ + - make lint_database_schema_files + specs: stage: test needs: diff --git a/Makefile b/Makefile index 04087052960..a46dd25d3fd 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz lint_analytics_events \ lint_analytics_events_sorted \ lint_country_dialing_codes \ + lint_database_schema_files \ lint_erb \ lint_font_glyphs \ lint_lockfiles \ @@ -256,6 +257,10 @@ update_pinpoint_supported_countries: ## Updates list of countries supported by P lint_country_dialing_codes: update_pinpoint_supported_countries ## Checks that countries supported by Pinpoint for voice and SMS are up to date (! git diff --name-only | grep config/country_dialing_codes.yml) || (echo "Error: Run 'make update_pinpoint_supported_countries' to update country codes"; exit 1) +lint_database_schema_files: ## Checks that database schema files have not changed + (! git diff --name-only | grep db/schema.rb) || (echo "Error: db/schema.rb does not match after running migrations"; exit 1) + (! git diff --name-only | grep db/worker_jobs_schema.rb) || (echo "Error: db/worker_jobs_schema.rb does not match after running migrations"; exit 1) + build_artifact $(ARTIFACT_DESTINATION_FILE): ## Builds zipped tar file artifact with IDP source code and Ruby/JS dependencies @echo "Building artifact into $(ARTIFACT_DESTINATION_FILE)" bundle config set --local cache_all true From cff618ecb27545b0fa27e92ea96573b9f6886f7e Mon Sep 17 00:00:00 2001 From: Colter <59977618+colter-nattrass@users.noreply.github.com> Date: Thu, 13 Jun 2024 16:05:12 -0600 Subject: [PATCH 09/15] changelog: Internal, Reporting, fixing column names and column order (#10817) * fixing column name and order for billing report v2 --- .../combined_invoice_supplement_report_v2.rb | 114 +++---- ...w_unique_monthly_user_counts_by_partner.rb | 28 +- ...bined_invoice_supplement_report_v2_spec.rb | 285 +++++++++--------- ...que_monthly_user_counts_by_partner_spec.rb | 56 ++-- 4 files changed, 245 insertions(+), 238 deletions(-) diff --git a/app/jobs/reports/combined_invoice_supplement_report_v2.rb b/app/jobs/reports/combined_invoice_supplement_report_v2.rb index cf2aafe3294..0259180d9f2 100644 --- a/app/jobs/reports/combined_invoice_supplement_report_v2.rb +++ b/app/jobs/reports/combined_invoice_supplement_report_v2.rb @@ -97,20 +97,35 @@ def combine_by_iaa_month( 'iaa_ial1_unique_users', 'iaa_ial2_unique_users', 'iaa_ial1_plus_2_unique_users', - 'partner_ial2_unique_users_year1', - 'partner_ial2_unique_users_year2', - 'partner_ial2_unique_users_year3', - 'partner_ial2_unique_users_year4', - 'partner_ial2_unique_users_year5', - 'partner_ial2_unique_users_year_greater_than_5', - 'partner_ial2_unique_users_unknown', - 'partner_ial2_new_unique_users_year1', - 'partner_ial2_new_unique_users_year2', - 'partner_ial2_new_unique_users_year3', - 'partner_ial2_new_unique_users_year4', - 'partner_ial2_new_unique_users_year5', - 'partner_ial2_new_unique_users_year_greater_than_5', - 'partner_ial2_new_unique_users_unknown', + 'partner_ial2_unique_user_events_year1', + 'partner_ial2_unique_user_events_year2', + 'partner_ial2_unique_user_events_year3', + 'partner_ial2_unique_user_events_year4', + 'partner_ial2_unique_user_events_year5', + 'partner_ial2_unique_user_events_year_greater_than_5', + 'partner_ial2_unique_user_events_unknown', + 'partner_ial2_new_unique_user_events_year1', + 'partner_ial2_new_unique_user_events_year2', + 'partner_ial2_new_unique_user_events_year3', + 'partner_ial2_new_unique_user_events_year4', + 'partner_ial2_new_unique_user_events_year5', + 'partner_ial2_new_unique_user_events_year_greater_than_5', + 'partner_ial2_new_unique_user_events_unknown', + + 'issuer_ial2_unique_user_events_year1', + 'issuer_ial2_unique_user_events_year2', + 'issuer_ial2_unique_user_events_year3', + 'issuer_ial2_unique_user_events_year4', + 'issuer_ial2_unique_user_events_year5', + 'issuer_ial2_unique_user_events_year_greater_than_5', + 'issuer_ial2_unique_user_events_unknown', + 'issuer_ial2_new_unique_user_events_year1', + 'issuer_ial2_new_unique_user_events_year2', + 'issuer_ial2_new_unique_user_events_year3', + 'issuer_ial2_new_unique_user_events_year4', + 'issuer_ial2_new_unique_user_events_year5', + 'issuer_ial2_new_unique_user_events_year_greater_than_5', + 'issuer_ial2_new_unique_user_events_unknown', 'issuer_ial1_total_auth_count', 'issuer_ial2_total_auth_count', @@ -119,20 +134,6 @@ def combine_by_iaa_month( 'issuer_ial1_unique_users', 'issuer_ial2_unique_users', 'issuer_ial1_plus_2_unique_users', - 'issuer_ial2_unique_users_year1', - 'issuer_ial2_unique_users_year2', - 'issuer_ial2_unique_users_year3', - 'issuer_ial2_unique_users_year4', - 'issuer_ial2_unique_users_year5', - 'issuer_ial2_unique_users_year_greater_than_5', - 'issuer_ial2_unique_users_unknown', - 'issuer_ial2_new_unique_users_year1', - 'issuer_ial2_new_unique_users_year2', - 'issuer_ial2_new_unique_users_year3', - 'issuer_ial2_new_unique_users_year4', - 'issuer_ial2_new_unique_users_year5', - 'issuer_ial2_new_unique_users_year_greater_than_5', - 'issuer_ial2_new_unique_users_unknown', ] by_issuer_iaa_issuer_year_months.each do |iaa_key, issuer_year_months| issuer_year_months.each do |issuer, year_months_data| @@ -169,20 +170,35 @@ def combine_by_iaa_month( (iaa_ial1_unique_users = extract(iaa_results, :unique_users, ial: 1)), (iaa_ial2_unique_users = extract(iaa_results, :unique_users, ial: 2)), iaa_ial1_unique_users + iaa_ial2_unique_users, - partner_results[:partner_ial2_unique_users_year1] || 0, - partner_results[:partner_ial2_unique_users_year2] || 0, - partner_results[:partner_ial2_unique_users_year3] || 0, - partner_results[:partner_ial2_unique_users_year4] || 0, - partner_results[:partner_ial2_unique_users_year5] || 0, - partner_results[:partner_ial2_unique_users_year_greater_than_5] || 0, - partner_results[:partner_ial2_unique_users_unknown] || 0, - partner_results[:partner_ial2_new_unique_users_year1] || 0, - partner_results[:partner_ial2_new_unique_users_year2] || 0, - partner_results[:partner_ial2_new_unique_users_year3] || 0, - partner_results[:partner_ial2_new_unique_users_year4] || 0, - partner_results[:partner_ial2_new_unique_users_year5] || 0, - partner_results[:partner_ial2_new_unique_users_year_greater_than_5] || 0, - partner_results[:partner_ial2_new_unique_users_unknown] || 0, + partner_results[:partner_ial2_unique_user_events_year1] || 0, + partner_results[:partner_ial2_unique_user_events_year2] || 0, + partner_results[:partner_ial2_unique_user_events_year3] || 0, + partner_results[:partner_ial2_unique_user_events_year4] || 0, + partner_results[:partner_ial2_unique_user_events_year5] || 0, + partner_results[:partner_ial2_unique_user_events_year_greater_than_5] || 0, + partner_results[:partner_ial2_unique_user_events_unknown] || 0, + partner_results[:partner_ial2_new_unique_user_events_year1] || 0, + partner_results[:partner_ial2_new_unique_user_events_year2] || 0, + partner_results[:partner_ial2_new_unique_user_events_year3] || 0, + partner_results[:partner_ial2_new_unique_user_events_year4] || 0, + partner_results[:partner_ial2_new_unique_user_events_year5] || 0, + partner_results[:partner_ial2_new_unique_user_events_year_greater_than_5] || 0, + partner_results[:partner_ial2_new_unique_user_events_unknown] || 0, + + issuer_profile_age_results[:partner_ial2_unique_user_events_year1] || 0, + issuer_profile_age_results[:partner_ial2_unique_user_events_year2] || 0, + issuer_profile_age_results[:partner_ial2_unique_user_events_year3] || 0, + issuer_profile_age_results[:partner_ial2_unique_user_events_year4] || 0, + issuer_profile_age_results[:partner_ial2_unique_user_events_year5] || 0, + issuer_profile_age_results[:partner_ial2_unique_user_events_year_greater_than_5] || 0, # rubocop:disable Layout/LineLength + issuer_profile_age_results[:partner_ial2_unique_user_events_unknown] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year1] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year2] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year3] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year4] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year5] || 0, + issuer_profile_age_results[:partner_ial2_new_unique_user_events_year_greater_than_5] || 0, # rubocop:disable Layout/LineLength + issuer_profile_age_results[:partner_ial2_new_unique_user_events_unknown] || 0, (ial1_total_auth_count = extract(issuer_results, :total_auth_count, ial: 1)), (ial2_total_auth_count = extract(issuer_results, :total_auth_count, ial: 2)), @@ -191,20 +207,6 @@ def combine_by_iaa_month( (issuer_ial1_unique_users = extract(issuer_results, :unique_users, ial: 1)), (issuer_ial2_unique_users = extract(issuer_results, :unique_users, ial: 2)), issuer_ial1_unique_users + issuer_ial2_unique_users, - issuer_profile_age_results[:partner_ial2_unique_users_year1] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_year2] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_year3] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_year4] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_year5] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_year_greater_than_5] || 0, - issuer_profile_age_results[:partner_ial2_unique_users_unknown] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year1] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year2] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year3] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year4] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year5] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_year_greater_than_5] || 0, - issuer_profile_age_results[:partner_ial2_new_unique_users_unknown] || 0, ] end end diff --git a/app/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner.rb b/app/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner.rb index ff52df4073e..1419dd15f84 100644 --- a/app/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner.rb +++ b/app/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner.rb @@ -83,21 +83,21 @@ def call(partner:, issuers:, start_date:, end_date:) iaa_start_date: date_range.begin.to_s, iaa_end_date: date_range.end.to_s, unique_user_proofed_events: this_month_user_proofed_events.count, - partner_ial2_unique_users_year1: unique_profiles_by_age[0].count, - partner_ial2_unique_users_year2: unique_profiles_by_age[1].count, - partner_ial2_unique_users_year3: unique_profiles_by_age[2].count, - partner_ial2_unique_users_year4: unique_profiles_by_age[3].count, - partner_ial2_unique_users_year5: unique_profiles_by_age[4].count, - partner_ial2_unique_users_year_greater_than_5: unique_profiles_by_age[:older].count, - partner_ial2_unique_users_unknown: unique_profiles_by_age[:unknown].count, + partner_ial2_unique_user_events_year1: unique_profiles_by_age[0].count, + partner_ial2_unique_user_events_year2: unique_profiles_by_age[1].count, + partner_ial2_unique_user_events_year3: unique_profiles_by_age[2].count, + partner_ial2_unique_user_events_year4: unique_profiles_by_age[3].count, + partner_ial2_unique_user_events_year5: unique_profiles_by_age[4].count, + partner_ial2_unique_user_events_year_greater_than_5: unique_profiles_by_age[:older].count, # rubocop:disable Layout/LineLength + partner_ial2_unique_user_events_unknown: unique_profiles_by_age[:unknown].count, new_unique_user_proofed_events: new_unique_user_proofed_events.count, - partner_ial2_new_unique_users_year1: new_unique_profiles_by_age[0].count, - partner_ial2_new_unique_users_year2: new_unique_profiles_by_age[1].count, - partner_ial2_new_unique_users_year3: new_unique_profiles_by_age[2].count, - partner_ial2_new_unique_users_year4: new_unique_profiles_by_age[3].count, - partner_ial2_new_unique_users_year5: new_unique_profiles_by_age[4].count, - partner_ial2_new_unique_users_year_greater_than_5: new_unique_profiles_by_age[:older].count, # rubocop:disable Layout/LineLength - partner_ial2_new_unique_users_unknown: new_unique_profiles_by_age[:unknown].count, + partner_ial2_new_unique_user_events_year1: new_unique_profiles_by_age[0].count, + partner_ial2_new_unique_user_events_year2: new_unique_profiles_by_age[1].count, + partner_ial2_new_unique_user_events_year3: new_unique_profiles_by_age[2].count, + partner_ial2_new_unique_user_events_year4: new_unique_profiles_by_age[3].count, + partner_ial2_new_unique_user_events_year5: new_unique_profiles_by_age[4].count, + partner_ial2_new_unique_user_events_year_greater_than_5: new_unique_profiles_by_age[:older].count, # rubocop:disable Layout/LineLength + partner_ial2_new_unique_user_events_unknown: new_unique_profiles_by_age[:unknown].count, } end # rubocop:enable Metrics/BlockLength diff --git a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb index 3d8f8ca4c61..52db27cf4d5 100644 --- a/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb +++ b/spec/jobs/reports/combined_invoice_supplement_report_v2_spec.rb @@ -99,20 +99,35 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(1) expect(row['iaa_ial2_unique_users'].to_i).to eq(2) expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(3) - expect(row['partner_ial2_unique_users_year1'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year4'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_unknown'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year4'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year1'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year4'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_unknown'].to_i).to eq(0) + + expect(row['issuer_ial2_unique_user_events_year1'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year2'].to_i).to eq(2) + expect(row['issuer_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year1'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year2'].to_i).to eq(2) + expect(row['issuer_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_unknown'].to_i).to eq(0) expect(row['issuer_ial1_total_auth_count'].to_i).to eq(7) expect(row['issuer_ial2_total_auth_count'].to_i).to eq(2) @@ -121,20 +136,6 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(1) expect(row['issuer_ial2_unique_users'].to_i).to eq(2) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(3) - expect(row['issuer_ial2_unique_users_year1'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year2'].to_i).to eq(2) - expect(row['issuer_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(2) - expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_unknown'].to_i).to eq(0) end end end @@ -278,20 +279,35 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(8) expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(8) - expect(row['partner_ial2_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_unique_users_year3'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year5'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_unknown'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_unknown'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year5'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_unknown'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_new_unique_user_events_year3'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year5'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_unknown'].to_i).to eq(1) + + expect(row['issuer_ial2_unique_user_events_year1'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year2'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year3'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year5'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_unknown'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year2'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year3'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year5'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_unknown'].to_i).to eq(1) expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) expect(row['issuer_ial2_total_auth_count'].to_i).to eq(4) @@ -300,20 +316,6 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(4) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(4) - expect(row['issuer_ial2_unique_users_year1'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year2'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year3'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year5'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_unknown'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_unknown'].to_i).to eq(1) end aggregate_failures do @@ -333,20 +335,35 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(8) expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(8) - expect(row['partner_ial2_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_unique_users_year3'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year5'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_unknown'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(2) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_unknown'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year5'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_unknown'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year2'].to_i).to eq(2) + expect(row['partner_ial2_new_unique_user_events_year3'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year5'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_unknown'].to_i).to eq(1) + + expect(row['issuer_ial2_unique_user_events_year1'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year2'].to_i).to eq(2) + expect(row['issuer_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year4'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year1'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year2'].to_i).to eq(2) + expect(row['issuer_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year4'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_unknown'].to_i).to eq(0) expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) expect(row['issuer_ial2_total_auth_count'].to_i).to eq(4) @@ -355,20 +372,6 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(4) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(4) - expect(row['issuer_ial2_unique_users_year1'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year2'].to_i).to eq(2) - expect(row['issuer_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year4'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(2) - expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_unknown'].to_i).to eq(0) end end end @@ -471,20 +474,35 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(1) expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year2'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year4'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_unknown'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year4'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year2'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year4'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_unknown'].to_i).to eq(0) + + expect(row['issuer_ial2_unique_user_events_year1'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year2'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year2'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year4'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_unknown'].to_i).to eq(0) expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) expect(row['issuer_ial2_total_auth_count'].to_i).to eq(1) @@ -493,20 +511,6 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(1) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year1'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year2'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_unknown'].to_i).to eq(0) end aggregate_failures do @@ -526,20 +530,35 @@ expect(row['iaa_ial1_unique_users'].to_i).to eq(0) expect(row['iaa_ial2_unique_users'].to_i).to eq(2) expect(row['iaa_ial1_plus_2_unique_users'].to_i).to eq(2) - expect(row['partner_ial2_unique_users_year1'].to_i).to eq(2) - expect(row['partner_ial2_unique_users_year2'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year2'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year4'].to_i).to eq(1) - expect(row['partner_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['partner_ial2_new_unique_users_unknown'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year1'].to_i).to eq(2) + expect(row['partner_ial2_unique_user_events_year2'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year2'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year4'].to_i).to eq(1) + expect(row['partner_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['partner_ial2_new_unique_user_events_unknown'].to_i).to eq(0) + + expect(row['issuer_ial2_unique_user_events_year1'].to_i).to eq(2) + expect(row['issuer_ial2_unique_user_events_year2'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year4'].to_i).to eq(1) + expect(row['issuer_ial2_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_unique_user_events_unknown'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year1'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year2'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year3'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year4'].to_i).to eq(1) + expect(row['issuer_ial2_new_unique_user_events_year5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_year_greater_than_5'].to_i).to eq(0) + expect(row['issuer_ial2_new_unique_user_events_unknown'].to_i).to eq(0) expect(row['issuer_ial1_total_auth_count'].to_i).to eq(0) expect(row['issuer_ial2_total_auth_count'].to_i).to eq(4) @@ -548,20 +567,6 @@ expect(row['issuer_ial1_unique_users'].to_i).to eq(0) expect(row['issuer_ial2_unique_users'].to_i).to eq(2) expect(row['issuer_ial1_plus_2_unique_users'].to_i).to eq(2) - expect(row['issuer_ial2_unique_users_year1'].to_i).to eq(2) - expect(row['issuer_ial2_unique_users_year2'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year4'].to_i).to eq(1) - expect(row['issuer_ial2_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_unique_users_unknown'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year1'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year2'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year3'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year4'].to_i).to eq(1) - expect(row['issuer_ial2_new_unique_users_year5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_year_greater_than_5'].to_i).to eq(0) - expect(row['issuer_ial2_new_unique_users_unknown'].to_i).to eq(0) end end end diff --git a/spec/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner_spec.rb b/spec/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner_spec.rb index e87a1ebec49..61f03c07580 100644 --- a/spec/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner_spec.rb +++ b/spec/services/db/monthly_sp_auth_count/new_unique_monthly_user_counts_by_partner_spec.rb @@ -259,21 +259,21 @@ iaa_start_date: partner_range.begin.to_s, iaa_end_date: partner_range.end.to_s, unique_user_proofed_events: 8, - partner_ial2_unique_users_year1: 2, - partner_ial2_unique_users_year2: 2, - partner_ial2_unique_users_year3: 1, - partner_ial2_unique_users_year4: 1, - partner_ial2_unique_users_year5: 2, - partner_ial2_unique_users_year_greater_than_5: 0, - partner_ial2_unique_users_unknown: 0, + partner_ial2_unique_user_events_year1: 2, + partner_ial2_unique_user_events_year2: 2, + partner_ial2_unique_user_events_year3: 1, + partner_ial2_unique_user_events_year4: 1, + partner_ial2_unique_user_events_year5: 2, + partner_ial2_unique_user_events_year_greater_than_5: 0, + partner_ial2_unique_user_events_unknown: 0, new_unique_user_proofed_events: 8, - partner_ial2_new_unique_users_year1: 2, - partner_ial2_new_unique_users_year2: 2, - partner_ial2_new_unique_users_year3: 1, - partner_ial2_new_unique_users_year4: 1, - partner_ial2_new_unique_users_year5: 2, - partner_ial2_new_unique_users_year_greater_than_5: 0, - partner_ial2_new_unique_users_unknown: 0, + partner_ial2_new_unique_user_events_year1: 2, + partner_ial2_new_unique_user_events_year2: 2, + partner_ial2_new_unique_user_events_year3: 1, + partner_ial2_new_unique_user_events_year4: 1, + partner_ial2_new_unique_user_events_year5: 2, + partner_ial2_new_unique_user_events_year_greater_than_5: 0, + partner_ial2_new_unique_user_events_unknown: 0, }, { partner: partner_key, @@ -282,21 +282,21 @@ iaa_start_date: partner_range.begin.to_s, iaa_end_date: partner_range.end.to_s, unique_user_proofed_events: 13, - partner_ial2_unique_users_year1: 4, - partner_ial2_unique_users_year2: 4, - partner_ial2_unique_users_year3: 1, - partner_ial2_unique_users_year4: 1, - partner_ial2_unique_users_year5: 0, - partner_ial2_unique_users_year_greater_than_5: 2, - partner_ial2_unique_users_unknown: 1, + partner_ial2_unique_user_events_year1: 4, + partner_ial2_unique_user_events_year2: 4, + partner_ial2_unique_user_events_year3: 1, + partner_ial2_unique_user_events_year4: 1, + partner_ial2_unique_user_events_year5: 0, + partner_ial2_unique_user_events_year_greater_than_5: 2, + partner_ial2_unique_user_events_unknown: 1, new_unique_user_proofed_events: 8, - partner_ial2_new_unique_users_year1: 3, - partner_ial2_new_unique_users_year2: 2, - partner_ial2_new_unique_users_year3: 0, - partner_ial2_new_unique_users_year4: 0, - partner_ial2_new_unique_users_year5: 0, - partner_ial2_new_unique_users_year_greater_than_5: 2, - partner_ial2_new_unique_users_unknown: 1, + partner_ial2_new_unique_user_events_year1: 3, + partner_ial2_new_unique_user_events_year2: 2, + partner_ial2_new_unique_user_events_year3: 0, + partner_ial2_new_unique_user_events_year4: 0, + partner_ial2_new_unique_user_events_year5: 0, + partner_ial2_new_unique_user_events_year_greater_than_5: 2, + partner_ial2_new_unique_user_events_unknown: 1, }, ] expect(results).to match_array(rows) From a756b31c1b9f30c5c59b80afc44b76d8771553e7 Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Fri, 14 Jun 2024 10:31:32 -0500 Subject: [PATCH 10/15] Update missing Chinese translations (#10819) changelog: Internal, Translations, Update missing Chinese translations --- config/locales/telephony/zh.yml | 6 ++--- config/locales/zh.yml | 30 ++++++++++++------------- spec/i18n_spec.rb | 39 +++++++++++++-------------------- 3 files changed, 33 insertions(+), 42 deletions(-) diff --git a/config/locales/telephony/zh.yml b/config/locales/telephony/zh.yml index 8abcf9d9ae5..214eb886899 100644 --- a/config/locales/telephony/zh.yml +++ b/config/locales/telephony/zh.yml @@ -1,9 +1,9 @@ --- zh: telephony: - account_deleted_notice: This text message confirms you have deleted your %{app_name} account. + account_deleted_notice: 这条信息确认你已删除了你的 %{app_name} 账户。 account_reset_cancellation_notice: 删除你 %{app_name} 账户的请求已被取消。 - account_reset_notice: 按照你的请求,你的 %{app_name} 账户会在 24 小时后删除。不想删除你的账户?登入你的 %{app_name} 账户去取消。 + account_reset_notice: 按照你的请求,你的 %{app_name} 账户会在%{interval}后删除。不想删除你的账户?登入你的 %{app_name} 账户去取消。 authentication_otp: sms: |- %{app_name}: 你的一次性代码是 %{code}。此代码在 %{expiration} 分钟后作废。请勿与任何人分享此代码。 @@ -18,7 +18,7 @@ zh: %{app_name}: 你的一次性代码是 %{code}。此代码在 %{expiration} 分钟后作废。请勿与任何人分享此代码。 @%{domain} #%{code} - voice: 你好! 你的%{format_length}%{format_type}一次性代码是 %{code}。你的一次性代码是 %{code}。 重复一下,你的一次性代码是 %{code}。此代码 %{expiration} 分钟后会作废。 + voice: 你好!你的%{format_length}-%{format_type} %{app_name}一次性代码是,%{code}。你的一次性代码是 ,%{code}。重复一下,你的一次性代码是 %{code}。此代码 %{expiration} 分钟后会作废。 doc_auth_link: |- %{app_name}: %{link} 你在验证身份以访问 %{sp_or_app_name}。拍张你身份证件的照片以继续。 error: diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 65b24b52706..bcc7b9a3bab 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -16,11 +16,11 @@ account_reset.delete_account.info: 被锁在账户之外时,取消账户应当 account_reset.delete_account.title: 删除账户应当是你最后的选择。 account_reset.pending.cancel_request: 取消请求 account_reset.pending.canceled: 我们已取消了你删除帐户的请求。 -account_reset.pending.confirm: 如果现在取消,你如果要删除账户的话,必须提出新请求并再等待24个小时。 +account_reset.pending.confirm: 如果现在取消,你如果要删除账户的话,必须提出新请求并再等待%{interval} 。 account_reset.pending.header: 你已提出删除账户的请求。 -account_reset.pending.wait_html: 要删除账户,有 24 小时的等待期。你会在 %{interval} 收到电邮,向你说明如何完成删除。 +account_reset.pending.wait_html: 要删除你的账户,有一个%{waiting_period}的等待期。你会在 %{interval} 收到电邮,向你说明如何完成删除。 account_reset.recovery_options.check_saved_credential: 查看你是否有已保存的凭据 -account_reset.recovery_options.check_webauthn_platform_info: 如果你在设立账户时设置了人脸或触摸解锁,务必使用你设立 %{app_name} 账户时使用的同一设备。 +account_reset.recovery_options.check_webauthn_platform_info: 如果你设立了人脸或触摸解锁,凭据可能已存在了一个密码管理器中,比如 iCloud Keychain 或者 Google Password Manager。尝试在用那个密码管理器的浏览器上使用人脸或触摸解锁。 account_reset.recovery_options.header: 你确定要删除账户吗? account_reset.recovery_options.help_text: 如果你被锁定在账户外而且仍然需要访问 %{app_name} ,请尝试以下步骤。 account_reset.recovery_options.try_another_device: 使用你也许选择了“记住设备”选项的另一个设备。 @@ -594,7 +594,7 @@ doc_auth.info.capture_status_capturing: 扫描中 doc_auth.info.capture_status_none: 对齐 doc_auth.info.capture_status_small_document: 靠近一些 doc_auth.info.capture_status_tap_to_capture: 点击来扫描 -doc_auth.info.exit.with_sp: 退出 %{app_name} 并返回 %{app_name} +doc_auth.info.exit.with_sp: 退出 %{app_name} 并返回 %{sp_name} doc_auth.info.exit.without_sp: 退出身份验证并到你的账户页面 doc_auth.info.getting_started_html: '%{sp_name} 需要确保你是你,而不是别人冒充你。 了解更多有关验证你身份的信息 %{link_html}' doc_auth.info.getting_started_learn_more: 了解有关验证你身份的更多信息 @@ -709,7 +709,7 @@ errors.manage_authenticator.unique_name_error: 名字已在使用。请使用一 errors.max_password_attempts_reached: 你输入了太多不正确的密码。你可以使用“忘了密码?”链接来重设密码。 errors.messages.already_confirmed: 已确认,请尝试登录 errors.messages.blank: 请填写这一字段。 -errors.messages.blank_cert_element_req: We cannot detect a certificate in your request. +errors.messages.blank_cert_element_req: 我们在你的请求中探查不到证书。 errors.messages.confirmation_code_incorrect: 验证码不对。你打字打对了吗? errors.messages.confirmation_invalid_token: 确认链接有误。链接已过期,或者你已确认了你的账户。 errors.messages.confirmation_period_expired: 过期的确认链接。你可以点击“重新发送确认说明”来得到另一个。 @@ -996,7 +996,7 @@ idv.cancel.description.gpo.warnings: idv.cancel.description.hybrid: 如果你现在取消的话,会被提示切换回电脑继续验证你的身份。 idv.cancel.description.start_over: 如果你重新开始,就会从头重新开始这一流程。 idv.cancel.headings.confirmation.hybrid: 你已取消了在该手机上上传身份证件照片 -idv.cancel.headings.exit.with_sp: 退出 %{app_name} 并返回 %{app_name} +idv.cancel.headings.exit.with_sp: 退出 %{app_name} 并返回 %{sp_name} idv.cancel.headings.exit.without_sp: 退出身份验证并到你的账户页面 idv.cancel.headings.prompt.hybrid: 你确定要取消在该手机上上传身份证件照片吗? idv.cancel.headings.prompt.standard: 取消验证身份? @@ -1019,7 +1019,7 @@ idv.failure.exceptions.internal_error: 处理你的请求时内部出错。请 idv.failure.exceptions.link: 请联系我们。 idv.failure.exceptions.post_office_search_error: 我们这边目前遇到技术困难。请再次尝试搜索一个邮局。如果该问题继续存在,请稍后回来。 idv.failure.exceptions.text_html: 请再试一次。如果这些错误一直出现,%{link_html}。 -idv.failure.exit.with_sp: 退出 %{app_name} 并返回 %{app_name} +idv.failure.exit.with_sp: 退出 %{app_name} 并返回 %{sp_name} idv.failure.exit.without_sp: 退出身份验证并到你的账户页面 idv.failure.gpo.rate_limited.heading: 请稍后再试 idv.failure.phone.heading: 我们无法将该电话号码与其他记录相匹配 @@ -1442,7 +1442,7 @@ openid_connect.authorization.errors.missing_ial: 缺失有效的 IAL 级别 openid_connect.authorization.errors.no_auth: Acr_values 未经授权 openid_connect.authorization.errors.no_valid_acr_values: 未找到可接受的 acr_values openid_connect.authorization.errors.no_valid_scope: 未找到有效的范围值 -openid_connect.authorization.errors.no_valid_vtr: No acceptable vots found +openid_connect.authorization.errors.no_valid_vtr: 未找到可接受的vots openid_connect.authorization.errors.prompt_invalid: 未找到有效的提示值 openid_connect.authorization.errors.redirect_uri_invalid: redirect_uri 有误 openid_connect.authorization.errors.redirect_uri_no_match: redirect_uri 与注册的 redirect_uri 不匹配 @@ -1606,7 +1606,7 @@ two_factor_authentication.aal2_request.phishing_resistant_html: '%{sp_na two_factor_authentication.aal2_request.piv_cac_only_html: '%{sp_name} 要求你的政府雇员身份证件,这是一种高安全水平的身份证实方法。' two_factor_authentication.account_reset.cancel_link: 取消你的请求 two_factor_authentication.account_reset.link: 删除你的账户 -two_factor_authentication.account_reset.pending: 你目前有个待处理的删除账户请求。从你提出请求到完成该流程需要 24 个小时请稍后回来查看。 +two_factor_authentication.account_reset.pending: 你目前有个待处理的删除账户请求。从你提出请求到完成该流程需要 %{interval}。请随后回来查看。 two_factor_authentication.account_reset.successful_cancel: 谢谢。你删除自己 %{app_name} 账户的请求已被取消。 two_factor_authentication.account_reset.text_html: 如果你无法使用上述身份证实方法,可以通过 %{link_html} 重设你的首选。 two_factor_authentication.attempt_remaining_warning_html.one: 你还可以再试 %{count} 次 。 @@ -1774,11 +1774,11 @@ user_mailer.account_reset_complete.subject: 账户已删除 user_mailer.account_reset_granted.button: 是的,继续删除 user_mailer.account_reset_granted.cancel_link_text: 请取消 user_mailer.account_reset_granted.help_html: 如果你不想删除你的账户,%{cancel_account_reset_html}。 -user_mailer.account_reset_granted.intro_html: 你的24小时等待期已结束。请完成流程第 2 步。

如果你无法找到自己的身份证实方法,选择“确认删除”来删除你的 %{app_name} 账户。

账户删除后,将来如果你需要访问使用 %{app_name}的参与这个项目的政府网站,可以使用同一电邮地址来设立一个新 %{app_name} 账户。

+user_mailer.account_reset_granted.intro_html: 你%{waiting_period}的等待期已结束。请完成流程的第 2 步。

如果您无法找到你的身份证实方法,请选择“确认删除”以删除你的 %{app_name} 帐户。

如果你将来需要访问使用 %{app_name} 的参与政府网站,可以在帐户删除后使用相同的电子邮件地址创建一个新的 %{app_name} 帐户.

user_mailer.account_reset_granted.subject: 删除你的 %{app_name} 账户 user_mailer.account_reset_request.cancel: 不想删除你的账户?登入你的 %{app_name} 账户来取消。 -user_mailer.account_reset_request.header: 你的账户会在 24 小时后删除。 -user_mailer.account_reset_request.intro_html: '作为安全措施, %{app_name} 要求一个两步流程来删除账户:

第一步:如果你丢失了身份证实方法但需删除账户,有一个 24 小时的等待期。如果你找到了身份证实方法, 可登入你的 %{app_name} 账户来取消这一请求。

第二步:24 小时等待期过了之后,你会收到一封电邮,请你确认删除 %{app_name} 账户。只有经你确认后,你的账户才会被删除。' +user_mailer.account_reset_request.header: 你的账户会在%{interval}后删除。 +user_mailer.account_reset_request.intro_html: 作为一项安全措施,%{app_name} 要求一个两步流程来删除你的帐户:

第一步:如果你丢失了身份证实方法但需删除账户,有一个%{waiting_period} 的等待期。如果你找到了身份证实方法,可以登录你的 %{app_name} 帐户来取消这个请求。

第二步:%{waiting_period}等待期之后,你会收到一封电邮,请你确认要删除 %{app_name} 账户。只有经你确认后,你的账户才会被删除。 user_mailer.account_reset_request.subject: 如何删除你的 %{app_name} 账户 user_mailer.account_verified.change_password_link: 更改密码 user_mailer.account_verified.contact_link: 联系我们 @@ -1845,9 +1845,9 @@ user_mailer.in_person_verified.greeting: 你好, user_mailer.in_person_verified.intro: 你于 %{date}在 %{location} 邮局成功地验证了身份。 user_mailer.in_person_verified.next_sign_in.with_sp.with_cta: 接下来请点击按钮或复制下面的连接来访问 %{sp_name} 并登录。 user_mailer.in_person_verified.next_sign_in.with_sp.without_cta: 你现在可以从 %{sp_name} 的网站登录。 -user_mailer.in_person_verified.next_sign_in.without_sp: 接下来请点击按钮或复制下面的连接来登录 %{sp_name}。 +user_mailer.in_person_verified.next_sign_in.without_sp: 接下来请点击按钮或复制下面的连接来登录 %{app_name}。 user_mailer.in_person_verified.sign_in: 登录 -user_mailer.in_person_verified.subject: 你在 %{sp_name} 成功地验证了身份 +user_mailer.in_person_verified.subject: 你在 %{app_name} 成功地验证了身份 user_mailer.in_person_verified.warning_contact_us_html: 如果你没有试图亲身验证身份,请登入 重设密码。要报告这件事,联系 %{app_name} 支持 %{app_name}。 user_mailer.letter_reminder_14_days.body_html: "

%{date_letter_was_se\ nt} 日你要求了带有验证码的信

登录%{app_name} 并输入验证码来完成验证你的身份。 %{help_link}.

" @@ -1876,7 +1876,7 @@ user_mailer.new_device_sign_in_before_2fa.reset_password: 重设密码 user_mailer.new_device_sign_in_before_2fa.subject: 你 %{app_name} 账户有新的登录 user_mailer.new_device_sign_in.disavowal_link: 重设你的密码 user_mailer.new_device_sign_in.help_html: 如果你没做此更改, %{disavowal_link_html}。要得到更多帮助,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。 -user_mailer.new_device_sign_in.info: '' +user_mailer.new_device_sign_in.info: 你的 %{app_name} 帐号刚刚在一个新设备上用于登录。 user_mailer.new_device_sign_in.subject: 用你 %{app_name} 账户进行的新登录 user_mailer.password_changed.disavowal_link: 重设你的密码 user_mailer.password_changed.help_html: 如果你没做此更改, %{disavowal_link_html}。要得到更多帮助,请访问 %{app_name_html} %{help_link_html} 或者 %{contact_link_html}。 diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index ba45f46f29d..4da02812e64 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -6,7 +6,7 @@ # List of keys allowed to contain different interpolation arguments across locales ALLOWED_INTERPOLATION_MISMATCH_KEYS = [ 'time.formats.event_timestamp_js', -].sort.freeze +].freeze ALLOWED_LEADING_OR_TRAILING_SPACE_KEYS = [ 'datetime.dotiw.last_word_connector', @@ -15,24 +15,7 @@ ].sort.freeze # These are keys with mismatch interpolation for specific locales -ALLOWED_INTERPOLATION_MISMATCH_LOCALE_KEYS = [ - # need to be fixed - 'zh.account_reset.pending.confirm', - 'zh.account_reset.pending.wait_html', - 'zh.account_reset.recovery_options.check_webauthn_platform_info', - 'zh.doc_auth.info.exit.with_sp', - 'zh.idv.cancel.headings.exit.with_sp', - 'zh.idv.failure.exit.with_sp', - 'zh.telephony.account_reset_notice', - 'zh.telephony.confirmation_otp.voice', - 'zh.two_factor_authentication.account_reset.pending', - 'zh.user_mailer.account_reset_granted.intro_html', - 'zh.user_mailer.account_reset_request.header', - 'zh.user_mailer.account_reset_request.intro_html', - 'zh.user_mailer.in_person_verified.next_sign_in.without_sp', - 'zh.user_mailer.in_person_verified.subject', - 'zh.user_mailer.new_device_sign_in.info', -].sort.freeze +ALLOWED_INTERPOLATION_MISMATCH_LOCALE_KEYS = [].freeze PUNCTUATION_PAIRS = { '{' => '}', @@ -90,9 +73,6 @@ class BaseTask { key: 'time.formats.sms_date' }, # for us date format # need to be fixed { key: 'account.email_language.name.zh', locales: %i[es fr] }, # needs to be translated - { key: 'errors.messages.blank_cert_element_req', locales: %i[zh] }, # needs to be translated - { key: 'openid_connect.authorization.errors.no_valid_vtr', locales: %i[zh] }, # needs to be translated - { key: 'telephony.account_deleted_notice', locales: %i[zh] }, # needs to be translated ].freeze # rubocop:enable Layout/LineLength @@ -260,14 +240,25 @@ def allowed_untranslated_key?(locale, key) keys = interpolation_arguments.group_by { |_k, v| v }. sort_by { |_k, v| v.length * -1 }.drop(1). - map { |x| x[1].flatten }.to_h.keys + flat_map { |x| x[1] }.to_h.keys missing_interpolation_argument_locale_keys += keys end + unallowed_interpolation_mismatch_locale_keys = + missing_interpolation_argument_locale_keys - ALLOWED_INTERPOLATION_MISMATCH_LOCALE_KEYS + + expect(unallowed_interpolation_mismatch_locale_keys).to( + be_empty, + <<~EOS, + There are mismatched interpolation arguments: + #{unallowed_interpolation_mismatch_locale_keys.pretty_inspect} + EOS + ) + unused_allowed_interpolation_mismatch_keys = ALLOWED_INTERPOLATION_MISMATCH_KEYS - missing_interpolation_argument_keys - expect(unused_allowed_interpolation_mismatch_keys.sort).to( + expect(unused_allowed_interpolation_mismatch_keys).to( be_empty, <<~EOS, ALLOWED_INTERPOLATION_MISMATCH_KEYS contains unused allowed interpolation mismatches. From 9c81a8086ca74392d5eea1951b8ab83886d42622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julia=20Sol=C3=B3rzano?= Date: Fri, 14 Jun 2024 12:41:41 -0400 Subject: [PATCH 11/15] Update email language preference to its associated language (#10808) * Update email language preference to be translated into the lang being described. changelog: User-Facing Improvements, Account Page, Update language email preference selection to be in the language being described. * fix spec --------- Co-authored-by: Mitchell Henke --- config/locales/en.yml | 6 ++--- config/locales/es.yml | 6 ++--- config/locales/fr.yml | 8 +++--- config/locales/zh.yml | 6 ++--- spec/features/account_email_language_spec.rb | 26 ++++++++++++-------- spec/i18n_spec.rb | 6 +++-- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index ff5c1e9270d..3c931ad39a4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -44,9 +44,9 @@ account.email_language.default: '%{language} (default)' account.email_language.edit_title: Edit email language preference account.email_language.languages_list: '%{app_name} allows you to receive your email communication in %{list}.' account.email_language.name.en: English -account.email_language.name.es: Spanish -account.email_language.name.fr: French -account.email_language.name.zh: Chinese +account.email_language.name.es: Español +account.email_language.name.fr: Français +account.email_language.name.zh: 中文 (简体) account.email_language.sentence_connector: or account.email_language.updated: Your email language preference has been updated. account.forget_all_browsers.longer_description: Once you choose to ‘forget all browsers,’ we’ll need additional information to know that it’s actually you signing in to your account. We’ll ask for a multi-factor authentication method (such as text/SMS code or a security key) each time you want to access your account. diff --git a/config/locales/es.yml b/config/locales/es.yml index 07d343233ef..e32ab74ce44 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -43,10 +43,10 @@ account.connected_apps.description: Con su cuenta de %{app_name}, puede conectar account.email_language.default: '%{language} (predeterminado)' account.email_language.edit_title: Editar la preferencia de idioma del correo electrónico account.email_language.languages_list: '%{app_name} le permite recibir su comunicación por correo electrónico en %{list}.' -account.email_language.name.en: Inglés +account.email_language.name.en: English account.email_language.name.es: Español -account.email_language.name.fr: Francés -account.email_language.name.zh: Chinese +account.email_language.name.fr: Français +account.email_language.name.zh: 中文 (简体) account.email_language.sentence_connector: o account.email_language.updated: Se actualizó su preferencia de idioma del correo electrónico. account.forget_all_browsers.longer_description: Una vez que elija “Olvidar todos los navegadores”, necesitaremos más información para saber que realmente es usted quien está iniciando sesión en su cuenta. Le pediremos un método de autenticación multifactor (como código de texto o de SMS, o una clave de seguridad) cada vez que desee acceder a su cuenta. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 95cf95f17cd..a70d855ed5f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -43,10 +43,10 @@ account.connected_apps.description: Avec votre compte %{app_name}, vous pouvez v account.email_language.default: '%{language} (par défaut)' account.email_language.edit_title: Modifier la langue dans laquelle vous préférez recevoir les e-mails account.email_language.languages_list: '%{app_name} vous permet de recevoir des communications par e-mail en %{list}.' -account.email_language.name.en: anglais -account.email_language.name.es: espagnol -account.email_language.name.fr: français -account.email_language.name.zh: Chinese +account.email_language.name.en: English +account.email_language.name.es: Español +account.email_language.name.fr: Français +account.email_language.name.zh: 中文 (简体) account.email_language.sentence_connector: ou account.email_language.updated: Votre langue de préférence pour les e-mails a été mise à jour. account.forget_all_browsers.longer_description: Une fois que vous aurez choisi d’« oublier tous les navigateurs », nous aurons besoin d’informations supplémentaires pour savoir que c’est bien vous qui vous connectez à votre compte. Nous vous demanderons une méthode d’authentification multi-facteurs (comme un code SMS/texto ou une clé de sécurité) chaque fois que vous souhaiterez accéder à votre compte. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index bcc7b9a3bab..2e4f95e01fa 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -43,9 +43,9 @@ account.connected_apps.description: 使用你的 %{app_name} 账户,你可以 account.email_language.default: '%{language} (默认)' account.email_language.edit_title: 编辑电邮语言选择 account.email_language.languages_list: '%{app_name} 允许你以 %{list}接受电邮沟通。' -account.email_language.name.en: 英文 -account.email_language.name.es: 西班牙文 -account.email_language.name.fr: 法文 +account.email_language.name.en: English +account.email_language.name.es: Español +account.email_language.name.fr: Français account.email_language.name.zh: 中文 (简体) account.email_language.sentence_connector: 或者 account.email_language.updated: 你的电邮语言选择已更新。 diff --git a/spec/features/account_email_language_spec.rb b/spec/features/account_email_language_spec.rb index 8d817c7f285..14f0d9b085b 100644 --- a/spec/features/account_email_language_spec.rb +++ b/spec/features/account_email_language_spec.rb @@ -16,30 +16,31 @@ end it 'lets them view their current email language' do - within(page.find('.profile-info-box', text: 'Language')) do - expect(page).to have_content('Spanish') + within(page.find('.profile-info-box', text: t('i18n.language'))) do + expect(page).to have_content(t("account.email_language.name.#{original_email_language}")) end end context 'changing their email language' do + let('chosen_email_language') { 'fr' } before do - within(page.find('.profile-info-box', text: 'Language')) do - click_link('Edit') + within(page.find('.profile-info-box', text: t('i18n.language'))) do + click_link(t('forms.buttons.edit')) end - choose 'Français' - click_button 'Submit' + choose t("account.email_language.name.#{chosen_email_language}") + click_button t('forms.buttons.submit.default') end it 'reflects the updated language preference' do - within(page.find('.profile-info-box', text: 'Language')) do - expect(page).to have_content('French') + within(page.find('.profile-info-box', text: t('i18n.language'))) do + expect(page).to have_content(t("account.email_language.name.#{chosen_email_language}")) end end it 'respects the language preference in emails, such as password reset emails' do within(page.find('.profile-info-box', text: 'Password')) do - click_link('Edit') + click_link(t('forms.buttons.edit')) end fill_in t('forms.passwords.edit.labels.password'), @@ -49,7 +50,12 @@ click_button 'Update' mail = ActionMailer::Base.deliveries.last - expect(mail.subject).to eq(I18n.t('devise.mailer.password_updated.subject', locale: 'fr')) + expect(mail.subject).to eq( + I18n.t( + 'devise.mailer.password_updated.subject', + locale: chosen_email_language, + ), + ) end end end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb index 4da02812e64..ebcabaded71 100644 --- a/spec/i18n_spec.rb +++ b/spec/i18n_spec.rb @@ -53,6 +53,10 @@ class BaseTask { key: 'i18n.locale.es', locales: %i[es fr zh] }, { key: 'i18n.locale.fr', locales: %i[es fr zh] }, { key: 'i18n.locale.zh', locales: %i[es fr zh] }, + { key: 'account.email_language.name.en', locales: %i[es fr zh] }, + { key: 'account.email_language.name.es', locales: %i[es fr zh] }, + { key: 'account.email_language.name.fr', locales: %i[es fr zh] }, + { key: 'account.email_language.name.zh', locales: %i[es fr zh] }, { key: 'account.navigation.menu', locales: %i[fr] }, # "Menu" is "Menu" in French { key: /^countries/ }, # Some countries have the same name across languages { key: 'date.formats.long', locales: %i[es zh] }, @@ -71,8 +75,6 @@ class BaseTask { key: 'time.formats.event_timestamp', locales: %i[zh] }, { key: 'time.formats.full_date', locales: %i[es] }, # format is the same in Spanish and English { key: 'time.formats.sms_date' }, # for us date format - # need to be fixed - { key: 'account.email_language.name.zh', locales: %i[es fr] }, # needs to be translated ].freeze # rubocop:enable Layout/LineLength From 5d25fa61d13f496f8cfd7573a1475f1fe8000908 Mon Sep 17 00:00:00 2001 From: "Davida (she/they)" Date: Fri, 14 Jun 2024 12:59:59 -0400 Subject: [PATCH 12/15] Add logging for certificate errors (#10818) * changelog: Internal, Metrics, Add certificate validation errors to event --- Gemfile | 2 +- Gemfile.lock | 6 +- app/controllers/saml_idp_controller.rb | 1 + app/services/analytics_events.rb | 4 ++ spec/controllers/saml_idp_controller_spec.rb | 59 +++++++++++++------- 5 files changed, 47 insertions(+), 25 deletions(-) diff --git a/Gemfile b/Gemfile index 04965f0318a..a6ea2e90942 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.21.2-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.21.4-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5255d1ce4a8..0a7fa861485 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,10 +35,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: 5ad9e188efdfa6597697dd87f9cb9e8efa8d7d09 - tag: 0.21.2-18f + revision: 5e9999ef8e9260cda74cfea0a637f754994e0f9d + tag: 0.21.4-18f specs: - saml_idp (0.21.2.pre.18f) + saml_idp (0.21.4.pre.18f) activesupport builder faraday diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index e5a7d7e1209..432fdc8e5f9 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -137,6 +137,7 @@ def capture_analytics # Logging to indicate if a validation bug fix will create a potentially breaking change analytics_payload[:encryption_cert_matches_matching_cert] = encryption_cert_matches_matching_cert? + analytics_payload[:cert_error_details] = saml_request.cert_errors end analytics.saml_auth(**analytics_payload) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 476848e51aa..aa1aa55db71 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -4979,6 +4979,8 @@ def rules_of_use_visit # @param [String] matching_cert_serial # @param [Boolean|nil] encryption_cert_matches_matching_cert If the encryption certificate # matches the request certificate in a successful, signed request + # @param [Hash] cert_error_details Details for errors that occurred because of an invalid + # signature def saml_auth( success:, errors:, @@ -4995,6 +4997,7 @@ def saml_auth( matching_cert_serial:, encryption_cert_matches_matching_cert: nil, error_details: nil, + cert_error_details: nil, **extra ) track_event( @@ -5014,6 +5017,7 @@ def saml_auth( request_signed:, matching_cert_serial:, encryption_cert_matches_matching_cert:, + cert_error_details:, **extra, ) end diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index df1f491b1da..309446663f6 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -1451,15 +1451,6 @@ def name_id_version(format_urn) ) end - let(:analytics_hash) do - { - success: true, - request_signed: authn_requests_signed, - matching_cert_serial:, - encryption_cert_matches_matching_cert:, - } - end - before do stub_analytics IdentityLinker.new(user, service_provider).link_identity @@ -1471,7 +1462,13 @@ def name_id_version(format_urn) generate_saml_response(user, auth_settings) expect(response.status).to eq(200) - expect(@analytics).to have_logged_event('SAML Auth', hash_including(analytics_hash)) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: false, + matching_cert_serial: nil, + encryption_cert_matches_matching_cert: nil, + ) + ) end end @@ -1479,21 +1476,23 @@ def name_id_version(format_urn) let(:authn_requests_signed) { true } context 'Matching certificate' do - let(:encryption_cert_matches_matching_cert) { true } - let(:matching_cert_serial) { saml_test_sp_cert_serial } - it 'notes that in the analytics event' do user.identities.last.update!(verified_attributes: ['email']) generate_saml_response(user, auth_settings) expect(response.status).to eq(200) - expect(@analytics).to have_logged_event('SAML Auth', hash_including(analytics_hash)) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + matching_cert_serial: saml_test_sp_cert_serial, + encryption_cert_matches_matching_cert: true, + cert_error_details: nil, + ) + ) end context 'Certificate sig validation fails because of namespace bug' do - let(:encryption_cert_matches_matching_cert) { false } - let(:matching_cert_serial) { nil } - let(:request_sp) { double } + let(:request_sp) { double } before do service_provider.update(certs: ['sp_sinatra_demo', 'saml_test_sp']) @@ -1507,17 +1506,22 @@ def name_id_version(format_urn) generate_saml_response(user, auth_settings) expect(response.status).to eq(200) - expect(@analytics).to have_logged_event('SAML Auth', hash_including(analytics_hash)) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + matching_cert_serial: nil, + encryption_cert_matches_matching_cert: false, + ) + ) end end end context 'Certificate does not match' do - let(:encryption_cert_matches_matching_cert) { true } let(:service_provider) do create( :service_provider, - certs: ['sp_sinatra_demo', 'saml_test_sp'], + certs: ['saml_test_sp'], active: true, assertion_consumer_logout_service_url: 'https://example.com', ) @@ -1541,9 +1545,22 @@ def name_id_version(format_urn) it 'notes that in the analytics event' do user.identities.last.update!(verified_attributes: ['email']) generate_saml_response(user, auth_settings) + cert_error_details = [ + { + cert: saml_test_sp_cert_serial, + error_code: :fingerprint_mismatch, + }, + ] expect(response.status).to eq(200) - expect(@analytics).to have_logged_event('SAML Auth', hash_including(analytics_hash)) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + matching_cert_serial:, + encryption_cert_matches_matching_cert: true, + cert_error_details:, + ) + ) end end end From f84e55776b05b81845a3f82193c660b8362b6f6c Mon Sep 17 00:00:00 2001 From: Stephen Shelton Date: Fri, 14 Jun 2024 16:07:53 -0400 Subject: [PATCH 13/15] Cutover review-apps to new cluster (#10791) * changelog: CI, Internal, Cutover review-apps to new cluster --- .gitlab-ci.yml | 64 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f2b52fae84..1d66592ec06 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -450,7 +450,7 @@ trigger_devops: name: dtzar/helm-kubectl:latest script: - kubectl config get-contexts - - export CONTEXT=$(kubectl config get-contexts | grep review-apps | awk '{print $1}' | head -1) + - export CONTEXT=$(kubectl config get-contexts | grep reviewapp | awk '{print $1}' | head -1) - kubectl config use-context "$CONTEXT" - |- export IDP_CONFIG=$(cat <- helm upgrade --install --namespace review-apps --debug - --set env="reviewapps-$CI_ENVIRONMENT_SLUG" + --set env="reviewapp-$CI_ENVIRONMENT_SLUG" --set idp.image.repository="${ECR_REGISTRY}/identity-idp/review" --set idp.image.tag="${CI_COMMIT_SHA}" --set worker.image.repository="${ECR_REGISTRY}/identity-idp/review" @@ -570,19 +570,19 @@ trigger_devops: --set-json idp.config="$IDP_CONFIG" --set-json worker.config="$WORKER_CONFIG" --set-json pivcac.config="$PIVCAC_CONFIG" - --set-json idp.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" - --set-json pivcac.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.review-app.pivcac.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" - --set-json dashboard.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG-review-app-dashboard.review-app.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" + --set-json idp.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" + --set-json pivcac.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG.pivcac.reviewapp.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" + --set-json dashboard.ingress.hosts="[{\"host\": \"$CI_ENVIRONMENT_SLUG-dashboard.reviewapp.identitysandbox.gov\", \"paths\": [{\"path\": \"/\", \"pathType\": \"Prefix\"}]}]" $CI_ENVIRONMENT_SLUG ./identity-idp-helm-chart - echo "DNS may take a while to propagate, so be patient if it doesn't show up right away" - - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name reviewapps'" + - echo "To access the rails console, first run 'aws-vault exec sandbox-power -- aws eks update-kubeconfig --name reviewapp'" - echo "Then run aws-vault exec sandbox-power -- kubectl exec -it service/$CI_ENVIRONMENT_SLUG-login-chart-idp -n review-apps -- /app/bin/rails console" - echo "Address of IDP review app:" - - echo https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + - echo https://$CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov - echo "Address of PIVCAC review app:" - - echo https://$CI_ENVIRONMENT_SLUG.review-app.pivcac.identitysandbox.gov + - echo https://$CI_ENVIRONMENT_SLUG.pivcac.reviewapp.identitysandbox.gov - echo "Address of Dashboard review app:" - - echo https://$CI_ENVIRONMENT_SLUG-review-app-dashboard.review-app.identitysandbox.gov + - echo https://$CI_ENVIRONMENT_SLUG-dashboard.reviewapp.identitysandbox.gov review-app: @@ -590,11 +590,11 @@ review-app: allow_failure: true needs: - job: build-review-image - resource_group: $CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + resource_group: $CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov extends: .deploy environment: name: review/$CI_COMMIT_REF_NAME - url: https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + url: https://$CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov on_stop: stop-review-app auto_stop_in: 2 days rules: @@ -603,9 +603,9 @@ review-app: when: never stop-review-app: - resource_group: $CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + resource_group: $CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov script: - - export CONTEXT=$(kubectl config get-contexts | grep review-apps | awk '{print $1}' | head -1) + - export CONTEXT=$(kubectl config get-contexts | grep reviewapp | awk '{print $1}' | head -1) - kubectl config use-context "$CONTEXT" - helm uninstall --namespace review-apps $CI_ENVIRONMENT_SLUG stage: review @@ -627,12 +627,12 @@ deploy_production: allow_failure: true needs: - job: build-review-image - resource_group: $CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + resource_group: $CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov extends: .deploy environment: name: production deployment_tier: production - url: https://$CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov + url: https://$CI_ENVIRONMENT_SLUG.reviewapp.identitysandbox.gov rules: - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" From efbcfbbb281d94c6eb5261bdac15dc9934f0d9cd Mon Sep 17 00:00:00 2001 From: Mitchell Henke Date: Fri, 14 Jun 2024 15:21:42 -0500 Subject: [PATCH 14/15] Add coverage metric to GitLab CI (#10821) changelog: Internal, Continuous Integration, Add coverage metric to GitLab CI --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d66592ec06..0e36154b270 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -391,6 +391,7 @@ coverage: - *bundle_install - bundle exec spec/simplecov_merger.rb - mv coverage/coverage/* coverage/ + coverage: '/LOC \(\d+.\d+\%\) covered/' artifacts: reports: coverage_report: From ec59a21a80b685ec50a4035d59e67b3f9b386486 Mon Sep 17 00:00:00 2001 From: Doug Price Date: Fri, 14 Jun 2024 18:31:31 -0400 Subject: [PATCH 15/15] A quick update to change the SP used for the Test OIDC links from (#10822) /test/oidc/login. I found the existing SP failed with errors for me. This changes makes the links work again. Note: the test routes are disabled in production envs (i.e., deployed envs) [skip changelog] --- app/controllers/test/oidc_test_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/test/oidc_test_controller.rb b/app/controllers/test/oidc_test_controller.rb index dd366676911..246d7321bb1 100644 --- a/app/controllers/test/oidc_test_controller.rb +++ b/app/controllers/test/oidc_test_controller.rb @@ -8,7 +8,7 @@ class OidcTestController < ApplicationController BIOMETRIC_REQUIRED = 'biometric-comparison-required' def initialize - @client_id = 'urn:gov:gsa:openidconnect:sp:test' + @client_id = 'urn:gov:gsa:openidconnect:sp:sinatra' super end