maxmind-geoip2 0.4.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 (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