platphorm-maxmind-db 1.1.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.
@@ -0,0 +1,539 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/db'
4
+ require 'minitest/autorun'
5
+ require 'mmdb_util'
6
+
7
+ class ReaderTest < Minitest::Test
8
+ def test_reader
9
+ modes = [
10
+ MaxMind::DB::MODE_FILE,
11
+ MaxMind::DB::MODE_MEMORY,
12
+ ]
13
+
14
+ modes.each do |mode|
15
+ [24, 28, 32].each do |record_size|
16
+ [4, 6].each do |ip_version|
17
+ filename = 'test/data/test-data/MaxMind-DB-test-ipv' +
18
+ ip_version.to_s + '-' + record_size.to_s + '.mmdb'
19
+ reader = MaxMind::DB.new(filename, mode: mode)
20
+ check_metadata(reader, ip_version, record_size)
21
+ if ip_version == 4
22
+ check_ipv4(reader, filename)
23
+ else
24
+ check_ipv6(reader, filename)
25
+ end
26
+ reader.close
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def test_reader_inspect
33
+ modes = [
34
+ MaxMind::DB::MODE_FILE,
35
+ MaxMind::DB::MODE_MEMORY,
36
+ ]
37
+
38
+ modes.each do |mode|
39
+ filename = 'test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
40
+ reader = MaxMind::DB.new(filename, mode: mode)
41
+ assert_instance_of(String, reader.inspect)
42
+ end
43
+ end
44
+
45
+ def test_get_with_prefix_len
46
+ decoder_record = {
47
+ 'array' => [1, 2, 3],
48
+ 'boolean' => true,
49
+ 'bytes' => "\x00\x00\x00*",
50
+ 'double' => 42.123456,
51
+ 'float' => 1.100000023841858,
52
+ 'int32' => -268_435_456,
53
+ 'map' => {
54
+ 'mapX' => {
55
+ 'arrayX' => [7, 8, 9],
56
+ 'utf8_stringX' => 'hello',
57
+ },
58
+ },
59
+ 'uint128' => 1_329_227_995_784_915_872_903_807_060_280_344_576,
60
+ 'uint16' => 0x64,
61
+ 'uint32' => 0x10000000,
62
+ 'uint64' => 0x1000000000000000,
63
+ 'utf8_string' => 'unicode! ☯ - ♫',
64
+ }
65
+
66
+ tests = [{
67
+ 'ip' => '1.1.1.1',
68
+ 'file_name' => 'MaxMind-DB-test-ipv6-32.mmdb',
69
+ 'expected_prefix_length' => 8,
70
+ 'expected_record' => nil,
71
+ }, {
72
+ 'ip' => '::1:ffff:ffff',
73
+ 'file_name' => 'MaxMind-DB-test-ipv6-24.mmdb',
74
+ 'expected_prefix_length' => 128,
75
+ 'expected_record' => {
76
+ 'ip' => '::1:ffff:ffff'
77
+ },
78
+ }, {
79
+ 'ip' => '::2:0:1',
80
+ 'file_name' => 'MaxMind-DB-test-ipv6-24.mmdb',
81
+ 'expected_prefix_length' => 122,
82
+ 'expected_record' => {
83
+ 'ip' => '::2:0:0'
84
+ },
85
+ }, {
86
+ 'ip' => '1.1.1.1',
87
+ 'file_name' => 'MaxMind-DB-test-ipv4-24.mmdb',
88
+ 'expected_prefix_length' => 32,
89
+ 'expected_record' => {
90
+ 'ip' => '1.1.1.1'
91
+ },
92
+ }, {
93
+ 'ip' => '1.1.1.3',
94
+ 'file_name' => 'MaxMind-DB-test-ipv4-24.mmdb',
95
+ 'expected_prefix_length' => 31,
96
+ 'expected_record' => {
97
+ 'ip' => '1.1.1.2'
98
+ },
99
+ }, {
100
+ 'ip' => '1.1.1.3',
101
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
102
+ 'expected_prefix_length' => 24,
103
+ 'expected_record' => decoder_record,
104
+ }, {
105
+ 'ip' => '::ffff:1.1.1.128',
106
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
107
+ 'expected_prefix_length' => 120,
108
+ 'expected_record' => decoder_record,
109
+ }, {
110
+ 'ip' => '::1.1.1.128',
111
+ 'file_name' => 'MaxMind-DB-test-decoder.mmdb',
112
+ 'expected_prefix_length' => 120,
113
+ 'expected_record' => decoder_record,
114
+ }, {
115
+ 'ip' => '200.0.2.1',
116
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
117
+ 'expected_prefix_length' => 0,
118
+ 'expected_record' => '::0/64',
119
+ }, {
120
+ 'ip' => '::200.0.2.1',
121
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
122
+ 'expected_prefix_length' => 64,
123
+ 'expected_record' => '::0/64',
124
+ }, {
125
+ 'ip' => '0:0:0:0:ffff:ffff:ffff:ffff',
126
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
127
+ 'expected_prefix_length' => 64,
128
+ 'expected_record' => '::0/64',
129
+ }, {
130
+ 'ip' => 'ef00::',
131
+ 'file_name' => 'MaxMind-DB-no-ipv4-search-tree.mmdb',
132
+ 'expected_prefix_length' => 1,
133
+ 'expected_record' => nil,
134
+ }]
135
+
136
+ tests.each do |test|
137
+ reader = MaxMind::DB.new('test/data/test-data/' + test['file_name'])
138
+ record, prefix_length = reader.get_with_prefix_length(test['ip'])
139
+
140
+ assert_equal(test['expected_prefix_length'], prefix_length,
141
+ format('expected prefix_length of %d for %s in %s but got %p',
142
+ test['expected_prefix_length'], test['ip'],
143
+ test['file_name'], prefix_length))
144
+
145
+ msg = format('expected_record for %s in %s', test['ip'], test['file_name'])
146
+ if test['expected_record'].nil?
147
+ assert_nil(record, msg)
148
+ else
149
+ assert_equal(test['expected_record'], record, msg)
150
+ end
151
+ end
152
+ end
153
+
154
+ def test_decoder
155
+ reader = MaxMind::DB.new(
156
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
157
+ )
158
+ record = reader.get('::1.1.1.0')
159
+ assert_equal([1, 2, 3], record['array'])
160
+ assert_equal(true, record['boolean'])
161
+ assert_equal("\x00\x00\x00*".b, record['bytes'])
162
+ assert_equal(42.123456, record['double'])
163
+ assert_in_delta(1.1, record['float'])
164
+ assert_equal(-268_435_456, record['int32'])
165
+ assert_equal(
166
+ {
167
+ 'mapX' => {
168
+ 'arrayX' => [7, 8, 9],
169
+ 'utf8_stringX' => 'hello',
170
+ },
171
+ },
172
+ record['map'],
173
+ )
174
+ assert_equal(100, record['uint16'])
175
+ assert_equal(268_435_456, record['uint32'])
176
+ assert_equal(1_152_921_504_606_846_976, record['uint64'])
177
+ assert_equal('unicode! ☯ - ♫', record['utf8_string'])
178
+ assert_equal(1_329_227_995_784_915_872_903_807_060_280_344_576, record['uint128'])
179
+ reader.close
180
+ end
181
+
182
+ def test_no_ipv4_search_tree
183
+ reader = MaxMind::DB.new(
184
+ 'test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb'
185
+ )
186
+ assert_equal('::0/64', reader.get('1.1.1.1'))
187
+ assert_equal('::0/64', reader.get('192.1.1.1'))
188
+ reader.close
189
+ end
190
+
191
+ def test_ipv6_address_in_ipv4_database
192
+ reader = MaxMind::DB.new(
193
+ 'test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
194
+ )
195
+ e = assert_raises ArgumentError do
196
+ reader.get('2001::')
197
+ end
198
+ assert_equal(
199
+ 'Error looking up 2001::. You attempted to look up an IPv6 address in an IPv4-only database.',
200
+ e.message,
201
+ )
202
+ reader.close
203
+ end
204
+
205
+ def test_bad_ip_parameter
206
+ reader = MaxMind::DB.new('test/data/test-data/GeoIP2-City-Test.mmdb')
207
+ e = assert_raises ArgumentError do
208
+ reader.get(Object.new)
209
+ end
210
+ assert_equal(
211
+ 'address family must be specified', # Not great, but type is ok
212
+ e.message,
213
+ )
214
+ reader.close
215
+ end
216
+
217
+ def test_broken_database
218
+ reader = MaxMind::DB.new(
219
+ 'test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb'
220
+ )
221
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
222
+ reader.get('2001:220::')
223
+ end
224
+ assert_equal(
225
+ 'The MaxMind DB file\'s data section contains bad data (unknown data type or corrupt data)',
226
+ e.message,
227
+ )
228
+ reader.close
229
+ end
230
+
231
+ def test_ip_validation
232
+ reader = MaxMind::DB.new(
233
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
234
+ )
235
+ e = assert_raises ArgumentError do
236
+ reader.get('not_ip')
237
+ end
238
+ assert(e.message.match(/invalid address/))
239
+ reader.close
240
+ end
241
+
242
+ def test_missing_database
243
+ e = assert_raises SystemCallError do
244
+ MaxMind::DB.new('file-does-not-exist.mmdb')
245
+ end
246
+ assert(e.message.match(/No such file or directory/))
247
+ end
248
+
249
+ def test_nondatabase
250
+ e = assert_raises MaxMind::DB::InvalidDatabaseError do
251
+ MaxMind::DB.new('README.md')
252
+ end
253
+ assert_equal(
254
+ 'Metadata section not found. Is this a valid MaxMind DB file?',
255
+ e.message,
256
+ )
257
+ end
258
+
259
+ def test_too_many_constructor_args
260
+ e = assert_raises ArgumentError do
261
+ MaxMind::DB.new('README.md', {}, 'blah')
262
+ end
263
+ assert(e.message.match(/wrong number of arguments/))
264
+ end
265
+
266
+ def test_no_constructor_args
267
+ e = assert_raises ArgumentError do
268
+ MaxMind::DB.new
269
+ end
270
+ assert(e.message.match(/wrong number of arguments/))
271
+ end
272
+
273
+ def test_too_many_get_args
274
+ reader = MaxMind::DB.new(
275
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
276
+ )
277
+ e = assert_raises ArgumentError do
278
+ reader.get('1.1.1.1', 'blah')
279
+ end
280
+ assert(e.message.match(/wrong number of arguments/))
281
+ reader.close
282
+ end
283
+
284
+ def test_no_get_args
285
+ reader = MaxMind::DB.new(
286
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
287
+ )
288
+ e = assert_raises ArgumentError do
289
+ reader.get
290
+ end
291
+ assert(e.message.match(/wrong number of arguments/))
292
+ reader.close
293
+ end
294
+
295
+ def test_metadata_args
296
+ reader = MaxMind::DB.new(
297
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
298
+ )
299
+ e = assert_raises ArgumentError do
300
+ reader.metadata('hi')
301
+ end
302
+ assert(e.message.match(/wrong number of arguments/))
303
+ reader.close
304
+ end
305
+
306
+ def test_metadata_unknown_attribute
307
+ reader = MaxMind::DB.new(
308
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
309
+ )
310
+ e = assert_raises NoMethodError do
311
+ reader.metadata.what
312
+ end
313
+ assert(e.message.match(/undefined method `what'/))
314
+ reader.close
315
+ end
316
+
317
+ def test_close
318
+ reader = MaxMind::DB.new(
319
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
320
+ )
321
+ reader.close
322
+ end
323
+
324
+ def test_double_close
325
+ reader = MaxMind::DB.new(
326
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
327
+ )
328
+ reader.close
329
+ reader.close
330
+ end
331
+
332
+ def test_closed_get
333
+ reader = MaxMind::DB.new(
334
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
335
+ )
336
+ reader.close
337
+ e = assert_raises IOError do
338
+ reader.get('1.1.1.1')
339
+ end
340
+ assert_equal('closed stream', e.message)
341
+ end
342
+
343
+ def test_closed_metadata
344
+ reader = MaxMind::DB.new(
345
+ 'test/data/test-data/MaxMind-DB-test-decoder.mmdb'
346
+ )
347
+ reader.close
348
+ assert_equal(
349
+ { 'en' => 'MaxMind DB Decoder Test database - contains every MaxMind DB data type' },
350
+ reader.metadata.description,
351
+ )
352
+ end
353
+
354
+ def test_threads
355
+ reader = MaxMind::DB.new(
356
+ 'test/data/test-data/GeoIP2-Domain-Test.mmdb'
357
+ )
358
+
359
+ num_threads = 16
360
+ num_lookups = 32
361
+ thread_lookups = []
362
+ num_threads.times do
363
+ thread_lookups << []
364
+ end
365
+
366
+ threads = []
367
+ num_threads.times do |i|
368
+ threads << Thread.new do
369
+ num_lookups.times do |j|
370
+ thread_lookups[i] << reader.get("65.115.240.#{j}")
371
+ thread_lookups[i] << reader.get("2a02:2770:3::#{j}")
372
+ end
373
+ end
374
+ end
375
+
376
+ threads.each(&:join)
377
+
378
+ thread_lookups.each do |a|
379
+ assert_equal(num_lookups * 2, a.length)
380
+ thread_lookups.each do |b|
381
+ assert_equal(a, b)
382
+ end
383
+ end
384
+
385
+ reader.close
386
+ end
387
+
388
+ # In these tests I am trying to exercise Reader#read_node directly. It is not
389
+ # too easy to test its behaviour with real databases, so construct dummy ones
390
+ # directly.
391
+ #
392
+ def test_read_node
393
+ tests = [
394
+ {
395
+ record_size: 24,
396
+ # Left record + right record
397
+ node_bytes: "\xab\xcd\xef".b + "\xbc\xfe\xfa".b,
398
+ left: 11_259_375,
399
+ right: 12_386_042,
400
+ check_left: "\x00\xab\xcd\xef".b.unpack('N').first,
401
+ check_right: "\x00\xbc\xfe\xfa".b.unpack('N').first,
402
+ },
403
+ {
404
+ record_size: 28,
405
+ # Left record (part) + middle byte + right record (part)
406
+ node_bytes: "\xab\xcd\xef".b + "\x12".b + "\xfd\xdc\xfa".b,
407
+ left: 28_036_591,
408
+ right: 50_191_610,
409
+ check_left: "\x01\xab\xcd\xef".b.unpack('N').first,
410
+ check_right: "\x02\xfd\xdc\xfa".b.unpack('N').first,
411
+ },
412
+ {
413
+ record_size: 32,
414
+ # Left record + right record
415
+ node_bytes: "\xab\xcd\xef\x12".b + "\xfd\xdc\xfa\x15".b,
416
+ left: 2_882_400_018,
417
+ right: 4_259_117_589,
418
+ check_left: "\xab\xcd\xef\x12".b.unpack('N').first,
419
+ check_right: "\xfd\xdc\xfa\x15".b.unpack('N').first,
420
+ },
421
+ ]
422
+
423
+ tests.each do |test|
424
+ buf = ''.b
425
+ buf += test[:node_bytes]
426
+
427
+ buf += "\x00".b * 16
428
+
429
+ buf += "\xab\xcd\xefMaxMind.com".b
430
+ buf += MMDBUtil.make_metadata_map(test[:record_size])
431
+
432
+ reader = MaxMind::DB.new(
433
+ buf, mode: MaxMind::DB::MODE_PARAM_IS_BUFFER
434
+ )
435
+
436
+ assert_equal(reader.metadata.record_size, test[:record_size])
437
+
438
+ assert_equal(test[:left], reader.send(:read_node, 0, 0))
439
+ assert_equal(test[:right], reader.send(:read_node, 0, 1))
440
+ assert_equal(test[:left], test[:check_left])
441
+ assert_equal(test[:right], test[:check_right])
442
+ end
443
+ end
444
+
445
+ def check_metadata(reader, ip_version, record_size)
446
+ metadata = reader.metadata
447
+
448
+ assert_equal(2, metadata.binary_format_major_version, 'major_version')
449
+ assert_equal(0, metadata.binary_format_minor_version, 'minor_version')
450
+ assert_operator(metadata.build_epoch, :>, 1_373_571_901, 'build_epoch')
451
+ assert_equal('Test', metadata.database_type, 'database_type')
452
+ assert_equal(
453
+ {
454
+ 'en' => 'Test Database',
455
+ 'zh' => 'Test Database Chinese',
456
+ },
457
+ metadata.description,
458
+ 'description',
459
+ )
460
+ assert_equal(ip_version, metadata.ip_version, 'ip_version')
461
+ assert_equal(%w[en zh], metadata.languages, 'languages')
462
+ assert_operator(metadata.node_count, :>, 36, 'node_count')
463
+ assert_equal(record_size, metadata.record_size, 'record_size')
464
+ end
465
+
466
+ def check_ipv4(reader, filename)
467
+ 6.times do |i|
468
+ address = "1.1.1.#{2**i}"
469
+ assert_equal(
470
+ { 'ip' => address },
471
+ reader.get(address),
472
+ "found expected data record for #{address} in #{filename}",
473
+ )
474
+ end
475
+
476
+ pairs = {
477
+ '1.1.1.3' => '1.1.1.2',
478
+ '1.1.1.5' => '1.1.1.4',
479
+ '1.1.1.7' => '1.1.1.4',
480
+ '1.1.1.9' => '1.1.1.8',
481
+ '1.1.1.15' => '1.1.1.8',
482
+ '1.1.1.17' => '1.1.1.16',
483
+ '1.1.1.31' => '1.1.1.16',
484
+ }
485
+ pairs.each do |key_address, value_address|
486
+ data = { 'ip' => value_address }
487
+ assert_equal(
488
+ data,
489
+ reader.get(key_address),
490
+ "found expected data record for #{key_address} in #{filename}",
491
+ )
492
+ end
493
+
494
+ ['1.1.1.33', '255.254.253.123'].each do |ip|
495
+ assert_nil(
496
+ reader.get(ip),
497
+ "#{ip} is not in #{filename}",
498
+ )
499
+ end
500
+ end
501
+
502
+ def check_ipv6(reader, filename)
503
+ subnets = [
504
+ '::1:ffff:ffff', '::2:0:0', '::2:0:40', '::2:0:50', '::2:0:58',
505
+ ]
506
+ subnets.each do |address|
507
+ assert_equal(
508
+ { 'ip' => address },
509
+ reader.get(address),
510
+ "found expected data record for #{address} in #{filename}",
511
+ )
512
+ end
513
+
514
+ pairs = {
515
+ '::2:0:1' => '::2:0:0',
516
+ '::2:0:33' => '::2:0:0',
517
+ '::2:0:39' => '::2:0:0',
518
+ '::2:0:41' => '::2:0:40',
519
+ '::2:0:49' => '::2:0:40',
520
+ '::2:0:52' => '::2:0:50',
521
+ '::2:0:57' => '::2:0:50',
522
+ '::2:0:59' => '::2:0:58',
523
+ }
524
+ pairs.each do |key_address, value_address|
525
+ assert_equal(
526
+ { 'ip' => value_address },
527
+ reader.get(key_address),
528
+ "found expected data record for #{key_address} in #{filename}",
529
+ )
530
+ end
531
+
532
+ ['1.1.1.33', '255.254.253.123', '89fa::'].each do |ip|
533
+ assert_nil(
534
+ reader.get(ip),
535
+ "#{ip} is not in #{filename}",
536
+ )
537
+ end
538
+ end
539
+ end