diff --git a/package.json b/package.json index 9bbfd91..ea731c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@osskit/fetch-enhancers", - "version": "4.6.0", + "version": "4.7.0", "repository": { "url": "https://github.com/osskit/fetch-enhancers" }, @@ -49,4 +49,4 @@ "*.{ts,js,json}": "prettier --write" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" -} +} \ No newline at end of file diff --git a/src/enhancers/withHeaders.ts b/src/enhancers/withHeaders.ts index eb59e94..b388d36 100644 --- a/src/enhancers/withHeaders.ts +++ b/src/enhancers/withHeaders.ts @@ -1,12 +1,28 @@ import type { Fetch } from '../types.js'; +const headersToObject = (headers?: HeadersInit): Record | undefined => { + if (!headers) { + return undefined; + } + if (headers instanceof Headers) { + return Object.fromEntries(headers.entries()); + } + if (Array.isArray(headers)) { + return Object.fromEntries(headers); + } + if (typeof headers === 'object') { + return { ...headers }; + } + return undefined; +}; + export const withHeaders = - (fetch: Fetch, getHeaders: (headers?: HeadersInit) => HeadersInit): Fetch => - (url, init) => + (fetch: Fetch, getHeaders: (headers?: HeadersInit) => HeadersInit | Promise): Fetch => + async (url, init) => fetch(url, { ...init, headers: { - ...init?.headers, - ...getHeaders(init?.headers), + ...headersToObject(init?.headers), + ...headersToObject(await getHeaders(init?.headers)), }, }); diff --git a/src/enhancers/withToken.ts b/src/enhancers/withToken.ts index f35413b..80f490c 100644 --- a/src/enhancers/withToken.ts +++ b/src/enhancers/withToken.ts @@ -1,17 +1,8 @@ import type { Fetch } from '../types.js'; +import { withHeaders } from './withHeaders.js'; -export const withToken = - (getToken: () => Promise | string, fetch: Fetch): Fetch => - async (url, innerInit) => { - const innerFetch = fetch; - const token = await getToken(); - - return innerFetch(url, { - ...innerInit, - headers: { - ...innerInit?.headers, - // eslint-disable-next-line @typescript-eslint/naming-convention - Authorization: `Bearer ${token}`, - }, - }); - }; +export const withToken = (getToken: () => Promise | string, fetch: Fetch): Fetch => + withHeaders(fetch, async () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + Authorization: `Bearer ${await getToken()}`, + })); diff --git a/tests/specs/withHeaders.spec.ts b/tests/specs/withHeaders.spec.ts index 97d2b46..3f01450 100644 --- a/tests/specs/withHeaders.spec.ts +++ b/tests/specs/withHeaders.spec.ts @@ -37,7 +37,70 @@ describe('withHeaders', () => { const { port } = server.address() as AddressInfo; const res = await fetchWithExtraHeaders(`http://127.0.0.1:${port}`, { headers: { id: 'id', bar: 'bar' } }); - await expect(res.text()).resolves.toMatchInlineSnapshot(`"{"id":"override","bar":"bar","foo":"foo"}"`); + await expect(res.json()).resolves.toStrictEqual({ id: 'override', bar: 'bar', foo: 'foo' }); + server.close(); + }); + + it('add headers when getHeaders returns a promise', async () => { + const fetchWithAsyncHeaders = withHeaders(fetch, async () => ({ foo: 'foo', id: 'override' })); + const server = createServer((req, res) => { + const response = JSON.stringify({ id: req.headers.id, bar: req.headers.bar, foo: req.headers.foo }); + res.writeHead(200); + res.end(response); + }); + + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const res = await fetchWithAsyncHeaders(`http://127.0.0.1:${port}`, { headers: { id: 'id', bar: 'bar' } }); + await expect(res.json()).resolves.toStrictEqual({ id: 'override', bar: 'bar', foo: 'foo' }); + server.close(); + }); + + it('merges headers when init.headers is a Headers instance', async () => { + const server = createServer((req, res) => { + const response = JSON.stringify({ foo: req.headers.foo, bar: req.headers.bar }); + res.writeHead(200); + res.end(response); + }); + + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const headers = new Headers({ bar: 'baz' }); + const fetchWithHeadersInstance = withHeaders(fetch, () => ({ foo: 'bar' })); + const res = await fetchWithHeadersInstance(`http://127.0.0.1:${port}`, { headers }); + await expect(res.json()).resolves.toStrictEqual({ foo: 'bar', bar: 'baz' }); + server.close(); + }); + + it('merges headers when init.headers is an object and getHeaders returns a Headers instance', async () => { + const server = createServer((req, res) => { + const response = JSON.stringify({ foo: req.headers.foo, bar: req.headers.bar }); + res.writeHead(200); + res.end(response); + }); + + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const headers = new Headers({ bar: 'baz' }); + const fetchWithHeadersInstance = withHeaders(fetch, () => new Headers({ foo: 'bar' })); + const res = await fetchWithHeadersInstance(`http://127.0.0.1:${port}`, { headers }); + await expect(res.json()).resolves.toStrictEqual({ foo: 'bar', bar: 'baz' }); + server.close(); + }); + + it('merges headers when init.headers is a Headers instance and getHeaders returns a Headers instance', async () => { + const server = createServer((req, res) => { + const response = JSON.stringify({ foo: req.headers.foo, bar: req.headers.bar }); + res.writeHead(200); + res.end(response); + }); + + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const headers = new Headers({ bar: 'baz' }); + const fetchWithHeadersInstance = withHeaders(fetch, () => new Headers({ foo: 'bar' })); + const res = await fetchWithHeadersInstance(`http://127.0.0.1:${port}`, { headers }); + await expect(res.json()).resolves.toStrictEqual({ foo: 'bar', bar: 'baz' }); server.close(); }); }); diff --git a/tests/specs/withToken.spec.ts b/tests/specs/withToken.spec.ts new file mode 100644 index 0000000..1f4f9a8 --- /dev/null +++ b/tests/specs/withToken.spec.ts @@ -0,0 +1,41 @@ +import { createServer } from 'node:http'; +import type { AddressInfo } from 'node:net'; +import { describe, it, expect } from 'vitest'; +import { withToken } from '../../src/index.js'; +import { waitForServer } from '../services/waitForServer.js'; + +describe('withToken', () => { + it('adds token to headers', async () => { + const server = createServer((req, res) => { + const response = JSON.stringify({ + authorization: req.headers.authorization, + }); + res.writeHead(200); + res.end(response); + }); + + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const fetchWithToken = withToken(() => 'mytoken', fetch); + const res = await fetchWithToken(`http://127.0.0.1:${port}`); + await expect(res.text()).resolves.toBe('{"authorization":"Bearer mytoken"}'); + }); + + it('merges headers when init.headers is a Headers instance', async () => { + const server = createServer((req, res) => { + const response = JSON.stringify({ + authorization: req.headers.authorization, + foo: req.headers.foo, + }); + res.writeHead(200); + res.end(response); + }); + await waitForServer(server); + const { port } = server.address() as AddressInfo; + const headers = new Headers({ foo: 'bar' }); + const fetchWithToken = withToken(() => 'mytoken', fetch); + const res = await fetchWithToken(`http://127.0.0.1:${port}`, { headers }); + await expect(res.text()).resolves.toBe('{"authorization":"Bearer mytoken","foo":"bar"}'); + server.close(); + }); +});