diff --git a/src/Navigator.js b/src/Navigator.js index d900753..e9d4fe3 100644 --- a/src/Navigator.js +++ b/src/Navigator.js @@ -81,6 +81,11 @@ class Navigator { return this.postUrl(href, body, config) } + async patch (rel, body, params = {}, config = {}) { + const { href } = this.resolveLink(rel, params) + return this.patchUrl(href, body, config) + } + async getUrl (url, params, config) { const { status, @@ -117,6 +122,26 @@ class Navigator { return this } + async patchUrl (url, body, config) { + const { + status, + location, + body: responseBody, + response + } = await this.options.patch(url, body, config) + + this._status = status + this._location = location + this._response = response + this._resource = Resource.fromObject(responseBody) + + if (this.options.followRedirects && status === 204) { + return this.followRedirect(config) + } + + return this + } + async followRedirect (config) { const fullLocation = makeAbsolute( this._location, this.getHeader('location')) diff --git a/src/axiosOptions.js b/src/axiosOptions.js index 83da965..bd2859e 100644 --- a/src/axiosOptions.js +++ b/src/axiosOptions.js @@ -18,7 +18,17 @@ const axiosPost = (url, body, config) => response })) +const axiosPatch = (url, body, config) => + axios.patch(url, body, { ...config, validateStatus: () => true }) + .then((response) => ({ + status: response.status, + body: response.data, + location: response.config.url, + response + })) + module.exports = { get: axiosGet, - post: axiosPost + post: axiosPost, + patch: axiosPatch } diff --git a/test/navigator/patch.spec.js b/test/navigator/patch.spec.js new file mode 100644 index 0000000..cec47ab --- /dev/null +++ b/test/navigator/patch.spec.js @@ -0,0 +1,212 @@ +import faker from 'faker' +import nock from 'nock' +import { expect } from 'chai' +import Resource from '../../src/Resource' +import Navigator from '../../src/Navigator' +import * as api from '../support/api' + +const baseUrl = faker.internet.url() + +describe('Navigator', () => { + beforeEach(() => { + nock.cleanAll() + }) + + it('updates resources in an API', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + api.onPatchRedirect(baseUrl, '/users', { + name: 'Thomas' + }, `${baseUrl}/users/thomas`) + + api.onGet(baseUrl, '/users/thomas', + new Resource() + .addProperty('name', 'Thomas')) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }) + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Thomas') + }) + + it('uses template params when updating resources', async () => { + api.onDiscover(baseUrl, {}, { + useritems: {href: '/users/{id}/items', templated: true} + }) + + api.onPatchRedirect(baseUrl, '/users/thomas/items', { + name: 'Sponge' + }, `${baseUrl}/users/thomas/items/1`) + + api.onGet(baseUrl, '/users/thomas/items/1', + new Resource() + .addProperty('name', 'Sponge')) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('useritems', { + name: 'Sponge' + }, { + id: 'thomas' + }) + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Sponge') + }) + + it('uses absolute url when location is relative', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + api.onPatchRedirect(baseUrl, '/users', { + name: 'Thomas' + }, `/users/thomas`) + + api.onGet(baseUrl, '/users/thomas', + new Resource() + .addProperty('name', 'Thomas')) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }) + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Thomas') + }) + + it('uses same configuration as provided on patch when following redirect', + async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + api.onPatchRedirect(baseUrl, '/users', { + name: 'Thomas' + }, + `${baseUrl}/users/thomas`, { + headers: { authorization: 'Bearer 1a2b3c4d' } + }) + + api.onGet(baseUrl, '/users/thomas', + new Resource().addProperty('name', 'Thomas'), { + headers: { authorization: 'Bearer 1a2b3c4d' } + }) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }, {}, { + headers: { + authorization: 'Bearer 1a2b3c4d' + } + }) + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Thomas') + }) + + it('does not follow location headers when the status is not 204', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users{?admin}', templated: true} + }) + + nock(baseUrl) + .patch('/users', {name: 'Thomas'}) + .reply(400) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }) + + expect(result.status()).to.equal(400) + }) + + it('does not follow location headers when the options say not to', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + api.onPatchRedirect(baseUrl, '/users', { + name: 'Thomas' + }, `${baseUrl}/users/thomas`) + + const discoveryResult = await Navigator.discover(baseUrl, {followRedirects: false}) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }) + + expect(result.status()).to.equal(204) + + expect(result.getHeader('location')) + .to.deep.equal(`${baseUrl}/users/thomas`) + }) + + it('continues the conversation even if we do not follow redirects', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + api.onPatchRedirect(baseUrl, '/users', { + name: 'Thomas' + }, `${baseUrl}/users/thomas`) + + api.onGet(baseUrl, '/users/thomas', + new Resource() + .addProperty('name', 'Thomas')) + + const discoveryResult = await Navigator.discover(baseUrl, {followRedirects: false}) + const patchResult = await discoveryResult.patch('users', { + name: 'Thomas' + }) + const result = await patchResult.followRedirect() + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Thomas') + }) + + it('adds header options for navigation', async () => { + api.onDiscover(baseUrl, {}, { + users: {href: '/users'} + }) + + const headers = { + authorization: 'some-token' + } + + api.onPatchRedirect(baseUrl, '/users', + {name: 'Thomas'}, + `${baseUrl}/users/thomas`, + {headers}) + + api.onGet(baseUrl, '/users/thomas', + new Resource() + .addProperty('name', 'Thomas')) + + const discoveryResult = await Navigator.discover(baseUrl) + const result = await discoveryResult.patch('users', { + name: 'Thomas' + }, {}, {headers}) + + expect(result.status()).to.equal(200) + + expect(result.resource().getProperty('name')) + .to.deep.equal('Thomas') + }) +}) diff --git a/test/support/api.js b/test/support/api.js index 9024545..7b453f2 100644 --- a/test/support/api.js +++ b/test/support/api.js @@ -19,3 +19,10 @@ export const onPostRedirect = (url, path, body, location, { headers } = {}) => .reply(201, undefined, { Location: location }) + +export const onPatchRedirect = (url, path, body, location, { headers } = {}) => + nock(url, { reqheaders: headers }) + .patch(path, body) + .reply(204, undefined, { + Location: location + })