maxmind-geoip2 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +32 -0
  5. data/README.md +52 -0
  6. data/lib/maxmind/geoip2.rb +1 -0
  7. data/lib/maxmind/geoip2/client.rb +313 -0
  8. data/lib/maxmind/geoip2/errors.rb +44 -3
  9. data/lib/maxmind/geoip2/model/anonymous_ip.rb +2 -2
  10. data/lib/maxmind/geoip2/model/country.rb +7 -0
  11. data/lib/maxmind/geoip2/model/insights.rb +14 -0
  12. data/lib/maxmind/geoip2/record/maxmind.rb +17 -0
  13. data/maxmind-geoip2.gemspec +2 -1
  14. data/test/test_client.rb +424 -0
  15. metadata +21 -64
  16. data/test/data/LICENSE +0 -4
  17. data/test/data/MaxMind-DB-spec.md +0 -570
  18. data/test/data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  19. data/test/data/README.md +0 -4
  20. data/test/data/bad-data/README.md +0 -7
  21. data/test/data/bad-data/libmaxminddb/libmaxminddb-offset-integer-overflow.mmdb +0 -0
  22. data/test/data/bad-data/maxminddb-golang/cyclic-data-structure.mmdb +0 -0
  23. data/test/data/bad-data/maxminddb-golang/invalid-bytes-length.mmdb +0 -1
  24. data/test/data/bad-data/maxminddb-golang/invalid-data-record-offset.mmdb +0 -0
  25. data/test/data/bad-data/maxminddb-golang/invalid-map-key-length.mmdb +0 -0
  26. data/test/data/bad-data/maxminddb-golang/invalid-string-length.mmdb +0 -1
  27. data/test/data/bad-data/maxminddb-golang/metadata-is-an-uint128.mmdb +0 -1
  28. data/test/data/bad-data/maxminddb-golang/unexpected-bytes.mmdb +0 -0
  29. data/test/data/perltidyrc +0 -12
  30. data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +0 -48
  31. data/test/data/source-data/GeoIP2-City-Test.json +0 -12852
  32. data/test/data/source-data/GeoIP2-Connection-Type-Test.json +0 -102
  33. data/test/data/source-data/GeoIP2-Country-Test.json +0 -15916
  34. data/test/data/source-data/GeoIP2-DensityIncome-Test.json +0 -14
  35. data/test/data/source-data/GeoIP2-Domain-Test.json +0 -452
  36. data/test/data/source-data/GeoIP2-Enterprise-Test.json +0 -687
  37. data/test/data/source-data/GeoIP2-ISP-Test.json +0 -12593
  38. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +0 -2061
  39. data/test/data/source-data/GeoIP2-Static-IP-Score-Test.json +0 -2132
  40. data/test/data/source-data/GeoIP2-User-Count-Test.json +0 -2837
  41. data/test/data/source-data/GeoLite2-ASN-Test.json +0 -37
  42. data/test/data/source-data/README +0 -15
  43. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  44. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  46. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  47. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  49. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  50. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  51. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  52. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  53. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  54. data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
  55. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  56. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  57. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  58. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  59. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  66. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  67. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  68. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  69. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  70. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  71. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  72. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  73. data/test/data/test-data/README.md +0 -26
  74. data/test/data/test-data/maps-with-pointers.raw +0 -0
  75. data/test/data/test-data/write-test-data.pl +0 -641
  76. data/test/data/tidyall.ini +0 -5
@@ -1,7 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module MaxMind::GeoIP2
4
- # An AddressNotFoundError means the IP address was not found in the database.
5
- class AddressNotFoundError < RuntimeError
3
+ # Disable this because I wish to ensure the MaxMind constant is defined,
4
+ # which apparently must be done without using the compact syntax.
5
+ #
6
+ # rubocop:disable Style/ClassAndModuleChildren
7
+
8
+ # A module for namespacing purposes.
9
+ module MaxMind
10
+ module GeoIP2
11
+ # An AddressNotFoundError means the IP address was not found in the
12
+ # database or the web service said the IP address was not found.
13
+ class AddressNotFoundError < RuntimeError
14
+ end
15
+
16
+ # An HTTPError means there was an unexpected HTTP status or response.
17
+ class HTTPError < RuntimeError
18
+ end
19
+
20
+ # An AddressInvalidError means the IP address was invalid.
21
+ class AddressInvalidError < RuntimeError
22
+ end
23
+
24
+ # An AddressReservedError means the IP address is reserved.
25
+ class AddressReservedError < RuntimeError
26
+ end
27
+
28
+ # An AuthenticationError means there was a problem authenticating to the
29
+ # web service.
30
+ class AuthenticationError < RuntimeError
31
+ end
32
+
33
+ # An InsufficientFundsError means the account is out of credits.
34
+ class InsufficientFundsError < RuntimeError
35
+ end
36
+
37
+ # A PermissionRequiredError means the account does not have permission to
38
+ # use the requested service.
39
+ class PermissionRequiredError < RuntimeError
40
+ end
41
+
42
+ # An InvalidRequestError means the web service returned an error and there
43
+ # is no more specific error class.
44
+ class InvalidRequestError < RuntimeError
45
+ end
6
46
  end
7
47
  end
48
+ # rubocop:enable Style/ClassAndModuleChildren
@@ -15,7 +15,7 @@ module MaxMind::GeoIP2::Model
15
15
  # This is true if the IP address is registered to an anonymous VPN
16
16
  # provider. If a VPN provider does not register subnets under names
17
17
  # associated with them, we will likely only flag their IP ranges using the
18
- # is_hosting_provider attribute.
18
+ # hosting_provider? method.
19
19
  #
20
20
  # @return [Boolean]
21
21
  def anonymous_vpn?
@@ -23,7 +23,7 @@ module MaxMind::GeoIP2::Model
23
23
  end
24
24
 
25
25
  # This is true if the IP address belongs to a hosting or VPN provider (see
26
- # description of the is_anonymous_vpn attribute).
26
+ # description of the anonymous_vpn? method).
27
27
  #
28
28
  # @return [Boolean]
29
29
  def hosting_provider?
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'maxmind/geoip2/record/continent'
4
4
  require 'maxmind/geoip2/record/country'
5
+ require 'maxmind/geoip2/record/maxmind'
5
6
  require 'maxmind/geoip2/record/represented_country'
6
7
  require 'maxmind/geoip2/record/traits'
7
8
 
@@ -20,6 +21,11 @@ module MaxMind::GeoIP2::Model
20
21
  # @return [MaxMind::GeoIP2::Record::Country]
21
22
  attr_reader :country
22
23
 
24
+ # Data related to your MaxMind account.
25
+ #
26
+ # @return [MaxMind::GeoIP2::Record::MaxMind]
27
+ attr_reader :maxmind
28
+
23
29
  # Registered country data for the IP address. This record represents the
24
30
  # country where the ISP has registered a given IP block and may differ from
25
31
  # the user's country.
@@ -49,6 +55,7 @@ module MaxMind::GeoIP2::Model
49
55
  record['country'],
50
56
  locales,
51
57
  )
58
+ @maxmind = MaxMind::GeoIP2::Record::MaxMind.new(record['maxmind'])
52
59
  @registered_country = MaxMind::GeoIP2::Record::Country.new(
53
60
  record['registered_country'],
54
61
  locales,
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/geoip2/model/city'
4
+
5
+ module MaxMind::GeoIP2::Model
6
+ # Model class for the data returned by the GeoIP2 Precision Insights web
7
+ # service.
8
+ #
9
+ # The only difference between the City and Insights model classes is which
10
+ # fields in each record may be populated. See
11
+ # https://dev.maxmind.com/geoip/geoip2/web-services for more details.
12
+ class Insights < City
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/geoip2/record/abstract'
4
+
5
+ module MaxMind::GeoIP2::Record
6
+ # Contains data about your account.
7
+ #
8
+ # This record is returned by all location services.
9
+ class MaxMind < Abstract
10
+ # The number of remaining queries you have for the service you are calling.
11
+ #
12
+ # @return [Integer, nil]
13
+ def queries_remaining
14
+ get('queries_remaining')
15
+ end
16
+ end
17
+ end
@@ -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.2.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'
@@ -20,5 +20,6 @@ Gem::Specification.new do |s|
20
20
  }
21
21
  s.required_ruby_version = '>= 2.4.0'
22
22
 
23
+ s.add_runtime_dependency 'http', ['~> 4.3']
23
24
  s.add_runtime_dependency 'maxmind-db', ['~> 1.1']
24
25
  end
@@ -0,0 +1,424 @@
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
+ 'network' => '1.2.3.0/24',
46
+ 'static_ip_score' => 1.3,
47
+ 'user_count' => 2,
48
+ },
49
+ }.freeze
50
+
51
+ CONTENT_TYPES = {
52
+ country: 'application/vnd.maxmind.com-country+json; charset=UTF-8; version=2.1',
53
+ }.freeze
54
+
55
+ def test_country
56
+ record = request(:country, '1.2.3.4')
57
+
58
+ assert_instance_of(MaxMind::GeoIP2::Model::Country, record)
59
+
60
+ assert_equal(42, record.continent.geoname_id)
61
+ assert_equal('NA', record.continent.code)
62
+ assert_equal({ 'en' => 'North America' }, record.continent.names)
63
+ assert_equal('North America', record.continent.name)
64
+
65
+ assert_equal(1, record.country.geoname_id)
66
+ assert_equal(false, record.country.in_european_union?)
67
+ assert_equal('US', record.country.iso_code)
68
+ assert_equal({ 'en' => 'United States of America' }, record.country.names)
69
+ assert_equal('United States of America', record.country.name)
70
+
71
+ assert_equal(11, record.maxmind.queries_remaining)
72
+
73
+ assert_equal(false, record.registered_country.in_european_union?)
74
+
75
+ assert_equal('1.2.3.0/24', record.traits.network)
76
+ end
77
+
78
+ def test_insights
79
+ record = request(:insights, '1.2.3.40')
80
+
81
+ assert_instance_of(MaxMind::GeoIP2::Model::Insights, record)
82
+
83
+ assert_equal(42, record.continent.geoname_id)
84
+
85
+ assert_equal('1.2.3.0/24', record.traits.network)
86
+ assert_equal(1.3, record.traits.static_ip_score)
87
+ assert_equal(2, record.traits.user_count)
88
+ end
89
+
90
+ def test_city
91
+ record = request(:city, '1.2.3.4')
92
+
93
+ assert_instance_of(MaxMind::GeoIP2::Model::City, record)
94
+
95
+ assert_equal('1.2.3.0/24', record.traits.network)
96
+ end
97
+
98
+ def test_me
99
+ record = request(:city, 'me')
100
+
101
+ assert_instance_of(MaxMind::GeoIP2::Model::City, record)
102
+ end
103
+
104
+ def test_no_body_error
105
+ assert_raises(
106
+ JSON::ParserError,
107
+ ) { request(:country, '1.2.3.5') }
108
+ end
109
+
110
+ def test_bad_body_error
111
+ assert_raises(
112
+ JSON::ParserError,
113
+ ) { request(:country, '2.2.3.5') }
114
+ end
115
+
116
+ def test_non_json_success_response
117
+ error = assert_raises(
118
+ MaxMind::GeoIP2::HTTPError,
119
+ ) { request(:country, '3.2.3.5') }
120
+
121
+ assert_equal(
122
+ 'Received a success response for country but it is not JSON: extra bad body',
123
+ error.message,
124
+ )
125
+ end
126
+
127
+ def test_invalid_ip_error
128
+ error = assert_raises(
129
+ MaxMind::GeoIP2::AddressInvalidError,
130
+ ) { request(:country, '1.2.3.6') }
131
+
132
+ assert_equal(
133
+ 'The value "1.2.3" is not a valid IP address',
134
+ error.message,
135
+ )
136
+ end
137
+
138
+ def test_no_error_body_ip_error
139
+ assert_raises(
140
+ JSON::ParserError,
141
+ ) { request(:country, '1.2.3.7') }
142
+ end
143
+
144
+ def test_missing_key_ip_error
145
+ error = assert_raises(
146
+ MaxMind::GeoIP2::HTTPError,
147
+ ) { request(:country, '1.2.3.71') }
148
+
149
+ assert_equal(
150
+ 'Received client error response (400) that is JSON but does not specify code or error keys: {"code":"HI"}',
151
+ error.message,
152
+ )
153
+ end
154
+
155
+ def test_weird_error_body_ip_error
156
+ error = assert_raises(
157
+ MaxMind::GeoIP2::HTTPError,
158
+ ) { request(:country, '1.2.3.8') }
159
+
160
+ assert_equal(
161
+ 'Received client error response (400) that is JSON but does not specify code or error keys: {"weird":42}',
162
+ error.message,
163
+ )
164
+ end
165
+
166
+ def test_500_error
167
+ error = assert_raises(
168
+ MaxMind::GeoIP2::HTTPError,
169
+ ) { request(:country, '1.2.3.10') }
170
+
171
+ assert_equal(
172
+ 'Received server error response (500) for country with body foo',
173
+ error.message,
174
+ )
175
+ end
176
+
177
+ def test_300_response
178
+ error = assert_raises(
179
+ MaxMind::GeoIP2::HTTPError,
180
+ ) { request(:country, '1.2.3.11') }
181
+
182
+ assert_equal(
183
+ 'Received unexpected response (300) for country with body bar',
184
+ error.message,
185
+ )
186
+ end
187
+
188
+ def test_406_error
189
+ error = assert_raises(
190
+ MaxMind::GeoIP2::HTTPError,
191
+ ) { request(:country, '1.2.3.12') }
192
+
193
+ assert_equal(
194
+ 'Received client error response (406) for country but it is not JSON: Cannot satisfy your Accept-Charset requirements',
195
+ error.message,
196
+ )
197
+ end
198
+
199
+ def test_address_not_found_error
200
+ error = assert_raises(
201
+ MaxMind::GeoIP2::AddressNotFoundError,
202
+ ) { request(:country, '1.2.3.13') }
203
+
204
+ assert_equal(
205
+ 'The address "1.2.3.13" is not in our database.',
206
+ error.message,
207
+ )
208
+ end
209
+
210
+ def test_address_reserved_error
211
+ error = assert_raises(
212
+ MaxMind::GeoIP2::AddressReservedError,
213
+ ) { request(:country, '1.2.3.14') }
214
+
215
+ assert_equal(
216
+ 'The address "1.2.3.14" is a private address.',
217
+ error.message,
218
+ )
219
+ end
220
+
221
+ def test_authorization_error
222
+ error = assert_raises(
223
+ MaxMind::GeoIP2::AuthenticationError,
224
+ ) { request(:country, '1.2.3.15') }
225
+
226
+ assert_equal(
227
+ 'An account ID and license key are required to use this service.',
228
+ error.message,
229
+ )
230
+ end
231
+
232
+ def test_missing_license_key_error
233
+ error = assert_raises(
234
+ MaxMind::GeoIP2::AuthenticationError,
235
+ ) { request(:country, '1.2.3.16') }
236
+
237
+ assert_equal(
238
+ 'A license key is required to use this service.',
239
+ error.message,
240
+ )
241
+ end
242
+
243
+ def test_missing_account_id_error
244
+ error = assert_raises(
245
+ MaxMind::GeoIP2::AuthenticationError,
246
+ ) { request(:country, '1.2.3.17') }
247
+
248
+ assert_equal(
249
+ 'An account ID is required to use this service.',
250
+ error.message,
251
+ )
252
+ end
253
+
254
+ def test_insufficient_funds_error
255
+ error = assert_raises(
256
+ MaxMind::GeoIP2::InsufficientFundsError,
257
+ ) { request(:country, '1.2.3.18') }
258
+
259
+ assert_equal(
260
+ 'The license key you have provided is out of queries.',
261
+ error.message,
262
+ )
263
+ end
264
+
265
+ def test_unexpected_code_error
266
+ error = assert_raises(
267
+ MaxMind::GeoIP2::InvalidRequestError,
268
+ ) { request(:country, '1.2.3.19') }
269
+
270
+ assert_equal(
271
+ 'Whoa!',
272
+ error.message,
273
+ )
274
+ end
275
+
276
+ def request(method, ip_address)
277
+ response = get_response(ip_address)
278
+
279
+ stub_request(:get, /geoip/)
280
+ .to_return(
281
+ body: response[:body],
282
+ headers: response[:headers],
283
+ status: response[:status],
284
+ )
285
+
286
+ client = MaxMind::GeoIP2::Client.new(
287
+ account_id: 42,
288
+ license_key: 'abcdef123456',
289
+ )
290
+
291
+ client.send(method, '1.2.3.4')
292
+ end
293
+
294
+ def get_response(ip_address)
295
+ responses = {
296
+ 'me' => {
297
+ body: JSON.generate(COUNTRY),
298
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
299
+ status: 200,
300
+ },
301
+ '1.2.3.4' => {
302
+ body: JSON.generate(COUNTRY),
303
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
304
+ status: 200,
305
+ },
306
+ '1.2.3.5' => {
307
+ body: '',
308
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
309
+ status: 200,
310
+ },
311
+ '2.2.3.5' => {
312
+ body: 'bad body',
313
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
314
+ status: 200,
315
+ },
316
+ '3.2.3.5' => {
317
+ body: 'extra bad body',
318
+ headers: {},
319
+ status: 200,
320
+ },
321
+ '1.2.3.40' => {
322
+ body: JSON.generate(INSIGHTS),
323
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
324
+ status: 200,
325
+ },
326
+ '1.2.3.6' => {
327
+ body: JSON.generate({
328
+ 'code' => 'IP_ADDRESS_INVALID',
329
+ 'error' => 'The value "1.2.3" is not a valid IP address',
330
+ }),
331
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
332
+ status: 400,
333
+ },
334
+ '1.2.3.7' => {
335
+ body: '',
336
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
337
+ status: 400,
338
+ },
339
+ '1.2.3.71' => {
340
+ body: JSON.generate({ 'code': 'HI' }),
341
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
342
+ status: 400,
343
+ },
344
+ '1.2.3.8' => {
345
+ body: JSON.generate({ 'weird': 42 }),
346
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
347
+ status: 400,
348
+ },
349
+ '1.2.3.10' => {
350
+ body: 'foo',
351
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
352
+ status: 500,
353
+ },
354
+ '1.2.3.11' => {
355
+ body: 'bar',
356
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
357
+ status: 300,
358
+ },
359
+ '1.2.3.12' => {
360
+ body: 'Cannot satisfy your Accept-Charset requirements',
361
+ headers: {},
362
+ status: 406,
363
+ },
364
+ '1.2.3.13' => {
365
+ body: JSON.generate({
366
+ 'code' => 'IP_ADDRESS_NOT_FOUND',
367
+ 'error' => 'The address "1.2.3.13" is not in our database.',
368
+ }),
369
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
370
+ status: 400,
371
+ },
372
+ '1.2.3.14' => {
373
+ body: JSON.generate({
374
+ 'code' => 'IP_ADDRESS_RESERVED',
375
+ 'error' => 'The address "1.2.3.14" is a private address.',
376
+ }),
377
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
378
+ status: 400,
379
+ },
380
+ '1.2.3.15' => {
381
+ body: JSON.generate({
382
+ 'code' => 'AUTHORIZATION_INVALID',
383
+ 'error' => 'An account ID and license key are required to use this service.',
384
+ }),
385
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
386
+ status: 401,
387
+ },
388
+ '1.2.3.16' => {
389
+ body: JSON.generate({
390
+ 'code' => 'LICENSE_KEY_REQUIRED',
391
+ 'error' => 'A license key is required to use this service.',
392
+ }),
393
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
394
+ status: 401,
395
+ },
396
+ '1.2.3.17' => {
397
+ body: JSON.generate({
398
+ 'code' => 'ACCOUNT_ID_REQUIRED',
399
+ 'error' => 'An account ID is required to use this service.',
400
+ }),
401
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
402
+ status: 401,
403
+ },
404
+ '1.2.3.18' => {
405
+ body: JSON.generate({
406
+ 'code' => 'INSUFFICIENT_FUNDS',
407
+ 'error' => 'The license key you have provided is out of queries.',
408
+ }),
409
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
410
+ status: 402,
411
+ },
412
+ '1.2.3.19' => {
413
+ body: JSON.generate({
414
+ 'code' => 'UNEXPECTED',
415
+ 'error' => 'Whoa!',
416
+ }),
417
+ headers: { 'Content-Type': CONTENT_TYPES[:country] },
418
+ status: 400,
419
+ },
420
+ }
421
+
422
+ responses[ip_address]
423
+ end
424
+ end