light-sentry-sdk/tests/env.test.ts

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