@data-eden/network
Advanced tools
Comparing version 0.2.2-beta.1 to 0.2.2-beta.2
@@ -1,45 +0,99 @@ | ||
import { beforeAll, afterAll, afterEach, expect, test, describe } from 'vitest'; | ||
import { | ||
beforeAll, | ||
beforeEach, | ||
afterAll, | ||
afterEach, | ||
expect, | ||
test, | ||
describe, | ||
} from 'vitest'; | ||
import * as http from 'http'; | ||
import { Response, Request } from 'cross-fetch'; | ||
// TODO: this import _should_ use `msw/node`, but the types do not resolve | ||
// properly when using `moduleResolution: 'node16'` | ||
import { setupServer } from 'msw/lib/node/index.js'; | ||
import { MockedRequest, rest } from 'msw'; | ||
import { buildFetch, Middleware, NormalizedFetch } from '@data-eden/network'; | ||
import { createServer } from '@data-eden/shared-test-utilities'; | ||
describe('@data-eden/fetch', function () { | ||
const restHandlers = [ | ||
rest.get('http://www.example.com/resource', (req, res, ctx) => { | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ status: 'success', headers: req.headers }) | ||
function getPrefixedIncomingHttpHeaders( | ||
headers: http.IncomingHttpHeaders, | ||
prefix: string | ||
): object { | ||
const result: Record<string, string | string[]> = {}; | ||
for (const key in headers) { | ||
const value = headers[key]; | ||
if (key.toLowerCase().startsWith(prefix.toLowerCase()) && value) { | ||
result[key] = value; | ||
} | ||
} | ||
return result; | ||
} | ||
function getPrefixedHeaders(headers: Headers, prefix: string): object { | ||
const result: Record<string, string | string[]> = {}; | ||
for (const [key, value] of headers) { | ||
if (key.toLowerCase().startsWith(prefix.toLowerCase()) && value) { | ||
result[key] = value; | ||
} | ||
} | ||
return result; | ||
} | ||
describe('@data-eden/fetch', async function () { | ||
const server = await createServer(); | ||
beforeAll(async () => await server.listen()); | ||
beforeEach(() => { | ||
server.get( | ||
'/resource', | ||
(request: http.IncomingMessage, response: http.ServerResponse) => { | ||
response.writeHead(200, { 'Content-Type': 'application/json' }); | ||
response.end( | ||
JSON.stringify({ | ||
customOriginalRequestHeaders: getPrefixedIncomingHttpHeaders( | ||
request.headers, | ||
'X-' | ||
), | ||
status: 'success', | ||
}) | ||
); | ||
} | ||
), | ||
server.post( | ||
'/resource/preview', | ||
(request: http.IncomingMessage, response: http.ServerResponse) => { | ||
response.writeHead(200, { 'Content-Type': 'application/json' }); | ||
response.end( | ||
JSON.stringify({ | ||
method: request.method, | ||
customOriginalRequestHeaders: getPrefixedIncomingHttpHeaders( | ||
request.headers, | ||
'X-' | ||
), | ||
status: 'success', | ||
}) | ||
); | ||
} | ||
); | ||
}), | ||
rest.post('http://www.example.com/resource/preview', (req, res, ctx) => { | ||
return res( | ||
ctx.status(200), | ||
ctx.json({ status: 'success', method: 'POST', headers: req.headers }) | ||
); | ||
}), | ||
rest.get('http://www.example.com/analytics', (_req, res, ctx) => { | ||
return res( | ||
ctx.set('x-call-id', '1234567'), | ||
ctx.status(200), | ||
ctx.json({ status: 'success' }) | ||
); | ||
}), | ||
]; | ||
const server = setupServer(...restHandlers); | ||
server.get( | ||
'/analytics', | ||
(_request: http.IncomingMessage, response: http.ServerResponse) => { | ||
response.writeHead(200, { | ||
'Content-Type': 'application/json', | ||
'x-call-id': '1234567', | ||
}); | ||
server.events.on('request:unhandled', (req: MockedRequest) => { | ||
console.log('%s %s has no handler', req.method, req.url.href); | ||
throw new Error('handle unhandled request!'); | ||
response.end( | ||
JSON.stringify({ | ||
status: 'success', | ||
}) | ||
); | ||
} | ||
); | ||
}); | ||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); | ||
afterEach(() => server.reset()); | ||
afterAll(() => server.close()); | ||
afterEach(() => server.resetHandlers()); | ||
@@ -62,2 +116,19 @@ const noopMiddleware: Middleware = async ( | ||
test('throws a helpful error when `fetch` is undefined', async () => { | ||
const originalFetch = globalThis.fetch; | ||
try { | ||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-explicit-any*/ | ||
delete (globalThis as any).fetch; | ||
expect(() => { | ||
buildFetch([]); | ||
}).toThrowErrorMatchingInlineSnapshot( | ||
'"@data-eden/network requires `fetch` to be available on`globalThis`. Did you forget to setup `cross-fetch/polyfill` before calling @data-eden/network\'s `buildFetch`?"' | ||
); | ||
} finally { | ||
globalThis.fetch = originalFetch; | ||
} | ||
}); | ||
test('should be able to return fetch without middlewares passed', async () => { | ||
@@ -68,3 +139,3 @@ expect.assertions(2); | ||
const response = await fetch('http://www.example.com/resource'); | ||
const response = await fetch(server.buildUrl('/resource')); | ||
@@ -74,12 +145,3 @@ expect(response.status).toEqual(200); | ||
{ | ||
"headers": { | ||
"headers": { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"host": "www.example.com", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
}, | ||
"names": {}, | ||
}, | ||
"customOriginalRequestHeaders": {}, | ||
"status": "success", | ||
@@ -95,21 +157,13 @@ } | ||
const response = await fetch('http://www.example.com/resource'); | ||
const response = await fetch(server.buildUrl('/resource')); | ||
expect(response.status).toEqual(200); | ||
expect(await response.json()).toMatchInlineSnapshot(` | ||
{ | ||
"headers": { | ||
"headers": { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"host": "www.example.com", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
"x-csrf": "a totally legit request", | ||
}, | ||
"names": {}, | ||
}, | ||
"status": "success", | ||
} | ||
`); | ||
{ | ||
"customOriginalRequestHeaders": { | ||
"x-csrf": "a totally legit request", | ||
}, | ||
"status": "success", | ||
} | ||
`); | ||
}); | ||
@@ -151,3 +205,3 @@ | ||
const tunneledRequest = new Request( | ||
`${url.protocol}//${url.hostname}${url.pathname}`, | ||
`${url.protocol}//${url.hostname}:${url.port}${url.pathname}`, | ||
{ | ||
@@ -165,3 +219,3 @@ method: 'POST', | ||
const response = await fetch('http://www.example.com/resource/preview'); | ||
const response = await fetch(server.buildUrl('/resource/preview')); | ||
@@ -171,15 +225,5 @@ expect(response.status).toEqual(200); | ||
{ | ||
"headers": { | ||
"headers": { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"content-length": "0", | ||
"content-type": "application/x-www-form-urlencoded;charset=UTF-8", | ||
"host": "www.example.com", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
"x-csrf": "a totally legit request", | ||
"x-http-method-override": "GET", | ||
}, | ||
"names": {}, | ||
"customOriginalRequestHeaders": { | ||
"x-csrf": "a totally legit request", | ||
"x-http-method-override": "GET", | ||
}, | ||
@@ -199,7 +243,5 @@ "method": "POST", | ||
) => { | ||
expect(request.headers).toMatchInlineSnapshot(` | ||
Headers { | ||
Symbol(map): {}, | ||
} | ||
`); | ||
expect(getPrefixedHeaders(request.headers, 'X-')).toMatchInlineSnapshot( | ||
'{}' | ||
); | ||
@@ -213,9 +255,7 @@ return next(request); | ||
) => { | ||
expect(request.headers).toMatchInlineSnapshot(` | ||
Headers { | ||
Symbol(map): {}, | ||
} | ||
`); | ||
expect(getPrefixedHeaders(request.headers, 'X-')).toMatchInlineSnapshot( | ||
'{}' | ||
); | ||
request.headers.set('two', 'true'); | ||
request.headers.set('x-two', 'true'); | ||
@@ -229,13 +269,9 @@ return next(request); | ||
) => { | ||
expect(request.headers).toMatchInlineSnapshot(` | ||
Headers { | ||
Symbol(map): { | ||
"two": [ | ||
"true", | ||
], | ||
}, | ||
expect(getPrefixedHeaders(request.headers, 'X-')).toMatchInlineSnapshot(` | ||
{ | ||
"x-two": "true", | ||
} | ||
`); | ||
request.headers.set('three', 'true'); | ||
request.headers.set('x-three', 'true'); | ||
@@ -247,17 +283,9 @@ return next(request); | ||
const response = await fetch('http://www.example.com/resource'); | ||
const response = await fetch(server.buildUrl('/resource')); | ||
expect(response.status).toEqual(200); | ||
expect(await response.json()).toMatchInlineSnapshot(` | ||
{ | ||
"headers": { | ||
"headers": { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"host": "www.example.com", | ||
"three": "true", | ||
"two": "true", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
}, | ||
"names": {}, | ||
"customOriginalRequestHeaders": { | ||
"x-three": "true", | ||
"x-two": "true", | ||
}, | ||
@@ -269,2 +297,51 @@ "status": "success", | ||
test('earlier middlewares can wrap the results of later middlewares', async () => { | ||
const steps: string[] = []; | ||
const a: Middleware = async (request: Request, next: NormalizedFetch) => { | ||
steps.push('a start'); | ||
const result = await next(request); | ||
steps.push('a end'); | ||
return result; | ||
}; | ||
const b: Middleware = async (request: Request, next: NormalizedFetch) => { | ||
steps.push('b start'); | ||
const result = await next(request); | ||
steps.push('b end'); | ||
return result; | ||
}; | ||
const c: Middleware = async (request: Request, next: NormalizedFetch) => { | ||
steps.push('c start'); | ||
const result = await next(request); | ||
steps.push('c end'); | ||
return result; | ||
}; | ||
const fetch = buildFetch([a, b, c]); | ||
const response = await fetch(server.buildUrl('/resource')); | ||
expect(steps).toMatchInlineSnapshot(` | ||
[ | ||
"a start", | ||
"b start", | ||
"c start", | ||
"c end", | ||
"b end", | ||
"a end", | ||
] | ||
`); | ||
expect(response.status).toEqual(200); | ||
}); | ||
test('should be able to introspect on the response as a middleware', async () => { | ||
@@ -279,15 +356,5 @@ expect.assertions(4); | ||
expect(response.headers).toMatchInlineSnapshot(` | ||
Headers { | ||
Symbol(map): { | ||
"content-type": [ | ||
"application/json", | ||
], | ||
"x-call-id": [ | ||
"1234567", | ||
], | ||
"x-powered-by": [ | ||
"msw", | ||
], | ||
}, | ||
expect(getPrefixedHeaders(response.headers, 'X-')).toMatchInlineSnapshot(` | ||
{ | ||
"x-call-id": "1234567", | ||
} | ||
@@ -302,3 +369,3 @@ `); | ||
const response = await fetch('http://www.example.com/analytics'); | ||
const response = await fetch(server.buildUrl('/analytics')); | ||
expect(response.status).toEqual(200); | ||
@@ -315,26 +382,18 @@ expect(await response.json()).toMatchInlineSnapshot(` | ||
server.use( | ||
rest.get('http://www.example.com/foo', (req, res, ctx) => { | ||
expect(req.headers).toMatchInlineSnapshot(` | ||
HeadersPolyfill { | ||
"headers": { | ||
"accept": "*/*", | ||
"accept-encoding": "gzip,deflate", | ||
"connection": "close", | ||
"host": "www.example.com", | ||
"user-agent": "node-fetch/1.0 (+https://github.com/bitinn/node-fetch)", | ||
"x-track": "signup", | ||
}, | ||
"names": Map { | ||
"x-track" => "x-track", | ||
"accept" => "accept", | ||
"user-agent" => "user-agent", | ||
"accept-encoding" => "accept-encoding", | ||
"connection" => "connection", | ||
"host" => "host", | ||
}, | ||
} | ||
`); | ||
return res(ctx.status(200), ctx.json({ status: 'success' })); | ||
}) | ||
server.get( | ||
'/foo', | ||
(request: http.IncomingMessage, response: http.ServerResponse) => { | ||
expect(getPrefixedIncomingHttpHeaders(request.headers, 'X-')) | ||
.toMatchInlineSnapshot(` | ||
{ | ||
"x-track": "signup", | ||
} | ||
`); | ||
response.writeHead(200, { 'Content-Type': 'application/json' }); | ||
response.end( | ||
JSON.stringify({ | ||
status: 'success', | ||
}) | ||
); | ||
} | ||
); | ||
@@ -363,5 +422,6 @@ | ||
let fetch = buildFetch([headerTransformationMiddleware]); | ||
let response = await fetch('http://www.example.com/foo', { | ||
let response = await fetch(server.buildUrl('/foo'), { | ||
headers: { 'X-Use-Case': 'signup' }, | ||
}); | ||
expect(await response.json()).toMatchInlineSnapshot(` | ||
@@ -368,0 +428,0 @@ { |
@@ -1,5 +0,4 @@ | ||
import { fetch } from 'cross-fetch'; | ||
export declare type Fetch = typeof fetch; | ||
export declare type NormalizedFetch = (request: Request) => Promise<Response>; | ||
export declare type Middleware = ( | ||
export type Fetch = typeof fetch; | ||
export type NormalizedFetch = (request: Request) => Promise<Response>; | ||
export type Middleware = ( | ||
/** | ||
@@ -27,3 +26,3 @@ * Request object to be manipulated by middleware | ||
/** | ||
* override the default cross-fetch implementation | ||
* override the default fetch implementation | ||
*/ | ||
@@ -30,0 +29,0 @@ fetch: Fetch; |
@@ -1,2 +0,1 @@ | ||
import { Request, fetch } from 'cross-fetch'; | ||
function combine(next, middleware) { | ||
@@ -14,2 +13,5 @@ return async (request) => { | ||
export function buildFetch(middlewares, options) { | ||
if (typeof fetch === 'undefined') { | ||
throw new Error("@data-eden/network requires `fetch` to be available on`globalThis`. Did you forget to setup `cross-fetch/polyfill` before calling @data-eden/network's `buildFetch`?"); | ||
} | ||
const _fetch = options?.fetch || fetch; | ||
@@ -16,0 +18,0 @@ const curriedMiddlewares = [...middlewares] |
{ | ||
"name": "@data-eden/network", | ||
"version": "0.2.2-beta.1", | ||
"version": "0.2.2-beta.2", | ||
"repository": { | ||
@@ -25,8 +25,7 @@ "type": "git", | ||
}, | ||
"dependencies": { | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"@data-eden/shared-test-utilities": "0.2.2-beta.2", | ||
"cross-fetch": "^3.1.5" | ||
}, | ||
"devDependencies": { | ||
"msw": "^0.42.0" | ||
}, | ||
"volta": { | ||
@@ -33,0 +32,0 @@ "extends": "../../package.json" |
@@ -1,3 +0,1 @@ | ||
import { Request, fetch } from 'cross-fetch'; | ||
export type Fetch = typeof fetch; | ||
@@ -32,3 +30,3 @@ | ||
/** | ||
* override the default cross-fetch implementation | ||
* override the default fetch implementation | ||
*/ | ||
@@ -57,2 +55,8 @@ fetch: Fetch; | ||
): Fetch { | ||
if (typeof fetch === 'undefined') { | ||
throw new Error( | ||
"@data-eden/network requires `fetch` to be available on`globalThis`. Did you forget to setup `cross-fetch/polyfill` before calling @data-eden/network's `buildFetch`?" | ||
); | ||
} | ||
const _fetch: NormalizedFetch = options?.fetch || fetch; | ||
@@ -59,0 +63,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
0
488
109864
2
2
- Removedcross-fetch@^3.1.5
- Removedcross-fetch@3.2.0(transitive)
- Removednode-fetch@2.7.0(transitive)
- Removedtr46@0.0.3(transitive)
- Removedwebidl-conversions@3.0.1(transitive)
- Removedwhatwg-url@5.0.0(transitive)