stealth.test.ts
1 import { describe, it, expect } from 'vitest'; 2 import { generateStealthJs } from './stealth.js'; 3 4 /** 5 * Tests for the stealth anti-detection module. 6 * 7 * We test the generated JS string for expected content and structure. 8 * Evaluating in Node is fragile because stealth patches target browser 9 * globals (navigator, Performance, HTMLIFrameElement) that don't exist 10 * or behave differently in Node. Instead we verify the code string 11 * contains the right patches and is syntactically valid. 12 */ 13 14 describe('generateStealthJs', () => { 15 it('returns a non-empty string', () => { 16 const code = generateStealthJs(); 17 expect(typeof code).toBe('string'); 18 expect(code.length).toBeGreaterThan(0); 19 }); 20 21 it('is a valid self-contained IIFE', () => { 22 const code = generateStealthJs(); 23 // Should start/end as an IIFE 24 expect(code.trim()).toMatch(/^\(\(\) => \{/); 25 expect(code.trim()).toMatch(/\}\)\(\)$/); 26 }); 27 28 it('patches navigator.webdriver', () => { 29 const code = generateStealthJs(); 30 expect(code).toContain("navigator, 'webdriver'"); 31 expect(code).toContain('() => false'); 32 }); 33 34 it('stubs window.chrome', () => { 35 const code = generateStealthJs(); 36 expect(code).toContain('window.chrome'); 37 expect(code).toContain('runtime'); 38 expect(code).toContain('loadTimes'); 39 expect(code).toContain('csi'); 40 }); 41 42 it('fakes navigator.plugins if empty', () => { 43 const code = generateStealthJs(); 44 expect(code).toContain('navigator.plugins'); 45 expect(code).toContain('PDF Viewer'); 46 expect(code).toContain('Chrome PDF Viewer'); 47 }); 48 49 it('ensures navigator.languages is non-empty', () => { 50 const code = generateStealthJs(); 51 expect(code).toContain('navigator.languages'); 52 expect(code).toContain("'en-US'"); 53 }); 54 55 it('normalizes Permissions.query for notifications', () => { 56 const code = generateStealthJs(); 57 expect(code).toContain('Permissions'); 58 expect(code).toContain('notifications'); 59 }); 60 61 it('cleans automation artifacts', () => { 62 const code = generateStealthJs(); 63 expect(code).toContain('__playwright'); 64 expect(code).toContain('__puppeteer'); 65 expect(code).toContain("'cdc_'"); 66 expect(code).toContain("'__cdc_'"); 67 }); 68 69 it('filters CDP patterns from Error.stack', () => { 70 const code = generateStealthJs(); 71 expect(code).toContain('puppeteer_evaluation_script'); 72 expect(code).toContain("'pptr:'"); 73 expect(code).toContain("'debugger://'"); 74 }); 75 76 it('neutralizes debugger statement traps', () => { 77 const code = generateStealthJs(); 78 // Should patch Function constructor with new.target / Reflect.construct 79 expect(code).toContain('_OrigFunction'); 80 expect(code).toContain('_PatchedFunction'); 81 expect(code).toContain('new.target'); 82 expect(code).toContain('Reflect.construct'); 83 // Should patch eval 84 expect(code).toContain('_origEval'); 85 expect(code).toContain('_patchedEval'); 86 // Regex to strip debugger (lookbehind for statement boundaries) 87 expect(code).toContain('_debuggerRe'); 88 }); 89 90 it('uses shared toString disguise via WeakMap', () => { 91 const code = generateStealthJs(); 92 // Shared infrastructure at the top of the IIFE 93 expect(code).toContain('_origToString'); 94 expect(code).toContain('WeakMap'); 95 expect(code).toContain('_disguised'); 96 expect(code).toContain('_disguise'); 97 // Should NOT have per-instance toString overrides on Function/eval 98 // (they go through _disguise instead) 99 }); 100 101 it('defends console method fingerprinting', () => { 102 const code = generateStealthJs(); 103 expect(code).toContain('_consoleMethods'); 104 expect(code).toContain("'log'"); 105 expect(code).toContain("'warn'"); 106 expect(code).toContain("'error'"); 107 expect(code).toContain('[native code]'); 108 // Uses saved _origToString reference 109 expect(code).toContain('_origToString.call'); 110 }); 111 112 it('defends window dimension detection', () => { 113 const code = generateStealthJs(); 114 expect(code).toContain('outerWidth'); 115 expect(code).toContain('outerHeight'); 116 expect(code).toContain('innerWidth'); 117 expect(code).toContain('innerHeight'); 118 }); 119 120 it('filters Performance API entries', () => { 121 const code = generateStealthJs(); 122 expect(code).toContain('getEntries'); 123 expect(code).toContain('getEntriesByType'); 124 expect(code).toContain('getEntriesByName'); 125 expect(code).toContain('_suspiciousPatterns'); 126 }); 127 128 it('cleans document $cdc_ properties', () => { 129 const code = generateStealthJs(); 130 expect(code).toContain("'$cdc_'"); 131 expect(code).toContain("'$chrome_'"); 132 }); 133 134 it('patches iframe contentWindow.chrome consistency', () => { 135 const code = generateStealthJs(); 136 expect(code).toContain('contentWindow'); 137 expect(code).toContain('HTMLIFrameElement'); 138 }); 139 140 it('uses non-enumerable guard flag on EventTarget.prototype', () => { 141 const code = generateStealthJs(); 142 expect(code).toContain('EventTarget.prototype'); 143 expect(code).toContain("'__lsn'"); 144 expect(code).toContain('enumerable: false'); 145 }); 146 147 it('generates syntactically valid JavaScript', () => { 148 const code = generateStealthJs(); 149 // new Function() parses the code without executing it in a real 150 // browser context, catching syntax errors from template literal issues. 151 expect(() => new Function(code)).not.toThrow(); 152 }); 153 });