Skip to content

Commit

Permalink
fix(client): Fixed issue with requesting JavaScript objects via fetch()
Browse files Browse the repository at this point in the history
(#12)
  • Loading branch information
thzinc authored Apr 26, 2017
1 parent 49d5fed commit 26dfda5
Show file tree
Hide file tree
Showing 8 changed files with 58 additions and 44 deletions.
29 changes: 28 additions & 1 deletion src/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,20 @@ class Client {
* @returns {Promise} Promise of a {@link https://developer.mozilla.org/en-US/docs/Web/API/Response Response} if successful, or of {@link ErrorResponse} if unsuccessful
*/
request(method, uri, options = {}) {
let {
body,
...rest
} = options;

if (body && !(body instanceof Blob)) {
body = Client.toBlob(body);
}

const opts = {
headers: {},
method,
...options,
body,
...rest,
};

if (JWT.get() && !opts.headers.Authorization && !opts.headers['Api-Key']) {
Expand Down Expand Up @@ -118,6 +128,23 @@ class Client {
this.authenticatedResolve = resolve;
});
}

/**
* @callback toBlobSelector
* @param {Object} object Object to map to a string
* @returns {string} String representation of object
*/

/**
* Makes a Blob object
* @param {Object} value Value to encode in Blob
* @param {toBlobSelector} [selector] Function to map the value to a string
* @param {string} [type=application/json] MIME type of Blob
* @returns {Blob} Instance of Blob
*/
static toBlob(value, selector = x => JSON.stringify(x, null, 2), type = 'application/json') {
return new Blob([selector(value)], { type });
}
}

export default Client;
10 changes: 5 additions & 5 deletions src/Client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';
import Client from './Client';
import { NotFoundResponse, ServerErrorResponse } from './responses';
import { toBlob } from './testutils';

chai.should();
chai.use(chaiAsPromised);
Expand Down Expand Up @@ -68,9 +67,10 @@ describe('When using the client to make POST requests', () => {
});

it('should make a successful POST request', () => {
const body = new FormData();
body.append('username', '[email protected]');
body.append('password', 'securepassword');
const body = Client.toBlob({
username: '[email protected]',
password: 'securepassword',
});

return client.post('/success', { body })
.then(response => response.json())
Expand All @@ -89,7 +89,7 @@ describe('When handling errors in requests through the client', () => {
beforeEach(() => {
fetchMock
.get('http://example.com/failure/404', new Response(new Blob(), { status: 404 }))
.get('http://example.com/failure/500', new Response(toBlob(serverErrorResponse), { status: 500 }))
.get('http://example.com/failure/500', new Response(Client.toBlob(serverErrorResponse), { status: 500 }))
.catch(503);
});
afterEach(fetchMock.restore);
Expand Down
30 changes: 15 additions & 15 deletions src/mocks.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import fetchMock from 'fetch-mock';
import { toBlob } from './testutils';
import Client from './Client';

export const charlie = {
setUpSuccessfulMock: (client) => {
fetchMock
.post(client.resolve('/1/login'), () => new Response(toBlob(charlie.token, s => s, 'plain/text')))
.post(client.resolve('/1/login/renew'), () => new Response(toBlob(charlie.token, s => s, 'plain/text')));
.post(client.resolve('/1/login'), () => new Response(Client.toBlob(charlie.token, s => s, 'plain/text')))
.post(client.resolve('/1/login/renew'), () => new Response(Client.toBlob(charlie.token, s => s, 'plain/text')));
},
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjMsImZuYW1lIjoiQ2hhcmxpZSIsImxuYW1lIjoiU2luZ2giLCJjdXN0Ijp7IlNZTkMiOiJTeW5jcm9tYXRpY3MifSwicHJpdiI6WyJtYW5hZ2UgdXNlcnMiLCJtYW5hZ2Ugc2ltIGNhcmRzIiwibWFuYWdlIGNlbGx1bGFyIHBsYW5zIiwibWFuYWdlIG1vZGVtcyIsIm1hbmFnZSB2ZWhpY2xlcyIsInRyYWNrIHZlaGljbGVzIiwidmVoaWNsZSBoaXN0b3J5IiwidmVoaWNsZSBzdGF0dXMiLCJtYW5hZ2Ugcm91dGVzIiwibWFuYWdlIHBlcmltZXRlcnMiLCJtb3ZlbWVudCBzaW11bGF0b3IiLCJtYW5hZ2UgYWxlcnRzIiwibWFuYWdlIHBvcnRhbCIsImFsZXJ0IGxvZyIsImNvbnRyb2wgbW9kZW1zIiwiYWNrIGhpc3RvcnkiLCJ3ZWIgbG9nIiwibW9kZW0gc2NyaXB0cyIsImdsb2JhbCBzdGF0dXMiLCJzZXJ2ZXIgbG9nIiwibWFuYWdlIHJvdXRlIHN0b3BzIiwiYXNzaWduIHZlaGljbGVzIiwic3RvcCB0aW1lcyIsInJlcG9ydHMiLCJhY2NvdW50aW5nIiwibWFuYWdlIGFjY291bnRzIiwibWFuYWdlIG1kdHMiLCJtYW5hZ2UgYXBjcyIsImVuZ2luZSBkaWFnbm9zdGljcyIsIm1hbmFnZSBkcml2ZXJzIiwiZmlsZSBkb3dubG9hZHMiLCJidW5jaGluZyIsIkRpc3BhdGNoIiwiTWFuYWdlIFZEIENvbnRyb2xsZXJzIiwiTWFuYWdlIFNpZ25zIiwiVmlzaXRvciBUcmFmZmljIiwiUG9ydGFsIFNlY3VyaXR5IiwiUHJpdmlsZWdlIFRlbXBsYXRlcyIsIkludmVudG9yeSBNYW5hZ2VtZW50IiwiTWFuYWdlIEludm9pY2VzIiwiTWFuYWdlIFF1b3RlcyIsIk1hbmFnZSBSZWNvbmNpbGlhdGlvbiIsIk1hbmFnZSBFbXBsb3llZXMiLCJNRFQgRW1lcmdlbmN5IENvbnRhY3RzIiwiYXZhcyIsIm1hbmFnZSBkZXN0aW5hdGlvbiBzaWduIiwiTWFuYWdlIFNjaGVkdWxlcyJdLCJzdWIiOiJjc2luZ2hAZXhhbXBsZS5jb20iLCJqdGkiOiJjZWJlZDEwNS0yYTVmLTRmOTgtYTVhMi1kZjg1MzJlNzk2NDEiLCJpYXQiOjE0ODU0NTIzODg5MTYsImV4cCI6MTQ4NTQ1MjQ0ODkxNn0.0PNzuAc-QuzcBEYA0mmBMTqADwoH8Dd6mxXlv0FjQhk',
payload: {
Expand Down Expand Up @@ -75,13 +75,13 @@ export const charlie = {
export const messageTemplates = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(messageTemplates.list),
Client.toBlob(messageTemplates.list),
{
headers: {
Link: '</1/SYNC/message_templates?page=1&perPage=10&q=5k&sort=>; rel="next", </1/SYNC/message_templates?page=1&perPage=10&q=5k&sort=>; rel="last"',
},
});
const singleResponse = () => new Response(toBlob(messageTemplates.getById(1)));
const singleResponse = () => new Response(Client.toBlob(messageTemplates.getById(1)));
const createResponse = () => new Response(undefined, {
headers: {
Location: '/1/SYNC/message_templates/1',
Expand Down Expand Up @@ -143,13 +143,13 @@ export const messageTemplates = {
export const routes = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(routes.list),
Client.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)));
const singleResponse = () => new Response(Client.toBlob(routes.getById(1)));

fetchMock
.get(client.resolve('/1/SYNC/routes?page=1&perPage=10&q=blue&sort='), listResponse)
Expand Down Expand Up @@ -178,13 +178,13 @@ export const routes = {
export const signs = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(signs.list),
Client.toBlob(signs.list),
{
headers: {
Link: '</1/SYNC/signs?page=1&perPage=10&q=first&sort=>; rel="next", </1/SYNC/signs?page=1&perPage=10&q=first&sort=>; rel="last"',
},
});
const singleResponse = () => new Response(toBlob(signs.getById(1)));
const singleResponse = () => new Response(Client.toBlob(signs.getById(1)));

fetchMock
.get(client.resolve('/1/SYNC/signs?page=1&perPage=10&q=first&sort='), listResponse)
Expand All @@ -204,13 +204,13 @@ export const signs = {
export const stops = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(stops.list),
Client.toBlob(stops.list),
{
headers: {
Link: '</1/SYNC/stops?page=1&perPage=10&q=1st&sort=>; rel="next", </1/SYNC/stops?page=1&perPage=10&q=1st&sort=>; rel="last"',
},
});
const singleResponse = () => new Response(toBlob(stops.getById(1)));
const singleResponse = () => new Response(Client.toBlob(stops.getById(1)));

fetchMock
.get(client.resolve('/1/SYNC/stops?page=1&perPage=10&q=1st&sort='), listResponse)
Expand All @@ -231,13 +231,13 @@ export const stops = {
export const tags = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(tags.list),
Client.toBlob(tags.list),
{
headers: {
Link: '</1/SYNC/tags?page=1&perPage=10&q=LA&sort=>; rel="next", </1/SYNC/tags?page=1&perPage=10&q=LA&sort=>; rel="last"',
},
});
const singleResponse = () => new Response(toBlob(tags.getById(3)));
const singleResponse = () => new Response(Client.toBlob(tags.getById(3)));
const postResponse = () => new Response(undefined, {
headers: {
Location: '/1/SYNC/tags/3',
Expand Down Expand Up @@ -269,13 +269,13 @@ export const tags = {
export const vehicles = {
setUpSuccessfulMock: (client) => {
const listResponse = () => new Response(
toBlob(vehicles.list),
Client.toBlob(vehicles.list),
{
headers: {
Link: '</1/SYNC/vehicles?page=1&perPage=10&q=12&sort=>; rel="next", </1/SYNC/vehicles?page=1&perPage=10&q=12&sort=>; rel="last"',
},
});
const singleResponse = () => new Response(toBlob(vehicles.getById(1)));
const singleResponse = () => new Response(Client.toBlob(vehicles.getById(1)));

fetchMock
.get(client.resolve('/1/SYNC/vehicles?page=1&perPage=10&q=12&sort='), listResponse)
Expand Down
5 changes: 3 additions & 2 deletions src/resources/MessageTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ class MessageTemplate extends Resource {
* @returns {Promise} If successful, the a hydrated instance of messageTemplate with id
*/
create() {
// eslint-disable-next-line no-unused-vars
const { client, hydrated, customerCode, ...body } = this;
return this.client.post(`/1/${customerCode}/message_templates`, { ...body })
return this.client.post(`/1/${customerCode}/message_templates`, { body })
.then(response => response.headers.get('location'))
.then((href) => {
const match = /\/\d+\/\S+\/message_templates\/(\d+)/.exec(href);
// console.warn(match);
return new MessageTemplate(this.client, { ...this, href, id: parseFloat(match[1]) });
});
}
Expand All @@ -81,6 +81,7 @@ class MessageTemplate extends Resource {
* @returns {Promise} if successful returns instance of this tag
*/
update() {
// eslint-disable-next-line no-unused-vars
const { client, hydrated, customerCode, ...body } = this;
return this.client.put(`/1/${this.customerCode}/message_templates/${this.id}`, { body })
.then(() => new MessageTemplate(this.client, { ...this }));
Expand Down
3 changes: 1 addition & 2 deletions src/resources/Page.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';
import Client from '../Client';
import Page from './Page';
import { toBlob } from '../testutils';

chai.should();
chai.use(chaiAsPromised);
Expand All @@ -13,7 +12,7 @@ describe('When getting a page of results', () => {
client.setAuthenticated();

const successfulListResponse = page => () => new Response(
toBlob(Array.from(new Array(10))
Client.toBlob(Array.from(new Array(10))
.map((_, i) => ({
href: `/example/${(i + (10 * (page - 1)))}`,
}))),
Expand Down
2 changes: 2 additions & 0 deletions src/resources/Tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Tag extends Resource {
* @returns {Promise} if successful returns a tag with the id included
*/
create() {
// eslint-disable-next-line no-unused-vars
const { client, hydrated, customerCode, ...body } = this;
return this.client.post(`/1/${this.customerCode}/tags`, { body })
.then(response => response.headers.get('location'))
Expand All @@ -63,6 +64,7 @@ class Tag extends Resource {
* @returns {Promise} if successful returns instance of this tag
*/
update() {
// eslint-disable-next-line no-unused-vars
const { client, hydrated, customerCode, ...body } = this;
return this.client.put(`/1/${this.customerCode}/tags/${this.id}`, { body })
.then(() => new Tag(this.client, { ...this }));
Expand Down
7 changes: 4 additions & 3 deletions src/resources/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import chaiAsPromised from 'chai-as-promised';
import fetchMock from 'fetch-mock';
import Track from './index';
import { ForbiddenResponse } from '../responses';
import { toBlob, resolveAt } from '../testutils';
import { resolveAt } from '../testutils';
import Client from '../Client';
import { charlie } from '../mocks';

chai.should();
Expand Down Expand Up @@ -77,8 +78,8 @@ describe('When unsuccessfully authenticating with the Track API client', () => {

beforeEach(() => {
fetchMock
.post(api.client.resolve('/1/login'), () => new Response(toBlob('', s => s, 'plain/text'), { status: 403 }))
.post(api.client.resolve('/1/login/renew'), () => new Response(toBlob('', s => s, 'plain/text'), { status: 403 }))
.post(api.client.resolve('/1/login'), () => new Response(Client.toBlob('', s => s, 'plain/text'), { status: 403 }))
.post(api.client.resolve('/1/login/renew'), () => new Response(Client.toBlob('', s => s, 'plain/text'), { status: 403 }))
.catch(503);

api.stopAutoRenew();
Expand Down
16 changes: 0 additions & 16 deletions src/testutils.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
/**
* @callback toBlobSelector
* @param {Object} object Object to map to a string
* @returns {string} String representation of object
*/

/**
* Makes a Blob object
* @param {Object} value Value to encode in Blob
* @param {toBlobSelector} [selector] Function to map the value to a string
* @param {string} [type=application/json] MIME type of Blob
* @returns {Blob} Instance of Blob
*/
export const toBlob = (value, selector = x => JSON.stringify(x, null, 2), type = 'application/json') =>
new Blob([selector(value)], { type });

/**
* Logs the result with a curried prefix
* @callback curriedLog
Expand Down

0 comments on commit 26dfda5

Please sign in to comment.