UNPKG

filestack-js

Version:

Official JavaScript library for Filestack

815 lines (662 loc) 23.9 kB
/* * 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(); }); }); }); };