diff --git a/README.md b/README.md index 274c80c..138bfe8 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,8 @@ Todos: - does it return as expected? - does it error when invalid id? - makeStandardFetcher() - - do all parameters get passed to real fetch as expected? - does serialisation work as expected? (formdata + json + string) - - does json responses get automatically parsed? - add all real providers - - fetcher for MW's simple-proxy - make default fetcher maker thing work with both undici and node-fetch Future todos: diff --git a/src/__test__/fetchers/simpleProxy.test.ts b/src/__test__/fetchers/simpleProxy.test.ts new file mode 100644 index 0000000..5066e63 --- /dev/null +++ b/src/__test__/fetchers/simpleProxy.test.ts @@ -0,0 +1,125 @@ +import { makeSimpleProxyFetcher } from "@/fetchers/simpleProxy"; +import { DefaultedFetcherOptions, FetcherOptions } from "@/fetchers/types"; +import { Headers } from "node-fetch"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("makeSimpleProxyFetcher()", () => { + const fetch = vi.fn(); + const fetcher = makeSimpleProxyFetcher("https://example.com/proxy", fetch); + + afterEach(() => { + vi.clearAllMocks(); + }); + + function setResult(type: "text" | "json", value: any) { + if (type === 'text') return fetch.mockResolvedValueOnce({ + headers: new Headers({ + "content-type": "text/plain", + }), + text() { + return Promise.resolve(value); + }, + }); + if (type === 'json') return fetch.mockResolvedValueOnce({ + headers: new Headers({ + "content-type": "application/json", + }), + json() { + return Promise.resolve(value); + }, + }); + } + + function expectFetchCall(ops: { inputUrl: string, input: DefaultedFetcherOptions, outputUrl?: string, output: any, outputBody: any }) { + expect(fetcher(ops.inputUrl, ops.input)).resolves.toEqual(ops.outputBody); + expect(fetch).toBeCalledWith(ops.outputUrl ?? ops.inputUrl, ops.output); + vi.clearAllMocks(); + } + + it('should pass options through', () => { + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + method: "GET", + query: {}, + headers: { + "X-Hello": "world", + }, + }, + outputUrl: `https://example.com/proxy?destination=${encodeURIComponent('https://google.com/')}`, + output: { + method: "GET", + headers: { + "X-Hello": "world", + }, + }, + outputBody: "hello world" + }) + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + method: "GET", + headers: {}, + query: { + "a": 'b', + } + }, + outputUrl: `https://example.com/proxy?destination=${encodeURIComponent('https://google.com/?a=b')}`, + output: { + method: "GET", + headers: {}, + }, + outputBody: "hello world" + }) + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + method: "GET", + query: {}, + headers: {}, + }, + outputUrl: `https://example.com/proxy?destination=${encodeURIComponent('https://google.com/')}`, + output: { + method: "GET", + headers: {}, + }, + outputBody: "hello world" + }) + }); + + it('should parse response correctly', () => { + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com/", + input: { + method: "POST", + query: {}, + headers: {}, + }, + outputUrl: `https://example.com/proxy?destination=${encodeURIComponent('https://google.com/')}`, + output: { + method: "POST", + headers: {}, + }, + outputBody: "hello world" + }) + setResult("json", { hello: 42 }); + expectFetchCall({ + inputUrl: "https://google.com/", + input: { + method: "POST", + query: {}, + headers: {}, + }, + outputUrl: `https://example.com/proxy?destination=${encodeURIComponent('https://google.com/')}`, + output: { + method: "POST", + headers: {}, + }, + outputBody: { hello: 42 } + }) + }); +}); diff --git a/src/__test__/fetchers/standard.test.ts b/src/__test__/fetchers/standard.test.ts new file mode 100644 index 0000000..6bad99a --- /dev/null +++ b/src/__test__/fetchers/standard.test.ts @@ -0,0 +1,125 @@ +import { makeStandardFetcher } from "@/fetchers/standardFetch"; +import { DefaultedFetcherOptions } from "@/fetchers/types"; +import { Headers } from "node-fetch"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("makeStandardFetcher()", () => { + const fetch = vi.fn(); + const fetcher = makeStandardFetcher(fetch); + + afterEach(() => { + vi.clearAllMocks(); + }); + + function setResult(type: "text" | "json", value: any) { + if (type === 'text') return fetch.mockResolvedValueOnce({ + headers: new Headers({ + "content-type": "text/plain", + }), + text() { + return Promise.resolve(value); + }, + }); + if (type === 'json') return fetch.mockResolvedValueOnce({ + headers: new Headers({ + "content-type": "application/json", + }), + json() { + return Promise.resolve(value); + }, + }); + } + + function expectFetchCall(ops: { inputUrl: string, input: DefaultedFetcherOptions, outputUrl?: string, output: any, outputBody: any }) { + expect(fetcher(ops.inputUrl, ops.input)).resolves.toEqual(ops.outputBody); + expect(fetch).toBeCalledWith(ops.outputUrl ?? ops.inputUrl, ops.output); + vi.clearAllMocks(); + } + + it('should pass options through', () => { + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + method: "GET", + query: {}, + headers: { + "X-Hello": "world", + }, + }, + outputUrl: "https://google.com/", + output: { + method: "GET", + headers: { + "X-Hello": "world", + }, + }, + outputBody: "hello world" + }) + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + method: "GET", + headers: {}, + query: { + "a": 'b', + } + }, + outputUrl: "https://google.com/?a=b", + output: { + method: "GET", + headers: {}, + }, + outputBody: "hello world" + }) + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com", + input: { + query: {}, + headers: {}, + method: "GET" + }, + outputUrl: "https://google.com/", + output: { + method: "GET", + headers: {}, + }, + outputBody: "hello world" + }) + }); + + it('should parse response correctly', () => { + setResult("text", "hello world"); + expectFetchCall({ + inputUrl: "https://google.com/", + input: { + query: {}, + headers: {}, + method: "POST" + }, + outputUrl: "https://google.com/", + output: { + method: "POST", + headers: {}, + }, + outputBody: "hello world" + }) + setResult("json", { hello: 42 }); + expectFetchCall({ + inputUrl: "https://google.com/", + input: { + query: {}, + headers: {}, + method: "POST" + }, + outputUrl: "https://google.com/", + output: { + method: "POST", + headers: {}, + }, + outputBody: { hello: 42 } + }) + }); +}); diff --git a/src/fetchers/simpleProxy.ts b/src/fetchers/simpleProxy.ts new file mode 100644 index 0000000..94e6b28 --- /dev/null +++ b/src/fetchers/simpleProxy.ts @@ -0,0 +1,35 @@ +import fetch from 'node-fetch'; + +import { makeFullUrl } from '@/fetchers/common'; +import { makeStandardFetcher } from '@/fetchers/standardFetch'; +import { Fetcher } from '@/fetchers/types'; + +const headerMap: Record = { + cookie: 'X-Cookie', + referer: 'X-Referer', + origin: 'X-Origin', +}; + +export function makeSimpleProxyFetcher(proxyUrl: string, f: typeof fetch): Fetcher { + const fetcher = makeStandardFetcher(f); + const proxiedFetch: Fetcher = async (url, ops) => { + const fullUrl = makeFullUrl(url, ops); + + const headerEntries = Object.entries(ops.headers).map((entry) => { + const key = entry[0].toLowerCase(); + if (headerMap[key]) return [headerMap[key], entry[1]]; + return entry; + }); + + return fetcher(proxyUrl, { + ...ops, + query: { + destination: fullUrl, + }, + headers: Object.fromEntries(headerEntries), + baseUrl: undefined, + }); + }; + + return proxiedFetch; +} diff --git a/src/index.ts b/src/index.ts index 6e95d93..1721525 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,3 +14,4 @@ export type { export { NotFoundError } from '@/utils/errors'; export { makeProviders } from '@/main/builder'; export { makeStandardFetcher } from '@/fetchers/standardFetch'; +export { makeSimpleProxyFetcher } from '@/fetchers/simpleProxy';