diff --git a/app/services/usps_in_person_proofing/mock/fixtures.rb b/app/services/usps_in_person_proofing/mock/fixtures.rb index 51ca638275c..45f7191dd86 100644 --- a/app/services/usps_in_person_proofing/mock/fixtures.rb +++ b/app/services/usps_in_person_proofing/mock/fixtures.rb @@ -5,6 +5,10 @@ def self.internal_server_error_response load_response_fixture('internal_server_error_response.json') end + def self.request_expired_token_response + load_response_fixture('request_expired_token_response.json') + end + def self.request_token_response load_response_fixture('request_token_response.json') end diff --git a/app/services/usps_in_person_proofing/mock/responses/request_expired_token_response.json b/app/services/usps_in_person_proofing/mock/responses/request_expired_token_response.json new file mode 100644 index 00000000000..0b69559b9a8 --- /dev/null +++ b/app/services/usps_in_person_proofing/mock/responses/request_expired_token_response.json @@ -0,0 +1,5 @@ +{ + "token_type": "Bearer", + "access_token": "==BAAyMP2ZHGOIeTd17YomIf7XjZUL4G93dboY1pTsuTJN0s9BwMYvOcIS9B3gRvloK2sroi9uFXdXrFuly7==", + "expires_in": 0 +} diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb index a2d6a0a61bd..9825ae2676d 100644 --- a/app/services/usps_in_person_proofing/proofer.rb +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -1,6 +1,7 @@ module UspsInPersonProofing class Proofer AUTH_TOKEN_CACHE_KEY = :usps_ippaas_api_auth_token + AUTH_TOKEN_REFRESH_THRESHOLD = 5 # Makes HTTP request to get nearby in-person proofing facilities # Requires address, city, state and zip code. @@ -18,11 +19,11 @@ def request_facilities(location) zipCode: location.zip_code, }.to_json - facilities = parse_facilities( - faraday.post(url, body, dynamic_headers) do |req| - req.options.context = { service_name: 'usps_facilities' } - end.body, - ) + response = faraday.post(url, body, dynamic_headers) do |req| + req.options.context = { service_name: 'usps_facilities' } + end.body + + facilities = parse_facilities(response) dedupe_facilities(facilities) end @@ -153,6 +154,10 @@ def faraday # already cached. # @return [Hash] Headers to add to USPS requests def dynamic_headers + token_remaining_time = Rails.cache.redis.ttl(AUTH_TOKEN_CACHE_KEY) + if token_remaining_time != -2 && token_remaining_time <= AUTH_TOKEN_REFRESH_THRESHOLD + retrieve_token! + end { 'Authorization' => token, 'RequestID' => request_id, diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb index e8343696ff0..63408298675 100644 --- a/spec/controllers/idv/review_controller_spec.rb +++ b/spec/controllers/idv/review_controller_spec.rb @@ -362,6 +362,9 @@ def show context 'user picked phone confirmation' do before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) idv_session.address_verification_mechanism = 'phone' idv_session.vendor_phone_confirmation = true idv_session.user_phone_confirmation = true diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index c2d0d81c951..bf5558be7ac 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -123,7 +123,6 @@ RSpec.shared_examples 'enrollment_encountering_an_error_that_has_a_nil_response' do |error_type:| it 'logs that response is not present' do expect(NewRelic::Agent).to receive(:notice_error).with(instance_of(error_type)) - job.perform(Time.zone.now) expect(job_analytics).to have_logged_event( @@ -147,6 +146,9 @@ let(:job_analytics) { FakeAnalytics.new } before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) ActiveJob::Base.queue_adapter = :test allow(job).to receive(:analytics).and_return(job_analytics) allow(IdentityConfig.store).to receive(:get_usps_proofing_results_job_reprocess_delay_minutes). diff --git a/spec/lib/tasks/dev_rake_spec.rb b/spec/lib/tasks/dev_rake_spec.rb index 3fba0d4927b..96b3eb42800 100644 --- a/spec/lib/tasks/dev_rake_spec.rb +++ b/spec/lib/tasks/dev_rake_spec.rb @@ -106,6 +106,9 @@ describe 'dev:random_in_person_users' do before(:each) do allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) ENV['VERIFIED'] = nil Rake::Task['dev:random_in_person_users'].reenable end diff --git a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb index 9944f472aab..5ca6594595d 100644 --- a/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb +++ b/spec/services/usps_in_person_proofing/enrollment_helper_spec.rb @@ -44,6 +44,11 @@ end context 'an establishing enrollment record exists for the user' do + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + end it 'updates the existing enrollment record' do expect(user.in_person_enrollments.length).to eq(1) diff --git a/spec/services/usps_in_person_proofing/proofer_spec.rb b/spec/services/usps_in_person_proofing/proofer_spec.rb index bca90d0a4dd..77451b38d03 100644 --- a/spec/services/usps_in_person_proofing/proofer_spec.rb +++ b/spec/services/usps_in_person_proofing/proofer_spec.rb @@ -4,6 +4,8 @@ include UspsIppHelper let(:subject) { UspsInPersonProofing::Proofer.new } + let(:root_url) { 'http://my.root.url' } + let(:usps_ipp_sponsor_id) { 1 } describe '#retrieve_token!' do it 'sets token and token_expires_at' do @@ -16,7 +18,6 @@ it 'calls the endpoint with the expected params' do stub_request_token - root_url = 'http://my.root.url' username = 'test username' password = 'test password' client_id = 'test client id' @@ -50,32 +51,6 @@ ) end - it 'reuses the cached auth token on subsequent requests' do - applicant = double( - 'applicant', - address: Faker::Address.street_address, - city: Faker::Address.city, - state: Faker::Address.state_abbr, - zip_code: Faker::Address.zip_code, - first_name: Faker::Name.first_name, - last_name: Faker::Name.last_name, - email: Faker::Internet.safe_email, - unique_id: '123456789', - ) - stub_request_token - stub_request_enroll - - subject.request_enroll(applicant) - subject.request_enroll(applicant) - subject.request_enroll(applicant) - - expect(WebMock).to have_requested(:post, %r{/oauth/authenticate}).once - expect(WebMock).to have_requested( - :post, - %r{/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant}, - ).times(3) - end - context 'when using redis as a backing store' do before do |ex| allow(Rails).to receive(:cache).and_return( @@ -83,6 +58,32 @@ ) end + it 'reuses the cached auth token on subsequent requests' do + applicant = double( + 'applicant', + address: Faker::Address.street_address, + city: Faker::Address.city, + state: Faker::Address.state_abbr, + zip_code: Faker::Address.zip_code, + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + email: Faker::Internet.safe_email, + unique_id: '123456789', + ) + stub_request_token + stub_request_enroll + + subject.request_enroll(applicant) + subject.request_enroll(applicant) + subject.request_enroll(applicant) + + expect(WebMock).to have_requested(:post, %r{/oauth/authenticate}).once + expect(WebMock).to have_requested( + :post, + %r{/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant}, + ).times(3) + end + it 'manually sets the expiration' do stub_request_token subject.retrieve_token! @@ -105,10 +106,37 @@ def check_facility(facility) expect(facility.zip_code_5).to be_present end + def set_up_expired_token(cache, redis) + allow(Rails).to receive(:cache).and_return( + cache, + ) + allow(cache).to receive(:redis).and_return(redis) + allow(redis).to receive(:ttl).and_return(0) + + stub_expired_request_token + end + + def check_for_token_refresh_and_method_call(cache, redis) + expect(IdentityConfig.store).to receive(:usps_ipp_root_url). + and_return(root_url).exactly(3).times + expect(IdentityConfig.store).to receive(:usps_ipp_sponsor_id). + and_return(usps_ipp_sponsor_id) + + expect(cache).to receive(:write).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(String), + hash_including(expires_at: an_instance_of(ActiveSupport::TimeWithZone)), + ).twice + + expect(redis).to receive(:expireat).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(Integer), + ).twice + + expect(cache).to receive(:read).with(UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY) + end + describe '#request_facilities' do - before do - stub_request_token - end let(:location) do double( 'Location', @@ -118,24 +146,53 @@ def check_facility(facility) zip_code: Faker::Address.zip_code, ) end + context 'when the token is valid' do + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + stub_request_token + end - it 'returns facilities' do - stub_request_facilities - facilities = subject.request_facilities(location) + it 'returns facilities' do + stub_request_facilities + facilities = subject.request_facilities(location) - check_facility(facilities[0]) + check_facility(facilities[0]) + end + + it 'does not return duplicates' do + stub_request_facilities_with_duplicates + facilities = subject.request_facilities(location) + + expect(facilities.length).to eq(9) + expect( + facilities.count do |post_office| + post_office.address == '3775 INDUSTRIAL BLVD' + end, + ).to eq(1) + end end - it 'does not return duplicates' do - stub_request_facilities_with_duplicates - facilities = subject.request_facilities(location) + context 'when the token is expired' do + let(:cache) { double(ActiveSupport::Cache::MemoryStore) } + let(:redis) { double(Redis) } + before do + set_up_expired_token(cache, redis) + stub_request_token + stub_request_facilities + end + + it 'fetches a new token' do + check_for_token_refresh_and_method_call(cache, redis) + + facilities = subject.request_facilities(location) + + expect(WebMock).to have_requested(:post, "#{root_url}/oauth/authenticate").twice - expect(facilities.length).to eq(9) - expect( - facilities.count do |post_office| - post_office.address == '3775 INDUSTRIAL BLVD' - end, - ).to eq(1) + expect(facilities.length).to eq(10) + check_facility(facilities[0]) + end end end @@ -149,10 +206,10 @@ def check_facility(facility) end describe '#request_enroll' do - it 'returns enrollment information' do - stub_request_token - stub_request_enroll - applicant = double( + let(:cache) { double(ActiveSupport::Cache::MemoryStore) } + let(:redis) { double(Redis) } + let(:applicant) do + double( 'applicant', address: Faker::Address.street_address, city: Faker::Address.city, @@ -163,144 +220,217 @@ def check_facility(facility) email: Faker::Internet.safe_email, unique_id: '123456789', ) - - enrollment = subject.request_enroll(applicant) - expect(enrollment.enrollment_code).to be_present - expect(enrollment.response_message).to be_present end + context 'when the token is valid' do + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + stub_request_token + end + it 'returns enrollment information' do + stub_request_enroll - it 'returns 400 error' do - stub_request_token - stub_request_enroll_bad_request_response - applicant = double( - 'applicant', - address: Faker::Address.street_address, - city: Faker::Address.city, - state: Faker::Address.state_abbr, - zip_code: Faker::Address.zip_code, - first_name: Faker::Name.first_name, - last_name: Faker::Name.last_name, - email: Faker::Internet.safe_email, - unique_id: '123456789', - ) + enrollment = subject.request_enroll(applicant) + expect(enrollment.enrollment_code).to be_present + expect(enrollment.response_message).to be_present + end + + it 'returns 400 error' do + stub_request_enroll_bad_request_response - expect { subject.request_enroll(applicant) }.to raise_error( - an_instance_of(Faraday::BadRequestError). - and(having_attributes( - response: include( - body: include( - 'responseMessage' => 'Sponsor for sponsorID 5 not found', + expect { subject.request_enroll(applicant) }.to raise_error( + an_instance_of(Faraday::BadRequestError). + and(having_attributes( + response: include( + body: include( + 'responseMessage' => 'Sponsor for sponsorID 5 not found', + ), ), - ), - )), - ) - end + )), + ) + end - it 'returns 500 error' do - stub_request_token - stub_request_enroll_internal_server_error_response - applicant = double( - 'applicant', - address: Faker::Address.street_address, - city: Faker::Address.city, - state: Faker::Address.state_abbr, - zip_code: Faker::Address.zip_code, - first_name: Faker::Name.first_name, - last_name: Faker::Name.last_name, - email: Faker::Internet.safe_email, - unique_id: '123456789', - ) + it 'returns 500 error' do + stub_request_enroll_internal_server_error_response - expect { subject.request_enroll(applicant) }.to raise_error( - an_instance_of(Faraday::ServerError). - and(having_attributes( - response: include( - body: include( - 'responseMessage' => 'An internal error occurred processing the request', + expect { subject.request_enroll(applicant) }.to raise_error( + an_instance_of(Faraday::ServerError). + and(having_attributes( + response: include( + body: include( + 'responseMessage' => 'An internal error occurred processing the request', + ), ), - ), - )), - ) + )), + ) + end + end + + context 'when the token is expired' do + before do + set_up_expired_token(cache, redis) + stub_request_enroll_expired_token + stub_request_enroll + end + + it 'fetches a new token and retries the attempt' do + check_for_token_refresh_and_method_call(cache, redis) + enrollment = subject.request_enroll(applicant) + + expect(enrollment.enrollment_code).to be_present + expect(enrollment.response_message).to be_present + end end end describe '#request_proofing_results' do - it 'returns failed enrollment information' do - stub_request_token - stub_request_failed_proofing_results - - applicant = double( + let(:applicant) do + double( 'applicant', unique_id: '123456789', enrollment_code: '123456789', ) - - proofing_results = subject.request_proofing_results( - applicant.unique_id, - applicant.enrollment_code, - ) - expect(proofing_results['status']).to eq 'In-person failed' - expect(proofing_results['fraudSuspected']).to eq false end + context 'when the token is valid' do + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + stub_request_token + end + it 'returns failed enrollment information' do + stub_request_failed_proofing_results - it 'returns passed enrollment information' do - stub_request_token - stub_request_passed_proofing_results + proofing_results = subject.request_proofing_results( + applicant.unique_id, + applicant.enrollment_code, + ) + expect(proofing_results['status']).to eq 'In-person failed' + expect(proofing_results['fraudSuspected']).to eq false + end - applicant = double( - 'applicant', - unique_id: '123456789', - enrollment_code: '123456789', - ) + it 'returns passed enrollment information' do + stub_request_passed_proofing_results - proofing_results = subject.request_proofing_results( - applicant.unique_id, - applicant.enrollment_code, - ) - expect(proofing_results['status']).to eq 'In-person passed' - expect(proofing_results['fraudSuspected']).to eq false + proofing_results = subject.request_proofing_results( + applicant.unique_id, + applicant.enrollment_code, + ) + expect(proofing_results['status']).to eq 'In-person passed' + expect(proofing_results['fraudSuspected']).to eq false + end + + it 'returns in-progress enrollment information' do + stub_request_in_progress_proofing_results + + expect do + subject.request_proofing_results( + applicant.unique_id, + applicant.enrollment_code, + ) + end.to raise_error( + an_instance_of(Faraday::BadRequestError). + and(having_attributes( + response: include( + body: include( + 'responseMessage' => 'Customer has not been to a post office to complete IPP', + ), + ), + )), + ) + end end + context 'when the token is expired' do + let(:cache) { double(ActiveSupport::Cache::MemoryStore) } + let(:redis) { double(Redis) } + before do + set_up_expired_token(cache, redis) + stub_request_proofing_results_with_forbidden_error + stub_request_passed_proofing_results + end - it 'returns in-progress enrollment information' do - stub_request_token - stub_request_in_progress_proofing_results + it 'fetches a new token and retries the attempt' do + expect(cache).to receive(:write).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(String), + hash_including(expires_at: an_instance_of(ActiveSupport::TimeWithZone)), + ).twice - applicant = double( - 'applicant', - unique_id: '123456789', - enrollment_code: '123456789', - ) + expect(redis).to receive(:expireat).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(Integer), + ).twice + + expect(cache).to receive(:read).with(UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY) - expect do - subject.request_proofing_results( + proofing_results = subject.request_proofing_results( applicant.unique_id, applicant.enrollment_code, ) - end.to raise_error( - an_instance_of(Faraday::BadRequestError). - and(having_attributes( - response: include( - body: include( - 'responseMessage' => 'Customer has not been to a post office to complete IPP', - ), - ), - )), - ) + + expect(proofing_results['status']).to eq 'In-person passed' + expect(proofing_results['fraudSuspected']).to eq false + end end end describe '#request_enrollment_code' do - it 'returns enrollment information' do - stub_request_token - stub_request_enrollment_code - applicant = double( + let(:applicant) do + double( 'applicant', unique_id: '123456789', ) + end + context 'when the token is valid' do + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + stub_request_token + end + + it 'returns enrollment information' do + stub_request_enrollment_code + + enrollment = subject.request_enrollment_code(applicant) + expect(enrollment['enrollmentCode']).to be_present + expect(enrollment['responseMessage']).to be_present + end + end - enrollment = subject.request_enrollment_code(applicant) - expect(enrollment['enrollmentCode']).to be_present - expect(enrollment['responseMessage']).to be_present + context 'when the token is expired' do + let(:cache) { double(ActiveSupport::Cache::MemoryStore) } + let(:redis) { double(Redis) } + before do + set_up_expired_token(cache, redis) + end + + it 'fetches a new token and retries the attempt' do + expect(IdentityConfig.store).to receive(:usps_ipp_sponsor_id). + and_return(usps_ipp_sponsor_id) + + stub_request_enrollment_code_with_forbidden_error + stub_request_enrollment_code + + expect(cache).to receive(:write).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(String), + hash_including(expires_at: an_instance_of(ActiveSupport::TimeWithZone)), + ).twice + + expect(redis).to receive(:expireat).with( + UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY, + an_instance_of(Integer), + ).twice + + expect(cache).to receive(:read).with(UspsInPersonProofing::Proofer::AUTH_TOKEN_CACHE_KEY) + + enrollment = subject.request_enrollment_code(applicant) + + expect(enrollment['enrollmentCode']).to be_present + expect(enrollment['responseMessage']).to be_present + end end end end diff --git a/spec/support/usps_ipp_helper.rb b/spec/support/usps_ipp_helper.rb index 5259b7e1487..939b6e71753 100644 --- a/spec/support/usps_ipp_helper.rb +++ b/spec/support/usps_ipp_helper.rb @@ -1,4 +1,12 @@ module UspsIppHelper + def stub_expired_request_token + stub_request(:post, %r{/oauth/authenticate}).to_return( + status: 200, + body: UspsInPersonProofing::Mock::Fixtures.request_expired_token_response, + headers: { 'content-type' => 'application/json' }, + ) + end + def stub_request_token stub_request(:post, %r{/oauth/authenticate}).to_return( status: 200, @@ -23,6 +31,13 @@ def stub_request_facilities_with_duplicates ) end + def stub_request_facilities_with_expired_token + stub_request( + :post, + %r{/ivs-ippaas-api/IPPRest/resources/rest/getIppFacilityList}, + ).to_raise(Faraday::ForbiddenError) + end + def stub_request_enroll stub_request(:post, %r{/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant}).to_return( status: 200, @@ -31,6 +46,13 @@ def stub_request_enroll ) end + def stub_request_enroll_expired_token + stub_request( + :post, + %r{/ivs-ippaas-api/IPPRest/resources/rest/optInIPPApplicant}, + ).to_raise(Faraday::ForbiddenError) + end + def stub_request_enroll_timeout_error stub_request( :post, @@ -196,6 +218,13 @@ def request_in_progress_proofing_results_args } end + def stub_request_proofing_results_with_forbidden_error + stub_request( + :post, + %r{/ivs-ippaas-api/IPPRest/resources/rest/getProofingResults}, + ).to_raise(Faraday::ForbiddenError) + end + def stub_request_proofing_results_with_timeout_error stub_request( :post, @@ -238,4 +267,11 @@ def stub_request_enrollment_code headers: { 'content-type' => 'application/json' }, ) end + + def stub_request_enrollment_code_with_forbidden_error + stub_request( + :post, + %r{/ivs-ippaas-api/IPPRest/resources/rest/requestEnrollmentCode}, + ).to_raise(Faraday::ForbiddenError) + end end