Skip to content
20 changes: 10 additions & 10 deletions app/services/arcgis_api/geocoder.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 52 additions & 10 deletions spec/services/arcgis_api/geocoder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,21 @@
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).
and_return(password)

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: {
Expand All @@ -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
4 changes: 2 additions & 2 deletions spec/support/arcgis_api_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down