maxmind-geoip2 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +1 -5
  4. data/README.dev.md +1 -1
  5. data/README.md +82 -8
  6. data/lib/maxmind/geoip2.rb +1 -0
  7. data/lib/maxmind/geoip2/client.rb +333 -0
  8. data/lib/maxmind/geoip2/errors.rb +37 -3
  9. data/lib/maxmind/geoip2/model/abstract.rb +19 -15
  10. data/lib/maxmind/geoip2/model/anonymous_ip.rb +62 -50
  11. data/lib/maxmind/geoip2/model/asn.rb +33 -29
  12. data/lib/maxmind/geoip2/model/city.rb +59 -55
  13. data/lib/maxmind/geoip2/model/connection_type.rb +27 -23
  14. data/lib/maxmind/geoip2/model/country.rb +64 -53
  15. data/lib/maxmind/geoip2/model/domain.rb +27 -23
  16. data/lib/maxmind/geoip2/model/enterprise.rb +13 -9
  17. data/lib/maxmind/geoip2/model/insights.rb +18 -0
  18. data/lib/maxmind/geoip2/model/isp.rb +45 -41
  19. data/lib/maxmind/geoip2/reader.rb +260 -233
  20. data/lib/maxmind/geoip2/record/abstract.rb +17 -13
  21. data/lib/maxmind/geoip2/record/city.rb +33 -29
  22. data/lib/maxmind/geoip2/record/continent.rb +32 -28
  23. data/lib/maxmind/geoip2/record/country.rb +47 -43
  24. data/lib/maxmind/geoip2/record/location.rb +64 -60
  25. data/lib/maxmind/geoip2/record/maxmind.rb +21 -0
  26. data/lib/maxmind/geoip2/record/place.rb +22 -18
  27. data/lib/maxmind/geoip2/record/postal.rb +26 -22
  28. data/lib/maxmind/geoip2/record/represented_country.rb +20 -16
  29. data/lib/maxmind/geoip2/record/subdivision.rb +42 -38
  30. data/lib/maxmind/geoip2/record/traits.rb +204 -191
  31. data/maxmind-geoip2.gemspec +11 -3
  32. data/test/data/bad-data/maxminddb-python/bad-unicode-in-map-key.mmdb +0 -0
  33. data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +1 -0
  34. data/test/data/source-data/GeoIP2-ISP-Test.json +3 -1
  35. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +87 -0
  36. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  37. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  38. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  39. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  40. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  41. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  42. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  43. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  44. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  46. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  47. data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  49. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  50. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  51. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  52. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  53. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  54. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  55. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  56. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  57. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  58. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  59. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  66. data/test/data/test-data/MaxMind-DB-test-pointer-decoder.mmdb +0 -0
  67. data/test/data/test-data/write-test-data.pl +68 -18
  68. data/test/test_client.rb +426 -0
  69. data/test/test_model_country.rb +16 -0
  70. data/test/test_reader.rb +59 -0
  71. metadata +113 -10
  72. data/Gemfile.lock +0 -38
@@ -5,7 +5,7 @@ Gem::Specification.new do |s|
5
5
  s.files = Dir['**/*']
6
6
  s.name = 'maxmind-geoip2'
7
7
  s.summary = 'A gem for interacting with the GeoIP2 webservices and databases.'
8
- s.version = '0.1.0'
8
+ s.version = '0.6.0'
9
9
 
10
10
  s.description = 'A gem for interacting with the GeoIP2 webservices and databases. MaxMind provides geolocation data as downloadable databases as well as through a webservice.'
11
11
  s.email = 'support@maxmind.com'
@@ -13,12 +13,20 @@ Gem::Specification.new do |s|
13
13
  s.licenses = ['Apache-2.0', 'MIT']
14
14
  s.metadata = {
15
15
  'bug_tracker_uri' => 'https://github.com/maxmind/GeoIP2-ruby/issues',
16
- 'changelog_uri' => 'https://github.com/maxmind/GeoIP2-ruby/blob/master/CHANGELOG.md',
17
- 'documentation_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
16
+ 'changelog_uri' => 'https://github.com/maxmind/GeoIP2-ruby/blob/main/CHANGELOG.md',
17
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/maxmind-geoip2',
18
18
  'homepage_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
19
19
  'source_code_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
20
20
  }
21
21
  s.required_ruby_version = '>= 2.4.0'
22
22
 
23
+ s.add_runtime_dependency 'connection_pool', ['~> 2.2']
24
+ s.add_runtime_dependency 'http', ['~> 4.3']
23
25
  s.add_runtime_dependency 'maxmind-db', ['~> 1.1']
26
+
27
+ s.add_development_dependency 'minitest'
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'rubocop'
30
+ s.add_development_dependency 'rubocop-performance'
31
+ s.add_development_dependency 'webmock'
24
32
  end
@@ -18,6 +18,7 @@
18
18
  "is_anonymous_vpn" : true,
19
19
  "is_hosting_provider" : true,
20
20
  "is_public_proxy" : true,
21
+ "is_residential_proxy" : true,
21
22
  "is_tor_exit_node" : true
22
23
  }
23
24
  },
@@ -12587,7 +12587,9 @@
12587
12587
  {
12588
12588
  "2c0f:ff80::/25" : {
12589
12589
  "autonomous_system_number" : 237,
12590
- "autonomous_system_organization" : "Merit Network Inc."
12590
+ "autonomous_system_organization" : "Merit Network Inc.",
12591
+ "isp" : "Merit Network Inc.",
12592
+ "organization" : "Merit Network Inc."
12591
12593
  }
12592
12594
  }
12593
12595
  ]
@@ -2057,5 +2057,92 @@
2057
2057
  "user_type" : "government"
2058
2058
  }
2059
2059
  }
2060
+ },
2061
+ {
2062
+ "::2.20.32.110/127" : {
2063
+ "city" : {
2064
+ "confidence" : 60,
2065
+ "geoname_id" : 315808,
2066
+ "names" : {
2067
+ "de" : "Elazığ",
2068
+ "en" : "Elâzığ",
2069
+ "fr" : "Elazığ",
2070
+ "ja" : "エラズー",
2071
+ "ru" : "Элязыг",
2072
+ "zh-CN" : "埃拉泽"
2073
+ }
2074
+ },
2075
+ "continent" : {
2076
+ "code" : "AS",
2077
+ "geoname_id" : 6255147,
2078
+ "names" : {
2079
+ "de" : "Asien",
2080
+ "en" : "Asia",
2081
+ "es" : "Asia",
2082
+ "fr" : "Asie",
2083
+ "ja" : "アジア",
2084
+ "pt-BR" : "Ásia",
2085
+ "ru" : "Азия",
2086
+ "zh-CN" : "亚洲"
2087
+ }
2088
+ },
2089
+ "country" : {
2090
+ "confidence" : 90,
2091
+ "geoname_id" : 298795,
2092
+ "iso_code" : "TR",
2093
+ "names" : {
2094
+ "de" : "Türkei",
2095
+ "en" : "Turkey",
2096
+ "es" : "Turquía",
2097
+ "fr" : "Turquie",
2098
+ "ja" : "トルコ共和国",
2099
+ "pt-BR" : "Turquia",
2100
+ "ru" : "Турция",
2101
+ "zh-CN" : "土耳其"
2102
+ }
2103
+ },
2104
+ "location" : {
2105
+ "accuracy_radius" : 50,
2106
+ "latitude" : 38.6229,
2107
+ "longitude" : 39.3217,
2108
+ "time_zone" : "Europe/Istanbul"
2109
+ },
2110
+ "postal" : {
2111
+ "code" : "23100",
2112
+ "confidence" : 30
2113
+ },
2114
+ "registered_country" : {
2115
+ "geoname_id" : 298795,
2116
+ "iso_code" : "TR",
2117
+ "names" : {
2118
+ "de" : "Türkei",
2119
+ "en" : "Turkey",
2120
+ "es" : "Turquía",
2121
+ "fr" : "Turquie",
2122
+ "ja" : "トルコ共和国",
2123
+ "pt-BR" : "Turquia",
2124
+ "ru" : "Турция",
2125
+ "zh-CN" : "土耳其"
2126
+ }
2127
+ },
2128
+ "subdivisions" : [
2129
+ {
2130
+ "confidence" : 60,
2131
+ "geoname_id" : 315807,
2132
+ "iso_code" : "23",
2133
+ "names" : {
2134
+ "en" : "Elazığ"
2135
+ }
2136
+ }
2137
+ ],
2138
+ "traits" : {
2139
+ "autonomous_system_number" : 8517,
2140
+ "autonomous_system_organization" : "National Academic Network and Information Center",
2141
+ "connection_type" : "Cable/DSL",
2142
+ "isp" : "National Academic Network and Information Center",
2143
+ "organization" : "National Academic Network and Information Center",
2144
+ "user_type" : "business"
2145
+ }
2146
+ }
2060
2147
  }
2061
2148
  ]
@@ -19,7 +19,7 @@ use Test::MaxMind::DB::Common::Util qw( standard_test_metadata );
19
19
  my $Dir = dirname( abs_path($0) );
20
20
 
21
21
  sub main {
22
- my @sizes = ( 24, 28, 32 );
22
+ my @sizes = ( 24, 28, 32 );
23
23
  my @ipv4_range = ( '1.1.1.1', '1.1.1.32' );
24
24
 
25
25
  my @ipv4_subnets = Net::Works::Network->range_as_subnets(@ipv4_range);
@@ -71,6 +71,7 @@ sub main {
71
71
  }
72
72
 
73
73
  write_decoder_test_db();
74
+ write_pointer_decoder_test_db();
74
75
  write_deeply_nested_structures_db();
75
76
 
76
77
  write_geoip2_dbs();
@@ -231,23 +232,7 @@ sub write_test_db {
231
232
  );
232
233
 
233
234
  sub write_decoder_test_db {
234
- my $writer = MaxMind::DB::Writer::Tree->new(
235
- ip_version => 6,
236
- record_size => 24,
237
- database_type => 'MaxMind DB Decoder Test',
238
- languages => ['en'],
239
- description => {
240
- en =>
241
- 'MaxMind DB Decoder Test database - contains every MaxMind DB data type',
242
- },
243
- alias_ipv6_to_ipv4 => 1,
244
- remove_reserved_networks => 0,
245
- map_key_type_callback => sub {
246
- my $key = $_[0];
247
- $key =~ s/X$//;
248
- return $key eq 'array' ? [ 'array', 'uint32' ] : $key;
249
- },
250
- );
235
+ my $writer = _decoder_writer();
251
236
 
252
237
  my @subnets
253
238
  = map { Net::Works::Network->new_from_string( string => $_ ) }
@@ -285,6 +270,70 @@ sub write_test_db {
285
270
 
286
271
  return;
287
272
  }
273
+
274
+ sub write_pointer_decoder_test_db {
275
+
276
+ # We want to create a database where most values are pointers
277
+ no warnings 'redefine';
278
+ local *MaxMind::DB::Writer::Serializer::_should_cache_value
279
+ = sub { 1 };
280
+ my $writer = _decoder_writer();
281
+
282
+ # We add these slightly different records so that we end up with
283
+ # pointers for the individual values in the maps, not just pointers
284
+ # to the map
285
+ $writer->insert_network(
286
+ '1.0.0.0/32',
287
+ {
288
+ %all_types,
289
+ booleanX => 0,
290
+ arrayX => [ 1, 2, 3, 4, ],
291
+ mapXX => {
292
+ utf8_stringX => 'hello',
293
+ arrayX => [ 7, 8, 9, 10 ],
294
+ booleanX => 0,
295
+ },
296
+ },
297
+ );
298
+
299
+ $writer->insert_network(
300
+ '1.1.1.0/32',
301
+ {
302
+ %all_types,
303
+
304
+ # This has to be 0 rather than 1 as otherwise the buggy
305
+ # Perl writer will think it is the same as an uint32 value of
306
+ # 1 and make a pointer to a value of a different type.
307
+ boolean => 0,
308
+ },
309
+ );
310
+
311
+ open my $fh, '>', "$Dir/MaxMind-DB-test-pointer-decoder.mmdb";
312
+ $writer->write_tree($fh);
313
+ close $fh;
314
+
315
+ return;
316
+ }
317
+
318
+ sub _decoder_writer {
319
+ return MaxMind::DB::Writer::Tree->new(
320
+ ip_version => 6,
321
+ record_size => 24,
322
+ database_type => 'MaxMind DB Decoder Test',
323
+ languages => ['en'],
324
+ description => {
325
+ en =>
326
+ 'MaxMind DB Decoder Test database - contains every MaxMind DB data type',
327
+ },
328
+ alias_ipv6_to_ipv4 => 1,
329
+ remove_reserved_networks => 0,
330
+ map_key_type_callback => sub {
331
+ my $key = $_[0];
332
+ $key =~ s/X*$//;
333
+ return $key eq 'array' ? [ 'array', 'uint32' ] : $key;
334
+ },
335
+ );
336
+ }
288
337
  }
289
338
 
290
339
  {
@@ -427,6 +476,7 @@ sub _universal_map_key_type_callback {
427
476
  is_in_european_union => 'boolean',
428
477
  is_legitimate_proxy => 'boolean',
429
478
  is_public_proxy => 'boolean',
479
+ is_residential_proxy => 'boolean',
430
480
  is_satellite_provider => 'boolean',
431
481
  is_tor_exit_node => 'boolean',
432
482
  iso_code => 'utf8_string',
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'maxmind/geoip2'
5
+ require 'minitest/autorun'
6
+ require 'webmock/minitest'
7
+
8
+ class ClientTest < Minitest::Test
9
+ COUNTRY = {
10
+ 'continent' => {
11
+ 'code' => 'NA',
12
+ 'geoname_id' => 42,
13
+ 'names' => { 'en' => 'North America' },
14
+ },
15
+ 'country' => {
16
+ 'geoname_id' => 1,
17
+ 'iso_code' => 'US',
18
+ 'names' => { 'en' => 'United States of America' },
19
+ },
20
+ 'maxmind' => {
21
+ 'queries_remaining' => 11,
22
+ },
23
+ 'traits' => {
24
+ 'ip_address' => '1.2.3.4',
25
+ 'network' => '1.2.3.0/24',
26
+ },
27
+ }.freeze
28
+
29
+ INSIGHTS = {
30
+ 'continent' => {
31
+ 'code' => 'NA',
32
+ 'geoname_id' => 42,
33
+ 'names' => { 'en' => 'North America' },
34
+ },
35
+ 'country' => {
36
+ 'geoname_id' => 1,
37
+ 'iso_code' => 'US',
38
+ 'names' => { 'en' => 'United States of America' },
39
+ },
40
+ 'maxmind' => {
41
+ 'queries_remaining' => 11,
42
+ },
43
+ 'traits' => {
44
+ 'ip_address' => '1.2.3.40',
45
+ 'is_residential_proxy' => true,
46
+ 'network' => '1.2.3.0/24',
47
+ 'static_ip_score' => 1.3,
48
+ 'user_count' => 2,
49
+ },
50
+ }.freeze
51
+
52
+ CONTENT_TYPES = {
53
+ country: 'application/vnd.maxmind.com-country+json; charset=UTF-8; version=2.1',
54
+ }.freeze
55
+
56
+ def test_country
57
+ record = request(:country, '1.2.3.4')
58
+
59
+ assert_instance_of(MaxMind::GeoIP2::Model::Country, record)
60
+
61
+ assert_equal(42, record.continent.geoname_id)
62
+ assert_equal('NA', record.continent.code)
63
+ assert_equal({ 'en' => 'North America' }, record.continent.names)
64
+ assert_equal('North America', record.continent.name)
65
+
66
+ assert_equal(1, record.country.geoname_id)
67
+ assert_equal(false, record.country.in_european_union?)
68
+ assert_equal('US', record.country.iso_code)
69
+ assert_equal({ 'en' => 'United States of America' }, record.country.names)
70
+ assert_equal('United States of America', record.country.name)
71
+
72
+ assert_equal(11, record.maxmind.queries_remaining)
73
+
74
+ assert_equal(false, record.registered_country.in_european_union?)
75
+
76
+ assert_equal('1.2.3.0/24', record.traits.network)
77
+ end
78
+
79
+ def test_insights
80
+ record = request(:insights, '1.2.3.40')
81
+
82
+ assert_instance_of(MaxMind::GeoIP2::Model::Insights, record)
83
+
84
+ assert_equal(42, record.continent.geoname_id)
85
+
86
+ assert_equal(true, record.traits.residential_proxy?)
87
+ assert_equal('1.2.3.0/24', record.traits.network)
88
+ assert_equal(1.3, record.traits.static_ip_score)
89
+ assert_equal(2, record.traits.user_count)
90
+ end
91
+
92
+ def test_city
93
+ record = request(:city, '1.2.3.4')
94
+
95
+ assert_instance_of(MaxMind::GeoIP2::Model::City, record)
96
+
97
+ assert_equal('1.2.3.0/24', record.traits.network)
98
+ end
99
+
100
+ def test_me
101
+ record = request(:city, 'me')
102
+
103
+ assert_instance_of(MaxMind::GeoIP2::Model::City, record)
104
+ end
105
+
106
+ def test_no_body_error
107
+ assert_raises(
108
+ JSON::ParserError,
109
+ ) { request(:country, '1.2.3.5') }
110
+ end
111
+
112
+ def test_bad_body_error
113
+ assert_raises(
114
+ JSON::ParserError,
115
+ ) { request(:country, '2.2.3.5') }
116
+ end
117
+
118
+ def test_non_json_success_response
119
+ error = assert_raises(
120
+ MaxMind::GeoIP2::HTTPError,
121
+ ) { request(:country, '3.2.3.5') }
122
+
123
+ assert_equal(
124
+ 'Received a success response for country but it is not JSON: extra bad body',
125
+ error.message,
126
+ )
127
+ end
128
+
129
+ def test_invalid_ip_error
130
+ error = assert_raises(
131
+ MaxMind::GeoIP2::AddressInvalidError,
132
+ ) { request(:country, '1.2.3.6') }
133
+
134
+ assert_equal(
135
+ 'The value "1.2.3" is not a valid IP address',
136
+ error.message,
137
+ )
138
+ end
139
+
140
+ def test_no_error_body_ip_error
141
+ assert_raises(
142
+ JSON::ParserError,
143
+ ) { request(:country, '1.2.3.7') }
144
+ end
145
+
146
+ def test_missing_key_ip_error
147
+ error = assert_raises(
148
+ MaxMind::GeoIP2::HTTPError,
149
+ ) { request(:country, '1.2.3.71') }
150
+
151
+ assert_equal(
152
+ 'Received client error response (400) that is JSON but does not specify code or error keys: {"code":"HI"}',
153
+ error.message,
154
+ )
155
+ end
156
+
157
+ def test_weird_error_body_ip_error
158
+ error = assert_raises(
159
+ MaxMind::GeoIP2::HTTPError,
160
+ ) { request(:country, '1.2.3.8') }
161
+
162
+ assert_equal(
163
+ 'Received client error response (400) that is JSON but does not specify code or error keys: {"weird":42}',
164
+ error.message,
165
+ )
166
+ end
167
+
168
+ def test_500_error
169
+ error = assert_raises(
170
+ MaxMind::GeoIP2::HTTPError,
171
+ ) { request(:country, '1.2.3.10') }
172
+
173
+ assert_equal(
174
+ 'Received server error response (500) for country with body foo',
175
+ error.message,
176
+ )
177
+ end
178
+
179
+ def test_300_response
180
+ error = assert_raises(
181
+ MaxMind::GeoIP2::HTTPError,
182
+ ) { request(:country, '1.2.3.11') }
183
+
184
+ assert_equal(
185
+ 'Received unexpected response (300) for country with body bar',
186
+ error.message,
187
+ )
188
+ end
189
+
190
+ def test_406_error
191
+ error = assert_raises(
192
+ MaxMind::GeoIP2::HTTPError,
193
+ ) { request(:country, '1.2.3.12') }
194
+
195
+ assert_equal(
196
+ 'Received client error response (406) for country but it is not JSON: Cannot satisfy your Accept-Charset requirements',
197
+ error.message,
198
+ )
199
+ end
200
+
201
+ def test_address_not_found_error
202
+ error = assert_raises(
203
+ MaxMind::GeoIP2::AddressNotFoundError,
204
+ ) { request(:country, '1.2.3.13') }
205
+
206
+ assert_equal(
207
+ 'The address "1.2.3.13" is not in our database.',
208
+ error.message,
209
+ )
210
+ end
211
+
212
+ def test_address_reserved_error
213
+ error = assert_raises(
214
+ MaxMind::GeoIP2::AddressReservedError,
215
+ ) { request(:country, '1.2.3.14') }
216
+
217
+ assert_equal(
218
+ 'The address "1.2.3.14" is a private address.',
219
+ error.message,
220
+ )
221
+ end
222
+
223
+ def test_authorization_error
224
+ error = assert_raises(
225
+ MaxMind::GeoIP2::AuthenticationError,
226
+ ) { request(:country, '1.2.3.15') }
227
+
228
+ assert_equal(
229
+ 'An account ID and license key are required to use this service.',
230
+ error.message,
231
+ )
232
+ end
233
+
234
+ def test_missing_license_key_error
235
+ error = assert_raises(
236
+ MaxMind::GeoIP2::AuthenticationError,
237
+ ) { request(:country, '1.2.3.16') }
238
+
239
+ assert_equal(
240
+ 'A license key is required to use this service.',
241
+ error.message,
242
+ )
243
+ end
244
+
245
+ def test_missing_account_id_error
246
+ error = assert_raises(
247
+ MaxMind::GeoIP2::AuthenticationError,
248
+ ) { request(:country, '1.2.3.17') }
249
+
250
+ assert_equal(
251
+ 'An account ID is required to use this service.',
252
+ error.message,
253
+ )
254
+ end
255
+
256
+ def test_insufficient_funds_error
257
+ error = assert_raises(
258
+ MaxMind::GeoIP2::InsufficientFundsError,
259
+ ) { request(:country, '1.2.3.18') }
260
+
261
+ assert_equal(
262
+ 'The license key you have provided is out of queries.',
263
+ error.message,
264
+ )
265
+ end
266
+
267
+ def test_unexpected_code_error
268
+ error = assert_raises(
269
+ MaxMind::GeoIP2::InvalidRequestError,
270
+ ) { request(:country, '1.2.3.19') }
271
+
272
+ assert_equal(
273
+ 'Whoa!',
274
+ error.message,
275
+ )
276
+ end
277
+
278
+ def request(method, ip_address)
279
+ response = get_response(ip_address)
280
+
281
+ stub_request(:get, /geoip/)
282
+ .to_return(
283
+ body: response[:body],
284
+ headers: response[:headers],
285
+ status: response[:status],
286
+ )
287
+
288
+ client = MaxMind::GeoIP2::Client.new(
289
+ account_id: 42,
290
+ license_key: 'abcdef123456',
291
+ )
292
+
293
+ client.send(method, '1.2.3.4')
294
+ end
295
+
296
+ def get_response(ip_address)
297
+ responses = {
298
+ 'me' => {
299
+ body: JSON.generate(COUNTRY),
300
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
301
+ status: 200,
302
+ },
303
+ '1.2.3.4' => {
304
+ body: JSON.generate(COUNTRY),
305
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
306
+ status: 200,
307
+ },
308
+ '1.2.3.5' => {
309
+ body: '',
310
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
311
+ status: 200,
312
+ },
313
+ '2.2.3.5' => {
314
+ body: 'bad body',
315
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
316
+ status: 200,
317
+ },
318
+ '3.2.3.5' => {
319
+ body: 'extra bad body',
320
+ headers: {},
321
+ status: 200,
322
+ },
323
+ '1.2.3.40' => {
324
+ body: JSON.generate(INSIGHTS),
325
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
326
+ status: 200,
327
+ },
328
+ '1.2.3.6' => {
329
+ body: JSON.generate({
330
+ 'code' => 'IP_ADDRESS_INVALID',
331
+ 'error' => 'The value "1.2.3" is not a valid IP address',
332
+ }),
333
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
334
+ status: 400,
335
+ },
336
+ '1.2.3.7' => {
337
+ body: '',
338
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
339
+ status: 400,
340
+ },
341
+ '1.2.3.71' => {
342
+ body: JSON.generate({ code: 'HI' }),
343
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
344
+ status: 400,
345
+ },
346
+ '1.2.3.8' => {
347
+ body: JSON.generate({ weird: 42 }),
348
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
349
+ status: 400,
350
+ },
351
+ '1.2.3.10' => {
352
+ body: 'foo',
353
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
354
+ status: 500,
355
+ },
356
+ '1.2.3.11' => {
357
+ body: 'bar',
358
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
359
+ status: 300,
360
+ },
361
+ '1.2.3.12' => {
362
+ body: 'Cannot satisfy your Accept-Charset requirements',
363
+ headers: {},
364
+ status: 406,
365
+ },
366
+ '1.2.3.13' => {
367
+ body: JSON.generate({
368
+ 'code' => 'IP_ADDRESS_NOT_FOUND',
369
+ 'error' => 'The address "1.2.3.13" is not in our database.',
370
+ }),
371
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
372
+ status: 400,
373
+ },
374
+ '1.2.3.14' => {
375
+ body: JSON.generate({
376
+ 'code' => 'IP_ADDRESS_RESERVED',
377
+ 'error' => 'The address "1.2.3.14" is a private address.',
378
+ }),
379
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
380
+ status: 400,
381
+ },
382
+ '1.2.3.15' => {
383
+ body: JSON.generate({
384
+ 'code' => 'AUTHORIZATION_INVALID',
385
+ 'error' => 'An account ID and license key are required to use this service.',
386
+ }),
387
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
388
+ status: 401,
389
+ },
390
+ '1.2.3.16' => {
391
+ body: JSON.generate({
392
+ 'code' => 'LICENSE_KEY_REQUIRED',
393
+ 'error' => 'A license key is required to use this service.',
394
+ }),
395
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
396
+ status: 401,
397
+ },
398
+ '1.2.3.17' => {
399
+ body: JSON.generate({
400
+ 'code' => 'ACCOUNT_ID_REQUIRED',
401
+ 'error' => 'An account ID is required to use this service.',
402
+ }),
403
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
404
+ status: 401,
405
+ },
406
+ '1.2.3.18' => {
407
+ body: JSON.generate({
408
+ 'code' => 'INSUFFICIENT_FUNDS',
409
+ 'error' => 'The license key you have provided is out of queries.',
410
+ }),
411
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
412
+ status: 402,
413
+ },
414
+ '1.2.3.19' => {
415
+ body: JSON.generate({
416
+ 'code' => 'UNEXPECTED',
417
+ 'error' => 'Whoa!',
418
+ }),
419
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
420
+ status: 400,
421
+ },
422
+ }
423
+
424
+ responses[ip_address]
425
+ end
426
+ end