390 lines
14 KiB
TypeScript
390 lines
14 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
getEnvInfo,
|
|
getContexts,
|
|
getContextId,
|
|
getPageUrl,
|
|
getReferrer,
|
|
sanitizeUrl,
|
|
shouldIgnoreUrl,
|
|
} from '../src/utils/env';
|
|
|
|
describe('env utils', () => {
|
|
const originalNavigator = global.navigator;
|
|
const originalUserAgent = navigator.userAgent;
|
|
|
|
function setUserAgent(ua: string) {
|
|
Object.defineProperty(global.navigator, 'userAgent', {
|
|
value: ua,
|
|
configurable: true,
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
describe('getEnvInfo', () => {
|
|
it('should return env info object', () => {
|
|
const env = getEnvInfo();
|
|
expect(env).toBeDefined();
|
|
expect(env).toHaveProperty('browser');
|
|
expect(env).toHaveProperty('os');
|
|
expect(env).toHaveProperty('device');
|
|
});
|
|
|
|
it('should have valid browser info', () => {
|
|
const env = getEnvInfo();
|
|
expect(env.browser.name).toBeDefined();
|
|
expect(env.browser.version).toBeDefined();
|
|
expect(env.browser.major).toBeDefined();
|
|
});
|
|
|
|
it('should have valid os info', () => {
|
|
const env = getEnvInfo();
|
|
expect(env.os.name).toBeDefined();
|
|
expect(env.os.version).toBeDefined();
|
|
});
|
|
|
|
it('should have valid device info', () => {
|
|
const env = getEnvInfo();
|
|
expect(['desktop', 'mobile', 'tablet']).toContain(env.device.type);
|
|
});
|
|
});
|
|
|
|
describe('browser detection', () => {
|
|
const testCases = [
|
|
{
|
|
name: 'Chrome on Windows',
|
|
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
browser: 'Chrome',
|
|
os: 'Windows 11',
|
|
device: 'desktop',
|
|
},
|
|
{
|
|
name: 'Firefox on macOS',
|
|
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
browser: 'Firefox',
|
|
os: 'macOS',
|
|
device: 'desktop',
|
|
},
|
|
{
|
|
name: 'Safari on macOS',
|
|
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
|
|
browser: 'Safari',
|
|
os: 'macOS',
|
|
device: 'desktop',
|
|
},
|
|
{
|
|
name: 'Edge on Windows',
|
|
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
|
|
browser: 'Edge',
|
|
os: 'Windows 11',
|
|
device: 'desktop',
|
|
},
|
|
{
|
|
name: 'WeChat',
|
|
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.33(0x1800212d)',
|
|
browser: 'WeChat',
|
|
os: 'iOS',
|
|
device: 'mobile',
|
|
},
|
|
{
|
|
name: 'iPhone Safari',
|
|
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
browser: 'Safari',
|
|
os: 'iOS',
|
|
device: 'mobile',
|
|
},
|
|
{
|
|
name: 'iPad Safari',
|
|
ua: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
browser: 'Safari',
|
|
os: 'iOS',
|
|
device: 'tablet',
|
|
},
|
|
{
|
|
name: 'Android Chrome',
|
|
ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
|
browser: 'Chrome',
|
|
os: 'Android',
|
|
device: 'mobile',
|
|
},
|
|
{
|
|
name: 'Samsung device',
|
|
ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36',
|
|
browser: 'Chrome',
|
|
os: 'Android',
|
|
device: 'mobile',
|
|
vendor: 'Samsung',
|
|
},
|
|
{
|
|
name: 'Opera',
|
|
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 OPR/106.0.0.0',
|
|
browser: 'Opera',
|
|
os: 'Windows 11',
|
|
device: 'desktop',
|
|
},
|
|
];
|
|
|
|
testCases.forEach(tc => {
|
|
it(`should detect ${tc.name}`, () => {
|
|
setUserAgent(tc.ua);
|
|
const env = getEnvInfo();
|
|
expect(env.browser.name).toBe(tc.browser);
|
|
expect(env.os.name).toBe(tc.os);
|
|
expect(env.device.type).toBe(tc.device);
|
|
});
|
|
});
|
|
|
|
it('should detect Unknown browser for unrecognized UA', () => {
|
|
setUserAgent('SomeCustomBot/1.0');
|
|
const env = getEnvInfo();
|
|
expect(env.browser.name).toBe('Unknown');
|
|
});
|
|
|
|
it('should detect Windows 11', () => {
|
|
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
// The current code doesn't distinguish Win10 vs Win11 based on Win64 x64 alone
|
|
const env = getEnvInfo();
|
|
expect(env.os.name).toMatch(/Windows/);
|
|
});
|
|
|
|
it('should parse browser version', () => {
|
|
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.5481.100 Safari/537.36');
|
|
const env = getEnvInfo();
|
|
expect(env.browser.version).toMatch(/^120/);
|
|
expect(env.browser.major).toBe('120');
|
|
});
|
|
|
|
it('should parse macOS version with underscores', () => {
|
|
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15');
|
|
const env = getEnvInfo();
|
|
expect(env.os.name).toBe('macOS');
|
|
expect(env.os.version).toContain('14.2.1');
|
|
});
|
|
});
|
|
|
|
describe('device detection', () => {
|
|
it('should detect iPhone as mobile Apple device', () => {
|
|
setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1');
|
|
const env = getEnvInfo();
|
|
expect(env.device.type).toBe('mobile');
|
|
expect(env.device.vendor).toBe('Apple');
|
|
expect(env.device.model).toBe('iPhone');
|
|
});
|
|
|
|
it('should detect iPad as tablet Apple device', () => {
|
|
setUserAgent('Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1');
|
|
const env = getEnvInfo();
|
|
expect(env.device.type).toBe('tablet');
|
|
expect(env.device.vendor).toBe('Apple');
|
|
expect(env.device.model).toBe('iPad');
|
|
});
|
|
|
|
it('should detect Samsung device', () => {
|
|
setUserAgent('Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
|
|
const env = getEnvInfo();
|
|
expect(env.device.vendor).toBe('Samsung');
|
|
expect(env.device.model).toBe('G991B');
|
|
});
|
|
|
|
it('should detect Pixel device', () => {
|
|
setUserAgent('Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
|
|
const env = getEnvInfo();
|
|
expect(env.device.vendor).toBe('Google');
|
|
expect(env.device.model).toBe('Pixel 8');
|
|
});
|
|
|
|
it('should detect Huawei device', () => {
|
|
setUserAgent('Mozilla/5.0 (Linux; Android 12; HUAWEI Mate40 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36');
|
|
const env = getEnvInfo();
|
|
expect(env.device.vendor).toBe('Huawei');
|
|
expect(env.device.model).toBe('Mate40');
|
|
});
|
|
});
|
|
|
|
describe('getContexts', () => {
|
|
it('should return contexts with encoded values', () => {
|
|
const contexts = getContexts();
|
|
expect(contexts).toBeDefined();
|
|
expect(contexts.browser).toBeDefined();
|
|
expect(contexts.os).toBeDefined();
|
|
expect(contexts.device).toBeDefined();
|
|
});
|
|
|
|
it('should use short codes for browser', () => {
|
|
const contexts = getContexts();
|
|
expect(contexts.browser.name.length).toBeLessThanOrEqual(10);
|
|
});
|
|
|
|
it('should use short codes for os', () => {
|
|
const contexts = getContexts();
|
|
expect(contexts.os.name.length).toBeLessThanOrEqual(10);
|
|
});
|
|
|
|
it('should map Chrome to c', () => {
|
|
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
const contexts = getContexts();
|
|
expect(contexts.browser.name).toBe('c');
|
|
});
|
|
|
|
it('should map Windows 11 to w11', () => {
|
|
setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
const contexts = getContexts();
|
|
expect(contexts.os.name).toBe('w11');
|
|
});
|
|
});
|
|
|
|
describe('getContextId', () => {
|
|
it('should return a hex string of up to 8 characters (32-bit hash)', () => {
|
|
const contextId = getContextId();
|
|
expect(contextId).toBeDefined();
|
|
expect(contextId.length).toBeLessThanOrEqual(16);
|
|
expect(contextId.length).toBeGreaterThan(0);
|
|
expect(contextId).toMatch(/^[a-f0-9]+$/i);
|
|
});
|
|
|
|
it('should return consistent id for same environment', () => {
|
|
const id1 = getContextId();
|
|
const id2 = getContextId();
|
|
expect(id1).toBe(id2);
|
|
});
|
|
});
|
|
|
|
describe('getPageUrl', () => {
|
|
it('should return current page URL or empty string', () => {
|
|
const url = getPageUrl();
|
|
expect(typeof url).toBe('string');
|
|
});
|
|
});
|
|
|
|
describe('getReferrer', () => {
|
|
it('should return referrer or empty string', () => {
|
|
const referrer = getReferrer();
|
|
expect(typeof referrer).toBe('string');
|
|
});
|
|
});
|
|
|
|
describe('sanitizeUrl', () => {
|
|
it('should filter sensitive parameters', () => {
|
|
const url = 'https://example.com/api?user=123&password=secret&token=abc';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toContain('password');
|
|
expect(sanitized).toContain('token');
|
|
// URL encoded [Filtered] = %5BFiltered%5D
|
|
expect(sanitized).toContain('%5BFiltered%5D');
|
|
});
|
|
|
|
it('should handle URLs without sensitive params', () => {
|
|
const url = 'https://example.com/api?page=1&limit=10';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toBe(url);
|
|
});
|
|
|
|
it('should handle invalid URLs', () => {
|
|
const sanitized = sanitizeUrl('not-a-url');
|
|
expect(sanitized).toBe('not-a-url');
|
|
});
|
|
|
|
it('should filter api_key parameter', () => {
|
|
const url = 'https://api.com/endpoint?api_key=secret123';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toContain('api_key');
|
|
expect(sanitized).toContain('%5BFiltered%5D');
|
|
});
|
|
|
|
it('should filter authorization headers in URL', () => {
|
|
const url = 'https://example.com/api?code=auth123';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toContain('code');
|
|
expect(sanitized).toContain('%5BFiltered%5D');
|
|
});
|
|
|
|
it('should filter sensitive params in hash', () => {
|
|
const url = 'https://example.com/page#access_token=secret123&other=value';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toContain('access_token');
|
|
expect(sanitized).toContain('%5BFiltered%5D');
|
|
});
|
|
|
|
it('should filter javascript: protocol', () => {
|
|
const url = 'javascript:alert(1)';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toBe('javascript:[filtered]');
|
|
});
|
|
|
|
it('should filter javascript: with whitespace', () => {
|
|
const url = ' javascript:alert(document.cookie)';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toBe('javascript:[filtered]');
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
const sanitized = sanitizeUrl('');
|
|
expect(sanitized).toBe('');
|
|
});
|
|
|
|
it('should handle null/undefined', () => {
|
|
expect(sanitizeUrl(null as any)).toBeNull();
|
|
expect(sanitizeUrl(undefined as any)).toBeUndefined();
|
|
});
|
|
|
|
it('should filter multiple sensitive params', () => {
|
|
const url = 'https://example.com/api?password=pwd&token=tkn&api_key=key&secret=sec&session=sess';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toContain('%5BFiltered%5D');
|
|
const filteredCount = (sanitized.match(/%5BFiltered%5D/g) || []).length;
|
|
expect(filteredCount).toBe(5);
|
|
});
|
|
|
|
it('should keep non-sensitive params unchanged', () => {
|
|
const url = 'https://example.com/api?page=1&limit=10&sort=desc';
|
|
const sanitized = sanitizeUrl(url);
|
|
expect(sanitized).toBe(url);
|
|
});
|
|
});
|
|
|
|
describe('shouldIgnoreUrl', () => {
|
|
it('should return false for empty patterns', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api', []);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should match string pattern', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api/health', ['/health']);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should not match non-matching pattern', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api/users', ['/health']);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should match wildcard pattern', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api/v1/users', ['/api/v1/*']);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should not match wildcard when no wildcard in URL', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api/v2/users', ['/api/v1/*']);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('should match regex pattern', () => {
|
|
const result = shouldIgnoreUrl('https://example.com/api/users/123', [/api\/users\/\d+/]);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should match multiple patterns (OR logic)', () => {
|
|
const patterns = ['/health', '/metrics', /\/debug/];
|
|
expect(shouldIgnoreUrl('https://example.com/health', patterns)).toBe(true);
|
|
expect(shouldIgnoreUrl('https://example.com/api/metrics', patterns)).toBe(true);
|
|
expect(shouldIgnoreUrl('https://example.com/debug/vars', patterns)).toBe(true);
|
|
expect(shouldIgnoreUrl('https://example.com/api/users', patterns)).toBe(false);
|
|
});
|
|
|
|
it('should match wildcard with multiple *', () => {
|
|
const result = shouldIgnoreUrl('https://api.example.com/v1/users/123/info', ['*.example.com/*/users/*']);
|
|
expect(result).toBe(true);
|
|
});
|
|
});
|
|
});
|