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); }); }); });