diff --git a/src/examples/get_routes.test.js b/src/examples/get_routes.test.js new file mode 100644 index 0000000..968490d --- /dev/null +++ b/src/examples/get_routes.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, routes as mockRoutes } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When searching for routes by name', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockRoutes.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a list of routes', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const routesPromise = api.customer('SYNC').routes() + .withQuery('blue') // Routes containing "blue" in their name + .getPage() + .then(page => page.list) + .then(routes => routes); // Do things with list of routes + + return routesPromise; + }); +}); + +describe('When retrieving a route by ID', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockRoutes.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a route', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const routesPromise = api.customer('SYNC').route(1) + .fetch() + .then(route => route); // Do things with route + + return routesPromise; + }); +}); diff --git a/src/mocks.js b/src/mocks.js index 52159d8..d89a385 100644 --- a/src/mocks.js +++ b/src/mocks.js @@ -72,6 +72,41 @@ export const charlie = { }, }; +export const routes = { + setUpSuccessfulMock: (client) => { + const listResponse = () => new Response( + toBlob(routes.list), + { + headers: { + Link: '; rel="next", ; rel="last"', + }, + }); + const singleResponse = () => new Response(toBlob(routes.getById(1))); + + fetchMock + .get(client.resolve('/1/SYNC/routes?page=1&perPage=10&q=blue&sort='), listResponse) + .get(client.resolve('/1/SYNC/routes/1'), singleResponse); + }, + getById: id => routes.list.find(v => v.id === id), + list: [ + { + href: '/1/SYNC/routes/1', + id: 1, + name: 'Blue Line', + short_name: 'Blue', + description: 'Servicing the Townsville community', + is_public: true, + color: '#0000FF', + text_color: '#FFFFFF', + patterns: [ + { + href: '/1/SYNC/patterns/1', + }, + ], + }, + ], +}; + export const signs = { setUpSuccessfulMock: (client) => { const listResponse = () => new Response( diff --git a/src/resources/Customer.js b/src/resources/Customer.js index ca87da2..fba563f 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -1,4 +1,6 @@ import Resource from './Resource'; +import Route from './Route'; +import RoutesContext from './RoutesContext'; import Sign from './Sign'; import SignsContext from './SignsContext'; import Vehicle from './Vehicle'; @@ -25,9 +27,26 @@ class Customer extends Resource { this.code = customerCode; } + /** + * Gets a context for querying this customer's routes + * @returns {RoutesContext} Context for querying this customer's routes + */ + routes() { + return this.resource(RoutesContext, this.code); + } + + /** + * Gets a route resource by id + * @param {Number} id Identity of the route + * @returns {Route} Route resource + */ + route(id) { + return this.resource(Route, Route.makeHref(this.code, id)); + } + /** * Gets a context for querying this customer's signs - * @returns {SignContext} Context for querying this customer's signs + * @returns {SignsContext} Context for querying this customer's signs */ signs() { return this.resource(SignsContext, this.code); @@ -44,7 +63,7 @@ class Customer extends Resource { /** * Gets a context for querying this customer's vehicles - * @returns {VehicleContext} Context for querying this customer's vehicles + * @returns {VehiclesContext} Context for querying this customer's vehicles */ vehicles() { return this.resource(VehiclesContext, this.code); diff --git a/src/resources/Customer.test.js b/src/resources/Customer.test.js index 1364eb0..d06a16b 100644 --- a/src/resources/Customer.test.js +++ b/src/resources/Customer.test.js @@ -2,6 +2,8 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; import Client from '../Client'; import Customer from './Customer'; +import Route from './Route'; +import RoutesContext from './RoutesContext'; import Sign from './Sign'; import SignsContext from './SignsContext'; import Vehicle from './Vehicle'; @@ -14,6 +16,8 @@ describe('When getting resources related to a customer', () => { const client = new Client(); const customer = new Customer(client, 'SYNC'); + it('should allow routes to be searched', () => customer.routes().should.be.instanceof(RoutesContext)); + 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 vehicles to be searched', () => customer.vehicles().should.be.instanceof(VehiclesContext)); diff --git a/src/resources/Route.js b/src/resources/Route.js new file mode 100644 index 0000000..66d6e26 --- /dev/null +++ b/src/resources/Route.js @@ -0,0 +1,62 @@ +import Resource from './Resource'; +import Assignment from './Assignment'; + +/** + * Route resource + */ +class Route extends Resource { + /** + * Creates a new route + * + * Will populate itself with the values given to it after the client parameter + * @example Assigning partial route data to a new instance + * const client = new Client(); + * const partialRouteData = { + * href: '/1/SYNC/routes/2', + * name: '9876', + * }; + * const route = new Route(client, partialRouteData); + * + * route.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'); + const references = { + assignment: newProperties.assignment && new Assignment(this.client, newProperties.assignment), + }; + + Object.assign(this, newProperties, { + hydrated, + ...references, + }); + } + + /** + * Makes a href for a given customer code and ID + * @param {string} customerCode Customer code + * @param {Number} id Route ID + * @returns {string} URI to instance of route + */ + static makeHref(customerCode, id) { + return { + href: `/1/${customerCode}/routes/${id}`, + }; + } + + /** + * Fetches the data for this route via the client + * @returns {Promise} If successful, a hydrated instance of this route + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(route => new Route(this.client, this, route)); + } +} + +export default Route; diff --git a/src/resources/Route.test.js b/src/resources/Route.test.js new file mode 100644 index 0000000..95df3e7 --- /dev/null +++ b/src/resources/Route.test.js @@ -0,0 +1,48 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import Route from './Route'; +import { routes as mockRoutes } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When instantiating a route based on customer and ID', () => { + const client = new Client(); + const route = new Route(client, Route.makeHref('SYNC', 1)); + + it('should set the href', () => route.href.should.equal('/1/SYNC/routes/1')); + it('should not be hydrated', () => route.hydrated.should.equal(false)); +}); + +describe('When instantiating a route based on an object', () => { + const client = new Client(); + const route = new Route(client, mockRoutes.getById(1)); + + it('should set the ID', () => route.id.should.equal(1)); + it('should set the href', () => route.href.should.equal('/1/SYNC/routes/1')); + it('should be hydrated', () => route.hydrated.should.equal(true)); + it('should have one pattern', () => route.patterns.length.should.equal(1)); + it('should have the expected pattern', () => route.patterns[0].href.should.equal('/1/SYNC/patterns/1')); +}); + +describe('When fetching a route based on customer and ID', () => { + const client = new Client(); + + beforeEach(() => mockRoutes.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Route(client, Route.makeHref('SYNC', 1)).fetch(); + }); + + it('should resolve the promise', () => promise.should.be.fulfilled); + it('should set the ID', () => promise.then(r => r.id).should.eventually.equal(1)); + it('should set the href', () => promise.then(r => r.href).should.eventually.equal('/1/SYNC/routes/1')); + it('should be hydrated', () => promise.then(r => r.hydrated).should.eventually.equal(true)); + it('should have one pattern', () => promise.then(r => r.patterns.length).should.eventually.equal(1)); + it('should have the expected pattern', () => promise.then(r => r.patterns[0].href).should.eventually.equal('/1/SYNC/patterns/1')); +}); diff --git a/src/resources/RoutesContext.js b/src/resources/RoutesContext.js new file mode 100644 index 0000000..980b088 --- /dev/null +++ b/src/resources/RoutesContext.js @@ -0,0 +1,48 @@ +import 'isomorphic-fetch'; +import PagedContext from './PagedContext'; +import Route from './Route'; + +/** + * Route querying context + * + * This is used to query the list of routes for a customer + */ +class RoutesContext extends PagedContext { + /** + * Creates a new route 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 routes = new RoutesContext(...); + * routes + * .withQuery('blue') + * .getPage() + * .then(page => ...); + * @param {string} term Query term to search for + * @returns {RoutesContext} 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 Route objects + * @see Route + */ + getPage() { + return this.page(Route, `/1/${this.code}/routes`); + } +} + +export default RoutesContext; diff --git a/src/resources/RoutesContext.test.js b/src/resources/RoutesContext.test.js new file mode 100644 index 0000000..7945fb4 --- /dev/null +++ b/src/resources/RoutesContext.test.js @@ -0,0 +1,34 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; +import Client from '../Client'; +import RoutesContext from './RoutesContext'; +import { routes as mockRoutes } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When building a query for routes', () => { + const client = new Client(); + client.setAuthenticated(); + + beforeEach(() => fetchMock + .get(client.resolve('/1/SYNC/routes?page=9&perPage=27&q=valid&sort=first_valid asc,second_valid desc'), mockRoutes.list) + .catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + const routes = new RoutesContext(client, 'SYNC'); + promise = routes + .withPage(9) + .withPerPage(27) + .withQuery('valid') + .sortedBy('ignored', 'desc') + .sortedBy('first_valid') + .thenBy('second_valid', 'desc') + .getPage(); + }); + + it('should make the expected request', () => promise.should.be.fulfilled); +}); diff --git a/src/resources/VehiclesContext.js b/src/resources/VehiclesContext.js index a526fe0..1bc3ebf 100644 --- a/src/resources/VehiclesContext.js +++ b/src/resources/VehiclesContext.js @@ -22,7 +22,7 @@ class VehiclesContext extends PagedContext { /** * Sets the query term for the context * @example - * const vehicles = new VehicleContext(...); + * const vehicles = new VehiclesContext(...); * vehicles * .withQuery('12') * .getPage()