diff --git a/.changeset/red-gifts-confess.md b/.changeset/red-gifts-confess.md new file mode 100644 index 000000000..6f68745f1 --- /dev/null +++ b/.changeset/red-gifts-confess.md @@ -0,0 +1,5 @@ +--- +"druxt-entity": minor +--- + +Updated DruxtEntityForm error handling. diff --git a/.changeset/soft-garlics-poke.md b/.changeset/soft-garlics-poke.md new file mode 100644 index 000000000..f91aacc89 --- /dev/null +++ b/.changeset/soft-garlics-poke.md @@ -0,0 +1,6 @@ +--- +"druxt": minor +"druxt-router": minor +--- + +Added improved error handling. diff --git a/packages/druxt/src/client.js b/packages/druxt/src/client.js index 12d54b637..c607f7dc7 100644 --- a/packages/druxt/src/client.js +++ b/packages/druxt/src/client.js @@ -52,6 +52,7 @@ class DruxtClient { // requests and errors. if (options.debug) { const log = this.log + // @TODO - Add test coverage. this.axios.interceptors.request.use((config) => { log.info(config.url) return config @@ -153,7 +154,15 @@ class DruxtClient { } if (Object.keys(permissions).length) { - throw new TypeError(`${res.data.meta.omitted.detail}\n\n Required permissions: ${Object.keys(permissions).join(', ')}.`) + const err = { + response: { + statusText: res.data.meta.omitted.detail, + data: { + errors: [{ detail: `Required permissions:\n - ${Object.keys(permissions).join('\n - ')}` }] + } + } + } + throw err } } } @@ -190,12 +199,63 @@ class DruxtClient { } ) } catch (err) { - response = (err.response || {}).data || err.message + this.error(err, { url: href }) } return response } + /** + * Throw a formatted error. + * + * @param {object} err - The error object + * + * @throws {Error} A formatted error. + */ + error(err, { url }) { + const title = [(err.response || {}).status, (err.response || {}).statusText].filter((s) => s).join(': ') + const meta = { url: [this.options.baseUrl, url].join('') } + + // Build message. + let message = [title] + + // Add meta information. + if (Object.values(meta).filter((o) => o)) { + message.push(Object.entries(meta).filter(([, v]) => v).map(([key, value]) => `${key.toUpperCase()}: ${value}`).join('\n')) + } + + // Add main error details. + if (((((err.response || {}).data || {}).errors || [])[0] || {}).detail) { + message.push(err.response.data.errors[0].detail) + } + + const error = Error(message.join('\n\n')) + error.response = err.response + throw error + } + + /** + * Execute an Axios GET request, with permission checking and error handling. + * + * @param {string} url - The URL to GET. + * @param {object} options - An Axios options object. + * + * @returns {object} The Axios response. + */ + async get(url, options) { + try { + const res = await this.axios.get(url, options) + + // Check that the response hasn't omitted data due to missing permissions. + this.checkPermissions(res) + + return res + } catch(err) { + // Throw formatted error. + this.error(err, { url }) + } + } + /** * Get a collection of resources from the JSON:API server. * @@ -215,11 +275,8 @@ class DruxtClient { const url = this.buildQueryUrl(href, query) - const res = await this.axios.get(url) - - this.checkPermissions(res) - - return res.data + const { data } = await this.get(url) + return data } /** @@ -267,7 +324,15 @@ class DruxtClient { return this.index[resource] ? this.index[resource] : false } - let index = ((await this.axios.get(this.options.endpoint) || {}).data || {}).links + const url = this.options.endpoint + const { data } = await this.get(url) + let index = data.links + + // Throw error if index is invalid. + if (typeof index !== 'object') { + const err = { response: { statusText: 'Invalid JSON:API endpoint' }} + this.error(err, { url }) + } // Remove Base URL from the resource URL. const baseUrl = this.options.baseUrl @@ -276,11 +341,19 @@ class DruxtClient { return [key, value] })) - // Use JSON API resource config to decorate the index. - // @TODO - Add test coverage + // Use JSON:API resource config to decorate the index. + // @TODO - Add test coverage. if (index[this.options.jsonapiResourceConfig]) { - const resources = await this.axios.get(index[this.options.jsonapiResourceConfig].href) - for (const resourceType in resources.data.data) { + let resources = [] + + // Get JSON:API resource config if permissions setup correctly. + try { + resources = (await this.get(index[this.options.jsonapiResourceConfig].href)).data.data + } catch(err) { + this.log.warn(err.message) + } + + for (const resourceType in resources) { const resource = resources.data.data[resourceType] const internal = resource.attributes.drupal_internal__id.split('--') @@ -334,12 +407,8 @@ class DruxtClient { } const url = this.buildQueryUrl(`${href}/${id}/${related}`, query) - try { - const related = await this.axios.get(url) - return related.data - } catch (e) { - return false - } + const { data } = await this.get(url) + return data } /** @@ -360,17 +429,14 @@ class DruxtClient { } let { href } = await this.getIndex(type) + // @TODO - Add test coverage. if (!href) { href = this.options.endpoint + '/' + type.replace('--', '/') } const url = this.buildQueryUrl(`${href}/${id}`, query) - try { - const resource = await this.axios.get(url) - return resource.data - } catch (e) { - return false - } + const { data } = await this.get(url) + return data } /** @@ -402,9 +468,10 @@ class DruxtClient { } ) } catch (err) { - response = (err.response || {}).data || err.message + this.error(err) } + // @TODO - Add test coverage. return response } } diff --git a/packages/druxt/src/components/DruxtModule.vue b/packages/druxt/src/components/DruxtModule.vue index 794db1486..689a21248 100644 --- a/packages/druxt/src/components/DruxtModule.vue +++ b/packages/druxt/src/components/DruxtModule.vue @@ -106,7 +106,11 @@ export default { // Fetch configuration. if ((this.$options.druxt || {}).fetchConfig) { - await this.$options.druxt.fetchConfig.call(this) + try { + await this.$options.druxt.fetchConfig.call(this) + } catch(err) { + return this.error(err) + } } // Build wrapper component object. @@ -137,7 +141,11 @@ export default { // Fetch resource. if ((this.$options.druxt || {}).fetchData) { - await this.$options.druxt.fetchData.call(this, component.settings) + try { + await this.$options.druxt.fetchData.call(this, component.settings) + } catch(err) { + return this.error(err) + } } // Get scoped slots. @@ -152,7 +160,7 @@ export default { watch: { model() { - if (this.component.props.value !== this.model) { + if (this.component.props && this.component.props.value !== this.model) { this.component.props.value = this.model // Only emit 'input' if using the default 'DruxtWrapper' component. @@ -171,6 +179,19 @@ export default { /** */ methods: { + /** + * Sets the component to render a DruxtDebug error message. + */ + error(err) { + this.component = { + is: 'DruxtDebug', + props: { + json: (err.response.data || {}).errors, + summary: [err.response.status, err.response.statusText].filter((s) => s).join(': ') + } + } + }, + /** * Get list of module wrapper components. * diff --git a/packages/druxt/test/__snapshots__/client.test.js.snap b/packages/druxt/test/__snapshots__/client.test.js.snap new file mode 100644 index 000000000..27a763d86 --- /dev/null +++ b/packages/druxt/test/__snapshots__/client.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DruxtClient checkPermissions 1`] = ` +Object { + "detail": "Required permissions: + - administer node fields", +} +`; + +exports[`DruxtClient createResource 1`] = ` +Object { + "detail": "subject: This value should not be null.", + "source": Object { + "pointer": "/data/attributes/subject", + }, + "status": "422", + "title": "Unprocessable Entity", +} +`; + +exports[`DruxtClient getIndex 1`] = ` +Object { + "statusText": "Invalid JSON:API endpoint", +} +`; + +exports[`DruxtClient getResource 1`] = ` +Object { + "detail": "The \\"entity\\" parameter was not converted for the path \\"/jsonapi/node/article/{entity}\\" (route name: \\"jsonapi.node--article.individual\\")", + "links": Object { + "info": Object { + "href": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5", + }, + "via": Object { + "href": "https://demo-api.druxtjs.org/en/jsonapi/node/article/missing", + }, + }, + "status": "404", + "title": "Not Found", +} +`; diff --git a/packages/druxt/test/client.test.js b/packages/druxt/test/client.test.js index f6de7d432..cc6c046aa 100644 --- a/packages/druxt/test/client.test.js +++ b/packages/druxt/test/client.test.js @@ -75,7 +75,12 @@ describe('DruxtClient', () => { } } } - expect(() => druxt.checkPermissions(res)).toThrow('Some resources have been omitted because of insufficient authorization.\n\n Required permissions: administer node fields.') + + try { + druxt.checkPermissions(res) + } catch(err) { + expect(err.response.data.errors[0]).toMatchSnapshot() + } }) test('createResource', async () => { @@ -89,13 +94,17 @@ describe('DruxtClient', () => { // Create a Feedback Contact message resource without require data. const data = { type: 'contact_message--feedback' } - response = await druxt.createResource(data) + try { + await druxt.createResource(data) + } catch(err) { + expect(err.response.data.errors[0]).toMatchSnapshot() + expect(err.response.data.errors.length).toBe(2) + } expect(mockAxios.post).toHaveBeenCalledWith( `${baseUrl}/en/jsonapi/contact_message/feedback`, { data }, { headers } ) - expect(response.errors.length).toBe(2) mockAxios.reset() // Create resource with required data. @@ -116,23 +125,26 @@ describe('DruxtClient', () => { mockAxios.reset() // Update resource. - await druxt.createResource(response.data.data) - expect(mockAxios.patch).toHaveBeenCalledWith( - `${baseUrl}/en/jsonapi/contact_message/feedback/${response.data.data.id}`, - { data: response.data.data }, - { headers } - ) + try { + await druxt.createResource(response.data.data) + } catch(err) { + expect(mockAxios.patch).toHaveBeenCalledWith( + `${baseUrl}/en/jsonapi/contact_message/feedback/${response.data.data.id}`, + { data: response.data.data }, + { headers } + ) + } }) test('getCollection', async () => { // Get a collection of 'node--page' resources. const collection = await druxt.getCollection('node--page') - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page`, undefined) expect(collection.data.length).toBe(1) // Get a filtered collection of 'node--page' resources. await druxt.getCollection('node--page', { 'filter[status]': 1 }) - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page?filter%5Bstatus%5D=1`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page?filter%5Bstatus%5D=1`, undefined) // Get a collection with headers set. await druxt.getCollection('node--page', {}, { headers: { 'X-Druxt': true }}) @@ -147,13 +159,13 @@ describe('DruxtClient', () => { // Get all of the 'test--all' resources. const query = new DrupalJsonApiParams().addFields('node--recipe', []).addPageLimit(5) await druxt.getCollectionAll('node--recipe', query) - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe?page%5Boffset%5D=5&page%5Blimit%5D=5&fields%5Bnode--recipe%5D=`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe?page%5Boffset%5D=5&page%5Blimit%5D=5&fields%5Bnode--recipe%5D=`, undefined) }) test('getIndex', async () => { const index = await druxt.getIndex() expect(mockAxios.get).toHaveBeenCalledTimes(1) - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi') + expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi', undefined) expect(Object.keys(index).length).toBe(64) expect(index[Object.keys(index)[0]]).toHaveProperty('href') @@ -162,12 +174,21 @@ describe('DruxtClient', () => { expect(mockAxios.get).toHaveBeenCalledTimes(1) expect(Object.keys(cachedIndex).length).toBe(64) + + // Simulate broken index. + delete druxt.index + druxt.get = () => ({ data: {} }) + try { + await druxt.getIndex() + } catch(err) { + expect(err.response).toMatchSnapshot() + } }) test('getIndex - resource', async () => { const resourceIndex = await druxt.getIndex('node--page') expect(mockAxios.get).toHaveBeenCalledTimes(1) - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi') + expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi', undefined) expect(resourceIndex).toHaveProperty('href') @@ -184,13 +205,13 @@ describe('DruxtClient', () => { const { type, id } = mockArticle.data const related = await druxt.getRelated(type, id, 'field_media_image') - expect(mockAxios.get).toHaveBeenCalledWith(`${baseUrl}/en/jsonapi/node/article/${id}/field_media_image`) + expect(mockAxios.get).toHaveBeenCalledWith(`${baseUrl}/en/jsonapi/node/article/${id}/field_media_image`, undefined) expect(related.data).toHaveProperty('type') try { await druxt.getRelated('node--fake', id, 'field_media_image') } catch(err) { - expect(mockAxios.get).toHaveBeenCalledWith(`/jsonapi/node/fake/${id}/field_media_image`) + expect(mockAxios.get).toHaveBeenCalledWith(`/jsonapi/node/fake/${id}/field_media_image`, undefined) } }) @@ -199,9 +220,12 @@ describe('DruxtClient', () => { const entity = await druxt.getResource(mockArticle.data.type, mockArticle.data.id) expect(entity.data).toHaveProperty('type', mockArticle.data.type) - const error = await druxt.getResource('missing', 'test') - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi/missing/test') - expect(error).toBe(false) + try { + await druxt.getResource('node--article', 'missing') + } catch(err) { + expect(err.response.data.errors[0]).toMatchSnapshot() + } + expect(mockAxios.get).toHaveBeenCalledWith(`${baseUrl}/en/jsonapi/node/article/missing`, undefined) const empty = await druxt.getResource() expect(empty).toBe(false) diff --git a/packages/druxt/test/components/DruxtModule.test.js b/packages/druxt/test/components/DruxtModule.test.js index 97203e5b6..1e70c1657 100644 --- a/packages/druxt/test/components/DruxtModule.test.js +++ b/packages/druxt/test/components/DruxtModule.test.js @@ -81,6 +81,37 @@ describe('DruxtModule component', () => { expect(wrapper.vm.$refs.module.value).toStrictEqual({ test: true }) }) + test('error', async () => { + const CustomModule = { + name: 'CustomModule', + extends: DruxtModule, + } + + const wrapper = mount(CustomModule, {localVue, mocks, stubs: ['DruxtWrapper'] }) + await wrapper.vm.$options.fetch.call(wrapper.vm) + + expect(wrapper.vm.component.is).toBe('DruxtWrapper') + + // Simulate error. + const err = { + response: { + status: '500', + statusText: 'Test error', + data: { + errors: [{ + details: 'Test error' + }] + } + } + } + wrapper.vm.error(err) + + // Expect the component to be DruxtDebug. + expect(wrapper.vm.component.is).toBe('DruxtDebug') + // Expect the component props to match snapshot. + expect(wrapper.vm.component.props).toMatchSnapshot() + }) + test('custom module - no wrapper', async () => { const CustomModule = { name: 'CustomModule', diff --git a/packages/druxt/test/components/__snapshots__/DruxtModule.test.js.snap b/packages/druxt/test/components/__snapshots__/DruxtModule.test.js.snap index 775f3dd6e..a2288d7f5 100644 --- a/packages/druxt/test/components/__snapshots__/DruxtModule.test.js.snap +++ b/packages/druxt/test/components/__snapshots__/DruxtModule.test.js.snap @@ -13,3 +13,14 @@ exports[`DruxtModule component custom module - wrapper 1`] = ` `; exports[`DruxtModule component defaults 1`] = `"
"`; + +exports[`DruxtModule component error 1`] = ` +Object { + "json": Array [ + Object { + "details": "Test error", + }, + ], + "summary": "500: Test error", +} +`; diff --git a/packages/druxt/test/stores/druxt.test.js b/packages/druxt/test/stores/druxt.test.js index 5bd5bb9f3..886a62801 100644 --- a/packages/druxt/test/stores/druxt.test.js +++ b/packages/druxt/test/stores/druxt.test.js @@ -125,7 +125,7 @@ describe('DruxtStore', () => { // - The request url is correct. // - Only 3 get requests are executed. // - Returned expected data with `_druxt_full` flag. - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page/${mockPage.data.id}`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page/${mockPage.data.id}`, undefined) expect(mockAxios.get).toHaveBeenCalledTimes(2) const expected = { _druxt_full: expect.anything(), @@ -170,7 +170,7 @@ describe('DruxtStore', () => { _druxt_partial: expect.anything(), ...mockRecipe } - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=`, undefined) expect(mockAxios.get).toHaveBeenCalledTimes(2) expect(resource).toStrictEqual(expected) expect(resource.data.attributes).toBe(undefined) @@ -187,7 +187,7 @@ describe('DruxtStore', () => { // - The request url is correct. // - One additional get request executed for missing field data. // - The additional data is present. - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=title`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=title`, undefined) expect(mockAxios.get).toHaveBeenCalledTimes(3) expect(Object.keys(partialResource.data.attributes)).toStrictEqual(['title']) @@ -202,7 +202,7 @@ describe('DruxtStore', () => { // - The request url is correct. // - One additional get request executed for only the missing field. // - All required data is present. - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=path`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockRecipe.data.id}?fields%5Bnode--recipe%5D=path`, undefined) expect(mockAxios.get).toHaveBeenCalledTimes(4) expect(Object.keys(mixedResource.data.attributes)).toStrictEqual(['title', 'path']) @@ -242,7 +242,7 @@ describe('DruxtStore', () => { // - Only 3 get requests are executed. // - Returned expected data with `_druxt_partial` flag. // - Included resources are stored. - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockResource.data.id}?include=field_media_image%2Cfield_media_image.field_media_image&fields%5Bnode--recipe%5D=field_media_image&fields%5Bmedia--image%5D=field_media_image&fields%5Bfile--file%5D=uri`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe/${mockResource.data.id}?include=field_media_image%2Cfield_media_image.field_media_image&fields%5Bnode--recipe%5D=field_media_image&fields%5Bmedia--image%5D=field_media_image&fields%5Bfile--file%5D=uri`, undefined) expect(mockAxios.get).toHaveBeenCalledTimes(2) expect(resource).toStrictEqual({ _druxt_partial: expect.anything(), diff --git a/packages/entity/src/components/DruxtEntityForm.vue b/packages/entity/src/components/DruxtEntityForm.vue index c4df45436..369600b25 100644 --- a/packages/entity/src/components/DruxtEntityForm.vue +++ b/packages/entity/src/components/DruxtEntityForm.vue @@ -91,16 +91,15 @@ export default { if (this.model.id) { method = 'updateResource' } - this.response = await this.$druxt[method](this.model) - // Handle the response - if (!this.response.errors) { + try { + this.response = await this.$druxt[method](this.model) const resource = this.response.data.data // Update the Vuex store. this.$store.commit('druxt/addResource', { resource }) this.$emit('submit', resource) - } - else { + } catch (err) { + this.response = err.response.data this.$emit('error', this.response) } diff --git a/packages/entity/test/components/DruxtEntityForm.test.js b/packages/entity/test/components/DruxtEntityForm.test.js index 096909be5..0811d18fb 100644 --- a/packages/entity/test/components/DruxtEntityForm.test.js +++ b/packages/entity/test/components/DruxtEntityForm.test.js @@ -77,6 +77,7 @@ describe('DruxtEntityForm', () => { // Submit. await wrapper.find('button#submit').trigger('click') await localVue.nextTick() + await localVue.nextTick() expect(wrapper.emitted().error).toBeFalsy() expect(wrapper.emitted().submit).toBeTruthy() expect(wrapper.vm.response.data.data.id).toBe('8e8d340a-04af-461a-ac63-12415d33e936') diff --git a/packages/router/src/router.js b/packages/router/src/router.js index 854e87a3c..de0d05250 100644 --- a/packages/router/src/router.js +++ b/packages/router/src/router.js @@ -294,7 +294,7 @@ class DruxtRouter { // @TODO - Add validation/error handling. const url = `/router/translate-path?path=${path}` - const response = await this.druxt.axios.get(url, { + const response = await this.druxt.get(url, { // Prevent invalid routes (404) from throwing validation errors. validateStatus: status => status < 500 }) @@ -354,9 +354,22 @@ class DruxtRouter { // Process Axios error. if (!(response.status >= 200 && response.status < 300)) { - const error = new Error - error.response = response - throw error + // Handle 404 errors. + if (response.status === 404) { + // Is the Decoupled Router installed? + if (typeof response.data !== 'object') { + response.data = { errors: [{ + detail: 'Please ensure the Decoupled Router module is installed and configured correctly.' + }]} + } + // Has the Decoupled Router provided an error message? + else if (response.data.message || response.data.details) { + response.data.errors = [{ detail: [response.data.message, response.data.details].filter((s) => s).join('\n') }] + } + } + + // Throw error. + this.druxt.error({ response }, { url }) } return route diff --git a/packages/router/test/__snapshots__/router.test.js.snap b/packages/router/test/__snapshots__/router.test.js.snap new file mode 100644 index 000000000..ba4a05664 --- /dev/null +++ b/packages/router/test/__snapshots__/router.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DruxtRouter checkPermissions 1`] = ` +Object { + "detail": "Required permissions: + - administer node fields", +} +`; + +exports[`DruxtRouter getRoute 1`] = ` +Object { + "config": Object { + "baseURL": "https://8080-magenta-koi-31w8trra.ws-us25.gitpod.io", + "headers": Object { + "Accept": "application/json, text/plain, */*", + }, + "maxBodyLength": -1, + "maxContentLength": -1, + "method": "get", + "options": Object {}, + "timeout": 0, + "transformRequest": Array [ + null, + ], + "transformResponse": Array [ + null, + ], + "url": "/router/translate-path?path=/broken-router", + "xsrfCookieName": "XSRF-TOKEN", + "xsrfHeaderName": "X-XSRF-TOKEN", + }, + "data": Object { + "errors": Array [ + Object { + "detail": "Please ensure the Decoupled Router module is installed and configured correctly.", + }, + ], + }, + "headers": Object { + "cache-control": "must-revalidate, no-cache, private", + "content-language": "en", + "content-type": "text/html; charset=UTF-8", + "expires": "Sun, 19 Nov 1978 05:00:00 GMT", + }, + "request": Object {}, + "status": 404, + "statusText": "Not Found", +} +`; diff --git a/packages/router/test/router.test.js b/packages/router/test/router.test.js index 2b60e2ac2..e2dbdb955 100644 --- a/packages/router/test/router.test.js +++ b/packages/router/test/router.test.js @@ -74,7 +74,11 @@ describe('DruxtRouter', () => { } } } - expect(() => router.checkPermissions(res)).toThrow('Some resources have been omitted because of insufficient authorization.\n\n Required permissions: administer node fields.') + try { + router.checkPermissions(res) + } catch(err) { + expect(err.response.data.errors[0]).toMatchSnapshot() + } }) test('get - entity', async () => { @@ -110,7 +114,7 @@ describe('DruxtRouter', () => { test('getIndex', async () => { const index = await router.getIndex() expect(mockAxios.get).toHaveBeenCalledTimes(1) - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi') + expect(mockAxios.get).toHaveBeenLastCalledWith('/jsonapi', undefined) expect(Object.keys(index).length).toBe(64) expect(index[Object.keys(index)[0]]).toHaveProperty('href') @@ -125,7 +129,7 @@ describe('DruxtRouter', () => { test('getIndex - resource', async () => { const resourceIndex = await router.getIndex('node--page') expect(mockAxios.get).toHaveBeenCalledTimes(1) - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi') + expect(mockAxios.get).toHaveBeenLastCalledWith('/jsonapi', undefined) expect(resourceIndex).toHaveProperty('href') @@ -183,9 +187,13 @@ describe('DruxtRouter', () => { const entity = await router.getResource({ type: mockPage.data.type, id: mockPage.data.id }) expect(entity).toHaveProperty('type', mockPage.data.type) - const error = await router.getResource({ id: 'test', type: 'missing' }) - expect(mockAxios.get).toHaveBeenCalledWith('/jsonapi/missing/test') - expect(error).toBe(false) + try { + await router.getResource({ id: 'test', type: 'missing' }) + } catch(err) { + expect(err.response.status).toBe(404) + expect(err.response.statusText).toBe('Not Found') + } + expect(mockAxios.get).toHaveBeenLastCalledWith('/jsonapi/missing/test', undefined) const empty = await router.getResource() expect(empty).toBe(false) @@ -194,18 +202,18 @@ describe('DruxtRouter', () => { // @deprecated test('getResources', async () => { const resources = await router.getResources('node--page') - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page`, undefined) expect(resources.length).toBe(1) await router.getResources('node--page', { 'filter[status]': 1 }) - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page?filter%5Bstatus%5D=1`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/page?filter%5Bstatus%5D=1`, undefined) const noResource = await router.getResources() expect(noResource).toBe(false) const query = new DrupalJsonApiParams().addFields('node--recipe', []).addPageLimit(5) await router.getResources('node--recipe', query, { all: true }) - expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe?page%5Boffset%5D=5&page%5Blimit%5D=5&fields%5Bnode--recipe%5D=`) + expect(mockAxios.get).toHaveBeenLastCalledWith(`${baseUrl}/en/jsonapi/node/recipe?page%5Boffset%5D=5&page%5Blimit%5D=5&fields%5Bnode--recipe%5D=`, undefined) }) test('getResourceByRoute', async () => { @@ -233,14 +241,23 @@ describe('DruxtRouter', () => { // Get the route of node/1. route = await router.getRoute('/node/1') - expect(route).toHaveProperty('isHomePath', false) + // Get 404 error. try { await router.getRoute('/error') } catch(err) { expect(err.response.status).toBe(404) expect(err.response.data.message).toBe('Unable to resolve path /error.') } + + // Test against a backend without the Decoupled Router installed. + // Note: Test data has was created against a modified backend. + const brokenRouter = new DruxtRouter('https://example.com', {}) + try { + await brokenRouter.getRoute('/broken-router') + } catch(err) { + expect(err.response).toMatchSnapshot() + } }) }) diff --git a/test/__fixtures__/get/30fe7f3dac13108813351384279769f1.json b/test/__fixtures__/get/30fe7f3dac13108813351384279769f1.json new file mode 100644 index 000000000..537e967d7 --- /dev/null +++ b/test/__fixtures__/get/30fe7f3dac13108813351384279769f1.json @@ -0,0 +1,57 @@ +{ + "data": { + "jsonapi": { + "version": "1.0", + "meta": { + "links": { + "self": { + "href": "http://jsonapi.org/format/1.0/" + } + } + } + }, + "errors": [ + { + "title": "Not Found", + "status": "404", + "detail": "The \"entity\" parameter was not converted for the path \"/jsonapi/node/article/{entity}\" (route name: \"jsonapi.node--article.individual\")", + "links": { + "via": { + "href": "https://demo-api.druxtjs.org/en/jsonapi/node/article/missing" + }, + "info": { + "href": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5" + } + } + } + ] + }, + "status": 404, + "statusText": "Not Found", + "headers": { + "content-type": "application/vnd.api+json", + "cache-control": "must-revalidate, no-cache, private", + "content-language": "en", + "expires": "Sun, 19 Nov 1978 05:00:00 GMT" + }, + "config": { + "url": "https://demo-api.druxtjs.org/en/jsonapi/node/article/missing", + "method": "get", + "headers": { + "Accept": "application/json, text/plain, */*" + }, + "baseURL": "https://demo-api.druxtjs.org", + "transformRequest": [ + null + ], + "transformResponse": [ + null + ], + "timeout": 0, + "xsrfCookieName": "XSRF-TOKEN", + "xsrfHeaderName": "X-XSRF-TOKEN", + "maxContentLength": -1, + "maxBodyLength": -1 + }, + "request": {} +} \ No newline at end of file diff --git a/test/__fixtures__/get/4247bb5f8fc99ec5cedd45cd602afc01.json b/test/__fixtures__/get/4247bb5f8fc99ec5cedd45cd602afc01.json new file mode 100644 index 000000000..c748d6178 --- /dev/null +++ b/test/__fixtures__/get/4247bb5f8fc99ec5cedd45cd602afc01.json @@ -0,0 +1,31 @@ +{ + "data": "\n\n \n \n\n\n\n\n\n\n