From 5505ee90e517cfe440a047002de4ecebd63fd1ae Mon Sep 17 00:00:00 2001 From: Mike North Date: Wed, 29 Jul 2015 13:56:32 -0700 Subject: [PATCH] Fixes #3603 - Validate JSON API documents returned by serializer#normalizeResponse --- .../lib/system/store/serializer-response.js | 32 +++++- .../store/json-api-validation-test.js | 104 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/ember-data/tests/integration/store/json-api-validation-test.js diff --git a/packages/ember-data/lib/system/store/serializer-response.js b/packages/ember-data/lib/system/store/serializer-response.js index 7ccf99a1bdf..64207d68507 100644 --- a/packages/ember-data/lib/system/store/serializer-response.js +++ b/packages/ember-data/lib/system/store/serializer-response.js @@ -2,6 +2,35 @@ import Model from 'ember-data/system/model/model'; const get = Ember.get; +/** + This is a helper method that validates a JSON API top-level document + + The format of a document is described here: + http://jsonapi.org/format/#document-top-level + + @method validateDocumentStructure + @param {Object} doc JSON API document + @return {array} An array of errors found in the document structure +*/ +export function validateDocumentStructure(doc) { + let errors = []; + if (!doc || typeof doc !== 'object') { + errors.push('Top level of a JSON API document must be an object'); + } else { + if (!('data' in doc) && + !('errors' in doc) && + !('meta' in doc)) { + errors.push('One or more of the following keys must be present: "data", "errors", "meta".'); + } else { + if (('data' in doc) && ('errors' in doc)) { + errors.push('Top level keys "errors" and "data" cannot both be present in a JSON API document'); + } + } + } + + return errors; +} + /** This is a helper method that always returns a JSON-API Document. @@ -16,7 +45,8 @@ const get = Ember.get; */ export function normalizeResponseHelper(serializer, store, modelClass, payload, id, requestType) { let normalizedResponse = serializer.normalizeResponse(store, modelClass, payload, id, requestType); - + let validationErrors = validateDocumentStructure(normalizedResponse); + Ember.assert(`normalizeResponse must return a valid JSON API document:\n\t* ${validationErrors.join('\n\t* ')}`, Ember.isEmpty(validationErrors)); // TODO: Remove after metadata refactor if (normalizedResponse.meta) { store._setMetadataFor(modelClass.modelName, normalizedResponse.meta); diff --git a/packages/ember-data/tests/integration/store/json-api-validation-test.js b/packages/ember-data/tests/integration/store/json-api-validation-test.js new file mode 100644 index 00000000000..5f4cf0e3b05 --- /dev/null +++ b/packages/ember-data/tests/integration/store/json-api-validation-test.js @@ -0,0 +1,104 @@ +var Person, store, env; +var run = Ember.run; + +module("integration/store/json-validation", { + setup: function() { + Person = DS.Model.extend({ + updatedAt: DS.attr('string'), + name: DS.attr('string'), + firstName: DS.attr('string'), + lastName: DS.attr('string') + }); + + env = setupStore({ + person: Person + }); + store = env.store; + }, + + teardown: function() { + run(store, 'destroy'); + } +}); + +test("when normalizeResponse returns undefined (or doesn't return), throws an error", function() { + + env.registry.register('serializer:person', DS.Serializer.extend({ + normalizeResponse() {} + })); + + env.registry.register('adapter:person', DS.Adapter.extend({ + findRecord() { + return Ember.RSVP.resolve({}); + } + })); + + throws(function () { + run(function() { + store.find('person', 1); + }); + }, /Top level of a JSON API document must be an object/); +}); + +test("when normalizeResponse returns null, throws an error", function() { + + env.registry.register('serializer:person', DS.Serializer.extend({ + normalizeResponse() {return null;} + })); + + env.registry.register('adapter:person', DS.Adapter.extend({ + findRecord() { + return Ember.RSVP.resolve({}); + } + })); + + throws(function () { + run(function() { + store.find('person', 1); + }); + }, /Top level of a JSON API document must be an object/); +}); + + +test("when normalizeResponse returns an empty object, throws an error", function() { + + env.registry.register('serializer:person', DS.Serializer.extend({ + normalizeResponse() {return {};} + })); + + env.registry.register('adapter:person', DS.Adapter.extend({ + findRecord() { + return Ember.RSVP.resolve({}); + } + })); + + throws(function () { + run(function() { + store.find('person', 1); + }); + }, /One or more of the following keys must be present/); +}); + +test("when normalizeResponse returns a document with both data and errors, throws an error", function() { + + env.registry.register('serializer:person', DS.Serializer.extend({ + normalizeResponse() { + return { + data: [], + errors: [] + }; + } + })); + + env.registry.register('adapter:person', DS.Adapter.extend({ + findRecord() { + return Ember.RSVP.resolve({}); + } + })); + + throws(function () { + run(function() { + store.find('person', 1); + }); + }, /cannot both be present/); +});