Skip to content

Commit e69c305

Browse files
committed
wip: fix a few bugs and add some tests
1 parent a9f81ee commit e69c305

11 files changed

+483
-7
lines changed

packages/util/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
],
1818
"exports": {
1919
".": "./dist/index.js",
20-
"./browser": "./dist/browser.js"
20+
"./browser": "./dist/browser.js",
21+
"./db": "./dist/db.js"
2122
},
2223
"main": "./dist/index.js",
2324
"types": "./dist/index.d.ts",
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { binarySearch } from './binarySearch'
3+
4+
describe('binarySearch', () => {
5+
describe('uint32Array tests', () => {
6+
const uint32Array = new Uint32Array([1, 3, 5, 7, 9, 11, 13, 15])
7+
8+
it('should find existing elements', () => {
9+
expect(binarySearch(uint32Array, 1)).toBe(0)
10+
expect(binarySearch(uint32Array, 7)).toBe(3)
11+
expect(binarySearch(uint32Array, 15)).toBe(7)
12+
})
13+
14+
it('should return insertion point for non-existing elements', () => {
15+
expect(binarySearch(uint32Array, 4)).toBe(1)
16+
expect(binarySearch(uint32Array, 10)).toBe(4)
17+
expect(binarySearch(uint32Array, 16)).toBe(7)
18+
})
19+
20+
it('should return null for elements smaller than all in the array', () => {
21+
expect(binarySearch(uint32Array, 0)).toBe(null)
22+
})
23+
24+
it('should work with an array of length 1', () => {
25+
const singleElementArray = new Uint32Array([5])
26+
expect(binarySearch(singleElementArray, 5)).toBe(0)
27+
expect(binarySearch(singleElementArray, 3)).toBe(null)
28+
expect(binarySearch(singleElementArray, 7)).toBe(0)
29+
})
30+
31+
it('should work with an empty array', () => {
32+
const emptyArray = new Uint32Array([])
33+
expect(binarySearch(emptyArray, 5)).toBe(null)
34+
})
35+
})
36+
37+
describe('bigUint64Array tests', () => {
38+
const bigUint64Array = new BigUint64Array([1n, 3n, 5n, 7n, 9n, 11n, 13n, 15n])
39+
40+
it('should find existing elements', () => {
41+
expect(binarySearch(bigUint64Array, 1n)).toBe(0)
42+
expect(binarySearch(bigUint64Array, 7n)).toBe(3)
43+
expect(binarySearch(bigUint64Array, 15n)).toBe(7)
44+
})
45+
46+
it('should return insertion point for non-existing elements', () => {
47+
expect(binarySearch(bigUint64Array, 4n)).toBe(1)
48+
expect(binarySearch(bigUint64Array, 10n)).toBe(4)
49+
expect(binarySearch(bigUint64Array, 16n)).toBe(7)
50+
})
51+
52+
it('should return null for elements smaller than all in the array', () => {
53+
expect(binarySearch(bigUint64Array, 0n)).toBe(null)
54+
})
55+
56+
it('should work with an array of length 1', () => {
57+
const singleElementArray = new BigUint64Array([5n])
58+
expect(binarySearch(singleElementArray, 5n)).toBe(0)
59+
expect(binarySearch(singleElementArray, 3n)).toBe(null)
60+
expect(binarySearch(singleElementArray, 7n)).toBe(0)
61+
})
62+
63+
it('should work with an empty array', () => {
64+
const emptyArray = new BigUint64Array([])
65+
expect(binarySearch(emptyArray, 5n)).toBe(null)
66+
})
67+
})
68+
69+
describe('edge cases', () => {
70+
it('should handle the maximum possible Uint32 value', () => {
71+
const maxUint32Array = new Uint32Array([4294967295])
72+
expect(binarySearch(maxUint32Array, 4294967295)).toBe(0)
73+
})
74+
75+
it('should handle the maximum possible BigUint64 value', () => {
76+
const maxBigUint64Array = new BigUint64Array([18446744073709551615n])
77+
expect(binarySearch(maxBigUint64Array, 18446744073709551615n)).toBe(0)
78+
})
79+
})
80+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { countryCodeToNumber } from './countryCodeToNumber'
3+
4+
describe('countryCodeToNumber', () => {
5+
it('should convert "AA" to 0', () => {
6+
expect(countryCodeToNumber('AA')).toBe(0)
7+
})
8+
9+
it('should convert "ZZ" to 675', () => {
10+
expect(countryCodeToNumber('ZZ')).toBe(675)
11+
})
12+
13+
it('should convert "CA" to 52', () => {
14+
expect(countryCodeToNumber('CA')).toBe(52)
15+
})
16+
17+
it('should convert "US" to 538', () => {
18+
expect(countryCodeToNumber('US')).toBe(538)
19+
})
20+
21+
it('should throw an error for lowercase input', () => {
22+
expect(() => countryCodeToNumber('aa')).toThrow('Input must be a valid two-letter country code (A-Z)')
23+
})
24+
25+
it('should throw an error for input with non-alphabetic characters', () => {
26+
expect(() => countryCodeToNumber('A1')).toThrow('Input must be a valid two-letter country code (A-Z)')
27+
})
28+
29+
it('should throw an error for input with more than two characters', () => {
30+
expect(() => countryCodeToNumber('USA')).toThrow('Input must be a valid two-letter country code (A-Z)')
31+
})
32+
33+
it('should throw an error for input with less than two characters', () => {
34+
expect(() => countryCodeToNumber('A')).toThrow('Input must be a valid two-letter country code (A-Z)')
35+
})
36+
37+
it('should throw an error for empty input', () => {
38+
expect(() => countryCodeToNumber('')).toThrow('Input must be a valid two-letter country code (A-Z)')
39+
})
40+
})
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import type { IpLocationApiInputSettings } from './getSettings'
2+
import { join } from 'node:path'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
import { DEFAULT_SETTINGS, LOCATION_FIELDS, MAIN_FIELDS } from '../constants'
5+
import { getSettings } from './getSettings'
6+
7+
describe('getSettings', () => {
8+
const originalEnv = process.env
9+
const originalArgv = process.argv
10+
11+
beforeEach(() => {
12+
vi.resetModules()
13+
process.env = { ...originalEnv }
14+
process.argv = [...originalArgv]
15+
})
16+
17+
afterEach(() => {
18+
process.env = originalEnv
19+
process.argv = originalArgv
20+
})
21+
22+
it('should return default settings when no input is provided', () => {
23+
const settings = getSettings()
24+
expect(settings).toMatchObject({
25+
licenseKey: DEFAULT_SETTINGS.licenseKey,
26+
series: DEFAULT_SETTINGS.series,
27+
fields: DEFAULT_SETTINGS.fields,
28+
language: DEFAULT_SETTINGS.language,
29+
smallMemory: DEFAULT_SETTINGS.smallMemory,
30+
smallMemoryFileSize: DEFAULT_SETTINGS.smallMemoryFileSize,
31+
})
32+
})
33+
34+
it('should override default settings with input settings', () => {
35+
const inputSettings: IpLocationApiInputSettings = {
36+
licenseKey: 'test-key',
37+
series: 'GeoIP2',
38+
fields: ['country', 'city'],
39+
language: 'es',
40+
smallMemory: true,
41+
smallMemoryFileSize: 8192,
42+
}
43+
const settings = getSettings(inputSettings)
44+
expect(settings).toMatchObject({
45+
licenseKey: 'test-key',
46+
series: 'GeoIP2',
47+
fields: ['country', 'city'],
48+
language: 'es',
49+
smallMemory: true,
50+
smallMemoryFileSize: 8192,
51+
})
52+
})
53+
54+
it('should use environment variables to override default settings', () => {
55+
process.env.ILA_LICENSE_KEY = 'env-key'
56+
process.env.ILA_SERIES = 'GeoIP2'
57+
process.env.ILA_FIELDS = 'country,city'
58+
process.env.ILA_LANGUAGE = 'fr'
59+
process.env.ILA_SMALL_MEMORY = 'true'
60+
process.env.ILA_SMALL_MEMORY_FILE_SIZE = '16384'
61+
62+
const settings = getSettings()
63+
expect(settings).toMatchObject({
64+
licenseKey: 'env-key',
65+
series: 'GeoIP2',
66+
fields: ['country', 'city'],
67+
language: 'fr',
68+
smallMemory: true,
69+
smallMemoryFileSize: 16384,
70+
})
71+
})
72+
73+
it('should use CLI arguments to override default and environment settings', () => {
74+
process.env.ILA_LICENSE_KEY = 'env-key'
75+
process.argv = [
76+
...process.argv,
77+
'ILA_LICENSE_KEY=cli-key',
78+
'ILA_SERIES=GeoIP2',
79+
'ILA_FIELDS=country,region1',
80+
'ILA_LANGUAGE=de',
81+
'ILA_SMALL_MEMORY=true',
82+
'ILA_SMALL_MEMORY_FILE_SIZE=32768',
83+
]
84+
85+
const settings = getSettings()
86+
expect(settings).toMatchObject({
87+
licenseKey: 'cli-key',
88+
series: 'GeoIP2',
89+
fields: ['country', 'region1'],
90+
language: 'de',
91+
smallMemory: true,
92+
smallMemoryFileSize: 32768,
93+
})
94+
})
95+
96+
it('should process "all" fields correctly', () => {
97+
const inputSettings: IpLocationApiInputSettings = {
98+
fields: 'all',
99+
}
100+
const settings = getSettings(inputSettings)
101+
expect(settings.fields).toEqual([...MAIN_FIELDS, ...LOCATION_FIELDS])
102+
103+
process.argv = [...process.argv, 'ILA_FIELDS=all']
104+
const settings2 = getSettings()
105+
expect(settings2.fields).toEqual([...MAIN_FIELDS, ...LOCATION_FIELDS])
106+
})
107+
108+
it('should filter out invalid fields', () => {
109+
const inputSettings: IpLocationApiInputSettings = {
110+
fields: ['country', 'invalid_field', 'city'] as any,
111+
}
112+
const settings = getSettings(inputSettings)
113+
expect(settings.fields).toEqual(['country', 'city'])
114+
})
115+
116+
it('should handle invalid fields input', () => {
117+
const inputSettings: IpLocationApiInputSettings = {
118+
fields: 0 as any,
119+
}
120+
const settings = getSettings(inputSettings)
121+
expect(settings.fields).toEqual(DEFAULT_SETTINGS.fields)
122+
})
123+
124+
it('should use default fields if all provided fields are invalid', () => {
125+
const inputSettings: IpLocationApiInputSettings = {
126+
fields: ['invalid_field1', 'invalid_field2'] as any,
127+
}
128+
const settings = getSettings(inputSettings)
129+
expect(settings.fields).toEqual(DEFAULT_SETTINGS.fields)
130+
})
131+
132+
it('should process directory paths correctly', () => {
133+
const inputSettings: IpLocationApiInputSettings = {
134+
dataDir: '../custom-data',
135+
tmpDataDir: '/tmp/custom-tmp',
136+
}
137+
const settings = getSettings(inputSettings)
138+
expect(settings.dataDir).toMatch(/custom-data$/)
139+
expect(settings.tmpDataDir).toBe('/tmp/custom-tmp')
140+
})
141+
142+
it('should calculate correct record sizes and database settings', () => {
143+
const inputSettings: IpLocationApiInputSettings = {
144+
fields: ['country', 'city', 'latitude', 'longitude'],
145+
smallMemory: true,
146+
smallMemoryFileSize: 4096,
147+
}
148+
const settings = getSettings(inputSettings)
149+
expect(settings.dataType).toBe('City')
150+
expect(settings.locationFile).toBe(true)
151+
expect(settings.mainRecordSize).toBeGreaterThan(0)
152+
expect(settings.locationRecordSize).toBeGreaterThan(0)
153+
expect(settings.v4.recordSize).toBeGreaterThan(settings.mainRecordSize)
154+
expect(settings.v6.recordSize).toBeGreaterThan(settings.mainRecordSize)
155+
expect(settings.v4.fileLineMax).toBeGreaterThan(0)
156+
expect(settings.v6.fileLineMax).toBeGreaterThan(0)
157+
158+
const settings2 = getSettings({
159+
fields: ['country', 'city', 'latitude', 'longitude'],
160+
smallMemory: true,
161+
smallMemoryFileSize: 0,
162+
})
163+
expect(settings2.dataType).toBe('City')
164+
expect(settings2.locationFile).toBe(true)
165+
expect(settings2.mainRecordSize).toBeGreaterThan(0)
166+
expect(settings2.locationRecordSize).toBeGreaterThan(0)
167+
expect(settings2.v4.recordSize).toBeGreaterThan(settings2.mainRecordSize)
168+
expect(settings2.v6.recordSize).toBeGreaterThan(settings2.mainRecordSize)
169+
expect(settings2.v4.fileLineMax).toBe(1)
170+
expect(settings2.v6.fileLineMax).toBe(1)
171+
})
172+
173+
it('should generate correct fieldDir', () => {
174+
const inputSettings: IpLocationApiInputSettings = {
175+
fields: ['country', 'city'],
176+
dataDir: '/custom/data/dir',
177+
}
178+
const settings = getSettings(inputSettings)
179+
const expectedFieldDir = join('/custom/data/dir', (16 + 2048).toString(36))
180+
expect(settings.fieldDir).toBe(expectedFieldDir)
181+
})
182+
183+
it('should handle invalid language input', () => {
184+
const inputSettings: IpLocationApiInputSettings = {
185+
language: 'invalid_language' as any,
186+
}
187+
const settings = getSettings(inputSettings)
188+
expect(settings.language).toBe(DEFAULT_SETTINGS.language)
189+
})
190+
191+
it('should handle invalid series input', () => {
192+
const inputSettings: IpLocationApiInputSettings = {
193+
series: 'InvalidSeries' as any,
194+
}
195+
const settings = getSettings(inputSettings)
196+
expect(settings.series).toBe(DEFAULT_SETTINGS.series)
197+
})
198+
})

packages/util/src/functions/getSettings.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export function getSettings(settings?: IpLocationApiInputSettings): IpLocationAp
133133
...DEFAULT_SETTINGS,
134134
...envSettings,
135135
...cliSettings,
136-
...settings,
136+
...(settings ?? {}),
137137
}
138138

139139
//* Process and validate individual settings
@@ -187,16 +187,21 @@ export function getSettings(settings?: IpLocationApiInputSettings): IpLocationAp
187187
* @returns Partial IpLocationApiInputSettings
188188
*/
189189
function getFromIlaObject(ilaObject: Record<string, string | undefined>): Partial<IpLocationApiInputSettings> {
190-
return {
190+
const settings = {
191191
licenseKey: ilaObject[getKey('licenseKey')],
192192
series: ilaObject[getKey('series')] as 'GeoLite2' | 'GeoIP2',
193193
dataDir: ilaObject[getKey('dataDir')],
194194
tmpDataDir: ilaObject[getKey('tmpDataDir')],
195195
fields: ilaObject[getKey('fields')] === 'all' ? 'all' : ilaObject[getKey('fields')]?.split(',') as IpLocationApiInputSettings['fields'],
196196
language: ilaObject[getKey('language')] as IpLocationApiInputSettings['language'],
197-
smallMemory: ilaObject[getKey('smallMemory')] === 'true',
198-
smallMemoryFileSize: ilaObject[getKey('smallMemoryFileSize')] ? Number.parseInt(ilaObject[getKey('smallMemoryFileSize')]!) : DEFAULT_SETTINGS.smallMemoryFileSize,
197+
smallMemory: ilaObject[getKey('smallMemory')] ? ilaObject[getKey('smallMemory')] === 'true' : undefined,
198+
smallMemoryFileSize: ilaObject[getKey('smallMemoryFileSize')] ? Number.parseInt(ilaObject[getKey('smallMemoryFileSize')]!) : undefined,
199199
}
200+
201+
//* Remove keys with undefined values
202+
return Object.fromEntries(
203+
Object.entries(settings).filter(([_, value]) => value !== undefined),
204+
) as Partial<IpLocationApiInputSettings>
200205
}
201206

202207
/**
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getUnderscoreFill } from './getUnderscoreFill'
3+
4+
describe('getUnderscoreFill', () => {
5+
it('should pad the string with underscores to the left', () => {
6+
expect(getUnderscoreFill('test', 8)).toBe('____test')
7+
})
8+
9+
it('should return the original string if it is already longer than the specified length', () => {
10+
expect(getUnderscoreFill('longstring', 5)).toBe('longstring')
11+
})
12+
13+
it('should return the original string if it is equal to the specified length', () => {
14+
expect(getUnderscoreFill('equal', 5)).toBe('equal')
15+
})
16+
17+
it('should handle empty string input', () => {
18+
expect(getUnderscoreFill('', 3)).toBe('___')
19+
})
20+
21+
it('should handle length of 0', () => {
22+
expect(getUnderscoreFill('test', 0)).toBe('test')
23+
})
24+
25+
it('should handle very long padding', () => {
26+
expect(getUnderscoreFill('x', 100)).toBe(`${'_'.repeat(99)}x`)
27+
})
28+
})

0 commit comments

Comments
 (0)