import http from 'node:http'; import https from 'node:https'; import got from 'got'; import undici from 'undici'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { PRESETS } from '@/utils/header-generator'; const originalGlobals = { fetch: globalThis.fetch, Headers: globalThis.Headers, FormData: globalThis.FormData, Request: globalThis.Request, Response: globalThis.Response, }; const originalHttp = { get: http.get, request: http.request, }; const originalHttps = { get: https.get, request: https.request, }; const originalEnv = { PROXY_URI: process.env.PROXY_URI, PROXY_AUTH: process.env.PROXY_AUTH, PROXY_URL_REGEX: process.env.PROXY_URL_REGEX, }; process.env.PROXY_URI = 'http://rsshub.proxy:2333/'; process.env.PROXY_AUTH = 'rsshubtest'; process.env.PROXY_URL_REGEX = 'headers'; await import('@/utils/request-rewriter'); const { config } = await import('@/config'); const { default: ofetch } = await import('@/utils/ofetch'); const createJsonResponse = () => Response.json( { ok: true }, { headers: { 'content-type': 'application/json', }, } ); describe('request-rewriter', () => { afterAll(() => { globalThis.fetch = originalGlobals.fetch; globalThis.Headers = originalGlobals.Headers; globalThis.FormData = originalGlobals.FormData; globalThis.Request = originalGlobals.Request; globalThis.Response = originalGlobals.Response; http.get = originalHttp.get; http.request = originalHttp.request; https.get = originalHttps.get; https.request = originalHttps.request; if (originalEnv.PROXY_URI === undefined) { delete process.env.PROXY_URI; } else { process.env.PROXY_URI = originalEnv.PROXY_URI; } if (originalEnv.PROXY_AUTH === undefined) { delete process.env.PROXY_AUTH; } else { process.env.PROXY_AUTH = originalEnv.PROXY_AUTH; } if (originalEnv.PROXY_URL_REGEX === undefined) { delete process.env.PROXY_URL_REGEX; } else { process.env.PROXY_URL_REGEX = originalEnv.PROXY_URL_REGEX; } }); afterEach(() => { vi.restoreAllMocks(); }); it('fetch', async () => { const fetchSpy = vi.spyOn(undici, 'fetch').mockImplementation(() => Promise.resolve(createJsonResponse())); try { await (await fetch('http://rsshub.test/headers')).json(); } catch { // ignore } // headers const headers: Headers = fetchSpy.mock.lastCall?.[0].headers; expect(headers.get('user-agent')).toBe(config.ua); expect(headers.get('accept')).toBeDefined(); expect(headers.get('referer')).toBe('http://rsshub.test'); expect(headers.get('sec-ch-ua')).toBeDefined(); expect(headers.get('sec-ch-ua-mobile')).toBeDefined(); expect(headers.get('sec-ch-ua-platform')).toBeDefined(); expect(headers.get('sec-fetch-site')).toBeDefined(); expect(headers.get('sec-fetch-mode')).toBeDefined(); expect(headers.get('sec-fetch-user')).toBeDefined(); expect(headers.get('sec-fetch-dest')).toBeDefined(); // proxy const options = fetchSpy.mock.lastCall?.[1]; const agentKey = Object.getOwnPropertySymbols(options?.dispatcher).find((s) => s.description === 'proxy agent options'); const agentUri = agentKey ? options?.dispatcher?.[agentKey].uri : null; expect(agentUri).toBe(process.env.PROXY_URI); // proxy auth const headersKey = Object.getOwnPropertySymbols(options?.dispatcher).find((s) => s.description === 'proxy headers'); const agentHeaders = headersKey ? options?.dispatcher?.[headersKey] : null; expect(agentHeaders['proxy-authorization']).toBe(`Basic ${process.env.PROXY_AUTH}`); // url regex not match { try { await (await fetch('http://rsshub.test/rss')).json(); } catch { // ignore } const options = fetchSpy.mock.lastCall?.[1]; expect(options?.dispatcher).toBeUndefined(); } }); it('ofetch', async () => { const fetchSpy = vi.spyOn(undici, 'fetch').mockImplementation(() => Promise.resolve(createJsonResponse())); try { await ofetch('http://rsshub.test/headers', { retry: 0, }); } catch { // ignore } // headers const headers: Headers = fetchSpy.mock.lastCall?.[0].headers; expect(headers.get('user-agent')).toBe(config.ua); expect(headers.get('accept')).toBeDefined(); expect(headers.get('referer')).toBe('http://rsshub.test'); expect(headers.get('sec-ch-ua')).toBeDefined(); expect(headers.get('sec-ch-ua-mobile')).toBeDefined(); expect(headers.get('sec-ch-ua-platform')).toBeDefined(); expect(headers.get('sec-fetch-site')).toBeDefined(); expect(headers.get('sec-fetch-mode')).toBeDefined(); expect(headers.get('sec-fetch-user')).toBeDefined(); expect(headers.get('sec-fetch-dest')).toBeDefined(); // proxy const options = fetchSpy.mock.lastCall?.[1]; const agentKey = Object.getOwnPropertySymbols(options?.dispatcher).find((s) => s.description === 'proxy agent options'); const agentUri = agentKey ? options?.dispatcher?.[agentKey].uri : null; expect(agentUri).toBe(process.env.PROXY_URI); // proxy auth const headersKey = Object.getOwnPropertySymbols(options?.dispatcher).find((s) => s.description === 'proxy headers'); const agentHeaders = headersKey ? options?.dispatcher?.[headersKey] : null; expect(agentHeaders['proxy-authorization']).toBe(`Basic ${process.env.PROXY_AUTH}`); // url regex not match { try { await ofetch('http://rsshub.test/rss', { retry: 0, }); } catch { // ignore } const options = fetchSpy.mock.lastCall?.[1]; expect(options?.dispatcher).toBeUndefined(); } }); it('ofetch custom ua', async () => { const fetchSpy = vi.spyOn(undici, 'fetch').mockImplementation(() => Promise.resolve(createJsonResponse())); const userAgent = config.trueUA; try { await ofetch('http://rsshub.test/headers', { retry: 0, headers: { 'user-agent': userAgent, }, }); } catch { // ignore } // headers const headers: Headers = fetchSpy.mock.lastCall?.[0].headers; expect(headers.get('user-agent')).toBe(userAgent); }); it('ofetch header preset', async () => { const fetchSpy = vi.spyOn(undici, 'fetch').mockImplementation(() => Promise.resolve(createJsonResponse())); try { await ofetch('http://rsshub.test/headers', { retry: 0, headerGeneratorOptions: PRESETS.MODERN_WINDOWS_CHROME, }); } catch { // ignore } // headers const headers: Headers = fetchSpy.mock.lastCall?.[0].headers; expect(headers.get('user-agent')).toBeDefined(); expect(headers.get('accept')).toBeDefined(); expect(headers.get('referer')).toBe('http://rsshub.test'); expect(headers.get('sec-ch-ua')).toBeDefined(); expect(headers.get('sec-ch-ua-mobile')).toBe('?0'); expect(headers.get('sec-ch-ua-platform')).toBe('"Windows"'); expect(headers.get('sec-fetch-site')).toBeDefined(); expect(headers.get('sec-fetch-mode')).toBeDefined(); expect(headers.get('sec-fetch-user')).toBeDefined(); expect(headers.get('sec-fetch-dest')).toBeDefined(); }); it('http', async () => { const httpSpy = vi.spyOn(http, 'request'); try { await got.get('http://rsshub.test/headers', { headers: { 'user-agent': undefined, accept: undefined, }, }); } catch { // ignore } // headers const options = httpSpy.mock.lastCall?.[1]; const headers = options?.headers; expect(headers?.['user-agent']).toBe(config.ua); expect(headers?.accept).toBeDefined(); expect(headers?.referer).toBe('http://rsshub.test'); // proxy const agentUri = options?.agent?.proxy?.href; expect(agentUri).toBe(process.env.PROXY_URI); expect(options?.agent?.proxyHeaders['proxy-authorization']).toBe(`Basic ${process.env.PROXY_AUTH}`); // url regex not match { try { await got.get('http://rsshub.test/rss', { headers: { 'user-agent': undefined, accept: undefined, }, }); } catch { // ignore } const options = httpSpy.mock.lastCall?.[1]; expect(options?.agent).toBeUndefined(); } }); it('rate limiter', async () => { vi.useFakeTimers(); const fetchSpy = vi.spyOn(undici, 'fetch').mockImplementation(() => Promise.resolve(createJsonResponse())); try { const { default: wrappedFetch } = await import('@/utils/request-rewriter/fetch'); const time = Date.now(); const tasks = Array.from({ length: 20 }).map(() => wrappedFetch('http://rsshub.test/headers')); await vi.advanceTimersByTimeAsync(3000); await Promise.all(tasks); expect(fetchSpy).toHaveBeenCalledTimes(20); expect(Date.now() - time).toBeGreaterThan(1500); } finally { vi.useRealTimers(); fetchSpy.mockRestore(); } }, 20000); });