Skip to content

Commit c18b3b6

Browse files
committed
Improve test coverage
1 parent 3e1f43d commit c18b3b6

3 files changed

Lines changed: 423 additions & 2 deletions

File tree

test/aggregate.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,92 @@ describe('aggregate', () => {
307307
expect(docs[0].topPaths[0].path).toBe('/about');
308308
});
309309

310+
it('sorts referrers by count descending', () => {
311+
const entries: ClassifiedEntry[] = [
312+
makeClassified({ referrer: 'https://low.com' }, { category: 'human' }),
313+
makeClassified({ referrer: 'https://high.com' }, { category: 'human' }),
314+
makeClassified({ referrer: 'https://high.com' }, { category: 'human' }),
315+
makeClassified({ referrer: 'https://high.com' }, { category: 'human' }),
316+
makeClassified({ referrer: 'https://mid.com' }, { category: 'human' }),
317+
makeClassified({ referrer: 'https://mid.com' }, { category: 'human' }),
318+
];
319+
const docs = aggregate(entries, { domain: 'example.com' });
320+
expect(docs[0].topReferrers[0].referrer).toBe('https://high.com');
321+
expect(docs[0].topReferrers[0].count).toBe(3);
322+
expect(docs[0].topReferrers[1].referrer).toBe('https://mid.com');
323+
expect(docs[0].topReferrers[2].referrer).toBe('https://low.com');
324+
});
325+
326+
it('sorts AI bot paths by count descending', () => {
327+
const entries: ClassifiedEntry[] = [
328+
makeClassified(
329+
{ path: '/low/' },
330+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
331+
),
332+
makeClassified(
333+
{ path: '/high/' },
334+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
335+
),
336+
makeClassified(
337+
{ path: '/high/' },
338+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
339+
),
340+
makeClassified(
341+
{ path: '/high/' },
342+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
343+
),
344+
makeClassified(
345+
{ path: '/mid/' },
346+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
347+
),
348+
makeClassified(
349+
{ path: '/mid/' },
350+
{ category: 'ai-crawler', botName: 'GPTBot', botCompany: 'OpenAI' },
351+
),
352+
];
353+
const docs = aggregate(entries, { domain: 'example.com' });
354+
const gpt = docs[0].aiBots.find((b) => b.name === 'GPTBot')!;
355+
expect(gpt.topPaths[0].path).toBe('/high/');
356+
expect(gpt.topPaths[0].count).toBe(3);
357+
expect(gpt.topPaths[1].path).toBe('/mid/');
358+
expect(gpt.topPaths[2].path).toBe('/low/');
359+
});
360+
361+
it('sorts agent paths by count descending', () => {
362+
const entries: ClassifiedEntry[] = [
363+
makeClassified(
364+
{ path: '/low/' },
365+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
366+
),
367+
makeClassified(
368+
{ path: '/high/' },
369+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
370+
),
371+
makeClassified(
372+
{ path: '/high/' },
373+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
374+
),
375+
makeClassified(
376+
{ path: '/high/' },
377+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
378+
),
379+
makeClassified(
380+
{ path: '/mid/' },
381+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
382+
),
383+
makeClassified(
384+
{ path: '/mid/' },
385+
{ category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' },
386+
),
387+
];
388+
const docs = aggregate(entries, { domain: 'example.com' });
389+
const claude = docs[0].agents.find((a) => a.name === 'Claude Code')!;
390+
expect(claude.topPaths[0].path).toBe('/high/');
391+
expect(claude.topPaths[0].count).toBe(3);
392+
expect(claude.topPaths[1].path).toBe('/mid/');
393+
expect(claude.topPaths[2].path).toBe('/low/');
394+
});
395+
310396
it('pre-filters signal entries by domain', () => {
311397
const entries: ClassifiedEntry[] = [
312398
makeClassified({}, { category: 'agent', botName: 'Claude Code', botCompany: 'Anthropic' }),

test/ip-ranges.test.ts

Lines changed: 270 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { describe, it, expect } from 'vitest';
2-
import { parseIpv4, parseCidr, matchesCidr, buildCidrIndex } from '../src/adapters/ip-ranges.js';
1+
import { describe, it, expect, vi } from 'vitest';
2+
import {
3+
parseIpv4,
4+
parseCidr,
5+
matchesCidr,
6+
buildCidrIndex,
7+
createCloudProviderLookup,
8+
createCountryLookup,
9+
createIpLookup,
10+
} from '../src/adapters/ip-ranges.js';
311

412
describe('parseIpv4', () => {
513
it('parses a valid IPv4 address', () => {
@@ -122,3 +130,263 @@ describe('buildCidrIndex', () => {
122130
expect(lookup('10.1.0.5')).toBe('wide');
123131
});
124132
});
133+
134+
describe('createCloudProviderLookup', () => {
135+
it('builds index from custom providers', async () => {
136+
const lookup = await createCloudProviderLookup({
137+
providers: [
138+
{
139+
name: 'test-cloud',
140+
fetch: async () => [
141+
{ cidr: '10.0.0.0/8', tag: 'test-cloud' },
142+
{ cidr: '172.16.0.0/12', tag: 'test-cloud' },
143+
],
144+
},
145+
],
146+
});
147+
expect(lookup('10.1.2.3')).toBe('test-cloud');
148+
expect(lookup('172.16.5.1')).toBe('test-cloud');
149+
expect(lookup('8.8.8.8')).toBeUndefined();
150+
});
151+
152+
it('combines ranges from multiple providers', async () => {
153+
const lookup = await createCloudProviderLookup({
154+
providers: [
155+
{ name: 'alpha', fetch: async () => [{ cidr: '10.0.0.0/8', tag: 'alpha' }] },
156+
{ name: 'beta', fetch: async () => [{ cidr: '192.168.0.0/16', tag: 'beta' }] },
157+
],
158+
});
159+
expect(lookup('10.1.2.3')).toBe('alpha');
160+
expect(lookup('192.168.1.1')).toBe('beta');
161+
});
162+
163+
it('gracefully handles provider fetch failures', async () => {
164+
const lookup = await createCloudProviderLookup({
165+
providers: [
166+
{
167+
name: 'failing',
168+
fetch: async () => {
169+
throw new Error('network error');
170+
},
171+
},
172+
{ name: 'working', fetch: async () => [{ cidr: '10.0.0.0/8', tag: 'working' }] },
173+
],
174+
});
175+
// The working provider's ranges should still be available
176+
expect(lookup('10.1.2.3')).toBe('working');
177+
});
178+
179+
it('returns empty lookup when all providers fail', async () => {
180+
const lookup = await createCloudProviderLookup({
181+
providers: [
182+
{
183+
name: 'broken',
184+
fetch: async () => {
185+
throw new Error('fail');
186+
},
187+
},
188+
],
189+
});
190+
expect(lookup('10.1.2.3')).toBeUndefined();
191+
});
192+
});
193+
194+
describe('createCountryLookup', () => {
195+
it('parses RIR delegation data for requested countries', async () => {
196+
// Simulate RIR delegation format: registry|CC|type|start|value|date|status
197+
const rirData = [
198+
'# Comment line',
199+
'apnic|CN|ipv4|1.0.0.0|256|20100101|allocated',
200+
'apnic|JP|ipv4|1.0.16.0|4096|20100101|allocated',
201+
'apnic|CN|ipv6|2001:200::|35|20100101|allocated',
202+
'apnic|CN|ipv4|223.255.254.0|512|20100101|allocated',
203+
].join('\n');
204+
205+
const mockFetch = vi.fn().mockResolvedValue({
206+
ok: true,
207+
text: async () => rirData,
208+
});
209+
vi.stubGlobal('fetch', mockFetch);
210+
211+
try {
212+
const lookup = await createCountryLookup(['CN'], { rirUrls: ['https://mock-rir/'] });
213+
// 1.0.0.0/256 = /24 (power of 2)
214+
expect(lookup('1.0.0.1')).toBe('CN');
215+
// JP entries should be excluded (only CN requested)
216+
expect(lookup('1.0.16.1')).toBeUndefined();
217+
// IPv6 lines should be skipped
218+
} finally {
219+
vi.unstubAllGlobals();
220+
}
221+
});
222+
223+
it('handles non-power-of-2 allocations by splitting into CIDR blocks', async () => {
224+
// 768 addresses = 512 + 256 (not a power of 2)
225+
const rirData = 'apnic|CN|ipv4|10.0.0.0|768|20100101|allocated\n';
226+
227+
const mockFetch = vi.fn().mockResolvedValue({
228+
ok: true,
229+
text: async () => rirData,
230+
});
231+
vi.stubGlobal('fetch', mockFetch);
232+
233+
try {
234+
const lookup = await createCountryLookup(['CN'], { rirUrls: ['https://mock-rir/'] });
235+
// 768 = 512 (/23) + 256 (/24)
236+
// First block: 10.0.0.0/23 covers 10.0.0.0 - 10.0.1.255
237+
expect(lookup('10.0.0.1')).toBe('CN');
238+
expect(lookup('10.0.1.255')).toBe('CN');
239+
// Second block: 10.0.2.0/24 covers 10.0.2.0 - 10.0.2.255
240+
expect(lookup('10.0.2.1')).toBe('CN');
241+
// Outside the allocation
242+
expect(lookup('10.0.3.1')).toBeUndefined();
243+
} finally {
244+
vi.unstubAllGlobals();
245+
}
246+
});
247+
248+
it('handles failed RIR fetch gracefully', async () => {
249+
const mockFetch = vi.fn().mockResolvedValue({ ok: false });
250+
vi.stubGlobal('fetch', mockFetch);
251+
252+
try {
253+
const lookup = await createCountryLookup(['CN'], { rirUrls: ['https://mock-rir/'] });
254+
expect(lookup('1.0.0.1')).toBeUndefined();
255+
} finally {
256+
vi.unstubAllGlobals();
257+
}
258+
});
259+
260+
it('handles RIR network errors gracefully', async () => {
261+
const mockFetch = vi.fn().mockRejectedValue(new Error('network error'));
262+
vi.stubGlobal('fetch', mockFetch);
263+
264+
try {
265+
const lookup = await createCountryLookup(['CN'], { rirUrls: ['https://mock-rir/'] });
266+
expect(lookup('1.0.0.1')).toBeUndefined();
267+
} finally {
268+
vi.unstubAllGlobals();
269+
}
270+
});
271+
272+
it('normalizes country codes to uppercase', async () => {
273+
const rirData = 'apnic|CN|ipv4|10.0.0.0|256|20100101|allocated\n';
274+
const mockFetch = vi.fn().mockResolvedValue({
275+
ok: true,
276+
text: async () => rirData,
277+
});
278+
vi.stubGlobal('fetch', mockFetch);
279+
280+
try {
281+
// Lowercase input should still match
282+
const lookup = await createCountryLookup(['cn'], { rirUrls: ['https://mock-rir/'] });
283+
expect(lookup('10.0.0.1')).toBe('CN');
284+
} finally {
285+
vi.unstubAllGlobals();
286+
}
287+
});
288+
289+
it('skips malformed RIR lines', async () => {
290+
const rirData = [
291+
'too|few|fields',
292+
'|CN|ipv4|10.0.0.0|256|20100101|allocated',
293+
'apnic|CN|ipv4|invalid-ip|256|20100101|allocated',
294+
'apnic|CN|ipv4|10.0.0.0|notanumber|20100101|allocated',
295+
'apnic|CN|ipv4|10.1.0.0|256|20100101|allocated',
296+
].join('\n');
297+
298+
const mockFetch = vi.fn().mockResolvedValue({
299+
ok: true,
300+
text: async () => rirData,
301+
});
302+
vi.stubGlobal('fetch', mockFetch);
303+
304+
try {
305+
const lookup = await createCountryLookup(['CN'], { rirUrls: ['https://mock-rir/'] });
306+
// Only the last valid line should produce a match
307+
expect(lookup('10.1.0.1')).toBe('CN');
308+
} finally {
309+
vi.unstubAllGlobals();
310+
}
311+
});
312+
});
313+
314+
describe('createIpLookup', () => {
315+
it('combines cloud and country lookups', async () => {
316+
const mockFetch = vi.fn().mockResolvedValue({
317+
ok: true,
318+
text: async () => 'apnic|CN|ipv4|10.0.0.0|256|20100101|allocated\n',
319+
});
320+
vi.stubGlobal('fetch', mockFetch);
321+
322+
try {
323+
const lookup = await createIpLookup({
324+
cloudProviders: {
325+
providers: [
326+
{ name: 'test', fetch: async () => [{ cidr: '192.168.0.0/16', tag: 'test-cloud' }] },
327+
],
328+
},
329+
countries: ['CN'],
330+
countryOptions: { rirUrls: ['https://mock-rir/'] },
331+
});
332+
333+
const cloudResult = lookup('192.168.1.1');
334+
expect(cloudResult.cloudProvider).toBe('test-cloud');
335+
336+
const countryResult = lookup('10.0.0.1');
337+
expect(countryResult.country).toBe('CN');
338+
339+
const noMatch = lookup('8.8.8.8');
340+
expect(noMatch.cloudProvider).toBeUndefined();
341+
expect(noMatch.country).toBeUndefined();
342+
} finally {
343+
vi.unstubAllGlobals();
344+
}
345+
});
346+
347+
it('skips cloud lookup when cloudProviders is false', async () => {
348+
const mockFetch = vi.fn().mockResolvedValue({
349+
ok: true,
350+
text: async () => 'apnic|CN|ipv4|10.0.0.0|256|20100101|allocated\n',
351+
});
352+
vi.stubGlobal('fetch', mockFetch);
353+
354+
try {
355+
const lookup = await createIpLookup({
356+
cloudProviders: false,
357+
countries: ['CN'],
358+
countryOptions: { rirUrls: ['https://mock-rir/'] },
359+
});
360+
361+
const result = lookup('10.0.0.1');
362+
expect(result.country).toBe('CN');
363+
expect(result.cloudProvider).toBeUndefined();
364+
} finally {
365+
vi.unstubAllGlobals();
366+
}
367+
});
368+
369+
it('skips country lookup when no countries specified', async () => {
370+
const lookup = await createIpLookup({
371+
cloudProviders: {
372+
providers: [{ name: 'test', fetch: async () => [{ cidr: '10.0.0.0/8', tag: 'test' }] }],
373+
},
374+
countries: [],
375+
});
376+
377+
const result = lookup('10.1.2.3');
378+
expect(result.cloudProvider).toBe('test');
379+
expect(result.country).toBeUndefined();
380+
});
381+
382+
it('returns empty info when no options match', async () => {
383+
const lookup = await createIpLookup({
384+
cloudProviders: {
385+
providers: [{ name: 'test', fetch: async () => [] }],
386+
},
387+
});
388+
389+
const result = lookup('8.8.8.8');
390+
expect(result).toEqual({});
391+
});
392+
});

0 commit comments

Comments
 (0)