431 lines
14 KiB
TypeScript
431 lines
14 KiB
TypeScript
import { Test } from '@nestjs/testing';
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
|
|
import { DeviceEnrichmentService } from './device-enrichment.service';
|
|
import { GovDetectionService } from './gov-detection.service';
|
|
|
|
const mockGovDetection = {
|
|
enrich: vi.fn().mockResolvedValue({
|
|
isGovernment: false,
|
|
orgType: 'NORMAL',
|
|
responseTier: 'ALLOW',
|
|
proxyType: 'NONE',
|
|
org: null,
|
|
asn: null,
|
|
}),
|
|
onModuleInit: vi.fn(),
|
|
};
|
|
|
|
describe('DeviceEnrichmentService', () => {
|
|
let service: DeviceEnrichmentService;
|
|
|
|
beforeEach(async () => {
|
|
const module = await Test.createTestingModule({
|
|
providers: [
|
|
DeviceEnrichmentService,
|
|
{ provide: GovDetectionService, useValue: mockGovDetection },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<DeviceEnrichmentService>(DeviceEnrichmentService);
|
|
});
|
|
|
|
describe('enrich', () => {
|
|
it('enriches device data from User-Agent and client data', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0',
|
|
'192.168.1.1',
|
|
{
|
|
screenWidth: 1920,
|
|
screenHeight: 1080,
|
|
viewportWidth: 1440,
|
|
viewportHeight: 900,
|
|
language: 'en-US',
|
|
timezone: 'America/New_York',
|
|
},
|
|
);
|
|
|
|
expect(result.deviceType).toBe('desktop');
|
|
expect(result.isBot).toBe(false);
|
|
expect(result.browser).toBe('Chrome');
|
|
expect(result.browserVersion).toBe('120.0');
|
|
expect(result.browserMajor).toBe(120);
|
|
expect(result.os).toBe('macOS');
|
|
expect(result.osVersion).toBe('10.15.7');
|
|
expect(result.screenWidth).toBe(1920);
|
|
expect(result.screenHeight).toBe(1080);
|
|
expect(result.viewportWidth).toBe(1440);
|
|
expect(result.viewportHeight).toBe(900);
|
|
expect(result.language).toBe('en-US');
|
|
expect(result.timezone).toBe('America/New_York');
|
|
expect(result.ipHash).toBeTruthy();
|
|
});
|
|
|
|
it('handles missing User-Agent gracefully', async () => {
|
|
const result = await service.enrich(undefined, undefined, {
|
|
screenWidth: 1024,
|
|
screenHeight: 768,
|
|
});
|
|
|
|
expect(result.deviceType).toBe('desktop');
|
|
expect(result.isBot).toBe(false);
|
|
expect(result.browser).toBeNull();
|
|
expect(result.browserVersion).toBeNull();
|
|
expect(result.os).toBeNull();
|
|
expect(result.osVersion).toBeNull();
|
|
expect(result.ipHash).toBeNull();
|
|
});
|
|
|
|
it('hashes IP address for privacy', async () => {
|
|
const result1 = await service.enrich(undefined, '192.168.1.1');
|
|
const result2 = await service.enrich(undefined, '192.168.1.2');
|
|
const result3 = await service.enrich(undefined, '192.168.1.1');
|
|
|
|
expect(result1.ipHash).toBeTruthy();
|
|
expect(result2.ipHash).toBeTruthy();
|
|
expect(result1.ipHash).not.toBe(result2.ipHash);
|
|
expect(result1.ipHash).toBe(result3.ipHash); // Same IP on same day = same hash
|
|
});
|
|
});
|
|
|
|
describe('parseUserAgent', () => {
|
|
it('parses Chrome User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
|
|
);
|
|
|
|
expect(result.browser).toBe('Chrome');
|
|
expect(result.browserVersion).toBe('120.0');
|
|
expect(result.browserMajor).toBe(120);
|
|
expect(result.os).toBe('Windows');
|
|
expect(result.osVersion).toBe('10/11');
|
|
});
|
|
|
|
it('parses Firefox User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
|
|
);
|
|
|
|
expect(result.browser).toBe('Firefox');
|
|
expect(result.browserVersion).toBe('122.0');
|
|
expect(result.browserMajor).toBe(122);
|
|
expect(result.os).toBe('Windows');
|
|
});
|
|
|
|
it('parses Safari User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
|
);
|
|
|
|
expect(result.browser).toBe('Safari');
|
|
expect(result.browserVersion).toBe('17.2');
|
|
expect(result.browserMajor).toBe(17);
|
|
expect(result.os).toBe('macOS');
|
|
expect(result.osVersion).toBe('10.15.7');
|
|
});
|
|
|
|
it('parses Edge User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/120.0.0.0',
|
|
);
|
|
|
|
expect(result.browser).toBe('Edge');
|
|
expect(result.browserVersion).toBe('120.0');
|
|
expect(result.browserMajor).toBe(120);
|
|
});
|
|
|
|
it('parses mobile Safari User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
|
|
);
|
|
|
|
expect(result.browser).toBe('Safari');
|
|
expect(result.os).toBe('iOS');
|
|
expect(result.osVersion).toBe('17.2');
|
|
expect(result.deviceType).toBe('mobile');
|
|
});
|
|
|
|
it('parses Android Chrome User-Agent correctly', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36',
|
|
);
|
|
|
|
expect(result.browser).toBe('Chrome');
|
|
expect(result.os).toBe('Android');
|
|
expect(result.osVersion).toBe('14');
|
|
expect(result.deviceType).toBe('mobile');
|
|
});
|
|
|
|
it('handles unknown User-Agent', async () => {
|
|
const result = await service.enrich(
|
|
'UnknownBot/1.0',
|
|
);
|
|
|
|
expect(result.browser).toBeNull();
|
|
expect(result.browserVersion).toBeNull();
|
|
expect(result.os).toBeNull();
|
|
expect(result.osVersion).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('detectDeviceType', () => {
|
|
it('detects desktop from no touch points', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0',
|
|
undefined,
|
|
{
|
|
touchPoints: 0,
|
|
viewportWidth: 1920,
|
|
},
|
|
);
|
|
|
|
expect(result.deviceType).toBe('desktop');
|
|
});
|
|
|
|
it('detects mobile from touch points and small viewport', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (iPhone) Safari/604.1',
|
|
undefined,
|
|
{
|
|
touchPoints: 5,
|
|
viewportWidth: 375,
|
|
},
|
|
);
|
|
|
|
expect(result.deviceType).toBe('mobile');
|
|
});
|
|
|
|
it('detects tablet from touch points and large viewport', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (iPad) Safari/604.1',
|
|
undefined,
|
|
{
|
|
touchPoints: 5,
|
|
viewportWidth: 1024,
|
|
},
|
|
);
|
|
|
|
expect(result.deviceType).toBe('tablet');
|
|
});
|
|
|
|
it('detects tablet from iPad User-Agent', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15',
|
|
);
|
|
|
|
expect(result.deviceType).toBe('tablet');
|
|
});
|
|
|
|
it('detects mobile from iPhone User-Agent', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) Mobile/15E148',
|
|
);
|
|
|
|
expect(result.deviceType).toBe('mobile');
|
|
});
|
|
|
|
it('detects mobile from Android Mobile User-Agent', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Linux; Android 14) Mobile Safari/537.36',
|
|
);
|
|
|
|
expect(result.deviceType).toBe('mobile');
|
|
});
|
|
|
|
it('detects tablet from Android without Mobile keyword', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Linux; Android 14; Tablet) AppleWebKit/537.36',
|
|
);
|
|
|
|
expect(result.deviceType).toBe('tablet');
|
|
});
|
|
});
|
|
|
|
describe('detectBot', () => {
|
|
it('detects Googlebot', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
|
|
);
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects Bingbot', async () => {
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
|
|
);
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects generic crawlers', async () => {
|
|
const result = await service.enrich('SomeCrawler/1.0');
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects headless browsers', async () => {
|
|
const result = await service.enrich('HeadlessChrome/120.0.0.0');
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects Puppeteer', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 HeadlessChrome/120.0.0.0 Safari/537.36 Puppeteer');
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects Playwright', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36 Playwright');
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects Selenium', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0 Selenium');
|
|
|
|
expect(result.isBot).toBe(true);
|
|
expect(result.deviceType).toBe('bot');
|
|
});
|
|
|
|
it('detects social media bots', async () => {
|
|
const facebookBot = await service.enrich('facebookexternalhit/1.1');
|
|
const twitterBot = await service.enrich('Twitterbot/1.0');
|
|
const linkedinBot = await service.enrich('LinkedInBot/1.0');
|
|
|
|
expect(facebookBot.isBot).toBe(true);
|
|
expect(twitterBot.isBot).toBe(true);
|
|
expect(linkedinBot.isBot).toBe(true);
|
|
});
|
|
|
|
it('does not detect normal browsers as bots', async () => {
|
|
const chrome = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Chrome/120.0.0.0');
|
|
const firefox = await service.enrich('Mozilla/5.0 (Windows NT 10.0) Firefox/122.0');
|
|
const safari = await service.enrich('Mozilla/5.0 (Macintosh) Safari/605.1.15');
|
|
|
|
expect(chrome.isBot).toBe(false);
|
|
expect(firefox.isBot).toBe(false);
|
|
expect(safari.isBot).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('mapWindowsVersion', () => {
|
|
it('maps Windows NT 10.0 to Windows 10/11', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 10.0)');
|
|
expect(result.osVersion).toBe('10/11');
|
|
});
|
|
|
|
it('maps Windows NT 6.3 to Windows 8.1', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 6.3)');
|
|
expect(result.osVersion).toBe('8.1');
|
|
});
|
|
|
|
it('maps Windows NT 6.2 to Windows 8', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 6.2)');
|
|
expect(result.osVersion).toBe('8');
|
|
});
|
|
|
|
it('maps Windows NT 6.1 to Windows 7', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 6.1)');
|
|
expect(result.osVersion).toBe('7');
|
|
});
|
|
|
|
it('handles unknown Windows NT version', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Windows NT 5.0)');
|
|
expect(result.osVersion).toBe('5.0');
|
|
});
|
|
});
|
|
|
|
describe('OS detection', () => {
|
|
it('detects macOS with version', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
|
|
expect(result.os).toBe('macOS');
|
|
expect(result.osVersion).toBe('10.15.7');
|
|
});
|
|
|
|
it('detects Linux', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (X11; Linux x86_64)');
|
|
expect(result.os).toBe('Linux');
|
|
});
|
|
|
|
it('detects iOS with version', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X)');
|
|
expect(result.os).toBe('iOS');
|
|
expect(result.osVersion).toBe('17.2');
|
|
});
|
|
|
|
it('detects Android with version', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Linux; Android 14)');
|
|
expect(result.os).toBe('Android');
|
|
expect(result.osVersion).toBe('14');
|
|
});
|
|
|
|
it('handles Android version with decimals', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Linux; Android 13.5)');
|
|
expect(result.os).toBe('Android');
|
|
expect(result.osVersion).toBe('13.5');
|
|
});
|
|
});
|
|
|
|
describe('client device data preservation', () => {
|
|
it('preserves all client device data', async () => {
|
|
const clientData = {
|
|
screenWidth: 1920,
|
|
screenHeight: 1080,
|
|
viewportWidth: 1440,
|
|
viewportHeight: 900,
|
|
pixelRatio: 2,
|
|
colorDepth: 24,
|
|
language: 'en-US',
|
|
languages: ['en-US', 'en', 'es'],
|
|
timezone: 'America/New_York',
|
|
timezoneOffset: -300,
|
|
deviceMemory: 8,
|
|
hardwareConcurrency: 8,
|
|
touchPoints: 0,
|
|
cookiesEnabled: true,
|
|
doNotTrack: false,
|
|
};
|
|
|
|
const result = await service.enrich(
|
|
'Mozilla/5.0 (Macintosh) Chrome/120.0.0.0',
|
|
undefined,
|
|
clientData,
|
|
);
|
|
|
|
expect(result.screenWidth).toBe(1920);
|
|
expect(result.screenHeight).toBe(1080);
|
|
expect(result.viewportWidth).toBe(1440);
|
|
expect(result.viewportHeight).toBe(900);
|
|
expect(result.pixelRatio).toBe(2);
|
|
expect(result.colorDepth).toBe(24);
|
|
expect(result.language).toBe('en-US');
|
|
expect(result.languages).toEqual(['en-US', 'en', 'es']);
|
|
expect(result.timezone).toBe('America/New_York');
|
|
expect(result.timezoneOffset).toBe(-300);
|
|
expect(result.deviceMemory).toBe(8);
|
|
expect(result.hardwareConcurrency).toBe(8);
|
|
expect(result.touchPoints).toBe(0);
|
|
expect(result.cookiesEnabled).toBe(true);
|
|
expect(result.doNotTrack).toBe(false);
|
|
});
|
|
|
|
it('handles missing client device data', async () => {
|
|
const result = await service.enrich('Mozilla/5.0 (Macintosh) Chrome/120.0.0.0');
|
|
|
|
expect(result.screenWidth).toBeUndefined();
|
|
expect(result.screenHeight).toBeUndefined();
|
|
expect(result.viewportWidth).toBeUndefined();
|
|
expect(result.viewportHeight).toBeUndefined();
|
|
expect(result.pixelRatio).toBeUndefined();
|
|
expect(result.colorDepth).toBeUndefined();
|
|
});
|
|
});
|
|
});
|