AMBARI-18158. Add UI unit tests for App.HttpClient (alexantonenko)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/a004067e Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/a004067e Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/a004067e Branch: refs/heads/branch-dev-logsearch Commit: a004067e603972639334f1649df043201e873c83 Parents: b453b02 Author: Alex Antonenko <hiv...@gmail.com> Authored: Tue Aug 16 13:00:43 2016 +0300 Committer: Alex Antonenko <hiv...@gmail.com> Committed: Tue Aug 16 13:04:50 2016 +0300 ---------------------------------------------------------------------- ambari-web/app/assets/test/tests.js | 1 + ambari-web/app/utils/http_client.js | 55 +-- .../global/errors_handler_controller_test.js | 1 + ambari-web/test/utils/http_client_test.js | 433 +++++++++++++++++++ 4 files changed, 451 insertions(+), 39 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/a004067e/ambari-web/app/assets/test/tests.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js index 658b6c8..00aabff 100644 --- a/ambari-web/app/assets/test/tests.js +++ b/ambari-web/app/assets/test/tests.js @@ -201,6 +201,7 @@ var files = [ 'test/utils/heatmap_test', 'test/utils/host_progress_popup_test', 'test/utils/hosts_test', + 'test/utils/http_client_test', 'test/utils/misc_test', 'test/utils/number_utils_test', 'test/utils/polling_test', http://git-wip-us.apache.org/repos/asf/ambari/blob/a004067e/ambari-web/app/utils/http_client.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/utils/http_client.js b/ambari-web/app/utils/http_client.js index 6b94d81..dbfc640 100644 --- a/ambari-web/app/utils/http_client.js +++ b/ambari-web/app/utils/http_client.js @@ -49,32 +49,10 @@ App.HttpClient = Em.Object.create({ * @param {string} url * @param {Object} ajaxOptions * @param {App.ServerDataMapper} mapper - json processor + * @param {boolean} isGetAsPost - if true, do POST-request equal to GET-request but with some params put to body * @param {callback} errorHandler */ - request: function (url, ajaxOptions, mapper, errorHandler) { - - if (!errorHandler) { - errorHandler = this.defaultErrorHandler; - } - - var xhr = new XMLHttpRequest(); - var curTime = App.dateTime(); - - xhr.open('GET', url + (url.indexOf('?') >= 0 ? '&_=' : '?_=') + curTime, true); - xhr.send(null); - - this.onReady(xhr, "", ajaxOptions, mapper, errorHandler, url); - }, - - /** - * Do POST-request equal to GET-request but with some params put to body - * @param {string} url - * @param {{params: string, success: callback, error: callback}} ajaxOptions - * @param {App.QuickDataMapper} mapper - * @param errorHandler - * @method getAsPostRequest - */ - getAsPostRequest: function (url, ajaxOptions, mapper, errorHandler) { + request: function (url, ajaxOptions, mapper, errorHandler, isGetAsPost) { if (!errorHandler) { errorHandler = this.defaultErrorHandler; @@ -82,19 +60,22 @@ App.HttpClient = Em.Object.create({ var xhr = new XMLHttpRequest(), curTime = App.dateTime(), - params = JSON.stringify({ - "RequestInfo": {"query" : ajaxOptions.params } - }); - - xhr.open('POST', url + (url.indexOf('?') >= 0 ? '&_=' : '?_=') + curTime, true); - xhr.setRequestHeader("X-Http-Method-Override", "GET"); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + method = isGetAsPost ? 'POST': 'GET', + params = isGetAsPost ? JSON.stringify({ + "RequestInfo": {"query" : ajaxOptions.params} + }) : null; + + xhr.open(method, url + (url.indexOf('?') >= 0 ? '&_=' : '?_=') + curTime, true); + if (isGetAsPost) { + xhr.setRequestHeader("X-Http-Method-Override", "GET"); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } xhr.send(params); this.onReady(xhr, "", ajaxOptions, mapper, errorHandler, url); }, - /* + /** This function checks if we get response from server Not using onreadystatechange cuz of possible closure */ @@ -140,12 +121,8 @@ App.HttpClient = Em.Object.create({ } var client = this, request = function () { - if (data.doGetAsPost && !App.get('testMode')) { - client.getAsPostRequest(url, data, mapper, errorHandler); - } - else { - client.request(url, data, mapper, errorHandler); - } + var isGetAsPost = Boolean(data.doGetAsPost && !App.get('testMode')); + client.request(url, data, mapper, errorHandler, isGetAsPost); url = null; data = null; mapper = null; @@ -169,6 +146,6 @@ App.HttpClient = Em.Object.create({ * @param {number} interval - frequency request */ post: function (url, data, mapper, errorHandler, interval) { - this.get(url, data, mapper, errorHandler, interval); + this.get.apply(this, arguments); } }); http://git-wip-us.apache.org/repos/asf/ambari/blob/a004067e/ambari-web/test/controllers/global/errors_handler_controller_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/global/errors_handler_controller_test.js b/ambari-web/test/controllers/global/errors_handler_controller_test.js index b255dcc..2a726ee 100644 --- a/ambari-web/test/controllers/global/errors_handler_controller_test.js +++ b/ambari-web/test/controllers/global/errors_handler_controller_test.js @@ -47,6 +47,7 @@ describe('App.ErrorsHandlerController', function () { describe("#saveErrorLogs()", function () { beforeEach(function() { + localStorage.removeItem('errors'); sinon.stub(controller, 'postUserPref'); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/a004067e/ambari-web/test/utils/http_client_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/utils/http_client_test.js b/ambari-web/test/utils/http_client_test.js new file mode 100644 index 0000000..9e158a4 --- /dev/null +++ b/ambari-web/test/utils/http_client_test.js @@ -0,0 +1,433 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var App = require('app'); +require('utils/http_client'); + +describe('App.HttpClient', function () { + + describe('#defaultErrorHandler', function () { + + var cases = [ + { + isAsserted: false, + title: 'no response text' + }, + { + responseText: null, + title: 'empty response text' + }, + { + responseText: 404, + title: 'invalid response text (number)' + }, + { + responseText: 'error', + title: 'invalid response text (string)' + }, + { + responseText: '{error}', + title: 'malformed response text (incorrect literal)' + }, + { + responseText: '{error: 404}', + title: 'malformed response text (no parentheses)' + }, + { + responseText: '{\'error\': 404}', + title: 'malformed response text (incorrect parentheses)' + }, + { + responseText: '{"error": 404}', + title: 'valid response text' + } + ]; + + cases.forEach(function (item) { + + describe(item.title, function () { + + var jqXHR = { + responseText: item.responseText + }; + + beforeEach(function () { + sinon.stub(App.ajax, 'defaultErrorHandler', Em.K); + sinon.spy(Em, 'assert'); + try { + App.HttpClient.defaultErrorHandler(jqXHR, '', '', 'http://localhost'); + } catch (e) {} + }); + + afterEach(function () { + App.ajax.defaultErrorHandler.restore(); + Em.assert.restore(); + }); + + it('default error handler call', function () { + expect(App.ajax.defaultErrorHandler.calledOnce).to.be.true; + }); + + it('default error handler arguments', function () { + expect(App.ajax.defaultErrorHandler.firstCall.args).to.eql([jqXHR, 'http://localhost']); + }); + + it('no console error assertion', function () { + expect(Em.assert.threw()).to.be.false; + }); + + }); + + }); + + }); + + describe('#request', function () { + + var errorHandler = Em.K, + ajaxOptions = { + params: 'property=value' + }, + mapper = {}, + cases = [ + { + url: 'url', + generatedUrl: 'url?_=1000', + errorHandler: null, + passedErrorHandler: App.HttpClient.defaultErrorHandler, + isGetAsPost: false, + method: 'GET', + setRequestHeaderCallCount: 0, + params: null, + title: 'no request parameters, default error handler' + }, + { + url: 'url?property0=value0&property1=value1', + generatedUrl: 'url?property0=value0&property1=value1&_=1000', + errorHandler: errorHandler, + passedErrorHandler: errorHandler, + isGetAsPost: true, + method: 'POST', + setRequestHeaderCallCount: 2, + params: '{"RequestInfo":{"query":"property=value"}}', + title: 'request parameters passed, POST request' + } + ]; + + cases.forEach(function (item) { + + describe(item.title, function () { + + beforeEach(function () { + sinon.stub(XMLHttpRequest.prototype, 'open', Em.K); + sinon.stub(XMLHttpRequest.prototype, 'setRequestHeader', Em.K); + sinon.stub(XMLHttpRequest.prototype, 'send', Em.K); + sinon.stub(App, 'dateTime').returns(1000); + sinon.stub(App.HttpClient, 'onReady', Em.K); + App.HttpClient.request(item.url, ajaxOptions, mapper, item.errorHandler, item.isGetAsPost); + }); + + afterEach(function () { + XMLHttpRequest.prototype.open.restore(); + XMLHttpRequest.prototype.setRequestHeader.restore(); + XMLHttpRequest.prototype.send.restore(); + App.dateTime.restore(); + App.HttpClient.onReady.restore(); + }); + + it('request method', function () { + expect(XMLHttpRequest.prototype.open.firstCall.args[0]).to.equal(item.method); + }); + + it('request URL', function () { + expect(XMLHttpRequest.prototype.open.firstCall.args[1]).to.equal(item.generatedUrl); + }); + + it('setting request headers', function () { + expect(XMLHttpRequest.prototype.setRequestHeader.callCount).to.equal(item.setRequestHeaderCallCount); + }); + + it('request params', function () { + expect(XMLHttpRequest.prototype.send.firstCall.args[0]).to.equal(item.params); + }); + + it('onReady callback: ajaxOptions', function () { + expect(App.HttpClient.onReady.firstCall.args[2]).to.eql(ajaxOptions); + }); + + it('onReady callback: mapper', function () { + expect(App.HttpClient.onReady.firstCall.args[3]).to.eql(mapper); + }); + + it('onReady callback: errorHandler', function () { + expect(App.HttpClient.onReady.firstCall.args[4]).to.eql(item.passedErrorHandler); + }); + + it('onReady callback: url', function () { + expect(App.HttpClient.onReady.firstCall.args[5]).to.equal(item.url); + }); + + }); + + }); + + }); + + describe('#onReady', function () { + + var clock, + xhr = { + responseText: '{"property": "value"}', + statusText: 'status', + abort: Em.K + }, + ajaxOptions = { + complete: Em.K + }, + mapper = { + map: Em.K + }, + mock = { + errorHandler: Em.K + }, + cases = [ + { + readyState: 4, + status: 200, + isCommitError: false, + commitCallCount: 1, + mapCallCount: 1, + completeCallCount: 1, + abortCallCount: 1, + errorHandlerCallCount: 0, + onReadyCallCount: 1, + title: 'successful request' + }, + { + readyState: 4, + status: 200, + isCommitError: true, + commitCallCount: 1, + mapCallCount: 1, + completeCallCount: 1, + abortCallCount: 1, + errorHandlerCallCount: 0, + onReadyCallCount: 1, + title: 'successful request, App.store.commit error' + }, + { + readyState: 4, + status: 404, + isCommitError: false, + commitCallCount: 0, + mapCallCount: 0, + completeCallCount: 0, + abortCallCount: 0, + errorHandlerCallCount: 1, + onReadyCallCount: 1, + title: 'failed request' + }, + { + readyState: 3, + status: 200, + isCommitError: false, + commitCallCount: 1, + mapCallCount: 1, + completeCallCount: 1, + abortCallCount: 1, + errorHandlerCallCount: 0, + onReadyCallCount: 2, + title: 'incomplete request, later successful' + }, + { + readyState: 3, + status: 404, + isCommitError: false, + commitCallCount: 0, + mapCallCount: 0, + completeCallCount: 0, + abortCallCount: 0, + errorHandlerCallCount: 1, + onReadyCallCount: 2, + title: 'incomplete request, later failed' + } + ]; + + cases.forEach(function (item) { + + describe(item.title, function () { + + beforeEach(function () { + clock = sinon.useFakeTimers(); + sinon.stub(App.store, 'commit'); + sinon.spy(xhr, 'abort'); + sinon.spy(mapper, 'map'); + sinon.spy(mock, 'errorHandler'); + sinon.spy(ajaxOptions, 'complete'); + sinon.spy(App.HttpClient, 'onReady'); + xhr.readyState = item.readyState; + xhr.status = item.status; + if (item.isCommitError) { + App.store.commit.throws(); + } + App.HttpClient.onReady(xhr, null, ajaxOptions, mapper, mock.errorHandler, 'url'); + clock.tick(10); + xhr.readyState = 4; + clock.tick(10); + }); + + afterEach(function () { + clock.restore(); + App.store.commit.restore(); + xhr.abort.restore(); + mapper.map.restore(); + mock.errorHandler.restore(); + ajaxOptions.complete.restore(); + App.HttpClient.onReady.restore(); + }); + + it('App.store.commit call', function () { + expect(App.store.commit.callCount).to.equal(item.commitCallCount); + }); + + it('mapping data', function () { + expect(mapper.map.callCount).to.equal(item.mapCallCount); + }); + + if (item.mapCallCount) { + it('mapped data', function () { + expect(mapper.map.alwaysCalledWith({ + property: 'value' + })).to.be.true; + }); + } + + it('complete callback call', function () { + expect(ajaxOptions.complete.callCount).to.equal(item.completeCallCount); + }); + + if (item.completeCallCount) { + it('complete callback context', function () { + expect(ajaxOptions.complete.alwaysCalledOn(App.HttpClient)).to.be.true; + }); + } + + it('abort request', function () { + expect(xhr.abort.callCount).to.equal(item.abortCallCount); + }); + + it('error handler call', function () { + expect(mock.errorHandler.callCount).to.equal(item.errorHandlerCallCount); + }); + + if (item.errorHandlerCallCount) { + it('error handler arguments', function () { + expect(mock.errorHandler.alwaysCalledWith(xhr, 'error', 'status', 'url')).to.be.true; + }); + } + + it('onReady iterations number', function () { + expect(App.HttpClient.onReady.callCount).to.equal(item.onReadyCallCount); + }); + + }); + + }); + + }); + + describe('#get', function () { + + var mapper = {}, + cases = [ + { + data: { + error: Em.clb + }, + errorHandler: Em.K, + passedErrorHandler: Em.K, + isGetAsPost: false, + title: 'custom error handler' + }, + { + data: { + error: Em.clb, + doGetAsPost: true + }, + interval: 1, + passedErrorHandler: Em.clb, + isGetAsPost: true, + title: 'error handler from data, interval provided, POST request' + } + ]; + + cases.forEach(function (item) { + + describe(item.title, function () { + + beforeEach(function () { + sinon.stub(App.HttpClient, 'request', Em.K); + sinon.stub($, 'periodic', function (options, callback) { + callback(); + }); + App.HttpClient.get('url', mapper, item.data, item.errorHandler, item.interval); + }); + + afterEach(function () { + App.HttpClient.request.restore(); + $.periodic.restore(); + }); + + it('request call', function () { + expect(App.HttpClient.request.calledOnce).to.be.true; + }); + + it('request arguments', function () { + expect(App.HttpClient.request.firstCall.args).to.eql(['url', item.data, mapper, item.passedErrorHandler, item.isGetAsPost]); + }); + + }); + + }); + + }); + + describe('#post', function () { + + var args = ['url', {}, {}, Em.K, 1]; + + beforeEach(function () { + sinon.stub(App.HttpClient, 'get', Em.K); + App.HttpClient.post.apply(App.HttpClient, args); + }); + + afterEach(function () { + App.HttpClient.get.restore(); + }); + + it('should call get method', function () { + expect(App.HttpClient.get.calledOnce).to.be.true; + }); + + it('get method arguments', function () { + expect(App.HttpClient.get.firstCall.args).to.eql(args); + }); + + }); + +}); \ No newline at end of file