UNPKG

evrythng

Version:

Official Javascript SDK for the EVRYTHNG API.

413 lines (369 loc) 13.6 kB
/* eslint-env jasmine */ import settings from '../../src/settings' import setup from '../../src/setup' import api from '../../src/api' import mockApi from '../helpers/apiMock' import fetchMock from 'fetch-mock' import apiUrl from '../helpers/apiUrl' import paths from '../helpers/paths' import responses from '../helpers/responses' import { operatorTemplate } from '../helpers/data' let request // Copy initial settings const initialSettings = Object.assign({}, settings) describe('api', () => { mockApi() afterEach((done) => { setup(initialSettings) request.then(done).catch(done.fail) }) describe('options', () => { describe('defaults', () => { it('should have default url and method', () => { request = api().then(() => { expect(fetchMock.lastUrl()).toEqual(apiUrl()) expect(fetchMock.lastOptions().method).toEqual('get') }) }) }) describe('global settings', () => { it('should merge defaults with global settings', () => { request = api().then(() => { const headers = fetchMock.lastOptions().headers expect(headers['content-type']).toBeDefined() expect(headers['content-type']).toEqual('application/json') }) }) it('should use global settings after change', () => { setup({ headers: { 'content-type': 'text/plain' } }) request = api().then(() => { const headers = fetchMock.lastOptions().headers expect(headers['content-type']).toBeDefined() expect(headers['content-type']).toEqual('text/plain') }) }) }) describe('custom options', () => { it('should merge custom options', () => { const method = 'post' request = api({ method }).then(() => { expect(fetchMock.lastOptions().method).toEqual(method) }) }) it('should merge nested headers', () => { const headers = { 'content-type': 'text/plain', accept: 'application/json' } request = api({ headers }).then(() => { expect(fetchMock.lastOptions().headers['content-type']).toEqual(headers['content-type']) expect(fetchMock.lastOptions().headers.accept).toEqual(headers.accept) }) }) it('should user apiKey if authorization header not provided', () => { const apiKey = 'apiKey' request = api({ apiKey }).then(() => { expect(fetchMock.lastOptions().headers.authorization).toEqual(apiKey) }) }) }) describe('interceptors', () => { const requestSpy = jasmine.createSpy('request') const responseSpy = jasmine.createSpy('response') const loggerInterceptor = { request: requestSpy, response: responseSpy } const mutatorInterceptor = { request (options) { options.headers.accept = 'application/json' options.method = 'post' return options }, response (res) { // assuming fullResponse: false res.foo = 'bar' return res } } const incrementInterceptor = { request (options) { options.body = options.body || { count: 0 } options.body.count++ return options }, response (res) { // assuming fullResponse: false res.count = res.count || 0 res.count++ return res } } const asyncIncrementInterceptor = { request (options) { return new Promise((resolve) => { setTimeout(() => { options.body = options.body || { count: 0 } options.body.count++ resolve(options) }, 10) }) }, response (res) { // assuming fullResponse: false return new Promise((resolve) => { setTimeout(() => { res.count = res.count || 0 res.count++ resolve(res) }, 10) }) } } const cancelInterceptor = { request (options, cancel) { cancel() } } const asyncCancelInterceptor = { request (options, cancel) { return new Promise((resolve) => { setTimeout(() => { cancel() resolve() }, 10) }) } } const rejectInterceptor = { response () { throw new Error('Rejected') } } const monkeyPatchInterceptor = { response (res) { const json = res.json res.json = function () { return json.apply(this, arguments).then((j) => { j.patch = 'patched' return j }) } return res } } beforeEach(() => { requestSpy.calls.reset() responseSpy.calls.reset() }) describe('request interceptors', () => { it('should run before request', () => { const interceptors = [loggerInterceptor] request = api({ interceptors }).then(() => { expect(requestSpy).toHaveBeenCalled() expect(requestSpy.calls.mostRecent().args[0]).toEqual( jasmine.objectContaining({ method: 'get' }) ) }) }) it('should be able to mutate request options', () => { const interceptors = [mutatorInterceptor] request = api({ interceptors }).then(() => { expect(fetchMock.lastOptions().method).toEqual('post') expect(fetchMock.lastOptions().headers.accept).toEqual('application/json') }) }) it('should use previous options if interceptor does not return', () => { const interceptors = [loggerInterceptor] request = api({ interceptors }).then(() => { expect(fetchMock.lastOptions().method).toEqual('get') }) }) it('should allow multiple interceptors that run in order', () => { const interceptors = [incrementInterceptor, incrementInterceptor] request = api({ interceptors }).then(() => { expect(fetchMock.lastOptions().body).toBeDefined() expect(fetchMock.lastOptions().body.count).toEqual(2) }) }) it('should allow async interceptors (return promises)', () => { const interceptors = [asyncIncrementInterceptor, asyncIncrementInterceptor] request = api({ interceptors }).then(() => { expect(fetchMock.lastOptions().body).toBeDefined() expect(fetchMock.lastOptions().body.count).toEqual(2) }) }) it('should be able to cancel requests', () => { fetchMock.reset() const interceptors = [cancelInterceptor] request = api({ interceptors }).catch((err) => { expect(err.cancelled).toBe(true) expect(fetchMock.called()).toBe(false) }) }) it('should be able to cancel request from async interceptor', () => { fetchMock.reset() const interceptors = [asyncCancelInterceptor] request = api({ interceptors }).catch((err) => { expect(err.cancelled).toBe(true) expect(fetchMock.called()).toBe(false) }) }) it('should not run interceptors after request is cancelled', () => { fetchMock.reset() const interceptors = [cancelInterceptor, loggerInterceptor] request = api({ interceptors }).catch(() => { expect(requestSpy).not.toHaveBeenCalled() expect(fetchMock.called()).toBe(false) }) }) it('should cancel the right request', () => { const firstRequest = api({ interceptors: [asyncCancelInterceptor] }) .then(() => expect(true).toBe(false)) // should not get here .catch(() => expect(true).toBe(true)) const secondRequest = api({ interceptors: [loggerInterceptor] }) .then(() => expect(true).toBe(true)) .catch(() => expect(true).toBe(false)) // should not get here request = Promise.all([firstRequest, secondRequest]) }) }) describe('response interceptors', () => { it('should run after the response', () => { const interceptors = [loggerInterceptor] request = api({ interceptors }).then(() => { expect(responseSpy).toHaveBeenCalled() expect(responseSpy).toHaveBeenCalledWith(responses.ok.body) }) }) it('should be able to mutate simple response', () => { const interceptors = [mutatorInterceptor] const url = paths.operators request = api({ interceptors, url }).then((res) => { expect(res.id).toBeDefined() expect(res.id).toEqual(operatorTemplate.id) expect(res.foo).toBeDefined() expect(res.foo).toEqual('bar') }) }) it('should allow multiple interceptors that run in order', () => { const interceptors = [incrementInterceptor, incrementInterceptor] request = api({ interceptors }).then((res) => { expect(res.count).toBeDefined() expect(res.count).toEqual(2) }) }) it('should allow async interceptors (return promises)', () => { const interceptors = [asyncIncrementInterceptor, asyncIncrementInterceptor] request = api({ interceptors }).then((res) => { expect(res.count).toBeDefined() expect(res.count).toEqual(2) }) }) it('should allow to reject a successful response', () => { const interceptors = [rejectInterceptor] request = api({ interceptors }) .then(() => expect(true).toBe(false)) // should not get here .catch(() => expect(true).toBe(true)) }) it('should allow monkey patch of full responses', () => { const interceptors = [monkeyPatchInterceptor] request = api({ interceptors, fullResponse: true }) .then((res) => res.json()) .then((res) => { expect(res.patch).toBeDefined() expect(res.patch).toEqual('patched') }) }) }) }) describe('fetch', () => { it('should join url with apiUrl', () => { const customOptions = { apiUrl: 'https://api-test.evrythng.net', url: paths.dummy } request = api(customOptions).then(() => { expect(fetchMock.lastUrl()).toEqual(`${customOptions.apiUrl}${customOptions.url}`) }) }) it('should build url with params options', () => { const params = { foo: 'bar' } request = api({ params }).then(() => { expect(fetchMock.lastUrl()).toEqual(apiUrl('?foo=bar')) }) }) it('should allow body with FormData', () => { const form = new FormData() form.append('foo', 'bar') request = api({ body: form }).then(() => { expect(fetchMock.lastOptions().body).toEqual(form) expect(fetchMock.lastOptions().headers['content-type']) }) }) }) describe('handle response', () => { it('should return json body by default', () => { request = api().then((res) => { expect(res).toEqual(responses.ok.body) }) }) it('should handle empty body', () => { request = api({ method: 'delete', url: paths.dummy }).then((res) => { expect(res).toBeUndefined() }) }) it('should reject on HTTP error code', function () { request = api({ url: paths.error }) .then(() => expect(true).toBe(false)) // should not get here .catch((res) => expect(res).toEqual(responses.error.generic.body)) }) it('should return full Response object with fullResponse option', () => { request = api({ fullResponse: true }).then((res) => { expect(res instanceof Response).toBe(true) expect(res.headers).toBeDefined() expect(res.ok).toBe(true) return res.json().then((body) => { expect(body).toEqual(responses.ok.body) }) }) }) it('should return full Response object even on HTTP error code', function () { request = api({ url: paths.error, fullResponse: true }) .then(() => expect(true).toBe(false)) // should not get here .catch((res) => { expect(res instanceof Response).toBe(true) expect(res.ok).toBe(false) return res.json().then((body) => { expect(body).toEqual(responses.error.generic.body) }) }) }) }) describe('callbacks', () => { const callbackSpy = jasmine.createSpy('callback') beforeEach(callbackSpy.calls.reset) it('should call callback without error on success', () => { request = api({ url: paths.dummy }, callbackSpy).then(() => { expect(callbackSpy).toHaveBeenCalledWith(null, responses.entity.multiple.body) }) }) it('should call callback with error on error', () => { request = api({ url: paths.error }, callbackSpy).catch(() => expect(callbackSpy).toHaveBeenCalledWith(responses.error.generic.body) ) }) }) }) })