diff --git a/app/services/arcgis_api/geocoder.rb b/app/services/arcgis_api/geocoder.rb index 0091924b401..ecf57e6cf74 100644 --- a/app/services/arcgis_api/geocoder.rb +++ b/app/services/arcgis_api/geocoder.rb @@ -1,13 +1,12 @@ module ArcgisApi class Geocoder - mattr_reader :token, :token_expires_at - Suggestion = Struct.new(:text, :magic_key, keyword_init: true) AddressCandidate = Struct.new( :address, :location, :street_address, :city, :state, :zip_code, keyword_init: true ) Location = Struct.new(:latitude, :longitude, keyword_init: true) + API_TOKEN_CACHE_KEY = :arcgis_api_token # These are option URL params that tend to apply to multiple endpoints # https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm#ESRI_SECTION2_38613C3FCB12462CAADD55B2905140BF @@ -58,15 +57,18 @@ def find_address_candidates(magic_key) # Makes a request to retrieve a new token # it expires after 1 hour - # @return [String] the token + # @return [String] Auth token def retrieve_token! - body = request_token - @@token_expires_at = Time.zone.at(body['expires']) - @@token = body['token'] + token, expires = request_token.fetch_values('token', 'expires') + Rails.cache.write(API_TOKEN_CACHE_KEY, token, expires_at: Time.zone.at(expires)) + token end - def token_valid? - token.present? && token_expires_at.present? && token_expires_at.future? + # Checks the cache for an unexpired token and returns that. + # If the cache has expired, retrieves a new token and returns it + # @return [String] Auth token + def token + Rails.cache.read(API_TOKEN_CACHE_KEY) || retrieve_token! end private @@ -136,8 +138,6 @@ def handle_api_errors(response_body) # # Returns the same value returned by that block of code. def dynamic_headers - retrieve_token! unless token_valid? - { 'Authorization' => "Bearer #{token}" } end diff --git a/config/environments/test.rb b/config/environments/test.rb index 0ae75ffc808..7a3bb2b6afa 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -7,7 +7,7 @@ config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } config.consider_all_requests_local = true config.action_controller.perform_caching = false - config.cache_store = :null_store + config.cache_store = :memory_store config.action_dispatch.show_exceptions = false config.action_controller.allow_forgery_protection = false config.active_support.test_order = :random diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 75d77ab663d..b1592c310bc 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -124,6 +124,10 @@ class Analytics IrsAttemptsApi::RedisClient.clear_attempts! end + config.before(:each) do + Rails.cache.clear + end + config.around(:each, type: :feature) do |example| Bullet.enable = true Capybara::Webmock.start diff --git a/spec/services/arcgis_api/geocoder_spec.rb b/spec/services/arcgis_api/geocoder_spec.rb index d80a7e5fc2e..d1d11e7ce77 100644 --- a/spec/services/arcgis_api/geocoder_spec.rb +++ b/spec/services/arcgis_api/geocoder_spec.rb @@ -86,17 +86,13 @@ subject.retrieve_token! expect(subject.token).to be_present - expect(subject.token_expires_at).to be_present end it 'calls the endpoint with the expected params' do stub_generate_token_response - root_url = 'http://my.root.url' username = 'test username' password = 'test password' - allow(IdentityConfig.store).to receive(:arcgis_api_root_url). - and_return(root_url) allow(IdentityConfig.store).to receive(:arcgis_api_username). and_return(username) allow(IdentityConfig.store).to receive(:arcgis_api_password). @@ -104,7 +100,7 @@ subject.retrieve_token! - expect(WebMock).to have_requested(:post, "#{root_url}/portal/sharing/rest/generateToken"). + expect(WebMock).to have_requested(:post, %r{/generateToken}). with( body: 'username=test+username&password=test+password&referer=www.example.com&f=json', headers: { @@ -124,21 +120,67 @@ subject.suggest('1') subject.suggest('100') subject.suggest('100 Main') + + expect(WebMock).to have_requested(:post, %r{/generateToken}).once + expect(WebMock).to have_requested(:get, %r{/suggest}).times(3) end it 'implicitly refreshes the token when expired' do - stub_generate_token_response(1.hour.from_now.to_i) + root_url = 'http://my.root.url' + allow(IdentityConfig.store).to receive(:arcgis_api_root_url). + and_return(root_url) + + stub_generate_token_response(expires_at: 1.hour.from_now.to_i, token: 'token1') stub_request_suggestions subject.suggest('100 Main') - expect(subject.token_valid?).to be(true) travel 2.hours - expect(subject.token_valid?).to be(false) - stub_generate_token_response + stub_generate_token_response(token: 'token2') stub_request_suggestions subject.suggest('100 Main') - expect(subject.token_valid?).to be(true) + + expect(WebMock).to have_requested(:post, %r{/generateToken}).twice + expect(WebMock).to have_requested(:get, %r{/suggest}). + with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization' => 'Bearer token1', + 'User-Agent' => 'Faraday v1.8.0', + }, + ).once + expect(WebMock).to have_requested(:get, %r{/suggest}). + with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization' => 'Bearer token2', + 'User-Agent' => 'Faraday v1.8.0', + }, + ).once + end + + it 'reuses the cached token across instances' do + stub_generate_token_response(token: 'token1') + stub_request_suggestions + stub_request_suggestions + + client1 = ArcgisApi::Geocoder.new + client2 = ArcgisApi::Geocoder.new + + client1.suggest('1') + client2.suggest('100') + + expect(WebMock).to have_requested(:get, %r{/suggest}). + with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization' => 'Bearer token1', + 'User-Agent' => 'Faraday v1.8.0', + }, + ).twice end end end diff --git a/spec/support/arcgis_api_helper.rb b/spec/support/arcgis_api_helper.rb index af139a2f424..4a2a18eac6e 100644 --- a/spec/support/arcgis_api_helper.rb +++ b/spec/support/arcgis_api_helper.rb @@ -40,10 +40,10 @@ def stub_request_candidates_error ) end - def stub_generate_token_response(expires_at = 1.hour.from_now.to_i) + def stub_generate_token_response(expires_at: 1.hour.from_now.to_i, token: 'abc123') stub_request(:post, %r{/generateToken}).to_return( status: 200, body: { - token: 'abc123', + token: token, expires: expires_at, ssl: true, }.to_json,