Skip to content

Commit

Permalink
feat(routes): Added support for getting routes for a customer (EN-153…
Browse files Browse the repository at this point in the history
…1) (#5)

feat(routes): Added support for getting routes for a customer (EN-1531)

fix(routes): Fixed comment

fix(routes): Added missing unit tests for Customer integration

fix(documentation): Updated comments to properly refer to *Context types
  • Loading branch information
thzinc authored Mar 28, 2017
1 parent dfa15a6 commit 4b4c6ec
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 3 deletions.
48 changes: 48 additions & 0 deletions src/examples/get_routes.test.js
Original file line number Diff line number Diff line change
@@ -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: '[email protected]', 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: '[email protected]', password: 'securepassword' });

const routesPromise = api.customer('SYNC').route(1)
.fetch()
.then(route => route); // Do things with route

return routesPromise;
});
});
35 changes: 35 additions & 0 deletions src/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ export const charlie = {
},
};

export const routes = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(routes.list),
{
headers: {
Link: '</1/SYNC/routes?page=1&perPage=10&q=blue&sort=>; rel="next", </1/SYNC/routes?page=1&perPage=10&q=blue&sort=>; 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(
Expand Down
23 changes: 21 additions & 2 deletions src/resources/Customer.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/resources/Customer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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));
Expand Down
62 changes: 62 additions & 0 deletions src/resources/Route.js
Original file line number Diff line number Diff line change
@@ -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 <caption>Assigning partial route data to a new instance</caption>
* 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;
48 changes: 48 additions & 0 deletions src/resources/Route.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});
48 changes: 48 additions & 0 deletions src/resources/RoutesContext.js
Original file line number Diff line number Diff line change
@@ -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;
34 changes: 34 additions & 0 deletions src/resources/RoutesContext.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
2 changes: 1 addition & 1 deletion src/resources/VehiclesContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 4b4c6ec

Please sign in to comment.