diff --git a/addon/mixins/ajax-request.js b/addon/mixins/ajax-request.js index 56d2c107..851400a7 100644 --- a/addon/mixins/ajax-request.js +++ b/addon/mixins/ajax-request.js @@ -196,7 +196,7 @@ export default Mixin.create({ request(url, options) { const hash = this.options(url, options); return new Promise((resolve, reject) => { - this.raw(url, hash) + this._makeRequest(url, hash) .then(({ response }) => { resolve(response); }) @@ -217,6 +217,19 @@ export default Mixin.create({ */ raw(url, options) { const hash = this.options(url, options); + return this._makeRequest(url, hash); + }, + + /** + * Shared method to actually make an AJAX request + * + * @method _makeRequest + * @private + * @param {string} url The url to make a request to + * @param {Object} hash The options for the request + * @return {Promise} The result of the request + */ + _makeRequest(url, hash) { const requestData = { type: hash.type, url: hash.url diff --git a/tests/acceptance/ajax-get-test.js b/tests/acceptance/ajax-get-test.js index 87104fdf..1ed0b0bb 100644 --- a/tests/acceptance/ajax-get-test.js +++ b/tests/acceptance/ajax-get-test.js @@ -1,5 +1,5 @@ import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert, expect } from 'chai'; +import { expect } from 'chai'; import destroyApp from 'dummy/tests/helpers/destroy-app'; import startApp from 'dummy/tests/helpers/start-app'; @@ -7,45 +7,41 @@ import startApp from 'dummy/tests/helpers/start-app'; import Pretender from 'pretender'; import { jsonFactory as json } from 'dummy/tests/helpers/json'; -const { equal, ok } = assert; - -let server, application; - describe('Acceptance | ajax-get component', function() { beforeEach(function() { - server = new Pretender(); - application = startApp(); + this.server = new Pretender(); + this.application = startApp(); }); afterEach(function() { - server.shutdown(); - destroyApp(application); + this.server.shutdown(); + destroyApp(this.application); }); - it('waiting for a route with async widget', function() { + it('waits for a route with async widget', function() { const PAYLOAD = [{ title: 'Foo' }, { title: 'Bar' }, { title: 'Baz' }]; - server.get('/posts', json(200, PAYLOAD), 300); + this.server.get('/posts', json(200, PAYLOAD), 300); visit('/'); andThen(function() { - equal(currentURL(), '/'); - ok(find('.ajax-get').length === 1); + expect(currentURL()).to.equal('/'); + expect(find('.ajax-get').length).to.equal(1); }); click('button:contains(Load Data)'); andThen(function() { - equal(find('.ajax-get li:eq(0)').text(), 'Foo'); - equal(find('.ajax-get li:eq(1)').text(), 'Bar'); - equal(find('.ajax-get li:eq(2)').text(), 'Baz'); + expect(find('.ajax-get li:eq(0)').text()).to.equal('Foo'); + expect(find('.ajax-get li:eq(1)').text()).to.equal('Bar'); + expect(find('.ajax-get li:eq(2)').text()).to.equal('Baz'); }); }); - it(`Ajax failure doesn't bubble up to console.` , function() { + it(`catches errors before they bubble to the console` , function() { const errorMessage = 'Not Found'; - server.get('/posts', json(404, errorMessage), 300); + this.server.get('/posts', json(404, errorMessage), 300); visit('/'); diff --git a/tests/acceptance/ember-data-integration-test.js b/tests/acceptance/ember-data-integration-test.js index 5c7fd51b..324623e5 100644 --- a/tests/acceptance/ember-data-integration-test.js +++ b/tests/acceptance/ember-data-integration-test.js @@ -21,7 +21,7 @@ describe('Acceptance | ember data integration', function() { destroyApp(application); }); - it('ember data adapter uses ember-ajax mixin', function() { + it('can apply the Ember Ajax mixin to an Ember Data adapter', function() { server.get('api/posts/1', function() { return jsonResponse(200, { data: { diff --git a/tests/integration/components/ajax-get-test.js b/tests/integration/components/ajax-get-test.js index 879a549e..24cd1911 100644 --- a/tests/integration/components/ajax-get-test.js +++ b/tests/integration/components/ajax-get-test.js @@ -1,6 +1,6 @@ import { describeComponent, it } from 'ember-mocha'; import { beforeEach, afterEach } from 'mocha'; -import { assert } from 'chai'; +import { expect } from 'chai'; import Pretender from 'pretender'; import { jsonFactory as json } from 'dummy/tests/helpers/json'; @@ -8,9 +8,6 @@ import wait from 'ember-test-helpers/wait'; import hbs from 'htmlbars-inline-precompile'; -const { equal } = assert; - -let server; describeComponent( 'ajax-get', 'AjaxGetComponent', @@ -19,17 +16,17 @@ describeComponent( }, function() { beforeEach(function() { - server = new Pretender(); + this.server = new Pretender(); }); afterEach(function() { - server.shutdown(); + this.server.shutdown(); }); it('clicking Load Data loads data', function() { const PAYLOAD = [{ title: 'Foo' }, { title: 'Bar' }, { title: 'Baz' }]; - server.get('/foo', json(200, PAYLOAD), 300); + this.server.get('/foo', json(200, PAYLOAD), 300); this.render(hbs` {{#ajax-get url="/foo" as |data isLoaded|}} @@ -48,9 +45,9 @@ describeComponent( this.$(`.ajax-get button`).click(); return wait().then(() => { - equal(this.$('.ajax-get li:eq(0)').text(), 'Foo'); - equal(this.$('.ajax-get li:eq(1)').text(), 'Bar'); - equal(this.$('.ajax-get li:eq(2)').text(), 'Baz'); + expect(this.$('.ajax-get li:eq(0)').text()).to.equal('Foo'); + expect(this.$('.ajax-get li:eq(1)').text()).to.equal('Bar'); + expect(this.$('.ajax-get li:eq(2)').text()).to.equal('Baz'); }); }); } diff --git a/tests/integration/components/async-widget-test.js b/tests/integration/components/async-widget-test.js index 20a48457..fa818af6 100644 --- a/tests/integration/components/async-widget-test.js +++ b/tests/integration/components/async-widget-test.js @@ -3,23 +3,20 @@ import hbs from 'htmlbars-inline-precompile'; import { describeComponent, it } from 'ember-mocha'; import { beforeEach, afterEach } from 'mocha'; -import { assert } from 'chai'; +import { expect } from 'chai'; + +import AjaxService from 'ember-ajax/services/ajax'; +import Pretender from 'pretender'; +import { jsonFactory as json } from 'dummy/tests/helpers/json'; +import wait from 'ember-test-helpers/wait'; -const { deepEqual, equal, throws } = assert; const { Component, Service, inject, computed } = Ember; - -import AjaxService from 'ember-ajax/services/ajax'; -import Pretender from 'pretender'; -import { jsonFactory as json } from 'dummy/tests/helpers/json'; -import wait from 'ember-test-helpers/wait'; - const PAYLOAD = { posts: [ { id: 1, title: 'hello world' } ] }; -let server; describeComponent( 'async-widget', @@ -29,15 +26,15 @@ describeComponent( }, function() { beforeEach(function() { - server = new Pretender(); + this.server = new Pretender(); }); afterEach(function() { - server.shutdown(); + this.server.shutdown(); }); it('service injected in component', function() { - server.get('/posts', json(200, PAYLOAD)); + this.server.get('/posts', json(200, PAYLOAD)); const authToken = 'foo'; this.register('service:session', Service.extend({ authToken })); @@ -85,14 +82,14 @@ describeComponent( this.render(hbs`{{async-widget id="async-widget" url="/posts"}}`); return component.loadData().then(function(response) { component.set('hello', 'world'); - equal(component.get('helloStyle'), 'hello world', 'run loop is not necessary'); - deepEqual(receivedHeaders[0], ['authToken', 'foo'], 'token was used session'); - deepEqual(response, PAYLOAD, 'recieved PAYLOAD'); + expect(component.get('helloStyle')).to.equal('hello world'); + expect(receivedHeaders[0]).to.deep.equal(['authToken', 'foo']); + expect(response).to.deep.equal(PAYLOAD); }); }); it.skip('error thrown in service can be caught in test', function() { - server.post('/posts/1', json(404, { error: 'not found' }), 200); + this.server.post('/posts/1', json(404, { error: 'not found' }), 200); this.register('service:ajax', AjaxService.extend({ customPOST(url) { @@ -113,13 +110,13 @@ describeComponent( {{/async-widget}}` ); - throws(function() { + expect(function() { this.$('.async-widget').click(); - }); + }).to.throw(); }); it('waiting for promises to complete', function() { - server.get('/foo', json(200, { foo: 'bar' }), 300); + this.server.get('/foo', json(200, { foo: 'bar' }), 300); this.register('component:async-widget', Component.extend({ layout: hbs`{{yield foo}}`, @@ -134,11 +131,11 @@ describeComponent( this.render(hbs`{{#async-widget classNames="async-widget" as |foo|}}Got: {{foo}} for foo{{/async-widget}}`); - equal(this.$('.async-widget').text(), 'Got: foo for foo'); + expect(this.$('.async-widget').text()).to.equal('Got: foo for foo'); this.$('.async-widget').click(); return wait().then(() => { - equal(this.$('.async-widget').text(), 'Got: bar for foo'); + expect(this.$('.async-widget').text()).to.equal('Got: bar for foo'); }); }); } diff --git a/tests/unit/ajax-request-test.js b/tests/unit/ajax-request-test.js deleted file mode 100644 index 95233f0e..00000000 --- a/tests/unit/ajax-request-test.js +++ /dev/null @@ -1,759 +0,0 @@ -import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { deepEqual, equal, notEqual, ok, strictEqual, throws } = assert; - -function textContains(full, part, message) { - ok(full.indexOf(part) >= 0, message); -} - -import Ember from 'ember'; -import AjaxRequest from 'ember-ajax/ajax-request'; -import Pretender from 'pretender'; -import { jsonResponse } from 'dummy/tests/helpers/json'; - -const { A } = Ember; - -describe('AjaxRequest', function() { - beforeEach(function() { - this.server = new Pretender(); - }); - - afterEach(function() { - this.server.shutdown(); - }); - - it('headers are set if the URL matches the host', function() { - this.server.get('http://example.com/test', (req) => { - const { requestHeaders } = req; - equal(requestHeaders['Content-Type'], 'application/json'); - equal(requestHeaders['Other-key'], 'Other Value'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - host: 'http://example.com', - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('http://example.com/test'); - }); - - it('headers are set if the URL is relative', function() { - this.server.get('/some/relative/url', (req) => { - const { requestHeaders } = req; - equal(requestHeaders['Content-Type'], 'application/json'); - equal(requestHeaders['Other-key'], 'Other Value'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('/some/relative/url'); - }); - - it('headers are set if the URL matches one of the RegExp trustedHosts', function() { - this.server.get('http://my.example.com', (req) => { - const { requestHeaders } = req; - equal(requestHeaders['Other-key'], 'Other Value'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - host: 'some-other-host.com', - trustedHosts: A([ - 4, - 'notmy.example.com', - /example\./ - ]), - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('http://my.example.com'); - }); - - it('headers are set if the URL matches one of the string trustedHosts', function() { - this.server.get('http://foo.bar.com', (req) => { - const { requestHeaders } = req; - equal(requestHeaders['Other-key'], 'Other Value'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - host: 'some-other-host.com', - trustedHosts: A([ - 'notmy.example.com', - /example\./, - 'foo.bar.com' - ]), - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('http://foo.bar.com'); - }); - - it('headers are not set if the URL does not match the host', function() { - this.server.get('http://example.com', (req) => { - const { requestHeaders } = req; - notEqual(requestHeaders['Other-key'], 'Other Value'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - host: 'some-other-host.com', - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('http://example.com'); - }); - - it('headers can be supplied on a per-request basis', function() { - this.server.get('http://example.com', (req) => { - const { requestHeaders } = req; - equal(requestHeaders['Per-Request-Key'], 'Some value', 'Request had per-request header'); - equal(requestHeaders['Other-key'], 'Other Value', 'Request had default header'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - host: 'http://example.com', - headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } - }); - - const service = new RequestWithHeaders(); - return service.request('http://example.com', { - headers: { - 'Per-Request-Key': 'Some value' - } - }); - }); - - it('options() sets raw data', function() { - const service = new AjaxRequest(); - const url = 'test'; - const type = 'GET'; - const ajaxOptions = service.options(url, { type, data: { key: 'value' } }); - - deepEqual(ajaxOptions, { - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', - data: { - key: 'value' - }, - dataType: 'json', - headers: {}, - type: 'GET', - url: '/test' - }); - }); - - it('options() sets options correctly', function() { - const service = new AjaxRequest(); - const url = 'test'; - const type = 'POST'; - const data = JSON.stringify({ key: 'value' }); - const ajaxOptions = service.options( - url, - { - type, - data, - contentType: 'application/json; charset=utf-8' - } - ); - - deepEqual(ajaxOptions, { - contentType: 'application/json; charset=utf-8', - data: '{"key":"value"}', - dataType: 'json', - headers: {}, - type: 'POST', - url: '/test' - }); - }); - - it('options() empty data', function() { - const service = new AjaxRequest(); - const url = 'test'; - const type = 'POST'; - const ajaxOptions = service.options(url, { type }); - - deepEqual(ajaxOptions, { - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', - dataType: 'json', - headers: {}, - type: 'POST', - url: '/test' - }); - }); - - it('can override the default `contentType` for the service', function() { - const defaultContentType = 'application/json'; - - class AjaxServiceWithDefaultContentType extends AjaxRequest { - get contentType() { - return defaultContentType; - } - } - - const service = new AjaxServiceWithDefaultContentType(); - const options = service.options(''); - equal(options.contentType, defaultContentType); - }); - - it('options() type defaults to GET', function() { - const service = new AjaxRequest(); - const url = 'test'; - const ajaxOptions = service.options(url); - - equal(ajaxOptions.type, 'GET'); - }); - - it('request() promise label is correct', function() { - const service = new AjaxRequest(); - let url = '/posts'; - let data = { - type: 'POST', - data: { - post: { title: 'Title', description: 'Some description.' } - } - }; - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(data.data)]; - - this.server.get(url, () => serverResponse); - this.server.post(url, () => serverResponse); - - const getPromise = service.request(url); - equal(getPromise._label, 'ember-ajax: GET /posts response'); - - const postPromise = service.request(url, data); - equal(postPromise._label, 'ember-ajax: POST /posts response'); - }); - - it('post() promise label is correct', function() { - const service = new AjaxRequest(); - const url = '/posts'; - const title = 'Title'; - const description = 'Some description.'; - let options = { - data: { - post: { title, description } - } - }; - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; - - this.server.post(url, () => serverResponse); - - const postPromise = service.post(url, options); - equal(postPromise._label, 'ember-ajax: POST /posts response'); - - return postPromise.then(function(response) { - deepEqual(response.post, options.data.post); - }); - }); - - it('put() promise label is correct', function() { - const service = new AjaxRequest(); - const url = '/posts/1'; - const title = 'Title'; - const description = 'Some description.'; - const id = 1; - const options = { - data: { - post: { id, title, description } - } - }; - - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; - - this.server.put(url, () => serverResponse); - - const putPromise = service.put(url, options); - equal(putPromise._label, 'ember-ajax: PUT /posts/1 response'); - - return putPromise.then(function(response) { - deepEqual(response.post, options.data.post); - }); - }); - - it('patch() promise label is correct', function() { - const service = new AjaxRequest(); - const url = '/posts/1'; - const description = 'Some description.'; - const options = { - data: { - post: { description } - } - }; - - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; - - this.server.patch(url, () => serverResponse); - - const patchPromise = service.patch(url, options); - equal(patchPromise._label, 'ember-ajax: PATCH /posts/1 response'); - - return patchPromise.then(function(response) { - deepEqual(response.post, options.data.post); - }); - }); - - it('del() promise label is correct', function() { - const service = new AjaxRequest(); - const url = '/posts/1'; - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - - this.server.delete(url, () => serverResponse); - - const delPromise = service.del(url); - equal(delPromise._label, 'ember-ajax: DELETE /posts/1 response'); - - return delPromise.then(function(response) { - deepEqual(response, {}); - }); - }); - - it('delete() promise label is correct', function() { - const service = new AjaxRequest(); - const url = '/posts/1'; - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - - this.server.delete(url, () => serverResponse); - - const deletePromise = service.delete(url); - equal(deletePromise._label, 'ember-ajax: DELETE /posts/1 response'); - - return deletePromise.then(function(response) { - deepEqual(response, {}); - }); - }); - - it('request with method option makes the correct type of request', function() { - const service = new AjaxRequest(); - const url = '/posts/1'; - const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; - - this.server.get(url, () => { - ok(false, 'Made a GET request'); - return serverResponse; - }); - - this.server.post(url, () => { - ok(true, 'Made a POST request'); - return serverResponse; - }); - - return service.request(url, { method: 'POST' }); - }); - - it('options() host is set on the url (url starting with `/`)', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com' - }); - - const service = new RequestWithHost(); - const url = '/users/me'; - const ajaxoptions = service.options(url); - - equal(ajaxoptions.url, 'https://discuss.emberjs.com/users/me'); - }); - - it('options() host is set on the url (url not starting with `/`)', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com' - }); - - const service = new RequestWithHost(); - const url = 'users/me'; - const ajaxoptions = service.options(url); - - equal(ajaxoptions.url, 'https://discuss.emberjs.com/users/me'); - }); - - it('options() host is overridable on a per-request basis', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com' - }); - - const service = new RequestWithHost(); - const url = 'users/me'; - const host = 'https://myurl.com'; - const ajaxoptions = service.options(url, { host }); - - equal(ajaxoptions.url, 'https://myurl.com/users/me'); - }); - - it('explicit host in URL overrides host property of class', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com' - }); - - const service = new RequestWithHost(); - const url = 'http://myurl.com/users/me'; - const ajaxOptions = service.options(url); - - equal(ajaxOptions.url, 'http://myurl.com/users/me'); - }); - - it('explicit host in URL overrides host property in request config', function() { - const service = new AjaxRequest(); - const host = 'https://discuss.emberjs.com'; - const url = 'http://myurl.com/users/me'; - const ajaxOptions = service.options(url, { host }); - - equal(ajaxOptions.url, 'http://myurl.com/users/me'); - }); - - it('explicit host in URL without a protocol does not override config property', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com' - }); - - const service = new RequestWithHost(); - const url = 'myurl.com/users/me'; - const ajaxOptions = service.options(url); - - equal(ajaxOptions.url, 'https://discuss.emberjs.com/myurl.com/users/me'); - }); - - it('options() namespace is set on the url (namespace starting with `/`)', function() { - const RequestWithHost = AjaxRequest.extend({ - namespace: '/api/v1' - }); - - const service = new RequestWithHost(); - - equal(service.options('/users/me').url, '/api/v1/users/me', 'url starting with `/`)'); - equal(service.options('users/me').url, '/api/v1/users/me', 'url not starting with `/`)'); - }); - - it('namespace can be set on a per-request basis', function() { - const service = new AjaxRequest(); - - equal(service.options('users/me', { namespace: 'api' }).url, '/api/users/me', 'url contains namespace'); - }); - - it('options() namespace is set on the url (namespace not starting with `/`)', function() { - const RequestWithHost = AjaxRequest.extend({ - namespace: 'api/v1' - }); - - const service = new RequestWithHost(); - - equal(service.options('/users/me').url, '/api/v1/users/me', 'url starting with `/`)'); - equal(service.options('users/me').url, '/api/v1/users/me', 'url not starting with `/`)'); - }); - - it('options() both host and namespace are set on the url', function() { - const RequestWithHost = AjaxRequest.extend({ - host: 'https://discuss.emberjs.com', - namespace: '/api/v1' - }); - - const service = new RequestWithHost(); - const url = '/users/me'; - const ajaxoptions = service.options(url); - - equal(ajaxoptions.url, 'https://discuss.emberjs.com/api/v1/users/me'); - }); - - it('it can get the full header list from class and request options', function() { - const RequestWithHeaders = AjaxRequest.extend({ - headers: { - 'Content-Type': 'application/vnd.api+json', - 'Other-Value': 'Some Value' - } - }); - - const service = new RequestWithHeaders(); - const headers = { 'Third-Value': 'Other Thing' }; - equal(Object.keys(service._getFullHeadersHash()).length, 2, 'Works without options'); - equal(Object.keys(service._getFullHeadersHash(headers)).length, 3, 'Includes passed-in headers'); - equal(Object.keys(service.headers).length, 2, 'Provided headers did not change default ones'); - }); - - it('it creates a detailed error message for unmatched server errors with an AJAX payload', function() { - const response = [408, { 'Content-Type': 'application/json' }, JSON.stringify( - { errors: [ 'Some error response' ] } - )]; - this.server.get('/posts', () => response); - - const service = new AjaxRequest(); - return service.request('/posts') - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(result) { - textContains(result.message, 'Some error response', 'Show payload as string'); - textContains(result.message, 'GET', 'Show AJAX method'); - textContains(result.message, '/posts', 'Show URL'); - }); - }); - - it('it creates a detailed error message for unmatched server errors with a text payload', function() { - const response = [408, { 'Content-Type': 'text/html' }, 'Some error response']; - this.server.get('/posts', () => response); - - const service = new AjaxRequest(); - return service.request('/posts') - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(result) { - textContains(result.message, 'Some error response', 'Show payload as string'); - textContains(result.message, 'GET', 'Show AJAX method'); - textContains(result.message, '/posts', 'Show URL'); - }); - }); - - it('it always returns error objects with status codes as strings', function() { - const response = [404, { 'Content-Type': 'application/json' }, '']; - this.server.get('/posts', () => response); - - const service = new AjaxRequest(); - return service.request('/posts') - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(result) { - strictEqual(result.errors[0].status, '404', 'status must be a string'); - }); - }); - - it('it coerces payload error response status codes to strings', function() { - const body = { - errors: [ - { status: 403, message: 'Permission Denied' } - ] - }; - const response = [403, { 'Content-Type': 'application/json' }, JSON.stringify(body)]; - this.server.get('/posts', () => response); - - const service = new AjaxRequest(); - return service.request('/posts') - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(result) { - strictEqual(result.errors[0].status, '403', 'status must be a string'); - strictEqual(result.errors[0].message, 'Permission Denied'); - }); - }); - - it('it throws an error when the user tries to use `.get` to make a request', function() { - const service = new AjaxRequest(); - service.set('someProperty', 'foo'); - - equal(service.get('someProperty'), 'foo', 'Can get a property'); - - throws(function() { - service.get('/users'); - }); - - throws(function() { - service.get('/users', {}); - }); - }); - - it('it JSON encodes JSON:API request data automatically', function() { - this.server.post('/test', ({ requestBody }) => { - const { foo } = JSON.parse(requestBody); - equal(foo, 'bar', 'Recieved JSON-encoded data'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - headers: { - 'Content-Type': 'application/vnd.api+json' - } - }); - - const service = new RequestWithHeaders(); - return service.post('/test', { - data: { - foo: 'bar' - } - }); - }); - - it('it does not JSON encode query parameters when JSON:API headers are present', function() { - this.server.get('/test', ({ queryParams }) => { - const { foo } = queryParams; - equal(foo, 'bar', 'Correctly received query param'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - headers: { - 'Content-Type': 'application/vnd.api+json' - } - }); - - const service = new RequestWithHeaders(); - return service.request('/test', { - data: { - foo: 'bar' - } - }); - }); - - it('it JSON encodes JSON:API "extension" request data automatically', function() { - this.server.post('/test', ({ requestBody }) => { - const { foo } = JSON.parse(requestBody); - equal(foo, 'bar', 'Recieved JSON-encoded data'); - return jsonResponse(); - }); - - const RequestWithHeaders = AjaxRequest.extend({ - headers: { - 'Content-Type': 'application/vnd.api+json; ext="ext1,ext2"' - } - }); - - const service = new RequestWithHeaders(); - return service.post('/test', { - data: { - foo: 'bar' - } - }); - }); - - it('normalizes errors into the appropriate format', function() { - const service = new AjaxRequest(); - - const jsonApiError = service.normalizeErrorResponse(400, {}, { - errors: [ - { status: 400, title: 'Foo' }, - { status: 400, title: 'Foo' } - ] - }); - deepEqual(jsonApiError, [ - { status: '400', title: 'Foo' }, - { status: '400', title: 'Foo' } - ], 'Normalizes an error in the JSON API format'); - - const payloadWithErrorStrings = service.normalizeErrorResponse(400, {}, { - errors: [ - 'This is an error', - 'This is another error' - ] - }); - deepEqual(payloadWithErrorStrings, [ - { status: '400', title: 'This is an error' }, - { status: '400', title: 'This is another error' } - ], 'Normalizes error payload with strings in errors property'); - - const payloadArrayOfObjects = service.normalizeErrorResponse(400, {}, [ - { status: 400, title: 'Foo' }, - { status: 400, title: 'Bar' } - ]); - deepEqual(payloadArrayOfObjects, [ - { status: '400', title: 'Foo', meta: { status: 400, title: 'Foo' } }, - { status: '400', title: 'Bar', meta: { status: 400, title: 'Bar' } } - ], 'Normalizes error array of objects'); - - const payloadArrayOfStrings = service.normalizeErrorResponse(400, {}, [ - 'Foo', 'Bar' - ]); - deepEqual(payloadArrayOfStrings, [ - { status: '400', title: 'Foo' }, - { status: '400', title: 'Bar' } - ], 'Normalizes error array of strings'); - - const payloadIsString = service.normalizeErrorResponse(400, {}, 'Foo'); - deepEqual(payloadIsString, [ - { - status: '400', - title: 'Foo' - } - ], 'Normalizes error string'); - - const payloadIsObject = service.normalizeErrorResponse(400, {}, { - title: 'Foo' - }); - deepEqual(payloadIsObject, [ - { - status: '400', - title: 'Foo', - meta: { - title: 'Foo' - } - } - ], 'Normalizes error object'); - }); - - it('it correctly creates the URL to request', function() { - class NamespaceLeadingSlash extends AjaxRequest { - static get slashType() { - return 'leading slash'; - } - get namespace() { - return '/bar'; - } - } - - class NamespaceTrailingSlash extends AjaxRequest { - static get slashType() { - return 'trailing slash'; - } - get namespace() { - return 'bar/'; - } - } - - class NamespaceTwoSlash extends AjaxRequest { - static get slashType() { - return 'leading and trailing slash'; - } - get namespace() { - return '/bar/'; - } - } - - class NamespaceNoSlash extends AjaxRequest { - static get slashType() { - return 'no slashes'; - } - get namespace() { - return 'bar'; - } - } - - const hosts = [ - { hostType: 'trailing slash', host: 'http://foo.com/' }, - { hostType: 'no trailing slash', host: 'http://foo.com' } - ]; - - [NamespaceLeadingSlash, NamespaceTrailingSlash, NamespaceTwoSlash, NamespaceNoSlash].forEach((Klass) => { - let req = new Klass(); - - hosts.forEach((exampleHost) => { - const { hostType, host } = exampleHost; - ['/baz', 'baz'].forEach((segment) => { - equal( - req._buildURL(segment, { host }), - 'http://foo.com/bar/baz', - `Host with ${hostType}, Namespace with ${Klass.slashType}, segment: ${segment}` - ); - }); - ['/baz/', 'baz/'].forEach((segment) => { - equal( - req._buildURL(segment, { host }), - 'http://foo.com/bar/baz/', - `Host with ${hostType}, Namespace with ${Klass.slashType}, segment: ${segment}` - ); - }); - }); - }); - - let req = new AjaxRequest(); - equal(req._buildURL('/baz', { host: 'http://foo.com' }), 'http://foo.com/baz', 'Builds URL correctly without namespace'); - equal(req._buildURL('/baz'), '/baz', 'Builds URL correctly without namespace or host'); - }); -}); - diff --git a/tests/unit/custom-waiter-test.js b/tests/unit/custom-waiter-test.js deleted file mode 100644 index b9836986..00000000 --- a/tests/unit/custom-waiter-test.js +++ /dev/null @@ -1,61 +0,0 @@ -import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { deepEqual, ok } = assert; - -import AjaxRequest from 'ember-ajax/ajax-request'; -import Pretender from 'pretender'; -import { jsonResponse } from 'dummy/tests/helpers/json'; -import wait from 'ember-test-helpers/wait'; - -let requestMade = false; - -function handleRequest() { - requestMade = true; - return jsonResponse(); -} - -describe('Custom waiter', function() { - beforeEach(function() { - this.server = new Pretender(); - - this.server.get('/test', handleRequest); - this.server.post('/test', handleRequest); - }); - afterEach(function() { - this.server.shutdown(); - requestMade = false; - }); - - it('an AJAX GET request can be waited on', function() { - const service = new AjaxRequest(); - service.request('/test'); - - return wait().then(function() { - ok(requestMade, 'wait resolved after request was made'); - }); - }); - - it('an AJAX POST request can be waited on', function() { - const service = new AjaxRequest(); - service.post('/test'); - - return wait().then(function() { - ok(requestMade, 'wait resolved after request was made'); - }); - }); - - it('a JSONP request can be waited on', function() { - let response; - - this.server.get('/jsonp', function(req) { - return [200, {}, `${req.queryParams.callback}({ "foo": "bar" })`]; - }); - - const ajax = new AjaxRequest(); - ajax.request('/jsonp', { dataType: 'jsonp' }).then((val) => response = val); - return wait().then(() => { - deepEqual(response, { foo: 'bar' }); - }); - }); -}); diff --git a/tests/unit/error-handlers-test.js b/tests/unit/error-handlers-test.js deleted file mode 100644 index 1e39a7f4..00000000 --- a/tests/unit/error-handlers-test.js +++ /dev/null @@ -1,68 +0,0 @@ -import Ember from 'ember'; -import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { ok } = assert; - -import AjaxRequest from 'ember-ajax/ajax-request'; -import { - ConflictError, - InvalidError, - UnauthorizedError, - ForbiddenError, - BadRequestError, - ServerError, - isTimeoutError -} from 'ember-ajax/errors'; -import Pretender from 'pretender'; -import { jsonFactory } from 'dummy/tests/helpers/json'; - -const { typeOf } = Ember; - -describe('unit/error-handlers-test', function() { - beforeEach(function() { - this.server = new Pretender(); - }); - - afterEach(function() { - this.server.shutdown(); - }); - - it('it handles a TimeoutError correctly', function() { - this.server.get('/posts', jsonFactory(200), 2); - const service = new AjaxRequest(); - return service.request('/posts', { timeout: 1 }) - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(reason) { - ok(isTimeoutError(reason), 'responded with a TimeoutError'); - ok(reason.errors && typeOf(reason.errors) === 'array', 'has errors array'); - }); - }); - - function errorHandlerTest(status, errorClass) { - it(`${status} handler`, function() { - this.server.get('/posts', jsonFactory(status)); - const service = new AjaxRequest(); - return service.request('/posts') - .then(function() { - ok(false, 'success handler should not be called'); - }) - .catch(function(reason) { - ok(reason instanceof errorClass); - ok(reason.errors && typeOf(reason.errors) === 'array', - 'has errors array'); - }); - }); - } - - errorHandlerTest(401, UnauthorizedError); - errorHandlerTest(403, ForbiddenError); - errorHandlerTest(409, ConflictError); - errorHandlerTest(422, InvalidError); - errorHandlerTest(400, BadRequestError); - errorHandlerTest(500, ServerError); - errorHandlerTest(502, ServerError); - errorHandlerTest(510, ServerError); -}); diff --git a/tests/unit/errors-test.js b/tests/unit/errors-test.js index dd73d389..c5f2a96b 100644 --- a/tests/unit/errors-test.js +++ b/tests/unit/errors-test.js @@ -91,73 +91,87 @@ describe('unit/errors-test - AjaxError', function() { ok(error instanceof ConflictError); }); - it('isUnauthorizedError: detects error code correctly', function() { - ok(isUnauthorizedError(401)); - }); + describe('isUnauthorizedError', function() { + it('detects error code correctly', function() { + ok(isUnauthorizedError(401)); + }); - it('isUnauthorizedError: detects error class correctly', function() { - const error = new UnauthorizedError(); - ok(isUnauthorizedError(error)); + it('detects error class correctly', function() { + const error = new UnauthorizedError(); + ok(isUnauthorizedError(error)); + }); }); - it('isForbiddenError: detects error code correctly', function() { - ok(isForbiddenError(403)); - }); + describe('isForbiddenError', function() { + it('detects error code correctly', function() { + ok(isForbiddenError(403)); + }); - it('isForbiddenError: detects error class correctly', function() { - const error = new ForbiddenError(); - ok(isForbiddenError(error)); + it('detects error class correctly', function() { + const error = new ForbiddenError(); + ok(isForbiddenError(error)); + }); }); - it('isNotFoundError: detects error code correctly', function() { - ok(isNotFoundError(404)); - notOk(isNotFoundError(400)); - }); + describe('isNotFoundError', function() { + it(': detects error code correctly', function() { + ok(isNotFoundError(404)); + notOk(isNotFoundError(400)); + }); - it('isNotFoundError: detects error class correctly', function() { - const error = new NotFoundError(); - const otherError = new Error(); - ok(isNotFoundError(error)); - notOk(isNotFoundError(otherError)); + it('detects error class correctly', function() { + const error = new NotFoundError(); + const otherError = new Error(); + ok(isNotFoundError(error)); + notOk(isNotFoundError(otherError)); + }); }); - it('isInvalidError: detects error code correctly', function() { - ok(isInvalidError(422)); - }); + describe('isInvalidError', function() { + it('detects error code correctly', function() { + ok(isInvalidError(422)); + }); - it('isInvalidError: detects error class correctly', function() { - const error = new InvalidError(); - ok(isInvalidError(error)); + it('detects error class correctly', function() { + const error = new InvalidError(); + ok(isInvalidError(error)); + }); }); - it('isBadRequestError: detects error code correctly', function() { - ok(isBadRequestError(400)); - }); + describe('isBadRequestError', function() { + it('detects error code correctly', function() { + ok(isBadRequestError(400)); + }); - it('isBadRequestError: detects error class correctly', function() { - const error = new BadRequestError(); - ok(isBadRequestError(error)); + it('detects error class correctly', function() { + const error = new BadRequestError(); + ok(isBadRequestError(error)); + }); }); - it('isServerError: detects error code correctly', function() { - notOk(isServerError(499)); - ok(isServerError(500)); - ok(isServerError(599)); - notOk(isServerError(600)); - }); + describe('isServerError', function() { + it('detects error code correctly', function() { + notOk(isServerError(499)); + ok(isServerError(500)); + ok(isServerError(599)); + notOk(isServerError(600)); + }); - it('isAjaxError: detects error class correctly', function() { - const ajaxError = new AjaxError(); - const notAjaxError = new Error(); - const ajaxErrorSubtype = new BadRequestError(); - ok(isAjaxError(ajaxError)); - notOk(isAjaxError(notAjaxError)); - ok(isAjaxError(ajaxErrorSubtype)); + it('detects error class correctly', function() { + const error = new ServerError(); + ok(isServerError(error)); + }); }); - it('isServerError: detects error class correctly', function() { - const error = new ServerError(); - ok(isServerError(error)); + describe('isAjaxError', function() { + it('detects error class correctly', function() { + const ajaxError = new AjaxError(); + const notAjaxError = new Error(); + const ajaxErrorSubtype = new BadRequestError(); + ok(isAjaxError(ajaxError)); + notOk(isAjaxError(notAjaxError)); + ok(isAjaxError(ajaxErrorSubtype)); + }); }); it('isTimeoutError: detects error class correctly', function() { @@ -165,23 +179,29 @@ describe('unit/errors-test - AjaxError', function() { ok(isTimeoutError(error)); }); - it('isAbortError: detects error class correctly', function() { - const error = new AbortError(); - ok(isAbortError(error)); + describe('isAbortError', function() { + it('detects error class correctly', function() { + const error = new AbortError(); + ok(isAbortError(error)); + }); }); - it('isConflictError: detects error code correctly', function() { - ok(isConflictError(409)); + describe('isConflictError', function() { + it('detects error code correctly', function() { + ok(isConflictError(409)); + }); }); - it('detects successful request correctly', function() { - notOk(isSuccess(100)); - notOk(isSuccess(199)); - ok(isSuccess(200)); - ok(isSuccess(299)); - notOk(isSuccess(300)); - ok(isSuccess(304)); - notOk(isSuccess(400)); - notOk(isSuccess(500)); + describe('isSuccess', function() { + it('detects successful request correctly', function() { + notOk(isSuccess(100)); + notOk(isSuccess(199)); + ok(isSuccess(200)); + ok(isSuccess(299)); + notOk(isSuccess(300)); + ok(isSuccess(304)); + notOk(isSuccess(400)); + notOk(isSuccess(500)); + }); }); }); diff --git a/tests/unit/export-test.js b/tests/unit/export-test.js index ee8b4b5f..3253f7d2 100644 --- a/tests/unit/export-test.js +++ b/tests/unit/export-test.js @@ -1,13 +1,11 @@ import { describe, it } from 'mocha'; -import { assert } from 'chai'; +import { expect } from 'chai'; import ajax from 'ember-ajax'; import request from 'ember-ajax/request'; -const { equal } = assert; - -describe('export', function() { - it('ember-ajax exports request function', function() { - equal(ajax, request); +describe('export structure', function() { + it('exports request function by default', function() { + expect(ajax).to.deep.equal(request); }); }); diff --git a/tests/unit/jsonp-test.js b/tests/unit/jsonp-test.js deleted file mode 100644 index bdddfc99..00000000 --- a/tests/unit/jsonp-test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { deepEqual } = assert; - -import AjaxRequest from 'ember-ajax/ajax-request'; -import Pretender from 'pretender'; - -describe('JSONP Requests', function() { - beforeEach(function() { - this.server = new Pretender(); - }); - afterEach(function() { - this.server.shutdown(); - }); - - it('it should make JSONP requests', function() { - this.server.get('/jsonp', function(req) { - return [200, {}, `${req.queryParams.callback}({ "foo": "bar" })`]; - }); - - const ajax = new AjaxRequest(); - return ajax.request('/jsonp', { - dataType: 'jsonp' - }) - .then((value) => { - deepEqual(value, { foo: 'bar' }, 'Promise resolved with correct value'); - }); - }); -}); diff --git a/tests/unit/mixins/ajax-request-test.js b/tests/unit/mixins/ajax-request-test.js index da63375e..68646795 100644 --- a/tests/unit/mixins/ajax-request-test.js +++ b/tests/unit/mixins/ajax-request-test.js @@ -1,15 +1,893 @@ +import { describe, beforeEach, afterEach, it } from 'mocha'; +import { expect } from 'chai'; +import wait from 'ember-test-helpers/wait'; + import Ember from 'ember'; -import AjaxRequestMixin from 'ember-ajax/mixins/ajax-request'; -import { describe, it } from 'mocha'; -import { assert } from 'chai'; +import AjaxRequest from 'ember-ajax/ajax-request'; +import { + ConflictError, + InvalidError, + UnauthorizedError, + ForbiddenError, + BadRequestError, + ServerError, + isTimeoutError +} from 'ember-ajax/errors'; +import Pretender from 'pretender'; +import { jsonResponse, jsonFactory } from 'dummy/tests/helpers/json'; -const { Object: EmberObject } = Ember; -const { ok } = assert; +const { A, typeOf } = Ember; describe('Unit | Mixin | ajax request', function() { - it('works', function() { - let AjaxRequestObject = EmberObject.extend(AjaxRequestMixin); - let subject = AjaxRequestObject.create(); - ok(subject); + beforeEach(function() { + this.server = new Pretender(); + }); + + afterEach(function() { + this.server.shutdown(); + }); + + it('headers are set if the URL matches the host', function() { + this.server.get('http://example.com/test', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Content-Type']).to.equal('application/json'); + expect(requestHeaders['Other-key']).to.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + host: 'http://example.com', + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('http://example.com/test'); + }); + + it('headers are set if the URL is relative', function() { + this.server.get('/some/relative/url', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Content-Type']).to.equal('application/json'); + expect(requestHeaders['Other-key']).to.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('/some/relative/url'); + }); + + it('headers are set if the URL matches one of the RegExp trustedHosts', function() { + this.server.get('http://my.example.com', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Other-key']).to.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + host: 'some-other-host.com', + trustedHosts: A([ + 4, + 'notmy.example.com', + /example\./ + ]), + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('http://my.example.com'); + }); + + it('headers are set if the URL matches one of the string trustedHosts', function() { + this.server.get('http://foo.bar.com', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Other-key']).to.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + host: 'some-other-host.com', + trustedHosts: A([ + 'notmy.example.com', + /example\./, + 'foo.bar.com' + ]), + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('http://foo.bar.com'); + }); + + it('headers are not set if the URL does not match the host', function() { + this.server.get('http://example.com', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Other-key']).to.not.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + host: 'some-other-host.com', + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('http://example.com'); + }); + + it('headers can be supplied on a per-request basis', function() { + this.server.get('http://example.com', (req) => { + const { requestHeaders } = req; + expect(requestHeaders['Per-Request-Key']).to.equal('Some value'); + expect(requestHeaders['Other-key']).to.equal('Other Value'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + host: 'http://example.com', + headers: { 'Content-Type': 'application/json', 'Other-key': 'Other Value' } + }); + + const service = new RequestWithHeaders(); + return service.request('http://example.com', { + headers: { + 'Per-Request-Key': 'Some value' + } + }); + }); + + describe('options method', function() { + it('sets raw data', function() { + const service = new AjaxRequest(); + const url = 'test'; + const type = 'GET'; + const ajaxOptions = service.options(url, { type, data: { key: 'value' } }); + + expect(ajaxOptions).to.deep.equal({ + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + data: { + key: 'value' + }, + dataType: 'json', + headers: {}, + type: 'GET', + url: '/test' + }); + }); + + it('sets options correctly', function() { + const service = new AjaxRequest(); + const url = 'test'; + const type = 'POST'; + const data = JSON.stringify({ key: 'value' }); + const ajaxOptions = service.options( + url, + { + type, + data, + contentType: 'application/json; charset=utf-8' + } + ); + + expect(ajaxOptions).to.deep.equal({ + contentType: 'application/json; charset=utf-8', + data: '{"key":"value"}', + dataType: 'json', + headers: {}, + type: 'POST', + url: '/test' + }); + }); + + it('can handle empty data', function() { + const service = new AjaxRequest(); + const url = 'test'; + const type = 'POST'; + const ajaxOptions = service.options(url, { type }); + + expect(ajaxOptions).to.deep.equal({ + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + dataType: 'json', + headers: {}, + type: 'POST', + url: '/test' + }); + }); + + it('is only called once per call to request', function() { + let numberOptionsCalls = 0; + + this.server.get('/foo', () => jsonResponse()); + + const MonitorOptionsCalls = AjaxRequest.extend({ + options() { + numberOptionsCalls = numberOptionsCalls + 1; + return this._super(...arguments); + } + }); + + const service = new MonitorOptionsCalls(); + return service.request('/foo') + .then(function() { + expect(numberOptionsCalls).to.equal(1); + }); + }); + + it('is only called once per call to raw', function() { + let numberOptionsCalls = 0; + + this.server.get('/foo', () => jsonResponse()); + + const MonitorOptionsCalls = AjaxRequest.extend({ + options() { + numberOptionsCalls = numberOptionsCalls + 1; + return this._super(...arguments); + } + }); + + const service = new MonitorOptionsCalls(); + return service.raw('/foo') + .then(function() { + expect(numberOptionsCalls).to.equal(1); + }); + }); + }); + + it('can override the default `contentType` for the service', function() { + const defaultContentType = 'application/json'; + + class AjaxServiceWithDefaultContentType extends AjaxRequest { + get contentType() { + return defaultContentType; + } + } + + const service = new AjaxServiceWithDefaultContentType(); + const options = service.options(''); + expect(options.contentType).to.equal(defaultContentType); + }); + + it('options() type defaults to GET', function() { + const service = new AjaxRequest(); + const url = 'test'; + const ajaxOptions = service.options(url); + + expect(ajaxOptions.type).to.equal('GET'); + }); + + it('request() promise label is correct', function() { + const service = new AjaxRequest(); + let url = '/posts'; + let data = { + type: 'POST', + data: { + post: { title: 'Title', description: 'Some description.' } + } + }; + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(data.data)]; + + this.server.get(url, () => serverResponse); + this.server.post(url, () => serverResponse); + + const getPromise = service.request(url); + expect(getPromise._label).to.equal('ember-ajax: GET /posts response'); + + const postPromise = service.request(url, data); + expect(postPromise._label).to.equal('ember-ajax: POST /posts response'); + }); + + it('post() promise label is correct', function() { + const service = new AjaxRequest(); + const url = '/posts'; + const title = 'Title'; + const description = 'Some description.'; + let options = { + data: { + post: { title, description } + } + }; + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; + + this.server.post(url, () => serverResponse); + + const postPromise = service.post(url, options); + expect(postPromise._label).to.equal('ember-ajax: POST /posts response'); + + return postPromise.then(function(response) { + expect(response.post).to.deep.equal(options.data.post); + }); + }); + + it('put() promise label is correct', function() { + const service = new AjaxRequest(); + const url = '/posts/1'; + const title = 'Title'; + const description = 'Some description.'; + const id = 1; + const options = { + data: { + post: { id, title, description } + } + }; + + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; + + this.server.put(url, () => serverResponse); + + const putPromise = service.put(url, options); + expect(putPromise._label).to.equal('ember-ajax: PUT /posts/1 response'); + + return putPromise.then(function(response) { + expect(response.post).to.deep.equal(options.data.post); + }); + }); + + it('patch() promise label is correct', function() { + const service = new AjaxRequest(); + const url = '/posts/1'; + const description = 'Some description.'; + const options = { + data: { + post: { description } + } + }; + + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify(options.data)]; + + this.server.patch(url, () => serverResponse); + + const patchPromise = service.patch(url, options); + expect(patchPromise._label).to.equal('ember-ajax: PATCH /posts/1 response'); + + return patchPromise.then(function(response) { + expect(response.post).to.deep.equal(options.data.post); + }); + }); + + it('del() promise label is correct', function() { + const service = new AjaxRequest(); + const url = '/posts/1'; + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; + + this.server.delete(url, () => serverResponse); + + const delPromise = service.del(url); + expect(delPromise._label).to.equal('ember-ajax: DELETE /posts/1 response'); + + return delPromise.then(function(response) { + expect(response).to.deep.equal({}); + }); + }); + + it('delete() promise label is correct', function() { + const service = new AjaxRequest(); + const url = '/posts/1'; + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; + + this.server.delete(url, () => serverResponse); + + const deletePromise = service.delete(url); + expect(deletePromise._label).to.equal('ember-ajax: DELETE /posts/1 response'); + + return deletePromise.then(function(response) { + expect(response).to.deep.equal({}); + }); + }); + + it('request with method option makes the correct type of request', function() { + const service = new AjaxRequest(); + const url = '/posts/1'; + const serverResponse = [200, { 'Content-Type': 'application/json' }, JSON.stringify({})]; + + this.server.get(url, () => { + throw new Error(`Shouldn't make an AJAX request`); + }); + + this.server.post(url, () => { + return serverResponse; + }); + + return service.request(url, { method: 'POST' }); + }); + + it('options() host is set on the url (url starting with `/`)', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com' + }); + + const service = new RequestWithHost(); + const url = '/users/me'; + const ajaxoptions = service.options(url); + + expect(ajaxoptions.url).to.equal('https://discuss.emberjs.com/users/me'); + }); + + it('options() host is set on the url (url not starting with `/`)', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com' + }); + + const service = new RequestWithHost(); + const url = 'users/me'; + const ajaxoptions = service.options(url); + + expect(ajaxoptions.url).to.equal('https://discuss.emberjs.com/users/me'); + }); + + it('options() host is overridable on a per-request basis', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com' + }); + + const service = new RequestWithHost(); + const url = 'users/me'; + const host = 'https://myurl.com'; + const ajaxoptions = service.options(url, { host }); + + expect(ajaxoptions.url).to.equal('https://myurl.com/users/me'); + }); + + it('explicit host in URL overrides host property of class', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com' + }); + + const service = new RequestWithHost(); + const url = 'http://myurl.com/users/me'; + const ajaxOptions = service.options(url); + + expect(ajaxOptions.url).to.equal('http://myurl.com/users/me'); + }); + + it('explicit host in URL overrides host property in request config', function() { + const service = new AjaxRequest(); + const host = 'https://discuss.emberjs.com'; + const url = 'http://myurl.com/users/me'; + const ajaxOptions = service.options(url, { host }); + + expect(ajaxOptions.url).to.equal('http://myurl.com/users/me'); + }); + + it('explicit host in URL without a protocol does not override config property', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com' + }); + + const service = new RequestWithHost(); + const url = 'myurl.com/users/me'; + const ajaxOptions = service.options(url); + + expect(ajaxOptions.url).to.equal('https://discuss.emberjs.com/myurl.com/users/me'); + }); + + it('options() namespace is set on the url (namespace starting with `/`)', function() { + const RequestWithHost = AjaxRequest.extend({ + namespace: '/api/v1' + }); + + const service = new RequestWithHost(); + + expect(service.options('/users/me').url).to.equal('/api/v1/users/me'); + expect(service.options('users/me').url).to.equal('/api/v1/users/me'); + }); + + it('namespace can be set on a per-request basis', function() { + const service = new AjaxRequest(); + + expect(service.options('users/me', { namespace: 'api' }).url).to.equal('/api/users/me'); + }); + + it('options() namespace is set on the url (namespace not starting with `/`)', function() { + const RequestWithHost = AjaxRequest.extend({ + namespace: 'api/v1' + }); + + const service = new RequestWithHost(); + + expect(service.options('/users/me').url).to.equal('/api/v1/users/me'); + expect(service.options('users/me').url).to.equal('/api/v1/users/me'); + }); + + it('options() both host and namespace are set on the url', function() { + const RequestWithHost = AjaxRequest.extend({ + host: 'https://discuss.emberjs.com', + namespace: '/api/v1' + }); + + const service = new RequestWithHost(); + const url = '/users/me'; + const ajaxoptions = service.options(url); + + expect(ajaxoptions.url).to.equal('https://discuss.emberjs.com/api/v1/users/me'); + }); + + it('it can get the full header list from class and request options', function() { + const RequestWithHeaders = AjaxRequest.extend({ + headers: { + 'Content-Type': 'application/vnd.api+json', + 'Other-Value': 'Some Value' + } + }); + + const service = new RequestWithHeaders(); + const headers = { 'Third-Value': 'Other Thing' }; + expect(Object.keys(service._getFullHeadersHash()).length).to.equal(2); + expect(Object.keys(service._getFullHeadersHash(headers)).length).to.equal(3); + expect(Object.keys(service.headers).length).to.equal(2); + }); + + it('it creates a detailed error message for unmatched server errors with an AJAX payload', function() { + const response = [408, { 'Content-Type': 'application/json' }, JSON.stringify( + { errors: [ 'Some error response' ] } + )]; + this.server.get('/posts', () => response); + + const service = new AjaxRequest(); + return service.request('/posts') + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(result) { + expect(result.message).to.contain('Some error response'); + expect(result.message).to.contain('GET'); + expect(result.message).to.contain('/posts'); + }); + }); + + it('it creates a detailed error message for unmatched server errors with a text payload', function() { + const response = [408, { 'Content-Type': 'text/html' }, 'Some error response']; + this.server.get('/posts', () => response); + + const service = new AjaxRequest(); + return service.request('/posts') + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(result) { + expect(result.message).to.contain('Some error response'); + expect(result.message).to.contain('GET'); + expect(result.message).to.contain('/posts'); + }); + }); + + it('it always returns error objects with status codes as strings', function() { + const response = [404, { 'Content-Type': 'application/json' }, '']; + this.server.get('/posts', () => response); + + const service = new AjaxRequest(); + return service.request('/posts') + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(result) { + expect(result.errors[0].status).to.equal('404'); + }); + }); + + it('it coerces payload error response status codes to strings', function() { + const body = { + errors: [ + { status: 403, message: 'Permission Denied' } + ] + }; + const response = [403, { 'Content-Type': 'application/json' }, JSON.stringify(body)]; + this.server.get('/posts', () => response); + + const service = new AjaxRequest(); + return service.request('/posts') + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(result) { + expect(result.errors[0].status).to.equal('403'); + expect(result.errors[0].message).to.equal('Permission Denied'); + }); + }); + + it('it throws an error when the user tries to use `.get` to make a request', function() { + const service = new AjaxRequest(); + service.set('someProperty', 'foo'); + + expect(service.get('someProperty')).to.equal('foo'); + + expect(function() { + service.get('/users'); + }).to.throw(); + + expect(function() { + service.get('/users', {}); + }).to.throw(); + }); + + it('it JSON encodes JSON:API request data automatically', function() { + this.server.post('/test', ({ requestBody }) => { + const { foo } = JSON.parse(requestBody); + expect(foo).to.equal('bar'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + headers: { + 'Content-Type': 'application/vnd.api+json' + } + }); + + const service = new RequestWithHeaders(); + return service.post('/test', { + data: { + foo: 'bar' + } + }); + }); + + it('it does not JSON encode query parameters when JSON:API headers are present', function() { + this.server.get('/test', ({ queryParams }) => { + const { foo } = queryParams; + expect(foo).to.equal('bar'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + headers: { + 'Content-Type': 'application/vnd.api+json' + } + }); + + const service = new RequestWithHeaders(); + return service.request('/test', { + data: { + foo: 'bar' + } + }); + }); + + it('it JSON encodes JSON:API "extension" request data automatically', function() { + this.server.post('/test', ({ requestBody }) => { + const { foo } = JSON.parse(requestBody); + expect(foo).to.equal('bar'); + return jsonResponse(); + }); + + const RequestWithHeaders = AjaxRequest.extend({ + headers: { + 'Content-Type': 'application/vnd.api+json; ext="ext1,ext2"' + } + }); + + const service = new RequestWithHeaders(); + return service.post('/test', { + data: { + foo: 'bar' + } + }); + }); + + it('normalizes errors into the appropriate format', function() { + const service = new AjaxRequest(); + + const jsonApiError = service.normalizeErrorResponse(400, {}, { + errors: [ + { status: 400, title: 'Foo' }, + { status: 400, title: 'Foo' } + ] + }); + expect(jsonApiError).to.deep.equal([ + { status: '400', title: 'Foo' }, + { status: '400', title: 'Foo' } + ]); + + const payloadWithErrorStrings = service.normalizeErrorResponse(400, {}, { + errors: [ + 'This is an error', + 'This is another error' + ] + }); + expect(payloadWithErrorStrings).to.deep.equal([ + { status: '400', title: 'This is an error' }, + { status: '400', title: 'This is another error' } + ]); + + const payloadArrayOfObjects = service.normalizeErrorResponse(400, {}, [ + { status: 400, title: 'Foo' }, + { status: 400, title: 'Bar' } + ]); + expect(payloadArrayOfObjects).to.deep.equal([ + { status: '400', title: 'Foo', meta: { status: 400, title: 'Foo' } }, + { status: '400', title: 'Bar', meta: { status: 400, title: 'Bar' } } + ]); + + const payloadArrayOfStrings = service.normalizeErrorResponse(400, {}, [ + 'Foo', 'Bar' + ]); + expect(payloadArrayOfStrings).to.deep.equal([ + { status: '400', title: 'Foo' }, + { status: '400', title: 'Bar' } + ]); + + const payloadIsString = service.normalizeErrorResponse(400, {}, 'Foo'); + expect(payloadIsString).to.deep.equal([ + { + status: '400', + title: 'Foo' + } + ]); + + const payloadIsObject = service.normalizeErrorResponse(400, {}, { + title: 'Foo' + }); + expect(payloadIsObject).to.deep.equal([ + { + status: '400', + title: 'Foo', + meta: { + title: 'Foo' + } + } + ]); + }); + + it('it correctly creates the URL to request', function() { + class NamespaceLeadingSlash extends AjaxRequest { + static get slashType() { + return 'leading slash'; + } + get namespace() { + return '/bar'; + } + } + + class NamespaceTrailingSlash extends AjaxRequest { + static get slashType() { + return 'trailing slash'; + } + get namespace() { + return 'bar/'; + } + } + + class NamespaceTwoSlash extends AjaxRequest { + static get slashType() { + return 'leading and trailing slash'; + } + get namespace() { + return '/bar/'; + } + } + + class NamespaceNoSlash extends AjaxRequest { + static get slashType() { + return 'no slashes'; + } + get namespace() { + return 'bar'; + } + } + + const hosts = [ + { hostType: 'trailing slash', host: 'http://foo.com/' }, + { hostType: 'no trailing slash', host: 'http://foo.com' } + ]; + + [NamespaceLeadingSlash, NamespaceTrailingSlash, NamespaceTwoSlash, NamespaceNoSlash].forEach((Klass) => { + let req = new Klass(); + + hosts.forEach((exampleHost) => { + const { host } = exampleHost; + ['/baz', 'baz'].forEach((segment) => { + expect(req._buildURL(segment, { host })).to.equal('http://foo.com/bar/baz'); + }); + ['/baz/', 'baz/'].forEach((segment) => { + expect(req._buildURL(segment, { host })).to.equal('http://foo.com/bar/baz/'); + }); + }); + }); + + let req = new AjaxRequest(); + expect(req._buildURL('/baz', { host: 'http://foo.com' })).to.equal('http://foo.com/baz'); + expect(req._buildURL('/baz')).to.equal('/baz'); + }); + + describe('JSONP Requests', function() { + it('should make JSONP requests', function() { + this.server.get('/jsonp', function(req) { + return [200, {}, `${req.queryParams.callback}({ "foo": "bar" })`]; + }); + + const ajax = new AjaxRequest(); + return ajax.request('/jsonp', { + dataType: 'jsonp' + }) + .then((value) => { + expect(value).to.deep.equal({ foo: 'bar' }); + }); + }); + }); + + describe('error handlers', function() { + it('handles a TimeoutError correctly', function() { + this.server.get('/posts', jsonFactory(200), 2); + const service = new AjaxRequest(); + return service.request('/posts', { timeout: 1 }) + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(reason) { + expect(isTimeoutError(reason)).to.be.ok; + expect(reason.errors && typeOf(reason.errors) === 'array').to.be.ok; + }); + }); + + function errorHandlerTest(status, errorClass) { + it(`handles a ${status} response correctly`, function() { + this.server.get('/posts', jsonFactory(status)); + const service = new AjaxRequest(); + return service.request('/posts') + .then(function() { + throw new Error('success handler should not be called'); + }) + .catch(function(reason) { + expect(reason instanceof errorClass).to.be.ok; + expect(reason.errors && typeOf(reason.errors) === 'array').to.be.ok; + }); + }); + } + + errorHandlerTest(401, UnauthorizedError); + errorHandlerTest(403, ForbiddenError); + errorHandlerTest(409, ConflictError); + errorHandlerTest(422, InvalidError); + errorHandlerTest(400, BadRequestError); + errorHandlerTest(500, ServerError); + errorHandlerTest(502, ServerError); + errorHandlerTest(510, ServerError); + }); + + describe('Custom waiter', function() { + beforeEach(function() { + this.requestMade = false; + + function handleRequest() { + this.requestMade = true; + return jsonResponse(); + } + + this.server.get('/test', handleRequest.bind(this)); + this.server.post('/test', handleRequest.bind(this)); + }); + + it('can wait on an AJAX GET request', function() { + const service = new AjaxRequest(); + service.request('/test'); + + return wait().then(() => { + expect(this.requestMade).to.be.ok; + }); + }); + + it('can wait on an AJAX POST request', function() { + const service = new AjaxRequest(); + service.post('/test'); + + return wait().then(() => { + expect(this.requestMade).to.be.ok; + }); + }); + + it('can wait on a JSONP request', function() { + let response; + + this.server.get('/jsonp', function(req) { + return [200, {}, `${req.queryParams.callback}({ "foo": "bar" })`]; + }); + + const ajax = new AjaxRequest(); + ajax.request('/jsonp', { dataType: 'jsonp' }).then((val) => response = val); + return wait().then(() => { + expect(response).to.deep.equal({ foo: 'bar' }); + }); + }); }); }); diff --git a/tests/unit/raw-test.js b/tests/unit/raw-test.js index 00cb9e22..378f869c 100644 --- a/tests/unit/raw-test.js +++ b/tests/unit/raw-test.js @@ -1,37 +1,35 @@ import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { deepEqual, equal, ok } = assert; +import { expect } from 'chai'; import Pretender from 'pretender'; import raw from 'ember-ajax/raw'; describe('raw', function() { beforeEach(function() { - this.api = new Pretender(); + this.server = new Pretender(); }); afterEach(function() { - this.api.shutdown(); + this.server.shutdown(); }); - it('raw() returns jqXHR', function() { + it('returns jqXHR', function() { const photos = [ { id: 10, src: 'http://media.giphy.com/media/UdqUo8xvEcvgA/giphy.gif' }, { id: 42, src: 'http://media0.giphy.com/media/Ko2pyD26RdYRi/giphy.gif' } ]; - this.api.get('/photos', function() { + this.server.get('/photos', function() { return [200, { 'Content-Type': 'application/json' }, JSON.stringify(photos)]; }); return raw('/photos') .then(function(data) { - deepEqual(data.response, photos, 'returned data is same as send data'); - ok(data.jqXHR, 'jqXHR is present'); - equal(data.textStatus, 'success', 'textStatus is success'); + expect(data.response).to.deep.equal(photos); + expect(data.jqXHR).to.be.ok; + expect(data.textStatus).to.equal('success'); }); }); - it('raw() rejects promise when 404 is returned', function() { - this.api.get('/photos', function() { + it('rejects promise when 404 is returned', function() { + this.server.get('/photos', function() { return [404, { 'Content-Type': 'application/json' }]; }); @@ -42,11 +40,11 @@ describe('raw', function() { }) .catch(function(response) { const { errorThrown } = response; - equal(errorThrown, 'Not Found'); + expect(errorThrown).to.equal('Not Found'); errorCalled = true; }) .finally(function() { - assert.equal(errorCalled, true, 'error handler was called'); + expect(errorCalled).to.be.ok; }); }); }); diff --git a/tests/unit/request-test.js b/tests/unit/request-test.js index d6a1267b..4eef936b 100644 --- a/tests/unit/request-test.js +++ b/tests/unit/request-test.js @@ -1,7 +1,5 @@ import { describe, beforeEach, afterEach, it } from 'mocha'; -import { assert } from 'chai'; - -const { deepEqual, equal, ok } = assert; +import { expect } from 'chai'; import { isNotFoundError } from 'ember-ajax/errors'; import Pretender from 'pretender'; @@ -15,7 +13,7 @@ describe('request', function() { this.server.shutdown(); }); - it('request() produces data', function() { + it('produces data', function() { const photos = [ { id: 10, src: 'http://media.giphy.com/media/UdqUo8xvEcvgA/giphy.gif' }, { id: 42, src: 'http://media0.giphy.com/media/Ko2pyD26RdYRi/giphy.gif' } @@ -24,11 +22,11 @@ describe('request', function() { return [200, { 'Content-Type': 'application/json' }, JSON.stringify(photos)]; }); return request('/photos').then(function(data) { - deepEqual(data, photos); + expect(data).to.deep.equal(photos); }); }); - it('request() rejects promise when 404 is returned', function() { + it('rejects promise when 404 is returned', function() { this.server.get('/photos', function() { return [404, { 'Content-Type': 'application/json' }]; }); @@ -39,12 +37,11 @@ describe('request', function() { errorCalled = false; }) .catch(function(response) { - ok(isNotFoundError(response)); + expect(isNotFoundError(response)).to.be.ok; errorCalled = true; }) .finally(function() { - equal(errorCalled, true, 'error handler was called'); + expect(errorCalled).to.be.ok; }); }); }); -