@v4fire/core
Version:
V4Fire core library
719 lines (718 loc) • 26.3 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _express = _interopRequireDefault(require("express"));
var _env = require("../../core/env");
var _combinators = require("../../core/iter/combinators");
var _stream = require("../../core/json/stream");
var _data = _interopRequireWildcard(require("../../core/data"));
var _request = _interopRequireWildcard(require("../../core/request"));
var _const = require("../../core/request/const");
var _response = _interopRequireDefault(require("../../core/request/response"));
var _headers = _interopRequireDefault(require("../../core/request/headers"));
var _node = _interopRequireDefault(require("../../core/request/engines/node"));
var _fetch = _interopRequireDefault(require("../../core/request/engines/fetch"));
var _xhr = _interopRequireDefault(require("../../core/request/engines/xhr"));
var _provider = _interopRequireDefault(require("../../core/request/engines/provider"));
var _class;
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
let TestRequestChainProvider = (0, _data.provider)(_class = class TestRequestChainProvider extends _data.default {
static request = _data.default.request({
engine: (0, _provider.default)(_data.default)
});
}) || _class;
const emptyBodyStatuses = [204, 304],
customEmptyBodyStatuses = [222, 323, 424, 525],
faviconBase64 = 'AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAnISL6JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL5JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL1JyEi9ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEihCchIpgnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEixCUgIRMmICEvJyEi5ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIrYnISKSJyEi9ichIlxQREUAHxobAichIo4nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISJeJyEiICchIuAnISJJJiAhbCYgITgmICEnJyEi4ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEiXichIiAnISLdJyEihichIuknISKkIRwdBCchIoUnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIl4nISIgJyEi4ichIu4nISL/JyEi8CYgIT8mICEhJyEi3CchIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISJeJyEiHychIuUnISL/JyEi/ychIv8nISKrIh0eBiYhInwnISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEiXSYgITUnISLvJyEi/ychIv8nISL/JyEi9CYgIUcmICEbJyEi1ichIv8nISL/JyEi/ychIv8nISL/JyEi/ichImknISKjJyEi/ychIv8nISL/JyEi/ychIv8nISKzIRwdBiYhIX0nISL/JyEi/ychIv8nISL/JyEi/ychIvwnISK+JyEi9CchIv8nISL/JyEi/ychIv8nISL/JyEi9yYhIoonISKzJyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ichIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL6JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL/JyEi/ychIv8nISL6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==';
describe('core/request', () => {
const engines = new Map([['node', _node.default], ['fetch', _fetch.default], ['xhr', _xhr.default], ['provider', (0, _provider.default)('Provider')], ['chain provider', (0, _provider.default)(TestRequestChainProvider)]]);
let request, defaultEngine, logOptions;
let api, server;
beforeAll(async () => {
api = _request.globalOpts.api;
_request.globalOpts.api = 'https://run.mocky.io';
logOptions = await (0, _env.get)('log');
(0, _env.set)('log', {
patterns: []
});
defaultEngine = _const.defaultRequestOpts.engine;
});
beforeEach(() => {
if (server) {
server.close();
}
server = createServer();
});
afterAll(async () => {
_request.globalOpts.api = api;
await server.close();
_const.defaultRequestOpts.engine = defaultEngine;
(0, _env.set)('log', logOptions);
});
engines.forEach((engine, name) => {
describe(`with the "${name}" engine`, () => {
beforeAll(() => {
if (name.includes('provider')) {
_const.defaultRequestOpts.engine = defaultEngine;
request = (0, _request.default)({
engine
});
} else {
_const.defaultRequestOpts.engine = engine;
request = _request.default;
}
});
it('blob `get`', async () => {
const data = await request('http://localhost:4000/favicon.ico').data;
expect(data.type).toBe('image/x-icon');
expect(data.size).toBe(1150);
});
it('json `get`', async () => {
const data = await request('http://localhost:4000/json/1').data;
expect(data).toEqual({
id: 1,
value: 'things'
});
});
it('json `get` with caching', async () => {
const url = 'http://localhost:4000/json/1',
get = request({
cacheStrategy: 'forever',
cacheTTL: 10
});
await get(url).data;
const req = await get(url);
expect(req.cache).toBe('memory');
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
return new Promise(resolve => {
setTimeout(async () => {
{
const req = await get(url);
expect(req.cache).toBeUndefined();
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
req.dropCache();
}
{
const req = await get(url);
expect(req.cache).toBeUndefined();
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
}
resolve();
}, 15);
});
});
it('text/xml `get`', async () => {
const data = await request('http://localhost:4000/xml/text').data;
expect(data.querySelector('foo').textContent).toBe('Hello world');
});
it('application/xml `get`', async () => {
const data = await request('http://localhost:4000/xml/app').data;
expect(data.querySelector('foo').textContent).toBe('Hello world');
});
it('xml `get` with a query', async () => {
const data = await request('http://localhost:4000/search', {
query: {
q: 'bla'
}
}).data;
expect(data.querySelector('results').children[0].textContent).toBe('one');
});
it('json `post`', async () => {
const req = await request('http://localhost:4000/json', {
method: 'POST',
body: {
id: 12345,
value: 'abc-def-ghi'
}
});
expect(await req.data).toEqual({
message: 'Success'
});
expect(req.response.status).toBe(201);
expect(req.response.ok).toBe(true);
});
it('json `post` with the specified response status', async () => {
let err;
try {
await request('http://localhost:4000/json', {
method: 'POST',
okStatuses: 200,
body: {
id: 12345,
value: 'abc-def-ghi'
}
});
} catch (e) {
err = e;
}
expect(err).toBeInstanceOf(_request.RequestError);
expect(err.type).toBe(_request.RequestError.InvalidStatus);
expect(err.message).toBe('[invalidStatus] POST http://localhost:4000/json 201');
expect(err.details.request.method).toBe('POST');
expect(err.details.response.status).toBe(201);
});
it('json `post` with encoders/decoders', async () => {
const req = await request('http://localhost:4000/json', {
method: 'POST',
encoder: [data => {
data.id = 12345;
return Promise.resolve(data);
}, data => {
data.value = data.value.join('-');
return data;
}],
decoder: data => {
data.message = 'ok';
return data;
},
body: {
value: ['abc', 'def', 'ghi']
}
});
expect(await req.data).toEqual({
message: 'ok'
});
expect(req.response.status).toBe(201);
});
it('json `post` with middlewares', async () => {
const req = await request('http://localhost:4000/json', {
method: 'POST',
middlewares: {
addId({
opts
}) {
opts.body.id = 12345;
},
serializeValue({
opts
}) {
opts.body.value = opts.body.value.join('-');
}
},
body: {
value: ['abc', 'def', 'ghi']
}
});
expect(await req.data).toEqual({
message: 'Success'
});
expect(req.response.status).toBe(201);
});
it('json `post` with a middleware that returns a function', async () => {
const req = await request('http://localhost:4000/json', {
method: 'POST',
middlewares: {
fakeResponse({
ctx
}) {
return () => ctx.wrapAsResponse({
message: 'fake'
});
}
},
body: {
value: ['abc', 'def', 'ghi']
}
});
expect(await req.data).toEqual({
message: 'fake'
});
expect(req.response.status).toBe(200);
});
it('json `put` with headers', async () => {
const req = await request('http://localhost:4000/json/2', {
method: 'PUT',
headers: {
Accept: 'application/json'
}
});
expect(await req.data).toBe('{"message": "Success"}');
expect(req.response.headers.get('Content-Type')).toBeNull();
expect(await req.response.json()).toEqual({
message: 'Success'
});
expect(req.response.status).toBe(200);
});
it('providing an API schema', async () => {
const req = await request('/1', {
api: {
protocol: 'http',
zone: () => 'localhost',
domain2: '',
domain3: '',
port: 4000,
namespace: 'json'
}
});
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
expect(req.response.status).toBe(200);
});
it('resolving an API schema to URL', async () => {
let resolvedUrl;
const engine = params => {
resolvedUrl = params.url;
return Promise.resolve(new _response.default(''));
};
await request('/then', {
api: {
protocol: 'https',
domain3: 'docs',
domain2: () => 'v4fire',
zone: 'rocks',
port: 8123,
namespace: 'core'
},
engine
});
expect(resolvedUrl).toEqual('https://docs.v4fire.rocks:8123/core/then');
});
it('request builder', async () => {
let get = request({
api: {
protocol: 'http',
port: 4000,
domain3: ''
}
});
get = get({
api: {
zone: 'localhost',
namespace: 'json',
domain2: ''
}
});
const req = await get('/1');
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
expect(req.response.status).toBe(200);
});
it('request factory', async () => {
const resolver = (url, params, type) => type === 'get' ? '/json/1' : '',
get = request('http://localhost:4000', resolver),
req = await get('get');
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
expect(req.response.status).toBe(200);
});
it('request factory with rewriting of URL', async () => {
const resolver = () => ['http://localhost:4000', 'json', 1],
get = request('https://run.mocky.io/v3/', resolver),
req = await get();
expect(await req.data).toEqual({
id: 1,
value: 'things'
});
expect(req.response.status).toBe(200);
});
it('catching 404', async () => {
let err;
try {
await request('http://localhost:4000/bla');
} catch (e) {
err = e;
}
expect(err).toBeInstanceOf(_request.RequestError);
expect(err.type).toBe(_request.RequestError.InvalidStatus);
expect(err.message).toBe('[invalidStatus] GET http://localhost:4000/bla 404');
expect(err.details.request.method).toBe('GET');
expect(err.details.response.status).toBe(404);
});
it('aborting of a request', async () => {
let err;
try {
const req = request('http://localhost:4000/json/1');
req.abort();
await req;
} catch (e) {
err = e;
}
expect(err).toBeInstanceOf(_request.RequestError);
expect(err.type).toBe(_request.RequestError.Abort);
expect(err.message).toBe('[abort] GET http://localhost:4000/json/1');
expect(err.details.request.method).toBe('GET');
expect(err.details.response).toBeUndefined();
});
it('request with a low timeout', async () => {
let err;
try {
await request('http://localhost:4000/delayed', {
timeout: 100
});
} catch (e) {
err = e;
}
expect(err).toBeInstanceOf(_request.RequestError);
expect(err.type).toBe(_request.RequestError.Timeout);
expect(err.message).toBe('[timeout] GET http://localhost:4000/delayed');
expect(err.details.response).toBeUndefined();
});
it('request with a high timeout', async () => {
const req = await request('http://localhost:4000/delayed', {
timeout: 500
});
expect(req.response.ok).toBe(true);
});
describe('responses with a no message body', () => {
for (const status of emptyBodyStatuses) {
it(`response with ${status} status`, async () => {
const req = await request(`http://localhost:4000/octet/${status}`, {
okStatuses: status
});
expect(await req.data).toBe(null);
});
}
for (const status of customEmptyBodyStatuses) {
it(`response with ${status} status included in custom no-content statuses`, async () => {
const req = await request(`http://localhost:4000/octet/${status}`, {
okStatuses: status,
noContentStatuses: status
});
expect(await req.data).toBe(null);
});
}
});
it('retrying of a request', async () => {
const req = request('http://localhost:4000/retry', {
retry: {
attempts: 5,
delay: () => 0
}
});
const body = await (await req).response.json();
expect(body.tryNumber).toBe(4);
});
it('retrying of a request with the specified delay between tries', async () => {
await retryDelayTest(() => 200, 200);
});
it('retrying of a request with the specified promisify delay between tries', async () => {
await retryDelayTest(() => new Promise(res => setTimeout(res, 200)), 200);
});
it('retrying with a speed up response', async () => {
const req = await request('http://localhost:4000/retry/speedup', {
timeout: 300,
retry: 2
});
expect(req.response.ok).toBe(true);
const body = await req.response.json();
expect(body.tryNumber).toBe(2);
});
it('failing after the given attempts', async () => {
let err;
try {
await request('http://localhost:4000/retry/bad', {
retry: 3
});
} catch (e) {
err = e;
}
const body = await err.details.response.json();
expect(err).toBeInstanceOf(_request.RequestError);
expect(err.type).toBe(_request.RequestError.InvalidStatus);
expect(err.message).toBe('[invalidStatus] GET http://localhost:4000/retry/bad 500');
expect(err.details.request.method).toBe('GET');
expect(err.details.response.status).toBe(500);
expect(body.tryNumber).toEqual(3);
});
it('responses an object that contains the "url" property', async () => {
const {
response: res1
} = await request('http://localhost:4000/json/1'),
{
response: res2
} = await request('http://localhost:4000/redirect');
expect(res1.url).toBe('http://localhost:4000/json/1');
expect(res2.url).toBe('http://localhost:4000/json/1');
});
it('response `headers` is an instance of the Headers class', async () => {
const {
response
} = await request('http://localhost:4000/header'),
{
headers
} = response;
expect(headers).toBeInstanceOf(_headers.default);
expect(headers['some-header-name']).toBe('some-header-value');
expect(headers.get('Some-Header-Name')).toBe('some-header-value');
});
it('checking the "redirected" property', async () => {
const {
response: res1
} = await request('http://localhost:4000/json/1'),
{
response: res2
} = await request('http://localhost:4000/redirect');
expect(res1.redirected).toBe(false);
expect(res2.redirected).toBe(true);
});
it('request promise is an async iterable object', async () => {
const chunkLengths = [],
req = request('http://localhost:4000/favicon.ico');
let loadedBefore = 0,
totalLength;
for await (const {
loaded,
total
} of req) {
if (totalLength == null) {
totalLength = total;
}
chunkLengths.push(loaded - loadedBefore);
loadedBefore = loaded;
}
expect(totalLength).toBe(1150);
expect(loadedBefore).toBe(1150);
expect(chunkLengths.every(len => len > 0)).toBe(true);
});
it('request response is an async iterable object', async () => {
const chunkLengths = [],
req = await request('http://localhost:4000/favicon.ico');
let loadedBefore = 0,
totalLength;
for await (const {
loaded,
total
} of req) {
if (totalLength == null) {
totalLength = total;
}
chunkLengths.push(loaded - loadedBefore);
loadedBefore = loaded;
}
expect(totalLength).toBe(1150);
expect(loadedBefore).toBe(1150);
expect(chunkLengths.every(len => len > 0)).toBe(true);
});
if (name === 'xhr') {
describe('listening XHR events', () => {
it('`progress`', async () => {
const req = request('http://localhost:4000/json/1'),
events = [];
req.emitter.on('progress', e => {
events.push(e.type);
});
await req;
expect(events.length).toBeGreaterThan(0);
expect(events[0]).toBe('progress');
});
it('`readystatechange`', async () => {
const req = request('http://localhost:4000/json/1'),
events = [];
req.emitter.on('readystatechange', e => {
events.push(e.type);
});
await req;
expect(events.length).toBeGreaterThan(0);
expect(events[0]).toBe('readystatechange');
});
});
} else {
it('getting a response from a stream', async () => {
{
const req = request('http://localhost:4000/favicon.ico'),
result = await convertStreamToBase64(req);
expect(result).toBe(faviconBase64);
}
{
const req = await request('http://localhost:4000/favicon.ico'),
result = await convertStreamToBase64(req);
expect(result).toBe(faviconBase64);
}
});
it('parsing JSON from a stream', async () => {
const req = request('http://localhost:4000/json/users', {
streamDecoder: data => (0, _combinators.sequence)((0, _stream.assemble)((0, _stream.pick)(data, 'total')), (0, _stream.streamArray)((0, _stream.andPick)(data, 'data')))
});
const res = [];
for await (const token of await req.stream) {
res.push(token);
}
expect(res).toEqual([3, {
index: 0,
value: {
name: 'Bob',
age: 21
}
}, {
index: 1,
value: {
name: 'Ben',
age: 45
}
}, {
index: 2,
value: {
name: 'Rob',
age: 32
}
}]);
});
}
async function retryDelayTest(delay, delayMS) {
const startTime = new Date().getTime();
const req = request('http://localhost:4000/retry', {
retry: {
attempts: 5,
delay
}
});
const body = await (await req).response.json(),
firstRequest = body.times[0];
const requestDelays = body.times.slice(1).reduce((acc, time, i) => acc.concat(time - firstRequest - i * delayMS), []);
expect(firstRequest - startTime).toBeLessThan(delayMS);
requestDelays.forEach(time => expect(time).toBeGreaterThanOrEqual(delayMS));
}
async function convertStreamToBase64(stream) {
let buffer = null,
pos = 0;
for await (const {
data,
loaded,
total
} of stream) {
if (buffer == null) {
buffer = new Uint8Array(total);
}
buffer.set(data, pos);
pos = loaded;
}
return Buffer.from(buffer).toString('base64');
}
});
});
});
function createServer() {
const serverApp = (0, _express.default)();
serverApp.use(_express.default.json());
serverApp.get('/json/1', (req, res) => {
res.status(200).json({
id: 1,
value: 'things'
});
});
serverApp.put('/json/2', (req, res) => {
if (req.get('Accept') === 'application/json') {
res.status(200).end('{"message": "Success"}');
} else {
res.sendStatus(422);
}
});
serverApp.post('/json', (req, res) => {
const {
body
} = req;
if (body.id === 12345 && body.value === 'abc-def-ghi') {
res.status(201).json({
message: 'Success'
});
} else {
res.sendStatus(422);
}
});
serverApp.get('/json/users', (req, res) => {
res.status(200).json({
total: 3,
data: [{
name: 'Bob',
age: 21
}, {
name: 'Ben',
age: 45
}, {
name: 'Rob',
age: 32
}]
});
});
serverApp.get('/xml/text', (req, res) => {
res.type('text/xml');
res.status(200).send('<foo>Hello world</foo>');
});
serverApp.get('/xml/app', (req, res) => {
res.type('application/xml');
res.status(200).send('<foo>Hello world</foo>');
});
serverApp.get('/search', (req, res) => {
const {
query
} = req;
if (query.q != null && /^[A-Za-z0-9]*$/.test(query.q)) {
res.type('application/xml');
res.status(200);
res.send('<results><result>one</result><result>two</result><result>three</result></results>');
} else {
res.sendStatus(422);
}
});
serverApp.get('/delayed', (req, res) => {
setTimeout(() => {
res.sendStatus(200);
}, 300);
});
serverApp.get('/favicon.ico', (req, res) => {
res.type('image/x-icon');
res.send(Buffer.from(faviconBase64, 'base64'));
});
for (const status of [].concat(emptyBodyStatuses, customEmptyBodyStatuses)) {
serverApp.get(`/octet/${status}`, (req, res) => {
res.type('application/octet-stream').status(status).end();
});
}
const triesBeforeSuccess = 3,
requestTimes = [];
let tryNumber = 0,
speed = 600;
serverApp.get('/retry', (req, res) => {
requestTimes.push(new Date().getTime());
if (tryNumber <= triesBeforeSuccess) {
res.sendStatus(500);
tryNumber++;
} else {
res.status(200);
res.json({
tryNumber,
times: requestTimes
});
}
});
serverApp.get('/retry/speedup', (req, res) => {
setTimeout(() => {
res.status(200);
res.json({
tryNumber
});
tryNumber++;
}, speed);
speed -= 200;
});
serverApp.get('/retry/bad', (req, res) => {
res.status(500);
res.json({
tryNumber
});
tryNumber++;
});
serverApp.get('/header', (req, res) => {
res.setHeader('Some-Header-Name', 'some-header-value');
res.sendStatus(200);
});
serverApp.get('/redirect', (req, res) => {
res.redirect('http://localhost:4000/json/1');
});
return serverApp.listen(4000);
}