maxmind-geoip2 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +72 -0
  5. data/LICENSE-APACHE +202 -0
  6. data/LICENSE-MIT +17 -0
  7. data/README.dev.md +4 -0
  8. data/README.md +326 -0
  9. data/Rakefile +14 -0
  10. data/lib/maxmind/geoip2.rb +4 -0
  11. data/lib/maxmind/geoip2/client.rb +328 -0
  12. data/lib/maxmind/geoip2/errors.rb +41 -0
  13. data/lib/maxmind/geoip2/model/abstract.rb +31 -0
  14. data/lib/maxmind/geoip2/model/anonymous_ip.rb +67 -0
  15. data/lib/maxmind/geoip2/model/asn.rb +43 -0
  16. data/lib/maxmind/geoip2/model/city.rb +79 -0
  17. data/lib/maxmind/geoip2/model/connection_type.rb +36 -0
  18. data/lib/maxmind/geoip2/model/country.rb +74 -0
  19. data/lib/maxmind/geoip2/model/domain.rb +36 -0
  20. data/lib/maxmind/geoip2/model/enterprise.rb +19 -0
  21. data/lib/maxmind/geoip2/model/insights.rb +18 -0
  22. data/lib/maxmind/geoip2/model/isp.rb +57 -0
  23. data/lib/maxmind/geoip2/reader.rb +279 -0
  24. data/lib/maxmind/geoip2/record/abstract.rb +26 -0
  25. data/lib/maxmind/geoip2/record/city.rb +42 -0
  26. data/lib/maxmind/geoip2/record/continent.rb +41 -0
  27. data/lib/maxmind/geoip2/record/country.rb +58 -0
  28. data/lib/maxmind/geoip2/record/location.rb +77 -0
  29. data/lib/maxmind/geoip2/record/maxmind.rb +21 -0
  30. data/lib/maxmind/geoip2/record/place.rb +32 -0
  31. data/lib/maxmind/geoip2/record/postal.rb +34 -0
  32. data/lib/maxmind/geoip2/record/represented_country.rb +27 -0
  33. data/lib/maxmind/geoip2/record/subdivision.rb +52 -0
  34. data/lib/maxmind/geoip2/record/traits.rb +204 -0
  35. data/maxmind-geoip2.gemspec +26 -0
  36. data/test/test_client.rb +424 -0
  37. data/test/test_model_country.rb +80 -0
  38. data/test/test_model_names.rb +47 -0
  39. data/test/test_reader.rb +459 -0
  40. metadata +130 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.authors = ['William Storey']
5
+ s.files = Dir['**/*']
6
+ s.name = 'maxmind-geoip2'
7
+ s.summary = 'A gem for interacting with the GeoIP2 webservices and databases.'
8
+ s.version = '0.4.0'
9
+
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
+ s.email = 'support@maxmind.com'
12
+ s.homepage = 'https://github.com/maxmind/GeoIP2-ruby'
13
+ s.licenses = ['Apache-2.0', 'MIT']
14
+ s.metadata = {
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://www.rubydoc.info/gems/maxmind-geoip2',
18
+ 'homepage_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
19
+ 'source_code_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
20
+ }
21
+ s.required_ruby_version = '>= 2.4.0'
22
+
23
+ s.add_runtime_dependency 'connection_pool', ['~> 2.2']
24
+ s.add_runtime_dependency 'http', ['~> 4.3']
25
+ s.add_runtime_dependency 'maxmind-db', ['~> 1.1']
26
+ 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
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/geoip2'
4
+ require 'minitest/autorun'
5
+
6
+ class CountryModelTest < Minitest::Test
7
+ RAW = {
8
+ 'continent' => {
9
+ 'code' => 'NA',
10
+ 'geoname_id' => 42,
11
+ 'names' => { 'en' => 'North America' },
12
+ },
13
+ 'country' => {
14
+ 'geoname_id' => 1,
15
+ 'iso_code' => 'US',
16
+ 'names' => { 'en' => 'United States of America' },
17
+ },
18
+ 'registered_country' => {
19
+ 'geoname_id' => 2,
20
+ 'is_in_european_union' => true,
21
+ 'iso_code' => 'DE',
22
+ 'names' => { 'en' => 'Germany' },
23
+ },
24
+ 'traits' => {
25
+ 'ip_address' => '1.2.3.4',
26
+ 'prefix_length' => 24,
27
+ },
28
+ }.freeze
29
+
30
+ def test_objects
31
+ model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
32
+ assert_equal(MaxMind::GeoIP2::Model::Country, model.class)
33
+ assert_equal(MaxMind::GeoIP2::Record::Continent, model.continent.class)
34
+ assert_equal(MaxMind::GeoIP2::Record::Country, model.country.class)
35
+ assert_equal(
36
+ MaxMind::GeoIP2::Record::Country,
37
+ model.registered_country.class,
38
+ )
39
+ assert_equal(
40
+ MaxMind::GeoIP2::Record::RepresentedCountry,
41
+ model.represented_country.class,
42
+ )
43
+ assert_equal(
44
+ MaxMind::GeoIP2::Record::Traits,
45
+ model.traits.class,
46
+ )
47
+ end
48
+
49
+ def test_values
50
+ model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
51
+
52
+ assert_equal(42, model.continent.geoname_id)
53
+ assert_equal('NA', model.continent.code)
54
+ assert_equal({ 'en' => 'North America' }, model.continent.names)
55
+ assert_equal('North America', model.continent.name)
56
+
57
+ assert_equal(1, model.country.geoname_id)
58
+ assert_equal(false, model.country.in_european_union?)
59
+ assert_equal('US', model.country.iso_code)
60
+ assert_equal({ 'en' => 'United States of America' }, model.country.names)
61
+ assert_equal('United States of America', model.country.name)
62
+ assert_nil(model.country.confidence)
63
+
64
+ assert_equal(2, model.registered_country.geoname_id)
65
+ assert_equal(true, model.registered_country.in_european_union?)
66
+ assert_equal('DE', model.registered_country.iso_code)
67
+ assert_equal({ 'en' => 'Germany' }, model.registered_country.names)
68
+ assert_equal('Germany', model.registered_country.name)
69
+ end
70
+
71
+ def test_unknown_record
72
+ model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
73
+ assert_raises(NoMethodError) { model.unknown_record }
74
+ end
75
+
76
+ def test_unknown_trait
77
+ model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
78
+ assert_raises(NoMethodError) { model.traits.unknown }
79
+ end
80
+ end