diff --git a/src/examples/create_and_end_calls.test.js b/src/examples/create_and_end_calls.test.js index 4e51c1e..9fe990d 100644 --- a/src/examples/create_and_end_calls.test.js +++ b/src/examples/create_and_end_calls.test.js @@ -2,7 +2,11 @@ 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'; +import { + charlie, + calls as mockCalls, + callParticipants as mockParticipants, +} from '../mocks'; chai.should(); chai.use(chaiAsPromised); @@ -63,3 +67,45 @@ describe('When ending a call', () => { return callPromise; }); }); + +describe('When adding a participant to a call', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockParticipants.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should add the participant', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const callId = 2; + const newParticipant = { type: 'user', user: '/1/SYNC/users/1' }; + const participantPromise = api.customer('SYNC').callParticipant(callId, newParticipant) + .create() + .then(participant => participant); + + return participantPromise; + }); +}); + +describe('When removing a participant from a call', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockParticipants.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should remove the participant', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const callId = 2; + const participantId = 3; + const participantPromise = api.customer('SYNC').callParticipant(callId, participantId) + .fetch() + .then(participant => participant.end()); + + return participantPromise; + }); +}); diff --git a/src/mocks/callParticipants.js b/src/mocks/callParticipants.js new file mode 100644 index 0000000..02d18d9 --- /dev/null +++ b/src/mocks/callParticipants.js @@ -0,0 +1,49 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import fetchMock from 'fetch-mock'; +import Client from '../Client'; + +const callParticipants = { + setUpSuccessfulMock: (client) => { + const singleResponse = () => new Response(Client.toBlob(callParticipants.getById(2, 3))); + const postResponse = () => new Response(undefined, { + headers: { + Location: '/1/SYNC/calls/2/participants/3', + }, + }); + const patchResponse = () => new Response(undefined, { + headers: {}, + }); + + fetchMock + .get(client.resolve('/1/SYNC/calls/2/participants/3'), singleResponse) + .post(client.resolve('/1/SYNC/calls/2/participants'), postResponse) + .patch(client.resolve('/1/SYNC/calls/2/participants/3'), patchResponse); + }, + getById: (callId, id) => callParticipants.list.find(v => + v.call.href === `/1/SYNC/calls/${callId}` && v.id === id), + list: [ + { + href: '/1/SYNC/calls/2/participants/3', + call: { href: '/1/SYNC/calls/2' }, + id: 3, + 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/2/participants/4', + call: { href: '/1/SYNC/calls/2' }, + id: 4, + 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 callParticipants; diff --git a/src/mocks/index.js b/src/mocks/index.js index b30c5c0..b64451c 100644 --- a/src/mocks/index.js +++ b/src/mocks/index.js @@ -6,6 +6,7 @@ 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 callParticipants } from './callParticipants'; export { default as dispatchMessages } from './dispatchMessages'; export { default as dispatchMessageBatches } from './dispatchMessageBatches'; export { default as drivers } from './drivers'; diff --git a/src/resources/CallParticipant.js b/src/resources/CallParticipant.js new file mode 100644 index 0000000..a8a7867 --- /dev/null +++ b/src/resources/CallParticipant.js @@ -0,0 +1,85 @@ +import Resource from './Resource'; + +/** + * CallParticipant resource + */ + +class CallParticipant extends Resource { + /** + * Creates a new call participant + * + * 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 ina ssigning 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, call ID, and ID + * @param {string} customerCode Alphanumeric code of the customer + * @param {Number} callId ID of the call + * @param {Number} id Call Participant ID + * @returns {string} URI to instance of call + */ + static makeHref(customerCode, callId, id) { + return { + href: `/1/${customerCode}/calls/${callId}/participants/${id}`, + code: customerCode, + }; + } + + /** + * Fetches the data for this call participant via the client + * @returns {Promise} If successful, a hydrated instance of this call participant + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(callParticipant => new CallParticipant(this.client, { ...this, ...callParticipant })); + } + + /** + * Adds a participant to a call via the client + * @returns {Promise} If successful, returns the call participant with IDs set + */ + create() { + const { client, hydrated, customerCode, callId, ...body } = this; + return this.client.post(`/1/${customerCode}/calls/${callId}/participants`, { body }) + .then(response => response.headers.get('location')) + .then((href) => { + const match = /\/\d+\/\S+\/calls\/(\d+)\/participants\/(\d+)/.exec(href); + return new CallParticipant(this.client, { + ...this, + href, + callId: parseFloat(match[1]), + id: parseFloat(match[2]), + }); + }); + } + + /** + * Ends the call for this participant via the client + * @returns {Promise} If successful, returns instance of this call participant + */ + end() { + const { href } = CallParticipant.makeHref(this.customerCode, this.callId, this.id); + this.connection_terminated = new Date().toISOString(); + return this.client.patch(href, { + body: [ + { + op: 'replace', + path: '/connection_terminated', + value: this.connection_terminated, + }, + ], + }); + } +} + +export default CallParticipant; diff --git a/src/resources/CallParticipant.test.js b/src/resources/CallParticipant.test.js new file mode 100644 index 0000000..4de4067 --- /dev/null +++ b/src/resources/CallParticipant.test.js @@ -0,0 +1,82 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import CallParticipant from './CallParticipant'; +import { callParticipants as mockParticipants } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When instantiating a call participant based on customer and IDs', () => { + const client = new Client(); + const callParticipant = new CallParticipant(client, CallParticipant.makeHref('SYNC', 2, 3)); + + it('should set the href', () => callParticipant.href.should.equal('/1/SYNC/calls/2/participants/3')); + it('should not be hydrated', () => callParticipant.hydrated.should.equal(false)); +}); + +describe('When instantiating a call participant based on an object', () => { + const client = new Client(); + const callParticipant = new CallParticipant(client, mockParticipants.getById(2, 3)); + + it('should set the ID', () => callParticipant.id.should.equal(3)); + it('should set the href', () => callParticipant.href.should.equal('/1/SYNC/calls/2/participants/3')); + it('should be hydrated', () => callParticipant.hydrated.should.equal(true)); +}); + +describe('When fetching a call participant based on customer and IDs', () => { + const client = new Client(); + + beforeEach(() => mockParticipants.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new CallParticipant(client, CallParticipant.makeHref('SYNC', 2, 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/2/participants/3')); + it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true)); +}); + +describe('When adding a call participant', () => { + const client = new Client(); + + beforeEach(() => mockParticipants.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new CallParticipant(client, { code: 'SYNC', callId: 2, user: '/1/SYNC/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/2/participants/3')); + it('should set the ID', () => promise.then(v => v.id).should.eventually.equal(3)); +}); + +describe('When updating a call participant', () => { + const client = new Client(); + + beforeEach(() => mockParticipants.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new CallParticipant(client, { code: 'SYNC', callId: 2, user: '/1/SYNC/users/1' }) + .create() + .then(participant => participant.end() + .then(() => participant)); + }); + + 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/2/participants/3')); + it('should set connection_terminated to a date', () => promise.then(v => v.connection_terminated).should.eventually.be.a('string')); +}); + diff --git a/src/resources/Customer.js b/src/resources/Customer.js index c977209..eb16775 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -7,6 +7,7 @@ import Assignment from './Assignment'; import Block from './Block'; import CustomerUsersContext from './CustomerUsersContext'; import Call from './Call'; +import CallParticipant from './CallParticipant'; import DispatchMessage from './DispatchMessage'; import DispatchMessagesContext from './DispatchMessagesContext'; import DispatchMessageBatch from './DispatchMessageBatch'; @@ -124,6 +125,20 @@ class Customer extends Resource { return this.resource(Call, { code: this.code, ...payload }); } + /** + * Gets a callParticipant resource by id + * @param {Number} callId ID of the call + * @param {Object} payload Identify of an existing or object representing a new call participant. + * @returns {CallParticipant} Call participant resource + */ + callParticipant(callId, payload) { + if (!isNaN(parseFloat(payload)) && isFinite(payload)) { + const newProperties = CallParticipant.makeHref(this.code, callId, payload); + return this.resource(CallParticipant, { callId, ...newProperties }); + } + return this.resource(CallParticipant, { code: this.code, callId, ...payload }); + } + /** * Gets a context for querying this customer's dispatch messages * @returns {DispatchMessagesContext} Context for querying this customer's dispatch messages diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js index bd562e8..28d91b7 100644 --- a/src/resources/Customer.test.js +++ b/src/resources/Customer.test.js @@ -8,6 +8,8 @@ import Agency from './Agency'; import Area from './Area'; import AreasContext from './AreasContext'; import Block from './Block'; +import Call from './Call'; +import CallParticipant from './CallParticipant'; import DispatchMessage from './DispatchMessage'; import DispatchMessagesContext from './DispatchMessagesContext'; import DispatchMessageBatch from './DispatchMessageBatch'; @@ -51,6 +53,8 @@ describe('When getting resources related to a customer', () => { it('should allow a list of areas to be retrieved', () => customer.areas().should.be.instanceOf(AreasContext)); it('should allow an assignment to be retrieved', () => customer.assignment().should.be.instanceOf(Assignment)); it('should allow a block to be retrieved', () => customer.block().should.be.instanceof(Block)); + it('should allow a call to be retrieved', () => customer.call().should.be.instanceOf(Call)); + it('should allow a call participant to be retrieved', () => customer.callParticipant().should.be.instanceOf(CallParticipant)); it('should allow dispatch messages to be searched', () => customer.dispatchMessages().should.be.instanceOf(DispatchMessagesContext)); it('should allow a dispatch message to be retrieved', () => customer.dispatchMessage().should.be.instanceOf(DispatchMessage)); it('should allow a dispatch message batch to be retrieved', () => customer.dispatchMessageBatch().should.be.instanceOf(DispatchMessageBatch));