index.test.ts
1 import * as fs from 'node:fs'; 2 import * as http from 'node:http'; 3 import * as os from 'node:os'; 4 import * as path from 'node:path'; 5 import { afterEach, describe, expect, it, vi } from 'vitest'; 6 import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js'; 7 8 const servers: http.Server[] = []; 9 const tempDirs: string[] = []; 10 11 afterEach(async () => { 12 vi.unstubAllEnvs(); 13 await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => { 14 server.close((err) => (err ? reject(err) : resolve())); 15 }))); 16 servers.length = 0; 17 for (const dir of tempDirs) { 18 try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } 19 } 20 tempDirs.length = 0; 21 }); 22 23 async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> { 24 const server = http.createServer(handler); 25 servers.push(server); 26 await new Promise<void>((resolve) => server.listen(0, hostname, resolve)); 27 const address = server.address(); 28 if (!address || typeof address === 'string') { 29 throw new Error('Failed to start test server'); 30 } 31 return `http://${hostname}:${address.port}`; 32 } 33 34 // Windows Defender can briefly lock newly-written .tmp files, causing EPERM. 35 // Retry once to handle this flakiness. 36 describe('download helpers', { retry: process.platform === 'win32' ? 2 : 0 }, () => { 37 it('resolves relative redirects against the original URL', () => { 38 expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin'); 39 expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next'); 40 }); 41 42 it('formats browser cookies into a Cookie header', () => { 43 expect(formatCookieHeader([ 44 { name: 'sid', value: 'abc', domain: 'example.com' }, 45 { name: 'ct0', value: 'def', domain: 'example.com' }, 46 ])).toBe('sid=abc; ct0=def'); 47 }); 48 49 it('fails after exceeding the redirect limit', async () => { 50 const baseUrl = await startServer((_req, res) => { 51 res.statusCode = 302; 52 res.setHeader('Location', '/loop'); 53 res.end(); 54 }); 55 56 const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-')); 57 tempDirs.push(tempDir); 58 const destPath = path.join(tempDir, 'file.txt'); 59 const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 }); 60 61 expect(result).toEqual({ 62 success: false, 63 size: 0, 64 error: 'Too many redirects (> 2)', 65 }); 66 expect(fs.existsSync(destPath)).toBe(false); 67 }); 68 69 it('does not forward cookies across cross-domain redirects', async () => { 70 let forwardedCookie: string | undefined; 71 const targetUrl = await startServer((req, res) => { 72 forwardedCookie = req.headers.cookie; 73 res.statusCode = 200; 74 res.end('ok'); 75 }, 'localhost'); 76 77 const redirectUrl = await startServer((_req, res) => { 78 res.statusCode = 302; 79 res.setHeader('Location', targetUrl); 80 res.end(); 81 }); 82 83 const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-')); 84 tempDirs.push(tempDir); 85 const destPath = path.join(tempDir, 'redirect.txt'); 86 const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' }); 87 88 expect(result).toEqual({ success: true, size: 2 }); 89 expect(forwardedCookie).toBeUndefined(); 90 expect(fs.readFileSync(destPath, 'utf8')).toBe('ok'); 91 }); 92 93 it('does not forward cookie headers across cross-domain redirects', async () => { 94 let forwardedCookie: string | undefined; 95 const targetUrl = await startServer((req, res) => { 96 forwardedCookie = req.headers.cookie; 97 res.statusCode = 200; 98 res.end('ok'); 99 }, 'localhost'); 100 101 const redirectUrl = await startServer((_req, res) => { 102 res.statusCode = 302; 103 res.setHeader('Location', targetUrl); 104 res.end(); 105 }); 106 107 const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-')); 108 tempDirs.push(tempDir); 109 const destPath = path.join(tempDir, 'redirect-header.txt'); 110 const result = await httpDownload(`${redirectUrl}/start`, destPath, { 111 headers: { Cookie: 'sid=header-cookie' }, 112 }); 113 114 expect(result).toEqual({ success: true, size: 2 }); 115 expect(forwardedCookie).toBeUndefined(); 116 expect(fs.readFileSync(destPath, 'utf8')).toBe('ok'); 117 }); 118 119 it('bypasses proxy settings for loopback downloads', async () => { 120 vi.stubEnv('HTTP_PROXY', 'http://127.0.0.1:9'); 121 122 const baseUrl = await startServer((_req, res) => { 123 res.statusCode = 200; 124 res.end('ok'); 125 }); 126 127 const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-dl-')); 128 tempDirs.push(tempDir); 129 const destPath = path.join(tempDir, 'loopback.txt'); 130 const result = await httpDownload(`${baseUrl}/ok`, destPath); 131 132 expect(result).toEqual({ success: true, size: 2 }); 133 expect(fs.readFileSync(destPath, 'utf8')).toBe('ok'); 134 }); 135 });