From 279c6d4a4a2ccf33e9004d75a4f801f62db86a98 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Tue, 28 Mar 2017 10:30:20 -0700 Subject: [PATCH] feat(stops): Add support for getting stops for a customer (EN-1527) (#4) feat(stops): Add support for getting stops for a customer (EN-1527) fix(stops): Added missing integration with Customer and example tests --- src/examples/get_stops.test.js | 48 +++++++++++++++++++++++++ src/mocks.js | 27 ++++++++++++++ src/resources/Customer.js | 19 ++++++++++ src/resources/Customer.test.js | 4 +++ src/resources/Sign.js | 2 +- src/resources/Stop.js | 57 ++++++++++++++++++++++++++++++ src/resources/Stop.test.js | 44 +++++++++++++++++++++++ src/resources/StopsContext.js | 48 +++++++++++++++++++++++++ src/resources/StopsContext.test.js | 31 ++++++++++++++++ src/resources/Vehicle.js | 2 +- 10 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 src/examples/get_stops.test.js create mode 100644 src/resources/Stop.js create mode 100644 src/resources/Stop.test.js create mode 100644 src/resources/StopsContext.js create mode 100644 src/resources/StopsContext.test.js diff --git a/src/examples/get_stops.test.js b/src/examples/get_stops.test.js new file mode 100644 index 0000000..62a5992 --- /dev/null +++ b/src/examples/get_stops.test.js @@ -0,0 +1,48 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Track from '../index'; +import { charlie, stops as mockStops } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When searching for stops by name', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockStops.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a list of stops', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const stopsPromise = api.customer('SYNC').stops() + .withQuery('1st') // Stops containing "1st" in their name + .getPage() + .then(page => page.list) + .then(stops => stops); // Do things with list of stops + + return stopsPromise; + }); +}); + +describe('When retrieving a stop by ID', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockStops.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a stop', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const stopsPromise = api.customer('SYNC').stop(1) + .fetch() + .then(stop => stop); // Do things with stop + + return stopsPromise; + }); +}); diff --git a/src/mocks.js b/src/mocks.js index d89a385..be57729 100644 --- a/src/mocks.js +++ b/src/mocks.js @@ -133,6 +133,33 @@ export const signs = { ], }; +export const stops = { + setUpSuccessfulMock: (client) => { + const listResponse = () => new Response( + toBlob(stops.list), + { + headers: { + Link: '; rel="next", ; rel="last"', + }, + }); + const singleResponse = () => new Response(toBlob(stops.getById(1))); + + fetchMock + .get(client.resolve('/1/SYNC/stops?page=1&perPage=10&q=1st&sort='), listResponse) + .get(client.resolve('/1/SYNC/stops/1'), singleResponse); + }, + getById: id => stops.list.find(v => v.id === id), + list: [ + { + href: '/1/SYNC/stops/1', + id: 1, + name: '1st/Main', + latitude: 34.081728, + longitude: -118.351585, + }, + ], +}; + export const vehicles = { setUpSuccessfulMock: (client) => { const listResponse = () => new Response( diff --git a/src/resources/Customer.js b/src/resources/Customer.js index fba563f..ee88185 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -3,6 +3,8 @@ import Route from './Route'; import RoutesContext from './RoutesContext'; import Sign from './Sign'; import SignsContext from './SignsContext'; +import Stop from './Stop'; +import StopsContext from './StopsContext'; import Vehicle from './Vehicle'; import VehiclesContext from './VehiclesContext'; @@ -61,6 +63,23 @@ class Customer extends Resource { return this.resource(Sign, Sign.makeHref(this.code, id)); } + /** + * Gets a context for querying this customer's stops + * @returns {StopContext} Context for querying this customer's stops + */ + stops() { + return this.resource(StopsContext, this.code); + } + + /** + * Gets a stop resource by id + * @param {Number} id Identity of the stop + * @returns {Stop} Stop resource + */ + stop(id) { + return this.resource(Stop, Stop.makeHref(this.code, id)); + } + /** * Gets a context for querying this customer's vehicles * @returns {VehiclesContext} Context for querying this customer's vehicles diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js index d06a16b..0c96c21 100644 --- a/src/resources/Customer.test.js +++ b/src/resources/Customer.test.js @@ -6,6 +6,8 @@ import Route from './Route'; import RoutesContext from './RoutesContext'; import Sign from './Sign'; import SignsContext from './SignsContext'; +import Stop from './Stop'; +import StopsContext from './StopsContext'; import Vehicle from './Vehicle'; import VehiclesContext from './VehiclesContext'; @@ -20,6 +22,8 @@ describe('When getting resources related to a customer', () => { it('should allow a route to be retrieved', () => customer.route().should.be.instanceof(Route)); it('should allow signs to be searched', () => customer.signs().should.be.instanceof(SignsContext)); it('should allow a sign to be retrieved', () => customer.sign().should.be.instanceof(Sign)); + it('should allow stops to be searched', () => customer.stops().should.be.instanceof(StopsContext)); + it('should allow a stop to be retrieved', () => customer.stop().should.be.instanceof(Stop)); it('should allow vehicles to be searched', () => customer.vehicles().should.be.instanceof(VehiclesContext)); it('should allow a vehicle to be retrieved', () => customer.vehicle().should.be.instanceof(Vehicle)); }); diff --git a/src/resources/Sign.js b/src/resources/Sign.js index 7cfb352..0dd8450 100644 --- a/src/resources/Sign.js +++ b/src/resources/Sign.js @@ -11,7 +11,7 @@ class Sign extends Resource { * @example Assigning partial sign data to a new instance * const client = new Client(); * const partialSignData = { - * href: '/1/SYNC/sign/2', + * href: '/1/SYNC/signs/2', * name: 'The second sign', * }; * const sign = new Sign(client, partialSignData); diff --git a/src/resources/Stop.js b/src/resources/Stop.js new file mode 100644 index 0000000..c7d058b --- /dev/null +++ b/src/resources/Stop.js @@ -0,0 +1,57 @@ +import Resource from './Resource'; + +/** + * Stop resource + */ +class Stop extends Resource { + /** + * Creates a new stop + * + * Will populate itself with the values given to it after the client parameter + * @example Assigning partial stop data to a new instance + * const client = new Client(); + * const partialStopData = { + * href: '/1/SYNC/stops/2', + * name: '9876', + * }; + * const stop = new Stop(client, partialStopData); + * + * stop.hydrated == true; + * @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'); + + Object.assign(this, newProperties, { + hydrated, + }); + } + + /** + * Makes a href for a given customer code and ID + * @param {string} customerCode Customer code + * @param {Number} id Stop ID + * @returns {string} URI to instance of stop + */ + static makeHref(customerCode, id) { + return { + href: `/1/${customerCode}/stops/${id}`, + }; + } + + /** + * Fetches the data for this stop via the client + * @returns {Promise} If successful, a hydrated instance of this stop + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(stop => new Stop(this.client, this, stop)); + } +} + +export default Stop; diff --git a/src/resources/Stop.test.js b/src/resources/Stop.test.js new file mode 100644 index 0000000..7def573 --- /dev/null +++ b/src/resources/Stop.test.js @@ -0,0 +1,44 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import Stop from './Stop'; +import { stops as mockStops } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When instantiating a stop based on customer and ID', () => { + const client = new Client(); + const stop = new Stop(client, Stop.makeHref('SYNC', 1)); + + it('should set the href', () => stop.href.should.equal('/1/SYNC/stops/1')); + it('should not be hydrated', () => stop.hydrated.should.equal(false)); +}); + +describe('When instantiating a stop based on an object', () => { + const client = new Client(); + const stop = new Stop(client, mockStops.getById(1)); + + it('should set the ID', () => stop.id.should.equal(1)); + it('should set the href', () => stop.href.should.equal('/1/SYNC/stops/1')); + it('should be hydrated', () => stop.hydrated.should.equal(true)); +}); + +describe('When fetching a stop based on customer and ID', () => { + const client = new Client(); + + beforeEach(() => mockStops.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Stop(client, Stop.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/stops/1')); + it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true)); +}); diff --git a/src/resources/StopsContext.js b/src/resources/StopsContext.js new file mode 100644 index 0000000..9458104 --- /dev/null +++ b/src/resources/StopsContext.js @@ -0,0 +1,48 @@ +import 'isomorphic-fetch'; +import PagedContext from './PagedContext'; +import Stop from './Stop'; + +/** + * Stop querying context + * + * This is used to query the list of stops for a customer + */ +class StopsContext extends PagedContext { + /** + * Creates a new stop 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; + } + + /** + * Sets the query term for the context + * @example + * const stops = new StopContext(...); + * stops + * .withQuery('12') + * .getPage() + * .then(page => ...); + * @param {string} term Query term to search for + * @returns {StopsContext} Returns itself + */ + withQuery(term) { + this.params.q = term; + return this; + } + + /** + * Gets the first page of results for this context + * @returns {Promise} If successful, a page of Stop objects + * @see Stop + */ + getPage() { + return this.page(Stop, `/1/${this.code}/stops`); + } +} + +export default StopsContext; diff --git a/src/resources/StopsContext.test.js b/src/resources/StopsContext.test.js new file mode 100644 index 0000000..1c4ea79 --- /dev/null +++ b/src/resources/StopsContext.test.js @@ -0,0 +1,31 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import StopsContext from './StopsContext'; +import { stops as mockStops } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When building a query for stops', () => { + const client = new Client(); + client.setAuthenticated(); + + beforeEach(() => fetchMock + .get(client.resolve('/1/SYNC/stops?page=9&perPage=27&q=valid&sort='), mockStops.list) + .catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + const stops = new StopsContext(client, 'SYNC'); + promise = stops + .withPage(9) + .withPerPage(27) + .withQuery('valid') + .getPage(); + }); + + it('should make the expected request', () => promise.should.be.fulfilled); +}); diff --git a/src/resources/Vehicle.js b/src/resources/Vehicle.js index 4ec7d9b..83765a8 100644 --- a/src/resources/Vehicle.js +++ b/src/resources/Vehicle.js @@ -12,7 +12,7 @@ class Vehicle extends Resource { * @example Assigning partial vehicle data to a new instance * const client = new Client(); * const partialVehicleData = { - * href: '/1/SYNC/vehicle/2', + * href: '/1/SYNC/vehicles/2', * name: '9876', * assignment: { * sign_in_type: 'Dispatch',