From 2c315f1236f4fcc22ee282f28b4a34f0b6074134 Mon Sep 17 00:00:00 2001 From: Jeff Cuevas-Koch <4649003+cuevaskoch@users.noreply.github.com> Date: Fri, 19 Mar 2021 09:14:06 -0700 Subject: [PATCH] feat(assets): EN-7240: Add Customer Asset support. (#96) Signed-off-by: Jeff Cuevas-Koch Co-authored-by: Jeff Cuevas-Koch --- src/examples/get_asset.test.js | 89 +++++++++++++++++++++++++++++ src/mocks/assets.js | 39 +++++++++++++ src/mocks/index.js | 1 + src/resources/Asset.js | 81 ++++++++++++++++++++++++++ src/resources/Asset.test.js | 81 ++++++++++++++++++++++++++ src/resources/AssetsContext.js | 32 +++++++++++ src/resources/AssetsContext.test.js | 30 ++++++++++ src/resources/Customer.js | 22 +++++++ src/resources/Customer.test.js | 4 ++ 9 files changed, 379 insertions(+) create mode 100644 src/examples/get_asset.test.js create mode 100644 src/mocks/assets.js create mode 100644 src/resources/Asset.js create mode 100644 src/resources/Asset.test.js create mode 100644 src/resources/AssetsContext.js create mode 100644 src/resources/AssetsContext.test.js diff --git a/src/examples/get_asset.test.js b/src/examples/get_asset.test.js new file mode 100644 index 0000000..bba4720 --- /dev/null +++ b/src/examples/get_asset.test.js @@ -0,0 +1,89 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Track from '../index'; +import { charlie, assets as mockAssets } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When searching for assets by name', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockAssets.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a list of assets', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const assetsPromise = api.customer('SYNC').assets() + .getPage() + .then(page => page.list) + .then(assets => assets); // Do things with list of assets + + return assetsPromise; + }); +}); + +describe('When retrieving an asset by ID', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockAssets.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get an asset', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const assetPromise = api.customer('SYNC').asset(1) + .fetch() + .then(asset => asset); // Do things with asset + + return assetPromise; + }); +}); + +describe('When creating an asset', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockAssets.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should create an asset', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const assetPromise = api.customer('SYNC').asset({ + base64_data: 'some_data', + asset_type: 'Logo', + filename: 'logo.png', + }) + .create() + .then(asset => asset); // Do things with asset + + return assetPromise; + }); +}); + +describe('When marking an asset saved', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockAssets.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should mark the asset saved', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const assetPromise = api.customer('SYNC').asset(1) + .fetch() + .then(asset => asset.markSaved()); + + return assetPromise; + }); +}); diff --git a/src/mocks/assets.js b/src/mocks/assets.js new file mode 100644 index 0000000..404bc20 --- /dev/null +++ b/src/mocks/assets.js @@ -0,0 +1,39 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import fetchMock from 'fetch-mock'; +import Client from '../Client'; + +const assets = { + setUpSuccessfulMock: (client) => { + const listResponse = () => new Response( + Client.toBlob(assets.list), { + headers: { + Link: '; rel="next", ; rel="last"', + }, + }); + const singleResponse = () => new Response(Client.toBlob(assets.getById(1))); + const postResponse = () => new Response(undefined, { + headers: { + Location: '/1/SYNC/assets/1', + }, + }); + const patchResponse = () => new Response(undefined, { + headers: {}, + }); + + fetchMock + .get(client.resolve('/1/SYNC/assets?page=1&per_page=10&sort='), listResponse) + .get(client.resolve('/1/SYNC/assets/1'), singleResponse) + .post(client.resolve('/1/SYNC/assets'), postResponse) + .patch(client.resolve('/1/SYNC/assets/1'), patchResponse); + }, + getById: id => assets.list.find(v => v.id === id), + list: [{ + href: '/1/SYNC/assets/1', + id: 1, + is_saved: true, + url: "http://example.com/file.png", + asset_type: "Logo", + }], +}; + +export default assets; diff --git a/src/mocks/index.js b/src/mocks/index.js index 064b956..0364ac2 100644 --- a/src/mocks/index.js +++ b/src/mocks/index.js @@ -4,6 +4,7 @@ import Client from '../Client'; export { default as agencies } from './agencies'; export { default as areas } from './areas'; +export { default as assets } from './assets'; export { default as blocks } from './blocks'; export { default as calls } from './calls'; export { default as callParticipants } from './callParticipants'; diff --git a/src/resources/Asset.js b/src/resources/Asset.js new file mode 100644 index 0000000..34327f1 --- /dev/null +++ b/src/resources/Asset.js @@ -0,0 +1,81 @@ +import Resource from './Resource'; + +/** + * Asset resource + */ +class Asset extends Resource { + /** + * Creates a new Asset. + * + * @param {Client} client Instance of pre-configured client + * @param {Array} rest Remaining arguments to use in assigning values to this instance + */ + constructor(client, ...rest) { + super(client); + + const newProperties = Object.assign({}, ...rest); + const hydrated = !Object.keys(newProperties).every(k => k === 'href' || k === 'code'); + + Object.assign(this, newProperties, { + hydrated, + }); + } + + /** + * Makes a href for a given customer code and ID + * @param {string} customerCode Customer code + * @param {number} id Asset ID + * @returns {{href: string}} URI to instance of asset + */ + static makeHref(customerCode, id) { + return { + href: `/1/${customerCode}/assets/${id}`, + code: customerCode, + }; + } + + /** + * Fetches the data for this asset via the client + * @returns {Promise} If successful, a hydrated instance of this asset + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(asset => new Asset(this.client, this, asset)); + } + + /** + * Saves data for a new asset via the client + * @returns {Promise} if successful, returns an asset with the id property set + */ + create() { + // eslint-disable-next-line no-unused-vars + const { client, hydrated, code, ...body } = this; + return this.client.post(`/1/${this.code}/assets`, { body }) + .then(response => response.headers.get('location')) + .then((href) => { + const match = /\/\d+\/\S+\/assets\/(\d+)/.exec(href); + return new Asset(this.client, { ...this, href, id: parseFloat(match[1]) }); + }); + } + + /** + * Updates this asset to mark it as permanently saved via the client + * @returns {Promise} if successful, returns an instance of this asset + */ + markSaved() { + const { href } = Asset.makeHref(this.code, this.id); + this.is_saved = true; + return this.client.patch(href, { + body: [ + { + op: 'replace', + path: '/is_saved', + value: this.is_saved, + }, + ], + }); + } +} + +export default Asset; \ No newline at end of file diff --git a/src/resources/Asset.test.js b/src/resources/Asset.test.js new file mode 100644 index 0000000..200ac1c --- /dev/null +++ b/src/resources/Asset.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 Asset from './Asset'; +import { assets as mockAssets } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When instantiating an asset based on customer and ID', () => { + const client = new Client(); + const asset = new Asset(client, Asset.makeHref('SYNC', 1)); + + it('should set the href', () => asset.href.should.equal('/1/SYNC/assets/1')); + it('should not be hydrated', () => asset.hydrated.should.equal(false)); +}); + +describe('When instantiating an asset based on an object', () => { + const client = new Client(); + const asset = new Asset(client, mockAssets.getById(1)); + + it('should set the ID', () => asset.id.should.equal(1)); + it('should set the href', () => asset.href.should.equal('/1/SYNC/assets/1')); + it('should be hydrated', () => asset.hydrated.should.equal(true)); +}); + +describe('When fetching an asset based on customer and ID', () => { + const client = new Client(); + + beforeEach(() => mockAssets.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Asset(client, Asset.makeHref('SYNC', 1)).fetch(); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the ID', () => promise.then(v => v.id).should.eventually.equal(1)); + it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/assets/1')); + it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true)); +}); + +describe('When creating an asset', () => { + const client = new Client(); + + beforeEach(() => mockAssets.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Asset(client, { code: 'SYNC', base64_data: 'some_data' }).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/assets/1')); + it('should set the ID', () => promise.then(v => v.id).should.eventually.equal(1)); +}); + +describe('When updating an asset', () => { + const client = new Client(); + + beforeEach(() => mockAssets.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Asset(client, { code: 'SYNC' }) + .create() + .then(asset => asset.markSaved() + .then(() => asset)); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the href', () => promise.then(v => v.href).should.eventually.equal('/1/SYNC/assets/1')); + it('should set is_saved to true', () => promise.then(v => v.is_saved).should.eventually.equal(true)); +}); diff --git a/src/resources/AssetsContext.js b/src/resources/AssetsContext.js new file mode 100644 index 0000000..13ed489 --- /dev/null +++ b/src/resources/AssetsContext.js @@ -0,0 +1,32 @@ +import 'isomorphic-fetch'; +import PagedContext from './PagedContext'; +import Asset from './Asset'; + +/** + * Asset querying context + * + * This is used to query the list of assets for a customer + */ +class AssetsContext extends PagedContext { + /** + * Creates a new asset context + * @param {Client} client Instance of pre-configured client + * @param {string} customerCode Customer code + * @param {object} params Object of querystring parameters to append to the URL + */ + constructor(client, customerCode, params) { + super(client, { ...params }); + this.code = customerCode; + } + + /** + * Gets the first page of results for this context + * @returns {Promise} If successful, a page of Asset objects + * @see Asset + */ + getPage() { + return this.page(Asset, `/1/${this.code}/assets`); + } +} + +export default AssetsContext; \ No newline at end of file diff --git a/src/resources/AssetsContext.test.js b/src/resources/AssetsContext.test.js new file mode 100644 index 0000000..2e61160 --- /dev/null +++ b/src/resources/AssetsContext.test.js @@ -0,0 +1,30 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import AssetsContext from './AssetsContext'; +import { assets as mockAssets } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When building a query for assets', () => { + const client = new Client(); + client.setAuthenticated(); + + beforeEach(() => fetchMock + .get(client.resolve('/1/SYNC/assets?page=1&per_page=10&sort='), mockAssets.list) + .catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + const assets = new AssetsContext(client, 'SYNC'); + promise = assets + .withPage(1) + .withPerPage(10) + .getPage(); + }); + + it('should make the expected request', () => promise.should.be.fulfilled); +}); diff --git a/src/resources/Customer.js b/src/resources/Customer.js index 62b23b1..3aa6a3e 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -3,6 +3,8 @@ import Resource from './Resource'; import Agency from './Agency'; import Area from './Area'; import AreasContext from './AreasContext'; +import Asset from './Asset'; +import AssetsContext from './AssetsContext'; import Assignment from './Assignment'; import Block from './Block'; import CustomerUsersContext from './CustomerUsersContext'; @@ -103,6 +105,26 @@ class Customer extends Resource { return this.resource(AreasContext, this.code); } + /** + * Gets an asset resource by id + * @param {Object} payload Identity of the asset or object representing a new call + * @returns {Asset} Asset resource + */ + asset(payload) { + if (!isNaN(parseFloat(payload)) && isFinite(payload)) { + return this.resource(Asset, Asset.makeHref(this.code, payload)); + } + return this.resource(Asset, { code: this.code, ...payload }); + } + + /** + * Gets a context for querying this customer's assets + * @returns {AssetsContext} Context for querying this customer's assets + */ + assets() { + return this.resource(AssetsContext, this.code); + } + /** * Gets a assignment resource by vehicle id * @param {Number} id Identity of the vehicle diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js index 2461a65..0f24254 100644 --- a/src/resources/Customer.test.js +++ b/src/resources/Customer.test.js @@ -7,6 +7,8 @@ import RealTimeClient from '../RealTimeClient'; import Agency from './Agency'; import Area from './Area'; import AreasContext from './AreasContext'; +import Asset from './Asset'; +import AssetsContext from './AssetsContext'; import Block from './Block'; import Call from './Call'; import CallParticipant from './CallParticipant'; @@ -55,6 +57,8 @@ describe('When getting resources related to a customer', () => { it('should allow the agency record to be retrieved', () => customer.agency().should.be.instanceOf(Agency)); it('should allow an area to be retrieved', () => customer.area().should.be.instanceOf(Area)); it('should allow a list of areas to be retrieved', () => customer.areas().should.be.instanceOf(AreasContext)); + it('should allow an asset to be retrieved', () => customer.asset().should.be.instanceOf(Asset)); + it('should allow a list of assets to be retrieved', () => customer.assets().should.be.instanceOf(AssetsContext)); 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));