maxmind-geoip2 0.1.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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