From 99841a0124e1f71de2f200f45f4642fb982f2656 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Tue, 21 May 2019 14:33:40 -0700 Subject: [PATCH] feat(Calls): EN-4357: Added call creation and ending support (#57) --- src/Client.js | 10 +++ src/examples/create_and_end_calls.test.js | 65 ++++++++++++++++++ src/mocks/calls.js | 54 +++++++++++++++ src/mocks/index.js | 1 + src/resources/Call.js | 82 +++++++++++++++++++++++ src/resources/Call.test.js | 81 ++++++++++++++++++++++ src/resources/Customer.js | 13 ++++ 7 files changed, 306 insertions(+) create mode 100644 src/examples/create_and_end_calls.test.js create mode 100644 src/mocks/calls.js create mode 100644 src/resources/Call.js create mode 100644 src/resources/Call.test.js diff --git a/src/Client.js b/src/Client.js index 40a0283..aff3b26 100644 --- a/src/Client.js +++ b/src/Client.js @@ -111,6 +111,16 @@ class Client { return this.request('PUT', ...args); } + /** + * Convenience method of request() to make an HTTP PATCH request + * @param {Array} args Any other arguments for request() + * @returns {Promise} Promise from request() + * @see request + */ + patch(...args) { + return this.request('PATCH', ...args); + } + /** * Convenience method of request() to make an HTTP DELETE request * @param {Array} args Any other arguments for request() diff --git a/src/examples/create_and_end_calls.test.js b/src/examples/create_and_end_calls.test.js new file mode 100644 index 0000000..4e51c1e --- /dev/null +++ b/src/examples/create_and_end_calls.test.js @@ -0,0 +1,65 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Track from '../index'; +import { charlie, calls as mockCalls } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When retrieving a call by ID', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockCalls.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a call', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const callPromise = api.customer('SYNC').call(3) + .fetch() + .then(call => call); // Do things with call + + return callPromise; + }); +}); + +describe('When creating a call', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockCalls.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should create a call', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const callPromise = api.customer('SYNC').call({ initiating_user: '/1/users/1' }) + .create() + .then(call => call); // Do things with call + + return callPromise; + }); +}); + +describe('When ending a call', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockCalls.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should end a call', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const callPromise = api.customer('SYNC').call(3) + .fetch() + .then(call => call.end()); + + return callPromise; + }); +}); diff --git a/src/mocks/calls.js b/src/mocks/calls.js new file mode 100644 index 0000000..499e2c2 --- /dev/null +++ b/src/mocks/calls.js @@ -0,0 +1,54 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import fetchMock from 'fetch-mock'; +import Client from '../Client'; + +const calls = { + setUpSuccessfulMock: (client) => { + const singleResponse = () => new Response(Client.toBlob(calls.getById(3))); + const postResponse = () => new Response(undefined, { + headers: { + Location: '/1/SYNC/calls/3', + }, + }); + const patchResponse = () => new Response(undefined, { + headers: {}, + }); + + fetchMock + .get(client.resolve('/1/SYNC/calls/3'), singleResponse) + .post(client.resolve('/1/SYNC/calls'), postResponse) + .patch(client.resolve('/1/SYNC/calls/3'), patchResponse); + }, + getById: id => calls.list.find(v => v.id === id), + list: [{ + href: '/1/SYNC/calls/3', + id: 3, + started: new Date().toISOString(), + ended: undefined, + initiating_user: '/1/users/1', + participants: [ + { + href: '/1/SYNC/calls/3/participants/1', + id: 1, + type: 'user', + external_session_id: '82576c1c-9351-4da3-8df1-e3063ae285e7', + connection_requested: new Date().toISOString(), + connection_established: undefined, + connection_terminated: undefined, + user: '/1/SYNC/users/1', + }, + { + href: '/1/SYNC/calls/3/participants/2', + id: 2, + type: 'vehicle', + external_session_id: '537fafaf-9eea-4b1a-b65c-096ee4ed462f', + connection_requested: new Date().toISOString(), + connection_established: undefined, + connection_terminated: undefined, + vehicle: '/1/SYNC/vehicles/1', + }, + ], + }], +}; + +export default calls; diff --git a/src/mocks/index.js b/src/mocks/index.js index d13b46f..af1c819 100644 --- a/src/mocks/index.js +++ b/src/mocks/index.js @@ -5,6 +5,7 @@ import Client from '../Client'; export { default as agencies } from './agencies'; export { default as areas } from './areas'; export { default as blocks } from './blocks'; +export { default as calls } from './calls'; export { default as dispatchMessages } from './dispatchMessages'; export { default as dispatchMessageBatches } from './dispatchMessageBatches'; export { default as drivers } from './drivers'; diff --git a/src/resources/Call.js b/src/resources/Call.js new file mode 100644 index 0000000..fb4c16d --- /dev/null +++ b/src/resources/Call.js @@ -0,0 +1,82 @@ +import Resource from './Resource'; + +/** + * Call resource + */ +class Call extends Resource { + /** + * Creates a new call + * + * Will populate itself with the values given to it after the client parameter + * @param {Client} client Instance of pre-configured client + * @param {Object} rest The object to use in assigning values to this instance + */ + constructor(client, rest) { + super(client); + const { code, ...newProperties } = rest; + this.customerCode = code; + const hydrated = !Object.keys(newProperties).every(k => k === 'href' || k === 'customerCode'); + Object.assign(this, newProperties, { + hydrated, + }); + } + + /** + * Makes a href for a given customer code and ID + * @param {string} customerCode Customer code + * @param {Number} id Call ID + * @returns {string} URI to instance of call + */ + static makeHref(customerCode, id) { + return { + href: `/1/${customerCode}/calls/${id}`, + code: customerCode, + }; + } + + /** + * Fetches the data for this call via the client + * @returns {Promise} If successful, a hydrated instance of this call + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(call => new Call(this.client, { ...this, ...call })); + } + + /** + * Saves data for a call via the client + * @returns {Promise} if successful returns a call with the id included + */ + create() { + // eslint-disable-next-line no-unused-vars + const { client, hydrated, customerCode, ...body } = this; + return this.client.post(`/1/${this.customerCode}/calls`, { body }) + .then(response => response.headers.get('location')) + .then((href) => { + const match = /\/\d+\/\S+\/calls\/(\d+)/.exec(href); + return new Call(this.client, { ...this, href, id: parseFloat(match[1]) }); + }); + } + + /** + * Updates data for a call via the client + * @returns {Promise} if successful returns instance of this call + */ + end() { + const { href } = Call.makeHref(this.customerCode, this.id); + this.ended = new Date().toISOString(); + return this.client.patch(href, { + body: [ + { + op: 'replace', + path: '/ended', + value: this.ended, + }, + ], + }); + } + +} + +export default Call; diff --git a/src/resources/Call.test.js b/src/resources/Call.test.js new file mode 100644 index 0000000..f4dae17 --- /dev/null +++ b/src/resources/Call.test.js @@ -0,0 +1,81 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import Call from './Call'; +import { calls as mockCalls } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When instantiating a call based on customer and ID', () => { + const client = new Client(); + const call = new Call(client, Call.makeHref('SYNC', 3)); + + it('should set the href', () => call.href.should.equal('/1/SYNC/calls/3')); + it('should not be hydrated', () => call.hydrated.should.equal(false)); +}); + +describe('When instantiating a call based on an object', () => { + const client = new Client(); + const call = new Call(client, mockCalls.getById(3)); + + it('should set the ID', () => call.id.should.equal(3)); + it('should set the href', () => call.href.should.equal('/1/SYNC/calls/3')); + it('should be hydrated', () => call.hydrated.should.equal(true)); +}); + +describe('When fetching a call based on customer and ID', () => { + const client = new Client(); + + beforeEach(() => mockCalls.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Call(client, Call.makeHref('SYNC', 3)).fetch(); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the ID', () => promise.then(v => v.id).should.eventually.equal(3)); + it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/calls/3')); + it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true)); +}); + +describe('When creating a call', () => { + const client = new Client(); + + beforeEach(() => mockCalls.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Call(client, { code: 'SYNC', initiating_user: '/1/users/1' }).create(); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/calls/3')); + it('should set the ID', () => promise.then(v => v.id).should.eventually.equal(3)); +}); + +describe('When updating a call', () => { + const client = new Client(); + + beforeEach(() => mockCalls.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Call(client, { code: 'SYNC', initiating_user: '/1/users/1' }) + .create() + .then(call => call.end() + .then(() => call)); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/calls/3')); + it('should set ended to a date', () => promise.then(v => v.ended).should.eventually.be.a('string')); +}); diff --git a/src/resources/Customer.js b/src/resources/Customer.js index 67bd988..94911fa 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -5,6 +5,7 @@ import Area from './Area'; import AreasContext from './AreasContext'; import Assignment from './Assignment'; import Block from './Block'; +import Call from './Call'; import DispatchMessage from './DispatchMessage'; import DispatchMessagesContext from './DispatchMessagesContext'; import DispatchMessageBatch from './DispatchMessageBatch'; @@ -109,6 +110,18 @@ class Customer extends Resource { return this.resource(Block, Block.makeHref(this.code, id)); } + /** + * Gets a call resource by id + * @param {Object} payload Identity of the call or object representing a new call + * @returns {Message} Call resource + */ + call(payload) { + if (!isNaN(parseFloat(payload)) && isFinite(payload)) { + return this.resource(Call, Call.makeHref(this.code, payload)); + } + return this.resource(Call, { code: this.code, ...payload }); + } + /** * Gets a context for querying this customer's dispatch messages * @returns {DispatchMessagesContext} Context for querying this customer's dispatch messages