From 77ae8366250bf85ec524701d6df064a7fa46fc8a Mon Sep 17 00:00:00 2001 From: Tyler Zuver Date: Tue, 4 Apr 2017 10:33:38 -0700 Subject: [PATCH] feat(tags): Added Tags support (EN-1575) --- src/examples/get_tags.test.js | 48 +++++++++++++++++++++++++++++++ src/mocks.js | 26 +++++++++++++++++ src/resources/Customer.js | 19 ++++++++++++ src/resources/Customer.test.js | 4 +++ src/resources/MessageTemplate.js | 3 +- src/resources/SignMessage.js | 3 +- src/resources/Tag.js | 22 ++++++++++++++ src/resources/Tag.test.js | 36 +++++++++++++++++++---- src/resources/TagsContext.js | 48 +++++++++++++++++++++++++++++++ src/resources/TagsContext.test.js | 31 ++++++++++++++++++++ 10 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 src/examples/get_tags.test.js create mode 100644 src/resources/TagsContext.js create mode 100644 src/resources/TagsContext.test.js diff --git a/src/examples/get_tags.test.js b/src/examples/get_tags.test.js new file mode 100644 index 0000000..edf4a08 --- /dev/null +++ b/src/examples/get_tags.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, tags as mockTags } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When searching for tags by name', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockTags.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a list of tags', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const tagsPromise = api.customer('SYNC').tags() + .withQuery('LA') // Tags containing "LA" in their name + .getPage() + .then(page => page.list) + .then(tags => tags); // Do things with list of tags + + return tagsPromise; + }); +}); + +describe('When retrieving a tag by ID', () => { + const api = new Track({ autoRenew: false }); + + beforeEach(() => charlie.setUpSuccessfulMock(api.client)); + beforeEach(() => mockTags.setUpSuccessfulMock(api.client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + it('should get a tag', () => { + api.logIn({ username: 'charlie@example.com', password: 'securepassword' }); + + const tagPromise = api.customer('SYNC').tag(3) + .fetch() + .then(tag => tag); // Do things with tag + + return tagPromise; + }); +}); diff --git a/src/mocks.js b/src/mocks.js index a6b2944..aa10538 100644 --- a/src/mocks.js +++ b/src/mocks.js @@ -221,6 +221,32 @@ export const stops = { ], }; +export const tags = { + setUpSuccessfulMock: (client) => { + const listResponse = () => new Response( + toBlob(tags.list), + { + headers: { + Link: '; rel="next", ; rel="last"', + }, + }); + const singleResponse = () => new Response(toBlob(tags.getById(3))); + + fetchMock + .get(client.resolve('/1/SYNC/tags?page=1&perPage=10&q=LA&sort='), listResponse) + .get(client.resolve('/1/SYNC/tags/3'), singleResponse); + }, + getById: id => tags.list.find(v => v.id === id), + list: [ + { + href: '/1/SYNC/tags/3', + id: 3, + name: 'DTLA', + customerId: 1, + }, + ], +}; + export const vehicles = { setUpSuccessfulMock: (client) => { const listResponse = () => new Response( diff --git a/src/resources/Customer.js b/src/resources/Customer.js index edfeb05..5ea1fd1 100644 --- a/src/resources/Customer.js +++ b/src/resources/Customer.js @@ -7,6 +7,8 @@ import Sign from './Sign'; import SignsContext from './SignsContext'; import Stop from './Stop'; import StopsContext from './StopsContext'; +import Tag from './Tag'; +import TagsContext from './TagsContext'; import Vehicle from './Vehicle'; import VehiclesContext from './VehiclesContext'; @@ -99,6 +101,23 @@ class Customer extends Resource { return this.resource(Stop, Stop.makeHref(this.code, id)); } + /** + * Gets a context for querying this customer's tags + * @returns {TagContext} Context for querying this customer's tags + */ + tags() { + return this.resource(TagsContext, this.code); + } + + /** + * Gets a tag resource by id + * @param {Number} id Identity of the tag + * @returns {Tag} Tag resource + */ + tag(id) { + return this.resource(Tag, Tag.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 f0abc64..598c08d 100644 --- a/src/resources/Customer.test.js +++ b/src/resources/Customer.test.js @@ -10,6 +10,8 @@ import Sign from './Sign'; import SignsContext from './SignsContext'; import Stop from './Stop'; import StopsContext from './StopsContext'; +import Tag from './Tag'; +import TagsContext from './TagsContext'; import Vehicle from './Vehicle'; import VehiclesContext from './VehiclesContext'; @@ -28,6 +30,8 @@ describe('When getting resources related to a customer', () => { 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 tags to be searched', () => customer.tags().should.be.instanceof(TagsContext)); + it('should allow a tag to be retrieved', () => customer.tag().should.be.instanceof(Tag)); 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/MessageTemplate.js b/src/resources/MessageTemplate.js index b64a040..954d11c 100644 --- a/src/resources/MessageTemplate.js +++ b/src/resources/MessageTemplate.js @@ -27,7 +27,8 @@ class MessageTemplate extends Resource { const newProperties = Object.assign({}, ...rest); const hydrated = !Object.keys(newProperties).every(k => k === 'href'); const references = { - sign_messages: newProperties.sign_messages && new SignMessage(this.client, newProperties.sign_messages), + sign_messages: newProperties.sign_messages && + new SignMessage(this.client, newProperties.sign_messages), }; Object.assign(this, newProperties, { diff --git a/src/resources/SignMessage.js b/src/resources/SignMessage.js index 6fe1637..d0ded39 100644 --- a/src/resources/SignMessage.js +++ b/src/resources/SignMessage.js @@ -22,7 +22,8 @@ class SignMessage extends Resource { const hydrated = !Object.keys(newProperties).every(k => k === 'href'); const references = { routes: newProperties.routes && newProperties.routes.map(x => new Route(this.client, x)), - schedules: newProperties.schedules && newProperties.schedules.map(x => new SignMessageSchedule(this.client, x)), + schedules: newProperties.schedules && + newProperties.schedules.map(x => new SignMessageSchedule(this.client, x)), signs: newProperties.signs && newProperties.signs.map(x => new Sign(this.client, x)), stops: newProperties.stops && newProperties.stops.map(x => new Stop(this.client, x)), tags: newProperties.tags && newProperties.tags.map(x => new Tag(this.client, x)), diff --git a/src/resources/Tag.js b/src/resources/Tag.js index 51df109..be64937 100644 --- a/src/resources/Tag.js +++ b/src/resources/Tag.js @@ -20,6 +20,28 @@ class Tag extends Resource { hydrated, }); } + + /** + * Makes a href for a given customer code and ID + * @param {string} customerCode Customer code + * @param {Number} id Tag ID + * @returns {string} URI to instance of tag + */ + static makeHref(customerCode, id) { + return { + href: `/1/${customerCode}/tags/${id}`, + }; + } + + /** + * Fetches the data for this tag via the client + * @returns {Promise} If successful, a hydrated instance of this tag + */ + fetch() { + return this.client.get(this.href) + .then(response => response.json()) + .then(tag => new Tag(this.client, this, tag)); + } } export default Tag; diff --git a/src/resources/Tag.test.js b/src/resources/Tag.test.js index 845a121..5e811ff 100644 --- a/src/resources/Tag.test.js +++ b/src/resources/Tag.test.js @@ -1,18 +1,44 @@ import chai from 'chai'; import chaiAsPromised from 'chai-as-promised'; +import fetchMock from 'fetch-mock'; import Client from '../Client'; import Tag from './Tag'; +import { tags as mockTags } from '../mocks'; chai.should(); chai.use(chaiAsPromised); +describe('When instantiating a tag based on customer and ID', () => { + const client = new Client(); + const tag = new Tag(client, Tag.makeHref('SYNC', 3)); + + it('should set the href', () => tag.href.should.equal('/1/SYNC/tags/3')); + it('should not be hydrated', () => tag.hydrated.should.equal(false)); +}); + describe('When instantiating a tag based on an object', () => { const client = new Client(); - const mockTag = { - href: '/1/SYNC/tags/1', - name: 'tag', - }; - const tag = new Tag(client, mockTag); + const tag = new Tag(client, mockTags.getById(3)); + it('should set the ID', () => tag.id.should.equal(3)); + it('should set the href', () => tag.href.should.equal('/1/SYNC/tags/3')); it('should be hydrated', () => tag.hydrated.should.equal(true)); }); + +describe('When fetching a tag based on customer and ID', () => { + const client = new Client(); + + beforeEach(() => mockTags.setUpSuccessfulMock(client)); + beforeEach(() => fetchMock.catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + promise = new Tag(client, Tag.makeHref('SYNC', 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/tags/3')); + it('should be hydrated', () => promise.then(v => v.hydrated).should.eventually.equal(true)); +}); diff --git a/src/resources/TagsContext.js b/src/resources/TagsContext.js new file mode 100644 index 0000000..ec064bf --- /dev/null +++ b/src/resources/TagsContext.js @@ -0,0 +1,48 @@ +import 'isomorphic-fetch'; +import PagedContext from './PagedContext'; +import Tag from './Tag'; + +/** + * Tag querying context + * + * This is used to query the list of tags for a customer + */ +class TagsContext extends PagedContext { + /** + * Creates a new tag 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 tags = new TagContext(...); + * tags + * .withQuery('LA') + * .getPage() + * .then(page => ...); + * @param {string} term Query term to search for + * @returns {TagsContext} 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 Tag objects + * @see Tag + */ + getPage() { + return this.page(Tag, `/1/${this.code}/tags`); + } +} + +export default TagsContext; diff --git a/src/resources/TagsContext.test.js b/src/resources/TagsContext.test.js new file mode 100644 index 0000000..8b9722c --- /dev/null +++ b/src/resources/TagsContext.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 TagsContext from './TagsContext'; +import { tags as mockTags } from '../mocks'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('When building a query for tags', () => { + const client = new Client(); + client.setAuthenticated(); + + beforeEach(() => fetchMock + .get(client.resolve('/1/SYNC/tags?page=9&perPage=27&q=valid&sort='), mockTags.list) + .catch(503)); + afterEach(fetchMock.restore); + + let promise; + beforeEach(() => { + const tags = new TagsContext(client, 'SYNC'); + promise = tags + .withPage(9) + .withPerPage(27) + .withQuery('valid') + .getPage(); + }); + + it('should make the expected request', () => promise.should.be.fulfilled); +});