diff --git a/addon/adapters/application.js b/addon/adapters/application.js index 8eb34a4..e347722 100644 --- a/addon/adapters/application.js +++ b/addon/adapters/application.js @@ -6,7 +6,7 @@ import Ember from 'ember'; import RSVP from 'rsvp'; import { pluralize } from 'ember-inflector'; -import FetchMixin from 'ember-jsonapi-resources/mixins/fetch'; +import FetchOrAjax from 'ember-fetchjax/utils/fetchjax'; const { Evented, getOwner } = Ember; @@ -18,7 +18,7 @@ const { Evented, getOwner } = Ember; @uses Ember.Evented @static */ -export default Ember.Object.extend(FetchMixin, Evented, { +export default Ember.Object.extend(Evented, { /** The name of the entity @@ -49,6 +49,41 @@ export default Ember.Object.extend(FetchMixin, Evented, { } }), + /** + @method init + */ + init() { + this._super(...arguments); + let fetchjax = new FetchOrAjax({ + useAjax: false, + ajax: Ember.$.ajax, + promise: Ember.RSVP.Promise, + deserialize: this.deserialize.bind(this) + }); + this._fetch = fetchjax.fetch.bind(fetchjax); + }, + + /** + @method deserialize + @param {Object} json payload from response + @param {Object} headers received from response + @param {Object} options passed into original request + */ + deserialize(json, headers, options) { + if (!json || json === '' || !json.data) { + return null; + } else if (options.isUpdate) { + json.data = this.serializer.transformAttributes(json.data); + this.cacheUpdate({ meta: json.meta, data: json.data, headers: headers }); + return json.data; + } else { + let resource = this.serializer.deserialize(json); + this.cacheResource({ meta: json.meta, data: resource, headers: headers }); + this.serializer.deserializeIncluded(json.included, { headers: headers }); + return resource; + } + }, + /** Find resource(s) using an id or a using a query `{id: '', query: {}}` @@ -348,11 +383,10 @@ export default Ember.Object.extend(FetchMixin, Evented, { fetch(url, options = {}) { url = this.fetchUrl(url); let isUpdate = this.fetchOptions(options); - if (this.get('useFetch')) { - return this._fetch(url, options, isUpdate); - } else { - return this._ajax(url, options, isUpdate); + if (isUpdate) { + options.isUpdate = true; } + return this._fetch(url, options); }, /** diff --git a/addon/mixins/fetch.js b/addon/mixins/fetch.js deleted file mode 100644 index 89de3e5..0000000 --- a/addon/mixins/fetch.js +++ /dev/null @@ -1,377 +0,0 @@ -/** - @module ember-jsonapi-resources - @submodule fetch -**/ -import Ember from 'ember'; -import RSVP from 'rsvp'; -import { ServerError, ClientError, FetchError } from 'ember-jsonapi-resources/utils/errors'; - -/** - Fetch/Ajax methods for use with an Adapter calls `cacheUpdate`, `cacheResource` - methods and a `serializer` injection. - - @class FetchMixin - @static -*/ -export default Ember.Mixin.create({ - /** - Flag indicates whether to use window.fetch or not, computed from `useAjax` - - @property useFetch - @type Boolean - */ - useFetch: Ember.computed('useAjax', function () { - let notFirefox = window.navigator.userAgent.indexOf("Firefox") === -1; - return !this.get('useAjax') && window.fetch && notFirefox; - }), - - /** - Flag to use $.ajax instead of window.fetch - - @property useAjax - @type Boolean - */ - useAjax: false, - - /** - Makes a fetch request via Fetch API (may use polyfill) - - See http://updates.html5rocks.com/2015/03/introduction-to-fetch - - @private - @method _fetch - @param {String} url - @param {Object} options - May include a query object or an update flag - @param {Boolean} isUpdate - @return {Promise} - */ - _fetch(url, options, isUpdate) { - return new RSVP.Promise(function(resolve, reject) { - window.fetch(url, options).then(function(response) { - if (response.status >= 500) { - this.fetchServerErrorHandler(response, reject); - } else if (response.status >= 400) { - this.fetchClientErrorHandler(response, reject); - } else if (response.status === 204) { - this.fetchNoContentHandler(response, resolve); - } else { - return this.fetchSuccessHandler(response, resolve, isUpdate); - } - }.bind(this)).catch(function(error) { - this.fetchErrorHandler(error, reject); - }.bind(this)); - }.bind(this)); - }, - - /** - Fetch server error handler ~ status >= 500 - - @method fetchServerErrorHandler - @param {Response} response - Fetch response - @param {Function} reject - Promise reject handler - */ - fetchServerErrorHandler(response, reject) { - response.text().then(function(respText) { - let msg = parseFetchErrorMessage(response); - let json = parseFetchErrorText(respText, response); - reject(new ServerError(msg, json)); - }); - }, - - /** - Fetch client error handler ~ status >= 400 - - @method fetchClientErrorHandler - @param {Response} response - Fetch response - @param {Function} reject - Promise reject handler - */ - fetchClientErrorHandler(response, reject) { - response.text().then(function(respText) { - let msg = parseFetchErrorMessage(response); - let json = parseFetchErrorText(respText, response); - reject(new ClientError(msg, json)); - }); - }, - - /** - Fetch generic error handler - - @method fetchErrorHandler - @param {Error|Response} error - Fetch error or response object - @param {Function} reject - Promise reject handler - */ - fetchErrorHandler(error, reject) { - let msg = 'Unable to Fetch resource(s)'; - if (error instanceof Error) { - msg = (error && error.message) ? error.message : msg; - reject(new FetchError(msg, error)); - } else if (typeof error.text === 'function') { - error.text().then(function(respText) { - msg = parseFetchErrorMessage(error); - reject(new FetchError(msg, parseFetchErrorText(respText, error))); - }); - } else { - reject(new FetchError(msg, error)); - } - }, - - /** - Fetch 204 No Content handler - - @method fetchNoContentHandler - @param {Response} response - Fetch response - @param {Function} resolve - Promise resolve handler - */ - fetchNoContentHandler(response, resolve) { - return response.text().then(function(resp) { - resolve(resp || ''); - }); - }, - - /** - Fetch 20x Success handler - - @method fetchSuccessHandler - @param {Response} response - Fetch response - @param {Function} resolve - Promise resolve handler - @param {Boolean} isUpdate - Used with patch to update a resource - */ - fetchSuccessHandler(response, resolve, isUpdate) { - return response.json().then(function(json) { - if (json.data === null) { - resolve(null); - } else if (isUpdate) { - json.data = this.serializer.transformAttributes(json.data); - this.cacheUpdate({ meta: json.meta, data: json.data, headers: response.headers }); - resolve(json.data); - } else { - let resource = this.serializer.deserialize(json); - this.cacheResource({ meta: json.meta, data: resource, headers: response.headers }); - this.serializer.deserializeIncluded(json.included, { headers: response.headers }); - resolve(resource); - } - }.bind(this)); - }, - - /** - Makes an XHR request via $.ajax - - @private - @method _ajax - @param {String} url - @param {Object} options - may include a query object or an update flag - @param {Boolean} isUpdate - @return {Promise} - @requires jQuery - */ - _ajax(url, options, isUpdate) { - options.data = options.body; - delete options.body; - return new RSVP.Promise(function(resolve, reject) { - Ember.$.ajax(url, options) - .done(this.ajaxDoneHandler(resolve, isUpdate)) - .fail(this.ajaxFailHandler(reject)); - }.bind(this)); - }, - - /** - @method ajaxFailHandler - @param {Function} reject - Promise reject handler - @return {Function} closure with reject handler - */ - ajaxFailHandler(reject) { - let _reject = reject; - /* - @param {Object} jqXHR - @param {String} textStatus - @param {String} errorThrown - */ - return function(jqXHR, textStatus, errorThrown) { - if (jqXHR.status >= 500) { - this.ajaxServerErrorHandler(jqXHR, textStatus, errorThrown, _reject); - } else if (jqXHR.status >= 400) { - this.ajaxClientErrorHandler(jqXHR, textStatus, errorThrown, _reject); - } else { - this.ajaxErrorHandler(jqXHR, textStatus, errorThrown, _reject); - } - }.bind(this); - }, - - /** - @method ajaxDoneHandler - @param {Function} resolve - Promise resolve handler - @param {Boolean} isUpdate - Used with patch to update a resource - @return {Function} closure with resolve handler - */ - ajaxDoneHandler(resolve, isUpdate) { - let _resolve = resolve, _isUpdate = isUpdate; - /* - @param {Object} json - payload - @param {String} textStatus - @param {jqXHR} jqXHR - */ - return function(json, textStatus, jqXHR) { - if (jqXHR.status === 204) { - this.ajaxNoContentHandler(json, textStatus, jqXHR, _resolve); - } else { - this.ajaxSuccessHandler(json, textStatus, jqXHR, _resolve, _isUpdate); - } - }.bind(this); - }, - - /** - Ajax server error handler ~ status >= 500 - - @method ajaxServerErrorHandler - @param {Object} jqXHR - @param {String} textStatus - @param {String} errorThrown - @param {Function} reject - Promise reject handler - */ - ajaxServerErrorHandler(jqXHR, textStatus, errorThrown, reject) { - let msg = 'The Service responded with ' + textStatus + ' ' + jqXHR.status; - let json = parseXhrErrorResponse(jqXHR, errorThrown); - reject(new ServerError(msg, json)); - }, - - /** - Ajax client error handler ~ status >= 400 - - @method ajaxClientErrorHandler - @param {Object} jqXHR - @param {String} textStatus - @param {String} errorThrown - @param {Function} reject - Promise reject handler - */ - ajaxClientErrorHandler(jqXHR, textStatus, errorThrown, reject) { - let msg = 'The API responded with a '+ jqXHR.status +' error.'; - let json = parseXhrErrorResponse(jqXHR, errorThrown); - reject(new ClientError(msg, json)); - }, - - /** - Ajax Generic error handler - - @method ajaxErrorHandler - @param {Object} jqXHR - @param {String} textStatus - @param {String} errorThrown - @param {Function} reject - Promise reject handler - */ - ajaxErrorHandler(jqXHR, textStatus, errorThrown, reject) { - let msg = (errorThrown) ? errorThrown : 'Unable to Fetch resource(s)'; - let json = parseXhrErrorResponse(jqXHR, errorThrown); - reject(new FetchError(msg, json)); - }, - - /** - Ajax 204 No Content handler - - @method ajaxNoContentHandler - @param {Object} json - payload should be empty - @param {String} textStatus - @param {jqXHR} jqXHR - @param {Function} resolve - Promise resolve handler - */ - ajaxNoContentHandler(json, textStatus, jqXHR, resolve) { - resolve(json || ''); - }, - - /** - Ajax 20x Success handler - - @method ajaxSuccessHandler - @param {Object} json - payload - @param {String} textStatus - @param {jqXHR} jqXHR - @param {Function} resolve - Promise resolve handler - @param {Boolean} isUpdate - Used with patch to update a resource - */ - ajaxSuccessHandler(json, textStatus, jqXHR, resolve, isUpdate) { - if (json.data === null) { - resolve(null); - return; - } - let headers = this._getAjaxHeaders(jqXHR); - if (isUpdate) { - json.data = this.serializer.transformAttributes(json.data); - this.cacheUpdate({ meta: json.meta, data: json.data, headers: headers }); - resolve(json.data); - } else { - let resource = this.serializer.deserialize(json); - this.cacheResource({ meta: json.meta, data: resource, headers: headers }); - this.serializer.deserializeIncluded(json.included, { headers: headers }); - resolve(resource); - } - }, - - /** - @private - @method _getXHRHeaders - @param {Object} jqXHR - @return {Object} - */ - _getAjaxHeaders(jqXHR) { - let headers = jqXHR.getAllResponseHeaders(); - headers = headers.split('\n'); - let headersDictionary = {}, key, value, header; - for (let i = 0; i < headers.length; i++) { - header = headers[i].split(': '); - if (header[0].trim() !== '') { - key = header[0].trim(); - value = header[1].trim(); - headersDictionary[key] = value; - } - } - return headersDictionary; - } - -}); - -function parseFetchErrorMessage(response) { - return [ - 'The API responded with a ', - response.status, - (response.statusText) ? ' (' + response.statusText + ') ' : '', - ' error.' - ].join(''); -} - -function parseFetchErrorText(text, response) { - let json; - try { - json = JSON.parse(text); - } catch (err) { - Ember.Logger.warn(err); - json = { - "errors": [{ - "status": response.status, - "detail": text - }] - }; - } - json = json || {}; - json.status = response.status; - return json; -} - -function parseXhrErrorResponse(jqXHR, errorThrown) { - let json = jqXHR.responseJSON; - if (!json) { - try { - if (jqXHR.responseText) { - json = JSON.parse(jqXHR.responseText); - } - } catch(err) { - Ember.Logger.warn(err); - } - } - json = json || {}; - json.status = jqXHR.status; - json.errors = json.errors || [{ - status: jqXHR.status, - detail: jqXHR.responseText, - message: errorThrown - }]; - return json; -} diff --git a/addon/utils/errors.js b/addon/utils/errors.js deleted file mode 100644 index 1c4365c..0000000 --- a/addon/utils/errors.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - @module ember-jsonapi-resources - @submodule utils -**/ - -/** - @class ServerError - @constructor - @param {String} message - @param {Object} response - @return {Error} -*/ -export function ServerError(message = 'Server Error', response = null) { - let _error = Error.prototype.constructor.call(this, message); - _error.name = this.name = 'ServerError'; - this.stack = _error.stack; - this.message = _error.message; - this.name = 'ServerError'; - this.response = response; - this.errors = (response) ? response.errors || null : null; - this.code = (response) ? response.status || null : null; -} -ServerError.prototype = errorProtoFactory(ServerError); - -/** - @class ClientError - @constructor - @param {String} message - @param {Object} response - @return {Error} -*/ -export function ClientError(message = 'Client Error', response = null) { - let _error = Error.prototype.constructor.call(this, message); - _error.name = this.name = 'ClientError'; - this.stack = _error.stack; - this.message = _error.message; - this.name = 'ClientError'; - this.response = response; - this.errors = (response) ? response.errors || null : null; - this.code = (response) ? response.status || null : null; -} -ClientError.prototype = errorProtoFactory(ClientError); - -/** - @class FetchError - @constructor - @param {String} message - @param {Error|Object} error or response object - @return {Error} -*/ -export function FetchError(message = 'Fetch Error', response = null) { - let _error = Error.prototype.constructor.call(this, message); - _error.name = this.name = 'FetchError'; - this.stack = (response && response.stack) ? response.stack : _error.stack; - this.message = (response && response.message) ? response.message : _error.message; - this.name = 'FetchError'; - this.error = (response instanceof Error) ? response : _error; - this.response = (response instanceof Error) ? null : response; - this.errors = (this.response) ? response.errors : null; - this.code = (response) ? response.status || null : null; -} -FetchError.prototype = errorProtoFactory(FetchError); - -function errorProtoFactory(ctor) { - return Object.create(Error.prototype, { - constructor: { - value: ctor, - writable: true, - configurable: true - } - }); -} diff --git a/app/mixins/fetch.js b/app/mixins/fetch.js deleted file mode 100644 index a7b467a..0000000 --- a/app/mixins/fetch.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-jsonapi-resources/mixins/fetch'; diff --git a/app/utils/errors.js b/app/utils/errors.js deleted file mode 100644 index 012e32d..0000000 --- a/app/utils/errors.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'ember-jsonapi-resources/utils/errors'; diff --git a/package.json b/package.json index 5009e5d..9ad9960 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ ], "dependencies": { "ember-cli-babel": "^5.1.7", + "ember-fetchjax": "0.6.0", "ember-inflector": "^1.9.1", "ember-runtime-enumerable-includes-polyfill": "^1.0.2", "inflection": "~1.7.1", diff --git a/tests/acceptance/polymorphic-test.js b/tests/acceptance/polymorphic-test.js index ace7647..758b506 100644 --- a/tests/acceptance/polymorphic-test.js +++ b/tests/acceptance/polymorphic-test.js @@ -82,12 +82,14 @@ function setupFetchResponses(sandbox) { resp = pictures1MockResponse(); break; case [apiUrl, 'pictures/1/imageable'].join('/'): + case pictures1Mock.data.relationships.imageable.links.related: resp = pictures1ImageableMockResponse(); break; case [apiUrl, 'pictures/5'].join('/'): resp = pictures5MockResponse(); break; case [apiUrl, 'pictures/5/imageable'].join('/'): + case pictures5Mock.data.relationships.imageable.links.related: resp = pictures5ImageableMockResponse(); break; default: diff --git a/tests/dummy/config/environment.js b/tests/dummy/config/environment.js index 88d7a7d..6e6dfd2 100644 --- a/tests/dummy/config/environment.js +++ b/tests/dummy/config/environment.js @@ -21,7 +21,7 @@ module.exports = function(environment) { APP: { // Here you can pass flags/options to your application instance // when it is created - API_HOST: 'http://api.pixelhandler.com', + API_HOST: '', API_PATH: 'api/v1', }, contentSecurityPolicy: { diff --git a/tests/unit/adapters/application-test.js b/tests/unit/adapters/application-test.js index 795dc99..bd86674 100644 --- a/tests/unit/adapters/application-test.js +++ b/tests/unit/adapters/application-test.js @@ -10,6 +10,7 @@ import postsMock from 'fixtures/api/posts'; import authorMock from 'fixtures/api/authors/1'; import employeeMock from 'fixtures/api/employees/1'; import supervisorMock from 'fixtures/api/supervisors/2'; +import { ServerError, ClientError } from 'ember-fetchjax/utils/errors'; let sandbox; @@ -757,17 +758,24 @@ test('when called with resource argument, #deleteResource calls #cacheRemove', f test('#fetch calls #fetchURL to customize if needed', function(assert) { assert.expect(2); const done = assert.async(); - const adapter = this.subject({type: 'posts', url: '/posts'}); - sandbox.stub(adapter, 'fetchUrl', function () {}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 204, - "text": function() { return RSVP.Promise.resolve(''); } - }); + + sandbox.spy(adapter, 'fetchUrl'); + mockFetchJax(sandbox, adapter, null); // 204 No Content + + let url = '/posts'; + let promise = adapter.fetch(url, { + method: 'PATCH', + body: JSON.stringify({ + data: { + id: '1', + type: 'posts', + attributes: { + title: 'changed' + } + } + }) }); - let url = '/posts'; - let promise = adapter.fetch(url, {method: 'PATCH', body: 'json string here'}); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.then(() => { @@ -781,18 +789,11 @@ test('#fetch calls #fetchOptions checking if the request is an update, if true s const done = assert.async(); const adapter = this.subject({type: 'posts', url: '/posts'}); - sandbox.stub(adapter, 'fetchUrl', function () {}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 202, - "json": function() { - return RSVP.Promise.resolve({data: {}, meta: {}}); - } - }); - }); + sandbox.stub(adapter, 'fetchUrl', function (url) { return url; }); + mockFetchJax(sandbox, adapter, { data: {} }); sandbox.stub(adapter, 'cacheResource', function () {}); adapter.serializer = { deserialize: sandbox.spy(), transformAttributes: sandbox.spy() }; - let promise = adapter.fetch('/posts', {method: 'PATCH', body: 'json string here', update: true}); + let promise = adapter.fetch('/posts', {method: 'PATCH', body: '{"data": null}', update: true}); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.then(() => { @@ -818,14 +819,8 @@ test('#cacheResource called after successful fetch', function(assert) { deserialize: function () { return postsMock.data; }, deserializeIncluded: function () { return; } }; - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 200, - "json": function() { - return RSVP.Promise.resolve(postsMock); - } - }); - }); + mockFetchJax(sandbox, adapter, postsMock); + let promise = adapter.fetch('/posts/1', {method: 'GET'}); assert.ok(typeof promise.then === 'function', 'returns a thenable'); @@ -841,14 +836,8 @@ test('#cacheUpdate called after #updateResource success', function(assert) { const adapter = this.subject(); sandbox.stub(adapter, 'cacheUpdate', function () {}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 200, - "json": function() { - return RSVP.Promise.resolve(postsMock); - } - }); - }); + mockFetchJax(sandbox, adapter, postsMock); + let payload = { data: { type: postMock.data.type, @@ -878,14 +867,8 @@ test('serializer#deserializeIncluded called after successful fetch', function(as deserialize: function () { return postMock.data; }, deserializeIncluded: sandbox.spy() }; - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 200, - "json": function() { - return RSVP.Promise.resolve(postMock); - } - }); - }); + mockFetchJax(sandbox, adapter, postMock); + let promise = adapter.fetch('/posts/1', { method: 'GET' }); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.then(() => { @@ -900,15 +883,9 @@ test('#fetch handles 5xx (ServerError) response status', function(assert) { const done = assert.async(); const adapter = this.subject({type: 'posts', url: '/posts'}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 500, - "text": function() { - return RSVP.Promise.resolve(''); - } - }); - }); - let promise = adapter.fetch('/posts', {method: 'POST', body: 'json string here'}); + + mockFetchJax(sandbox, adapter, new ServerError('Server Error', {status: 500})); + let promise = adapter.fetch('/posts', {method: 'POST', body: '{"data": null}'}); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.catch((error) => { @@ -923,17 +900,10 @@ test('#fetch handles 4xx (Client Error) response status', function(assert) { const done = assert.async(); const adapter = this.subject({type: 'posts', url: '/posts'}); - const mockError = {errors: [{status: 404, title: 'I am an error'}]}; sandbox.stub(adapter, 'fetchUrl', function () {}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 404, - "text": function() { - return RSVP.Promise.resolve(JSON.stringify(mockError)); - } - }); - }); - let promise = adapter.fetch('/posts', { method: 'POST', body: 'json string here' }); + const mockError = { errors: [{status: 404, title: 'I am an error'}], status: 404 }; + mockFetchJax(sandbox, adapter, new ClientError('Not Found', mockError)); + let promise = adapter.fetch('/posts', { method: 'POST', body: '{"data": null}' }); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.catch((error) => { assert.ok(error.name, 'Client Error', '4xx response throws a custom error'); @@ -950,15 +920,10 @@ test('#fetch handles 204 (Success, no content) response status w/o calling deser const adapter = this.subject({type: 'posts', url: '/posts'}); sandbox.stub(adapter, 'fetchUrl', function () {}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 204, - "text": function() { return RSVP.Promise.resolve(''); } - }); - }); + mockFetchJax(sandbox, adapter, ''); sandbox.stub(adapter, 'cacheResource', function () {}); adapter.serializer = {deserialize: sandbox.spy(), deserializeIncluded: Ember.K}; - let promise = adapter.fetch('/posts', {method: 'PATCH', body: 'json string here'}); + let promise = adapter.fetch('/posts', {method: 'PATCH', body: '{"data": null}'}); assert.ok(typeof promise.then === 'function', 'returns a thenable'); promise.then(() => { @@ -973,12 +938,7 @@ test('#fetch handles 200 (Success) response status', function(assert) { const done = assert.async(); const adapter = this.subject({type: 'posts', url: '/posts'}); - sandbox.stub(window, 'fetch', function () { - return RSVP.Promise.resolve({ - "status": 200, - "json": function() { return RSVP.Promise.resolve(postMock); } - }); - }); + mockFetchJax(sandbox, adapter, postMock); sandbox.stub(adapter, 'cacheResource', function () {}); adapter.serializer = { deserialize: sandbox.spy(), deserializeIncluded: Ember.K }; let promise = adapter.fetch('/posts/1', { method: 'GET' }); @@ -1072,3 +1032,15 @@ function mockSerializer(mock = {}) { transformAttributes: function(json) { return json; } }; } + +function mockFetchJax(sandbox, adapter, response) { + sandbox.stub(adapter, '_fetch', function (url, options) { // fetchjax instance + // skip the call to window.fetch, mock successful call back to adapter's deserialize method + adapter.deserialize(response, options.headers, options); + if (response instanceof Error) { + return Ember.RSVP.Promise.reject(response); + } else { + return Ember.RSVP.Promise.resolve(response); + } + }); +} diff --git a/tests/unit/mixins/fetch-test.js b/tests/unit/mixins/fetch-test.js deleted file mode 100644 index 14a02c7..0000000 --- a/tests/unit/mixins/fetch-test.js +++ /dev/null @@ -1,165 +0,0 @@ -import Ember from 'ember'; -import RSVP from 'rsvp'; -import FetchMixin from '../../../mixins/fetch'; -import { module, test } from 'qunit'; - -let sandbox; - -function RSVPonerror(error) { - throw new Error(error); -} - -module('Unit | Mixin | fetch', { - beforeEach() { - sandbox = window.sinon.sandbox.create(); - this.server = sandbox.useFakeServer(); - this.server.xhr.useFilters = true; - // Filtered requests will not be faked - this.server.xhr.addFilter( function( method, url ) { - return url.match(/\/write-blanket-coverage/); - }); - this.server.autoRespond = true; - RSVP.configure('onerror', RSVPonerror); - window.localStorage.removeItem('AuthorizationHeader'); - let FetchObject = Ember.Object.extend(FetchMixin); - this.subject = FetchObject.create(); - }, - afterEach() { - sandbox.restore(); - window.localStorage.removeItem('AuthorizationHeader'); - delete this.subject; - delete this.server; - } -}); - -test('#useAjax default value is false', function(assert) { - assert.equal(this.subject.get('useAjax'), false, 'useAjax flag is false'); -}); - -test('#useFetch default value is true', function(assert) { - assert.equal(this.subject.get('useFetch'), true, 'useFetch flag is true'); -}); - -test('#_ajax handles 5xx (Server Error)', function(assert) { - assert.expect(3); - const done = assert.async(); - this.server.respondWith('GET', '/posts', [500, {}, ""]); - let promise = this.subject._ajax('/posts', { method: 'GET' }, false); - assert.ok(typeof promise.then === 'function', 'returns a thenable'); - promise.catch(function(error) { - assert.equal(error.name, 'ServerError', '5xx response throws a custom error'); - assert.equal(error.code, 500, 'error code 500'); - done(); - }); -}); - -test('#_ajax handles 4xx (Client Error)', function(assert) { - assert.expect(5); - const done = assert.async(); - this.server.respondWith('GET', '/posts/101', [ - 404, - {'Content-Type':'application/vnd.api+json'}, - JSON.stringify({ - "errors": [{ - "title": "Record not found", - "detail": "The record identified by 101 could not be found.", - "code": 404, - "status": "not_found" - }] - }) - ]); - let promise = this.subject._ajax('/posts/101', { method: 'GET' }, false); - assert.ok(typeof promise.then === 'function', 'returns a thenable'); - promise.catch(function(error) { - assert.ok(error.name, 'Client Error', '4xx response throws a custom error'); - assert.ok(Array.isArray(error.errors), '4xx error includes errors'); - assert.equal(error.errors[0].code, 404, '404 error code is in errors list'); - assert.equal(error.code, 404, 'error code 404'); - done(); - }); -}); - -test('#_ajax handles 3xx error', function(assert) { - assert.expect(3); - const done = assert.async(); - this.server.respondWith('GET', '/posts', [302, {}, ""]); - let promise = this.subject._ajax('/posts', { method: 'GET' }, false); - assert.ok(typeof promise.then === 'function', 'returns a thenable'); - promise.catch(function(error) { - assert.equal(error.name, 'FetchError', 'unknown error response throws a custom error'); - assert.equal(error.code, 302, '302 error code'); - done(); - }); -}); - -test('#_ajax handles 204 (Success, no content)', function(assert) { - assert.expect(2); - const done = assert.async(); - this.server.respondWith('PATCH', '/posts/101', [204, {}, ""]); - let promise = this.subject._ajax('/posts/101', { method: 'PATCH', body: 'json string here' }, false); - assert.ok(typeof promise.then === 'function', 'returns a thenable'); - promise.then(function(res) { - assert.equal(res, "", 'No content in the response'); - done(); - }); -}); - -test('#_ajax handles 200 (Success) response status', function(assert) { - assert.expect(3); - const done = assert.async(); - this.server.respondWith('GET', '/posts/101', [ - 200, {'Content-Type':'application/vnd.api+json'}, JSON.stringify({ - data: { id: 101, attributes: { name: 'Yo Post' }} - }) - ]); - this.subject.serializer = { deserialize: function(res) { return res.data; }, deserializeIncluded: Ember.K }; - this.subject.cacheResource = sandbox.spy(); - let promise = this.subject._ajax('/posts/101', { method: 'GET' }, false); - assert.ok(typeof promise.then === 'function', 'returns a thenable'); - promise.then(function(res) { - assert.equal(res.id, 101, 'has data content in the response'); - assert.ok(this.subject.cacheResource.calledOnce, '#cacheResource method called'); - done(); - }.bind(this)); -}); - -test('#_getAjaxHeaders', function(assert) { - let jqXHR = { - getAllResponseHeaders: function() { - let resHdr = "Content-Type: application/vnd.api+json\n"; - resHdr += "Cache-Control: max-age=0, private, must-revalidate\n"; - return resHdr; - } - }; - let headers = this.subject._getAjaxHeaders(jqXHR); - assert.equal(headers['Content-Type'], 'application/vnd.api+json', 'JSAON API header ok'); - assert.equal(headers['Cache-Control'], 'max-age=0, private, must-revalidate', 'Catch control header ok'); -}); - -test('#ajaxSuccessHandler handles response for empty to-one relationship', function(assert) { - assert.expect(1); - let result = void 0; - let resolve = function(resp) { result = resp; }; - let jqXHR = { - responseText: '{"data":null}' - }; - let json = JSON.parse(jqXHR.responseText); - this.subject.ajaxSuccessHandler(json, 'success', jqXHR, resolve, false); - assert.ok(result === null, 'resolved with `null`'); -}); - -test('#fetchSuccessHandler handles response for empty to-one relationship', function(assert) { - assert.expect(1); - let done = assert.async(); - let response = { - "status": 200, - "json": function() { return RSVP.Promise.resolve({ data: null }); } - }; - let result = void 0; - let resolve = function(resp) { result = resp; }; - let promise = this.subject.fetchSuccessHandler(response, resolve, false); - promise.then(function() { - assert.ok(result === null, 'resolved with `null`'); - done(); - }); -}); diff --git a/tests/unit/utils/errors-test.js b/tests/unit/utils/errors-test.js deleted file mode 100644 index a34d753..0000000 --- a/tests/unit/utils/errors-test.js +++ /dev/null @@ -1,34 +0,0 @@ -import { ServerError, ClientError, FetchError } from 'ember-jsonapi-resources/utils/errors'; -import { module, test } from 'qunit'; - -module('Unit | Utility | errors'); - -test('ServerError', function(assert) { - assert.throws( - function() { - throw new ServerError(); - }, - Error, - "raised ServerError is an instance of Error" - ); -}); - -test('ClientError', function(assert) { - assert.throws( - function() { - throw new ClientError(); - }, - Error, - "raised ClientError is an instance of Error" - ); -}); - -test('FetchError', function(assert) { - assert.throws( - function() { - throw new FetchError(); - }, - Error, - "raised FetchError is an instance of Error" - ); -});