Files
vercel/packages/node-bridge/test/helpers.test.js
2022-02-17 20:36:37 -08:00

843 lines
24 KiB
JavaScript
Vendored

const fetch = require('node-fetch');
const listen = require('test-listen');
const qs = require('querystring');
const { createServerWithHelpers } = require('../helpers');
const mockListener = jest.fn();
const consumeEventMock = jest.fn();
const mockBridge = { consumeEvent: consumeEventMock };
let server;
let url;
async function fetchWithProxyReq(_url, opts = {}) {
if (opts.body) {
// eslint-disable-next-line
opts = { ...opts, body: Buffer.from(opts.body) };
}
consumeEventMock.mockImplementationOnce(() => opts);
return fetch(_url, {
...opts,
headers: { ...opts.headers, 'x-now-bridge-request-id': '2' },
});
}
beforeEach(async () => {
mockListener.mockClear();
consumeEventMock.mockClear();
mockListener.mockImplementation((req, res) => {
res.send('hello');
});
consumeEventMock.mockImplementation(() => ({}));
server = createServerWithHelpers(mockListener, mockBridge);
url = await listen(server);
});
afterEach(async () => {
await server.close();
});
describe('contract with @vercel/node-bridge', () => {
test('should call consumeEvent with the correct reqId', async () => {
await fetchWithProxyReq(`${url}/`);
expect(consumeEventMock).toHaveBeenLastCalledWith('2');
});
test('should not expose the request id header', async () => {
await fetchWithProxyReq(`${url}/`, { headers: { 'x-test-header': 'ok' } });
const [{ headers }] = mockListener.mock.calls[0];
expect(headers['x-now-bridge-request-id']).toBeUndefined();
expect(headers['x-test-header']).toBe('ok');
});
});
describe('all helpers', () => {
const nowHelpers = [
['query', 0],
['cookies', 0],
['body', 0],
['status', 1],
['redirect', 1],
['send', 1],
['json', 1],
];
test('should not recalculate req properties twice', async () => {
const spy = jest.fn(() => {});
const nowReqHelpers = nowHelpers.filter(([, i]) => i === 0);
mockListener.mockImplementation((req, res) => {
spy(...nowReqHelpers.map(h => req[h]));
spy(...nowReqHelpers.map(h => req[h]));
res.end();
});
await fetchWithProxyReq(`${url}/?who=bill`, {
method: 'POST',
body: JSON.stringify({ who: 'mike' }),
headers: { 'content-type': 'application/json', cookie: 'who=jim' },
});
// here we test that bodySpy is called twice with exactly the same arguments
for (let i = 0; i < 3; i += 1) {
expect(spy.mock.calls[0][i]).toBe(spy.mock.calls[1][i]);
}
});
test('should be able to overwrite request properties', async () => {
const spy = jest.fn(() => {});
mockListener.mockImplementation((...args) => {
nowHelpers.forEach(([prop, n]) => {
/* eslint-disable */
args[n][prop] = 'ok';
args[n][prop] = 'ok2';
spy(args[n][prop]);
});
args[1].end();
});
await fetchWithProxyReq(url);
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
test('should be able to reconfig request properties', async () => {
const spy = jest.fn(() => {});
mockListener.mockImplementation((...args) => {
nowHelpers.forEach(([prop, n]) => {
// eslint-disable-next-line
Object.defineProperty(args[n], prop, { value: 'ok' });
Object.defineProperty(args[n], prop, { value: 'ok2' });
spy(args[n][prop]);
});
args[1].end();
});
await fetchWithProxyReq(url);
nowHelpers.forEach((_, i) => expect(spy.mock.calls[i][0]).toBe('ok2'));
});
});
describe('req.query', () => {
test('req.query should reflect querystring in the url', async () => {
await fetchWithProxyReq(`${url}/?who=bill&where=us`);
expect(mockListener.mock.calls[0][0].query).toMatchObject({
who: 'bill',
where: 'us',
});
});
test('req.query should turn multiple params with same name into an array', async () => {
await fetchWithProxyReq(`${url}/?a=2&a=1`);
expect(mockListener.mock.calls[0][0].query).toMatchObject({
a: ['2', '1'],
});
});
test('req.query should be {} when there is no querystring', async () => {
await fetchWithProxyReq(url);
const [{ query }] = mockListener.mock.calls[0];
expect(Object.keys(query).length).toBe(0);
});
});
describe('req.cookies', () => {
test('req.cookies should reflect req.cookie header', async () => {
await fetchWithProxyReq(url, {
headers: {
cookie: 'who=bill; where=us',
},
});
expect(mockListener.mock.calls[0][0].cookies).toMatchObject({
who: 'bill',
where: 'us',
});
});
});
describe('req.body', () => {
test('req.body should be undefined by default', async () => {
await fetchWithProxyReq(url);
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
test('req.body should be undefined if content-type is not defined', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
});
expect(mockListener.mock.calls[0][0].body).toBe(undefined);
});
test('req.body should be a string when content-type is `text/plain`', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'text/plain' },
});
expect(mockListener.mock.calls[0][0].body).toBe('hello');
});
test('req.body should be a buffer when content-type is `application/octet-stream`', async () => {
await fetchWithProxyReq(url, {
method: 'POST',
body: 'hello',
headers: { 'content-type': 'application/octet-stream' },
});
const [{ body }] = mockListener.mock.calls[0];
const str = body.toString();
expect(Buffer.isBuffer(body)).toBe(true);
expect(str).toBe('hello');
});
test('req.body should be an object when content-type is `application/x-www-form-urlencoded`', async () => {
const obj = { who: 'mike' };
await fetchWithProxyReq(url, {
method: 'POST',
body: qs.encode(obj),
headers: { 'content-type': 'application/x-www-form-urlencoded' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(obj);
});
test('req.body should be an object when content-type is `application/json`', async () => {
const json = {
who: 'bill',
where: 'us',
};
await fetchWithProxyReq(url, {
method: 'POST',
body: JSON.stringify(json),
headers: { 'content-type': 'application/json' },
});
expect(mockListener.mock.calls[0][0].body).toMatchObject(json);
});
test('should work when body is empty and content-type is `application/json`', async () => {
mockListener.mockImplementation((req, res) => {
console.log(req.body);
res.end();
});
const res = await fetchWithProxyReq(url, {
method: 'POST',
body: '',
headers: { 'content-type': 'application/json' },
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({});
});
test('should be able to try/catch parse errors', async () => {
const bodySpy = jest.fn(() => {});
mockListener.mockImplementation((req, res) => {
try {
if (req.body === undefined) res.status(400);
} catch (error) {
bodySpy(error);
} finally {
res.end();
}
});
await fetchWithProxyReq(url, {
method: 'POST',
body: '{"wrong":"json"',
headers: { 'content-type': 'application/json' },
});
expect(bodySpy).toHaveBeenCalled();
const [error] = bodySpy.mock.calls[0];
expect(error.message).toMatch(/invalid json/i);
expect(error.statusCode).toBe(400);
});
});
describe('res.status', () => {
test('res.status() should set the status code', async () => {
mockListener.mockImplementation((req, res) => {
res.status(404);
res.end();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(404);
});
test('res.status() should be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.status(404));
res.end();
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
});
describe('res.redirect', () => {
test('should redirect to login', async () => {
mockListener.mockImplementation((req, res) => {
res.redirect('/login');
res.end();
});
const res = await fetchWithProxyReq(url, { redirect: 'manual' });
expect(res.status).toBe(307);
expect(res.headers.get('location')).toBe(url + '/login');
});
test('should redirect with status code 301', async () => {
mockListener.mockImplementation((req, res) => {
res.redirect(301, '/login');
res.end();
});
const res = await fetchWithProxyReq(url, { redirect: 'manual' });
expect(res.status).toBe(301);
expect(res.headers.get('location')).toBe(url + '/login');
});
test('should show friendly error for invalid redirect', async () => {
let error;
mockListener.mockImplementation((req, res) => {
try {
res.redirect(307);
} catch (err) {
error = err;
}
res.end();
});
await fetchWithProxyReq(url, { redirect: 'manual' });
expect(error.message).toBe(
`Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').`
);
});
test('should show friendly error in case of passing null as first argument redirect', async () => {
let error;
mockListener.mockImplementation((req, res) => {
try {
res.redirect(null);
} catch (err) {
error = err;
}
res.end();
});
await fetchWithProxyReq(url, { redirect: 'manual' });
expect(error.message).toBe(
`Invalid redirect arguments. Please use a single argument URL, e.g. res.redirect('/destination') or use a status code and URL, e.g. res.redirect(307, '/destination').`
);
});
});
// tests based on expressjs test suite
// see https://github.com/expressjs/express/blob/master/test/res.send.js
describe('res.send', () => {
test('should be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.send('hello'));
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
describe('res.send()', () => {
test('should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(await res.text()).toBe('');
});
});
describe('.send(null)', () => {
test('should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send(null);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-length')).toBe('0');
expect(await res.text()).toBe('');
});
});
describe('.send(undefined)', () => {
test('should set body to ""', async () => {
mockListener.mockImplementation((req, res) => {
res.send(undefined);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(await res.text()).toBe('');
});
});
describe('.send(String)', () => {
test('should send as html', async () => {
mockListener.mockImplementation((req, res) => {
res.send('<p>hey</p>');
});
const res = await fetchWithProxyReq(url);
expect(res.headers.get('content-type')).toBe('text/html; charset=utf-8');
expect(await res.text()).toBe('<p>hey</p>');
});
test('should set Content-Length', async () => {
mockListener.mockImplementation((req, res) => {
res.send('½ + ¼ = ¾');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(Number(res.headers.get('content-length'))).toBe(12);
expect(await res.text()).toBe('½ + ¼ = ¾');
});
test('should set ETag', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Array(1000).join('-'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe(
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
);
});
test('should not override Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/plain');
res.send('hey');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
expect(await res.text()).toBe('hey');
});
test('should override charset in Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=iso-8859-1');
res.send('hey');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
expect(await res.text()).toBe('hey');
});
});
describe('.send(Buffer)', () => {
test('should keep charset in Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=iso-8859-1');
res.send(Buffer.from('hi'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe(
'text/plain; charset=iso-8859-1'
);
expect(await res.text()).toBe('hi');
});
test('should set Content-Length', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Buffer.from('½ + ¼ = ¾'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(Number(res.headers.get('content-length'))).toBe(12);
expect(await res.text()).toBe('½ + ¼ = ¾');
});
test('should send as octet-stream', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Buffer.from('hello'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('application/octet-stream');
expect((await res.buffer()).toString('hex')).toBe(
Buffer.from('hello').toString('hex')
);
});
test('should set ETag', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Buffer.alloc(999, '-'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe(
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
);
});
test('should not override Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(Buffer.from('hey'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
expect(await res.text()).toBe('hey');
});
test('should not override ETag', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('ETag', '"foo"');
res.send(Buffer.from('hey'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe('"foo"');
expect(await res.text()).toBe('hey');
});
});
describe('.send(Object)', () => {
test('should send as application/json', async () => {
mockListener.mockImplementation((req, res) => {
res.send({ name: 'tobi' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('Content-Type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('{"name":"tobi"}');
});
});
describe('when the request method is HEAD', () => {
test('should ignore the body', async () => {
mockListener.mockImplementation((req, res) => {
res.send('yay');
});
// TODO: fix this test
// node-fetch is automatically ignoring the body so this test will never fail
const res = await fetchWithProxyReq(url, { method: 'HEAD' });
expect(res.status).toBe(200);
expect((await res.buffer()).toString()).toBe('');
});
});
describe('when .statusCode is 204', () => {
test('should strip Content-* fields, Transfer-Encoding field, and body', async () => {
mockListener.mockImplementation((req, res) => {
res.statusCode = 204;
res.setHeader('Transfer-Encoding', 'chunked');
res.send('foo');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(204);
expect(res.headers.get('Content-Type')).toBe(null);
expect(res.headers.get('Content-Length')).toBe(null);
expect(res.headers.get('Transfer-Encoding')).toBe(null);
expect(await res.text()).toBe('');
});
});
describe('when .statusCode is 304', () => {
test('should strip Content-* fields, Transfer-Encoding field, and body', async () => {
mockListener.mockImplementation((req, res) => {
res.statusCode = 304;
res.setHeader('Transfer-Encoding', 'chunked');
res.send('foo');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(304);
expect(res.headers.get('Content-Type')).toBe(null);
expect(res.headers.get('Content-Length')).toBe(null);
expect(res.headers.get('Transfer-Encoding')).toBe(null);
expect(await res.text()).toBe('');
});
});
// test('should always check regardless of length', async () => {
// const etag = '"asdf"';
// mockListener.mockImplementation((req, res) => {
// res.setHeader('ETag', etag);
// res.send('hey');
// });
// const res = await fetchWithProxyReq(url, {
// headers: { 'If-None-Match': etag },
// });
// expect(res.status).toBe(304);
// });
// test('should respond with 304 Not Modified when fresh', async () => {
// const etag = '"asdf"';
// mockListener.mockImplementation((req, res) => {
// res.setHeader('ETag', etag);
// res.send(Array(1000).join('-'));
// });
// const res = await fetchWithProxyReq(url, {
// headers: { 'If-None-Match': etag },
// });
// expect(res.status).toBe(304);
// });
// test('should not perform freshness check unless 2xx or 304', async () => {
// const etag = '"asdf"';
// mockListener.mockImplementation((req, res) => {
// res.status(500);
// res.setHeader('ETag', etag);
// res.send('hey');
// });
// const res = await fetchWithProxyReq(url, {
// headers: { 'If-None-Match': etag },
// });
// expect(res.status).toBe(500);
// expect(await res.text()).toBe('hey');
// });
describe('etag', () => {
test('should send ETag', async () => {
mockListener.mockImplementation((req, res) => {
res.send('kajdslfkasdf');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe('W/"c-IgR/L5SF7CJQff4wxKGF/vfPuZ0"');
});
test('should send ETag for empty string response', async () => {
mockListener.mockImplementation((req, res) => {
res.send('');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe('W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"');
});
test('should send ETag for long response', async () => {
mockListener.mockImplementation((req, res) => {
res.send(Array(1000).join('-'));
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe(
'W/"3e7-qPnkJ3CVdVhFJQvUBfF10TmVA7g"'
);
});
test('should not override ETag when manually set', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('etag', '"asdf"');
res.send('hello');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe('"asdf"');
});
test('should not send ETag for res.send()', async () => {
mockListener.mockImplementation((req, res) => {
res.send();
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('ETag')).toBe(null);
});
});
});
// tests based on expressjs test suite
// see https://github.com/expressjs/express/blob/master/test/res.json.js
describe('res.json', () => {
test('should send be chainable', async () => {
const spy = jest.fn();
mockListener.mockImplementation((req, res) => {
spy(res, res.json({ hello: 'world' }));
});
await fetchWithProxyReq(url);
const [a, b] = spy.mock.calls[0];
expect(a).toBe(b);
});
test('res.json() should send an empty body', async () => {
mockListener.mockImplementation((req, res) => {
res.json();
});
await fetchWithProxyReq(url);
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('');
});
describe('.json(object)', () => {
test('should not override previous Content-Types', async () => {
mockListener.mockImplementation((req, res) => {
res.setHeader('content-type', 'application/vnd.example+json');
res.json({ hello: 'world' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/vnd.example+json; charset=utf-8'
);
expect(await res.text()).toBe('{"hello":"world"}');
});
test('should set Content-Length and Content-Type', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ hello: '½ + ¼ = ¾' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(Number(res.headers.get('content-length'))).toBe(24);
expect(await res.text()).toBe('{"hello":"½ + ¼ = ¾"}');
});
describe('when given primitives', () => {
test('should respond with json for null', async () => {
mockListener.mockImplementation((req, res) => {
res.json(null);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('null');
});
test('should respond with json for Number', async () => {
mockListener.mockImplementation((req, res) => {
res.json(300);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('300');
});
test('should respond with json for String', async () => {
mockListener.mockImplementation((req, res) => {
res.json('str');
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('"str"');
});
});
test('should respond with json when given an array', async () => {
mockListener.mockImplementation((req, res) => {
res.json(['foo', 'bar', 'baz']);
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('["foo","bar","baz"]');
});
test('should respond with json when given an object', async () => {
mockListener.mockImplementation((req, res) => {
res.json({ name: 'tobi' });
});
const res = await fetchWithProxyReq(url);
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toBe(
'application/json; charset=utf-8'
);
expect(await res.text()).toBe('{"name":"tobi"}');
});
});
});