filestack-js
Version:
Official JavaScript library for Filestack
815 lines (662 loc) • 23.9 kB
text/typescript
/*
* Copyright (c) 2018 by Filestack
* Some rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* istanbul ignore file */
import nock from 'nock';
import * as zlib from 'zlib';
import { FsHttpMethod, FsRequestOptions } from '../types';
import { FsCancelToken } from '../token';
import { FsRequestError, FsRequestErrorCode } from '../error';
export const adaptersHttpAbstract = (adapter: any, adapterName: string) => {
describe(`Request/Adapters/${adapterName}`, () => {
let scope;
const url = 'https://somewrongdom.moc';
beforeEach(() => {
nock.cleanAll();
scope = null;
});
beforeEach(() => {
scope = nock(url).defaultReplyHeaders({
'access-control-allow-origin': function (req) { return req.getHeader('origin')?.toString(); },
'access-control-allow-methods': function (req) { return req.getHeader('access-control-request-method')?.toString(); },
'access-control-allow-headers': function (req) { return req.getHeader('access-control-request-headers')?.toString(); },
});
if (adapterName === 'xhr') {
scope.options(/.*/).reply(200);
}
});
describe('request basic', () => {
it('should make correct request (https)', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').reply(200, 'ok', { 'Content-Type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual('ok');
scope.done();
});
it('should make correct request (http)', async () => {
const httpUrl = url.replace('https', 'http');
const scopeHttp = nock(httpUrl).defaultReplyHeaders({
'access-control-allow-origin': function (req) { return req.getHeader('origin')?.toString(); },
'access-control-allow-methods': function (req) { return req.getHeader('access-control-request-method')?.toString(); },
'access-control-allow-headers': function (req) { return req.getHeader('access-control-request-headers')?.toString(); },
});
if (adapterName === 'xhr') {
scopeHttp.options(/.*/).reply(200);
}
const options = {
url: httpUrl,
method: FsHttpMethod.GET,
};
scopeHttp.get('/').reply(200, 'ok', { 'Content-Type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual('ok');
scopeHttp.done();
});
it('should add https protocol if no protocol is provided', async () => {
const options = {
url: url.replace('https://', ''),
method: FsHttpMethod.GET,
};
scope.get('/').reply(200, 'ok', { 'Content-Type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual('ok');
scope.done();
});
it('should handle string as data param', async () => {
const msg = 'Some test stream data';
const mock = jest
.fn()
.mockName('bufferData')
.mockReturnValue(msg);
const options = {
url: url,
method: FsHttpMethod.POST,
data: msg,
};
scope.post('/').reply(200, function(_, data) {
return mock(data);
}, { 'Content-Type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual(msg);
expect(mock).toHaveBeenLastCalledWith(msg);
scope.done();
});
// gzip support is handled by the browser on xhr side
if (adapterName !== 'xhr') {
it('should handle deflate response', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
const data = zlib.gzipSync(Buffer.from('ok', 'utf-8'));
scope.get('/').reply(200, data, { 'Content-encoding': 'gzip, deflate', 'Content-type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual('ok');
scope.done();
});
it('should handle 204 gzip request', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').reply(204, '', { 'Content-encoding': 'gzip, deflate', 'Content-type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(204);
expect(res.data).toEqual(null);
scope.done();
});
it('should handle Buffer as data param', async () => {
const msg = 'Some test stream data';
const mock = jest
.fn()
.mockName('bufferData')
.mockReturnValue('ok');
const options = {
url: url,
method: FsHttpMethod.POST,
data: Buffer.from(msg, 'utf-8'),
};
scope.post('/').reply(200, function(_, data) {
return mock(data);
}, { 'Content-type': 'text/plain' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(mock).toHaveBeenLastCalledWith(msg);
scope.done();
});
it('should throw error when data type is unsupported', () => {
const Readable = require('stream').Readable;
const options = {
url: url,
method: FsHttpMethod.POST,
data: Readable.from(['test']),
};
const requestAdapter = new adapter();
return expect(requestAdapter.request(options)).rejects.toEqual(expect.any(FsRequestError));
});
}
it('should make request with auth', async () => {
const auth = {
username: 'test',
password: 'test',
};
const options = {
url: url,
method: FsHttpMethod.GET,
auth,
};
scope.options('/').reply(200, 'ok');
scope
.get('/')
.basicAuth({ user: auth.username, pass: auth.password })
.reply(200, 'ok', { 'access-control-allow-origin': '*' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
});
it('should throw an error on empty username', async () => {
const auth = {
username: null,
password: 'test',
};
const options = {
url: url,
method: FsHttpMethod.GET,
auth,
};
const requestAdapter = new adapter();
return expect(requestAdapter.request(options)).rejects.toEqual(expect.any(FsRequestError));
});
it('should throw an error on empty password', async () => {
const auth = {
username: 'test',
password: null,
};
const options = {
url: url,
method: FsHttpMethod.GET,
auth,
};
const requestAdapter = new adapter();
return expect(requestAdapter.request(options)).rejects.toEqual(expect.any(FsRequestError));
});
it('should overwrite auth header if auth data is provided', async () => {
const auth = {
username: 'test',
password: 'test',
};
const options = {
url: url,
method: FsHttpMethod.GET,
auth,
headers: {
Authorization: 'test123',
},
};
scope.options('/').reply(200, 'ok');
scope
.get('/')
.basicAuth({ user: auth.username, pass: auth.password })
.reply(200, 'ok');
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
});
it('should contain default headers', async () => {
const mock = jest
.fn()
.mockName('default/headers')
.mockReturnValue('ok');
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').reply(200, function(_, data) {
return mock(this.req.headers);
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(mock).toHaveBeenCalledWith(
expect.objectContaining({ 'filestack-source': expect.any(String), 'filestack-trace-id': expect.any(String), 'filestack-trace-span': expect.any(String) })
);
});
it('should omit default headers', async () => {
const mock = jest
.fn()
.mockName('default/headers')
.mockReturnValue('ok');
const options = {
url: url,
method: FsHttpMethod.GET,
filestackHeaders: false,
};
scope.get('/').reply(200, function(_, data) {
return mock(this.req.headers);
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(mock).toHaveBeenCalledWith(
expect.not.objectContaining({ 'filestack-source': expect.any(String), 'filestack-trace-id': expect.any(String), 'filestack-trace-span': expect.any(String) })
);
});
it('should skip undefined headers', async () => {
const mock = jest
.fn()
.mockName('undefined/headers')
.mockReturnValue('ok');
const options = {
url: url,
method: FsHttpMethod.GET,
headers: {
test: undefined,
},
};
scope.get('/').reply(200, function(_, data) {
return mock(this.req.headers);
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(mock).toHaveBeenCalledWith(expect.not.objectContaining({ test: undefined }));
scope.done();
});
});
describe('redirects ', () => {
// xhr redirects are handled by the browser
if (adapterName !== 'xhr') {
it('should follow 302 redirect', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
const response = { test: 123 };
scope.get('/').reply(302, 'ok', {
location: `${url}/resp`,
});
scope.get('/resp').reply(200, response, {
'Content-type': 'application/json',
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual(response);
scope.done();
});
it('should throw error when no location is provided', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').reply(302, 'ok', { location: '' });
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.REDIRECT);
}
scope.done();
});
it('should throw error (REDIRECT) on max redirects', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
if (adapterName === 'xhr') {
scope.options('/').reply(200, 'ok');
}
scope
.get('/')
.reply(302, 'ok', {
location: `${url}/a`,
})
.get('/a')
.reply(301, 'ok', {
location: `${url}/b`,
})
.get('/b')
.reply(302, 'ok', {
location: `${url}/c`,
})
.get('/c')
.reply(301, 'ok', {
location: `${url}/d`,
})
.get('/d')
.reply(302, 'ok', {
location: `${url}/e`,
})
.get('/e')
.reply(301, 'ok', {
location: `${url}/f`,
})
.get('/f')
.reply(302, 'ok', {
location: `${url}/g`,
})
.get('/g')
.reply(301, 'ok', {
location: `${url}/h`,
})
.get('/h')
.reply(302, 'ok', {
location: `${url}/i`,
})
.get('/i')
.reply(301, 'ok', {
location: `${url}/j`,
})
.get('/j')
.reply(302, 'ok', {
location: `${url}/k`,
});
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.REDIRECT);
}
scope.done();
});
it('should throw error on redirect loop', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope
.get('/')
.reply(302, 'ok', { location: `${url}/a` })
.get('/a')
.reply(302, 'ok', { location: `${url}/a` })
.get('/a')
.reply(200, 'ok');
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.REDIRECT);
}
});
}
});
if (adapterName === 'xhr') {
describe('request form', () => {
it('Should send form data', async () => {
const form = new FormData();
const options = {
url: url,
method: FsHttpMethod.POST,
data: form,
};
const resp = { form: 'ok' };
scope.post('/').reply(200, resp, { 'Content-type': 'application/json' });
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
expect(res.status).toEqual(200);
expect(res.data).toEqual(resp);
scope.done();
});
});
}
describe('4xx and 5xx errors handling', () => {
it('should handle 4xx response', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
const errorResp = { test: 123 };
scope.get('/').reply(404, errorResp, {
'Content-type': 'application/json',
});
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.REQUEST);
expect(err.response.status).toEqual(404);
expect(err.response.data).toEqual(errorResp);
}
scope.done();
});
it('should handle 5xx response', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
const errorResp = { test: 123 };
scope.get('/').reply(501, errorResp, {
'Content-type': 'application/json',
});
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.SERVER);
expect(err.response.status).toEqual(501);
expect(err.response.data).toEqual(errorResp);
}
scope.done();
});
});
if (adapterName === 'xhr') {
describe('progress event', () => {
it('should handle upload progress', async () => {
const progressSpy = jest
.fn()
.mockName('bufferData')
.mockReturnThis();
const buf = Buffer.alloc(1024);
buf.fill('a');
const options: FsRequestOptions = {
url: `${url}/progress`,
method: FsHttpMethod.POST,
onProgress: progressSpy,
data: buf,
};
scope.options('/progress').reply(200, 'ok', {
'Content-type': 'application/json',
});
scope.post('/progress').reply(200, 'ok', {
'Content-type': 'application/json',
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
// for jsdom we cannot check progress event correctly
expect(progressSpy).toHaveBeenCalled();
});
});
}
describe('cancelToken', () => {
it('Should throw abort request when token will be called', async () => {
const token = new FsCancelToken();
const options = {
url: `${url}/timeout`,
method: FsHttpMethod.GET,
cancelToken: token,
};
scope
.get('/timeout')
.delay(2000)
.reply(201, 'ok', {
'content-type': 'application/json',
});
setTimeout(() => {
token.cancel();
}, 150);
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.ABORTED);
}
});
it('Should not throw undefined error when cancel token will be called after request finished', async () => {
const token = new FsCancelToken();
const options = {
url: url,
method: FsHttpMethod.GET,
cancelToken: token,
};
scope.get('/').reply(200, 'ok', {
'Content-type': 'text/plain',
});
const requestAdapter = new adapter();
const res = await requestAdapter.request(options);
token.cancel();
expect(res.status).toEqual(200);
expect(res.data).toEqual('ok');
scope.done();
});
});
describe('Network errors', () => {
it('should throw an error on domain not found', async () => {
const options = {
url: 'https://some-badd-url.er',
method: FsHttpMethod.GET,
};
const requestAdapter = new adapter();
return expect(requestAdapter.request(options)).rejects.toEqual(expect.any(FsRequestError));
});
it.skip('Should throw an FilestackError on socket abort with FsRequestErrorCode.TIMEOUTED code', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
timeout: 50,
};
scope
.get('/')
.delay(2000)
.reply(200, 'ok', {
'Content-type': 'application/json',
});
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.TIMEOUT);
}
scope.done();
});
it('Should throw an FilestackError on response ECONNREFUSED error with FsRequestErrorCode.NETWORK code', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').replyWithError({ code: 'ECONNREFUSED' });
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.NETWORK);
}
scope.done();
});
it('Should throw an FilestackError on response ECONNRESET error with FsRequestErrorCode.NETWORK code', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').replyWithError({ code: 'ECONNRESET' });
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.NETWORK);
}
scope.done();
});
it('Should throw an FilestackError on response ENOTFOUND error with FsRequestErrorCode.NETWORK code', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
};
scope.get('/').replyWithError({ code: 'ENOTFOUND' });
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.NETWORK);
}
scope.done();
});
it.skip('Should abort request on timeout', async () => {
const options = {
url: url,
method: FsHttpMethod.GET,
timeout: 1000,
};
scope
.get('/')
.delay(2000)
.reply(200, 'ok');
try {
const requestAdapter = new adapter();
await requestAdapter.request(options);
// return error in try will not emit error
expect(false).toEqual(true);
} catch (err) {
expect(err).toEqual(expect.any(FsRequestError));
expect(err.code).toEqual(FsRequestErrorCode.TIMEOUT);
}
scope.done();
});
});
});
};