diff --git a/packages/-ember-data/tests/unit/adapters/rest-adapter/fetch-options-test.js b/packages/-ember-data/tests/unit/adapters/rest-adapter/fetch-options-test.js new file mode 100644 index 00000000000..07cdb6a3e1d --- /dev/null +++ b/packages/-ember-data/tests/unit/adapters/rest-adapter/fetch-options-test.js @@ -0,0 +1,148 @@ +import { module, test } from 'qunit'; +import { fetchOptions } from '@ember-data/adapter/rest'; + +module('unit/adapters/rest-adapter/fetch-options', function(hooks) { + test("fetchOptions removes undefined query params when method is POST and 'data' is an object", function(assert) { + assert.expect(1); + + const dataAsObject = { + a: 1, + b: undefined, + c: 3, + d: null, + e: 0, + f: false, + }; + + const undefinedQueryStringOptions = { + url: 'https://emberjs.com', + method: 'POST', + data: dataAsObject, + }; + + let options = fetchOptions(undefinedQueryStringOptions); + assert.deepEqual(options.body, '{"a":1,"c":3,"d":null,"e":0,"f":false}'); + }); + + test('fetchOptions sets the request body correctly when the method is not GET or HEAD', function(assert) { + assert.expect(3); + + const baseOptions = { + url: '/', + method: 'POST', + data: { a: 1 }, + }; + + // Tests POST method. + let options = fetchOptions(baseOptions); + assert.equal(options.body, JSON.stringify(baseOptions.data), 'POST request body correctly set'); + + // Tests PUT method. + baseOptions.method = 'PUT'; + options = fetchOptions(baseOptions); + assert.equal(options.body, JSON.stringify(baseOptions.data), 'PUT request body correctly set'); + + // Tests DELETE method. + baseOptions.method = 'DELETE'; + options = fetchOptions(baseOptions); + assert.equal(options.body, JSON.stringify(baseOptions.data), 'DELETE request has the correct body'); + }); + + test("fetchOptions sets the request body correctly when the method is POST and 'data' is a string", function(assert) { + assert.expect(1); + + // Tests stringified objects. + const stringifiedData = JSON.stringify({ a: 1, b: 2 }); + const optionsWithStringData = { + url: 'https://emberjs.com', + method: 'POST', + data: stringifiedData, + }; + + let options = fetchOptions(optionsWithStringData); + assert.equal(options.body, stringifiedData); + }); + + test('fetchOptions does not set a request body when the method is GET or HEAD', function(assert) { + assert.expect(4); + + const baseOptions = { + url: '/', + method: 'GET', + data: { a: 1 }, + }; + + let options = fetchOptions(baseOptions); + assert.strictEqual(options.body, undefined, 'GET request does not have a request body'); + + baseOptions.method = 'HEAD'; + options = fetchOptions(baseOptions); + assert.strictEqual(options.body, undefined, 'HEAD request does not have a request body'); + + baseOptions.data = {}; + options = fetchOptions(baseOptions); + assert.strictEqual( + options.body, + undefined, + 'HEAD request does not have a request body when `data` is an empty object' + ); + + baseOptions.method = 'GET'; + options = fetchOptions(baseOptions); + assert.strictEqual( + options.body, + undefined, + 'GET request does not have a request body when `data` is an empty object' + ); + }); + + test("fetchOptions correctly processes an empty 'data' object", function(assert) { + assert.expect(2); + + const getData = { + url: 'https://emberjs.com', + method: 'GET', + data: {}, + }; + + const getOptions = fetchOptions(getData); + assert.equal(getOptions.url.indexOf('?'), -1, 'A question mark is not added if there are no query params to add'); + + const postData = { + url: 'https://emberjs.com', + method: 'POST', + data: {}, + }; + + const postOptions = fetchOptions(postData); + assert.equal(postOptions.body, '{}', "'options.body' is an empty object"); + }); + + test("fetchOptions sets the request body correctly when 'data' is FormData", function(assert) { + assert.expect(1); + + const formData = new FormData(); + const postData = { + url: 'https://emberjs.com', + method: 'POST', + data: formData, + }; + + const postOptions = fetchOptions(postData); + assert.strictEqual(postOptions.body, formData, "'options.body' is the FormData passed in"); + }); + + test("fetchOptions sets the request body correctly when 'data' is a String", function(assert) { + assert.expect(1); + + let stringBody = JSON.stringify({ a: 1, b: 2, c: 3 }); + const postData = { + url: 'https://emberjs.com', + method: 'POST', + data: stringBody, + }; + + const postOptions = fetchOptions(postData); + assert.equal(postOptions.body, stringBody, "'options.body' is the String passed in"); + }); +}); diff --git a/packages/adapter/addon/rest.js b/packages/adapter/addon/rest.js index 9309d8ad4db..4e67d528354 100644 --- a/packages/adapter/addon/rest.js +++ b/packages/adapter/addon/rest.js @@ -1363,7 +1363,19 @@ export function fetchOptions(options, adapter) { } else { // NOTE: a request's body cannot be an object, so we stringify it if it is. // JSON.stringify removes keys with values of `undefined` (mimics jQuery.ajax). - options.body = JSON.stringify(options.data); + // If the data is not a POJO (it's a String, FormData, etc), we just set it. + // If the data is a string, we assume it's a stringified object. + + /* We check for Objects this way because we want the logic inside the consequent to run + * if `options.data` is a POJO, not if it is a data structure whose `typeof` returns "object" + * when it's not (Array, FormData, etc). The reason we don't use `options.data.constructor` + * to check is in case `data` is an object with no prototype (e.g. created with null). + */ + if (Object.prototype.toString.call(options.data) === '[object Object]') { + options.body = JSON.stringify(options.data); + } else { + options.body = options.data; + } } }