Permalink
Browse files

Wrap Ruby's Timeout module around the RestClient call to enforce time…

…out if caused by bad internet connection or slow or invalid DNS lookup. Added WebMock to tests to have reliable tests.
  • Loading branch information...
1 parent c0b6028 commit 93b4aa2ee280bf3d1f367c2ef3389a35df21c220 @harley harley committed Feb 2, 2012
Showing with 238 additions and 12 deletions.
  1. +3 −0 .gitignore
  2. +6 −0 Gemfile.lock
  3. +1 −0 geo_ip.gemspec
  4. +35 −12 lib/geo_ip.rb
  5. +192 −0 spec/geo_ip_spec.rb
  6. +1 −0 spec/spec_helper.rb
View
3 .gitignore
@@ -21,3 +21,6 @@ pkg
## PROJECT::SPECIFIC
spec/api.yml
+
+## RVM
+.rvmrc
View
6 Gemfile.lock
@@ -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)
@@ -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)
View
1 geo_ip.gemspec
@@ -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
View
47 lib/geo_ip.rb
@@ -9,6 +9,7 @@ class GeoIp
@@api_key = nil
@@timeout = 1
+ @@fallback_timeout = 3
class << self
def api_key
@@ -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:
@@ -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
View
192 spec/geo_ip_spec.rb
@@ -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']
@@ -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'
@@ -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'
@@ -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
View
1 spec/spec_helper.rb
@@ -1,3 +1,4 @@
require 'rubygems'
require 'bundler/setup'
require 'geo_ip'
+require 'webmock/rspec'

0 comments on commit 93b4aa2

Please sign in to comment.