Skip to content

Commit

Permalink
Merge pull request #6 from harleyttd/add_timeout_and_webmock
Browse files Browse the repository at this point in the history
Wrap Ruby's Timeout module around the RestClient call
  • Loading branch information
Jeroen Jacobs committed Mar 2, 2012
2 parents c0b6028 + 93b4aa2 commit ce8fc74
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ pkg

## PROJECT::SPECIFIC
spec/api.yml

## RVM
.rvmrc
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ PATH
GEM
remote: http://rubygems.org/
specs:
addressable (2.2.6)
crack (0.3.1)
diff-lcs (1.1.2)
json (1.5.3)
mime-types (1.16)
Expand All @@ -21,10 +23,14 @@ GEM
rspec-expectations (2.6.0)
diff-lcs (~> 1.1.2)
rspec-mocks (2.6.0)
webmock (1.7.10)
addressable (~> 2.2, > 2.2.5)
crack (>= 0.1.7)

PLATFORMS
ruby

DEPENDENCIES
geo_ip!
rspec (~> 2.5)
webmock (~> 1.7.10)
1 change: 1 addition & 0 deletions geo_ip.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Gem::Specification.new do |s|
s.add_dependency 'json', '~> 1.4'
s.add_dependency 'rest-client', '~> 1.6.1'
s.add_development_dependency 'rspec', '~> 2.5'
s.add_development_dependency 'webmock', '~> 1.7.10'
end
47 changes: 35 additions & 12 deletions lib/geo_ip.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class GeoIp

@@api_key = nil
@@timeout = 1
@@fallback_timeout = 3

class << self
def api_key
Expand All @@ -27,6 +28,29 @@ def timeout= timeout
@@timeout = timeout
end

def fallback_timeout
@@fallback_timeout
end

def fallback_timeout= fallback_timeout
@@fallback_timeout = fallback_timeout
end

def set_defaults_if_necessary options
options[:precision] ||= :city
options[:timezone] ||= false
raise 'Invalid precision' unless [:country, :city].include?(options[:precision])
raise 'Invalid timezone' unless [true, false].include?(options[:timezone])
end

def lookup_url ip, options = {}
set_defaults_if_necessary options
raise 'API key must be set first: GeoIp.api_key = \'YOURKEY\'' if self.api_key.nil?
raise 'Invalid IP address' unless ip.to_s =~ IPV4_REGEXP

"#{SERVICE_URL}#{options[:precision] == :city || options[:timezone] ? CITY_API : COUNTRY_API}?key=#{api_key}&ip=#{ip}&format=json&timezone=#{options[:timezone]}"
end

# Retreive the remote location of a given ip address.
#
# It takes two optional arguments:
Expand All @@ -36,32 +60,31 @@ def timeout= timeout
# ==== Example:
# GeoIp.geolocation('209.85.227.104', {:precision => :city, :timezone => true})
def geolocation ip, options={}
@precision = options[:precision] || :city
@timezone = options[:timezone] || false
raise 'API key must be set first: GeoIp.api_key = \'YOURKEY\'' if self.api_key.nil?
raise 'Invalid IP address' unless ip.to_s =~ IPV4_REGEXP
raise 'Invalid precision' unless [:country, :city].include?(@precision)
raise 'Invalid timezone' unless [true, false].include?(@timezone)
url = "#{SERVICE_URL}#{@precision == :city || @timezone ? CITY_API : COUNTRY_API}?key=#{api_key}&ip=#{ip}&format=json&timezone=#{@timezone}"
parsed_response = JSON.parse RestClient::Request.execute(:method => :get, :url => url, :timeout => self.timeout)
convert_keys parsed_response
location = nil
Timeout.timeout(self.fallback_timeout) do
parsed_response = JSON.parse RestClient::Request.execute(:method => :get, :url => lookup_url(ip, options), :timeout => self.timeout)
location = convert_keys(parsed_response, options)
end

location
end

private
def convert_keys hash
def convert_keys hash, options
set_defaults_if_necessary options
location = {}
location[:ip] = hash['ipAddress']
location[:status_code] = hash['statusCode']
location[:status_message] = hash['statusMessage']
location[:country_code] = hash['countryCode']
location[:country_name] = hash['countryName']
if @precision == :city
if options[:precision] == :city
location[:region_name] = hash['regionName']
location[:city] = hash['cityName']
location[:zip_code] = hash['zipCode']
location[:latitude] = hash['latitude']
location[:longitude] = hash['longitude']
if @timezone
if options[:timezone]
location[:timezone] = hash['timeZone']
end
end
Expand Down
192 changes: 192 additions & 0 deletions spec/geo_ip_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@
IP_GOOGLE_US = '209.85.227.104'
IP_PRIVATE = '10.0.0.1'
IP_LOCAL = '127.0.0.1'
# Use WebMock as default to speed up tests and for local development without a connection
# Change this to false to have tests make real http requests if you want. Perhaps to check whether IpInfoDb's API has changed
# However, you may need to increase the GeoIp.fallback_timeout variable if Timeout exceptions occur when tests are run
USE_WEBMOCK = true

describe 'GeoIp' do
before :all do
unless USE_WEBMOCK
puts "Running tests WITHOUT WebMock. You will need an internet connection. You may need to increase the GeoIp.fallback_timeout amount."
WebMock.disable!
end
end

def stub_geolocation(ip, options = {}, &block)
if USE_WEBMOCK
stub_request(:get, GeoIp.lookup_url(ip, options)).
with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}).
to_return(:status => 200, :body => yield, :headers => {})
end
end

before :each do
api_config = YAML.load_file(File.dirname(__FILE__) + '/api.yml')
GeoIp.api_key = api_config['key']
Expand All @@ -23,27 +42,91 @@

context 'city' do
it 'should return the correct city for a public ip address' do
stub_geolocation(IP_GOOGLE_US) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end

geolocation = GeoIp.geolocation(IP_GOOGLE_US)
geolocation[:country_code].should == 'US'
geolocation[:country_name].should == 'UNITED STATES'
geolocation[:city].should == 'MONTEREY PARK'
end

it 'should return nothing city for a private ip address' do
stub_geolocation(IP_PRIVATE) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "10.0.0.1",
"countryCode" : "-",
"countryName" : "-",
"regionName" : "-",
"cityName" : "-",
"zipCode" : "-",
"latitude" : "0",
"longitude" : "0",
"timeZone" : "-"
}]
end

geolocation = GeoIp.geolocation(IP_PRIVATE)
geolocation[:country_code].should == '-'
geolocation[:country_name].should == '-'
geolocation[:city].should == '-'
end

it 'should return nothing for localhost ip address' do
stub_geolocation(IP_LOCAL) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "127.0.0.1",
"countryCode" : "-",
"countryName" : "-",
"regionName" : "-",
"cityName" : "-",
"zipCode" : "-",
"latitude" : "0",
"longitude" : "0",
"timeZone" : "-"
}]
end

geolocation = GeoIp.geolocation(IP_LOCAL)
geolocation[:country_code].should == '-'
geolocation[:country_name].should == '-'
geolocation[:city].should == '-'
end

it 'should return the correct city for a public ip address when explicitly requiring it' do
stub_geolocation(IP_GOOGLE_US) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end

geolocation = GeoIp.geolocation(IP_GOOGLE_US, :precision => :city)
geolocation[:country_code].should == 'US'
geolocation[:country_name].should == 'UNITED STATES'
Expand All @@ -53,24 +136,60 @@

context 'country' do
it 'should return the correct country for a public ip address' do
stub_geolocation(IP_GOOGLE_US, :precision => :country) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US, :precision => :country)
geolocation[:country_code].should == 'US'
geolocation[:country_name].should == 'UNITED STATES'
end

it 'should return nothing country for a private ip address' do
stub_geolocation(IP_PRIVATE, :precision => :country) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "10.0.0.1",
"countryCode" : "-",
"countryName" : "-"
}]
end
geolocation = GeoIp.geolocation(IP_PRIVATE, :precision => :country)
geolocation[:country_code].should == '-'
geolocation[:country_name].should == '-'
end

it 'should return nothing country for localhost ip address' do
stub_geolocation(IP_LOCAL, :precision => :country) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "127.0.0.1",
"countryCode" : "-",
"countryName" : "-"
}]
end
geolocation = GeoIp.geolocation(IP_LOCAL, :precision => :country)
geolocation[:country_code].should == '-'
geolocation[:country_name].should == '-'
end

it 'should not return the city for a public ip address' do
stub_geolocation(IP_GOOGLE_US, :precision => :country) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US, :precision => :country)
geolocation[:country_code].should == 'US'
geolocation[:country_name].should == 'UNITED STATES'
Expand All @@ -80,23 +199,96 @@

context 'timezone' do
it 'should return the correct timezone information for a public ip address' do
stub_geolocation(IP_GOOGLE_US, :timezone => true) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US, :timezone => true)
geolocation[:timezone].should == '-08:00' # This one is likely to break when dst changes.
end

it 'should not return the timezone information when explicitly not requesting it' do
stub_geolocation(IP_GOOGLE_US, :timezone => false) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US, :timezone => false)
geolocation[:timezone].should be_nil
end

it 'should not return the timezone information when not requesting it' do
stub_geolocation(IP_GOOGLE_US) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US)
geolocation[:timezone].should be_nil
end

it 'should not return the timezone information when country precision is selected' do
stub_geolocation(IP_GOOGLE_US, :precision => :country, :timezone => true) do
%[{
"statusCode" : "OK",
"statusMessage" : "",
"ipAddress" : "209.85.227.104",
"countryCode" : "US",
"countryName" : "UNITED STATES",
"regionName" : "CALIFORNIA",
"cityName" : "MONTEREY PARK",
"zipCode" : "91754",
"latitude" : "34.0505",
"longitude" : "-118.13",
"timeZone" : "-08:00"
}]
end
geolocation = GeoIp.geolocation(IP_GOOGLE_US, :precision => :country, :timezone => true)
geolocation[:timezone].should be_nil
end
end

context "timeout" do
it 'should trigger timeout when the request is taking too long' do
stub_request(:get, GeoIp.lookup_url(IP_GOOGLE_US)).to_timeout
lambda { GeoIp.geolocation(IP_GOOGLE_US) }.should raise_exception("Request Timeout")
end

it 'should trigger fallback timeout when RestClient is taking too long to send the request', :focus => true do
GeoIp.fallback_timeout = 1
RestClient::Request.stub(:execute) { sleep 1 }
lambda { GeoIp.geolocation(IP_GOOGLE_US) }.should raise_exception(Timeout::Error)
end
end
end
Loading

0 comments on commit ce8fc74

Please sign in to comment.