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 Page not found | Drupal\n \n\n\n\n \n\n\n\n \n \n \n Skip to main content\n \n \n
\n \n
\n
\n
\n
\n \n \n \n\n
\n\n
\n\n
\n \n
\n\n
\n
\n
\n \n \n \n

Page not found

\n\n\n
\n\n \n
\n
\n\n
\n
\n \n\n
\n
\n\n
\n\n
\n \n\n
\n
\n \n
\n
\n\n
\n\n
\n \n\n
\n \n
\n
\n \n \n The requested page could not be found.\n
\n\n
\n\n
\n
\n\n
\n\n \n \n\n\n\n\n \n\n", + "status": 404, + "statusText": "Not Found", + "headers": { + "content-type": "text/html; charset=UTF-8", + "cache-control": "must-revalidate, no-cache, private", + "content-language": "en", + "expires": "Sun, 19 Nov 1978 05:00:00 GMT" + }, + "config": { + "url": "/jsonapi/missing/test", + "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/5abff89771a88320499f59dddd71033c.json b/test/__fixtures__/get/5abff89771a88320499f59dddd71033c.json new file mode 100644 index 000000000..f75495cd6 --- /dev/null +++ b/test/__fixtures__/get/5abff89771a88320499f59dddd71033c.json @@ -0,0 +1,32 @@ +{ + "data": "\n\n \n \n\n\n\n\n\n\n Page not found | Drush Site-Install\n \n\n\n\n \n \n \n \n Skip to main content\n \n \n
\n
\n
\n
\n
\n
\n \n\n\n
\n\n
\n
\n \n \n \n \"Home\"\n \n
\n \n
\n
\n\n
\n\n
\n \n\n\n
\n\n
\n
\n
\n \n
\n
\n
\n
\n
\n \n \n
\n \n\n
\n
\n\n
\n\n
\n
\n \n
\n
\n \n \n
\n \n\n

Page not found

\n\n\n
\n
\n
\n \n \n
\n The requested page could not be found.\n
\n
\n\n
\n\n
\n
\n
\n \n
\n
\n
\n \n
\n
\n\n
\n\n \n \n \n\n", + "status": 404, + "statusText": "Not Found", + "headers": { + "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" + }, + "config": { + "url": "/router/translate-path?path=/broken-router", + "method": "get", + "headers": { + "Accept": "application/json, text/plain, */*" + }, + "baseURL": "https://8080-magenta-koi-31w8trra.ws-us25.gitpod.io", + "transformRequest": [ + null + ], + "transformResponse": [ + null + ], + "timeout": 0, + "xsrfCookieName": "XSRF-TOKEN", + "xsrfHeaderName": "X-XSRF-TOKEN", + "maxContentLength": -1, + "maxBodyLength": -1, + "options": {} + }, + "request": {} +} \ No newline at end of file diff --git a/test/__mocks__/axios.js b/test/__mocks__/axios.js index 1e08417dc..6f7531639 100644 --- a/test/__mocks__/axios.js +++ b/test/__mocks__/axios.js @@ -42,10 +42,6 @@ const mockData = async (request, file) => { // Mock 'get' requests. mockAxios.get = jest.fn((url, options) => { - if (url === '/jsonapi/missing/test') { - throw new Error('Error') - } - const file = path.resolve('./test/__fixtures__/get', md5(url) + '.json') const request = { method: 'get', url, options } return mockData(request, file)