You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by ak...@apache.org on 2014/05/20 14:21:34 UTC

[1/4] git commit: AMBARI-5820. Add unit tests for mixins. (Max Shepel via akovalenko)

Repository: ambari
Updated Branches:
  refs/heads/trunk 07193440d -> 82ffda436


AMBARI-5820. Add unit tests for mixins. (Max Shepel via akovalenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/de909721
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/de909721
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/de909721

Branch: refs/heads/trunk
Commit: de9097218a6bdb9509bfe2f25c2436cb99b03def
Parents: 0719344
Author: Aleksandr Kovalenko <ak...@hortonworks.com>
Authored: Tue May 20 15:01:56 2014 +0300
Committer: Aleksandr Kovalenko <ak...@hortonworks.com>
Committed: Tue May 20 15:01:56 2014 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |   3 +
 .../common/chart/storm_linear_time_test.js      |  77 +++++++++++
 .../test/mixins/common/localStorage_test.js     |  77 +++++++++++
 .../host_components/decommissionable_test.js    | 131 +++++++++++++++++++
 4 files changed, 288 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/de909721/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 ec8650e..d820fcd 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -97,6 +97,9 @@ require('test/mappers/runs_mapper_test');
 require('test/mappers/service_mapper_test');
 require('test/mappers/status_mapper_test');
 require('test/mappers/users_mapper_test');
+require('test/mixins/common/chart/storm_linear_time_test');
+require('test/mixins/common/localStorage_test');
+require('test/mixins/main/host/details/host_components/decommissionable_test');
 require('test/utils/configs/defaults_providers/yarn_defaults_provider_test');
 require('test/utils/configs/defaults_providers/tez_defaults_provider_test');
 require('test/utils/configs/defaults_providers/hive_defaults_provider_test');

http://git-wip-us.apache.org/repos/asf/ambari/blob/de909721/ambari-web/test/mixins/common/chart/storm_linear_time_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/common/chart/storm_linear_time_test.js b/ambari-web/test/mixins/common/chart/storm_linear_time_test.js
new file mode 100644
index 0000000..b9bc19b
--- /dev/null
+++ b/ambari-web/test/mixins/common/chart/storm_linear_time_test.js
@@ -0,0 +1,77 @@
+/**
+ * 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('mixins/common/chart/storm_linear_time');
+
+var slt,
+  template,
+  series,
+  jsonDataFalse = {
+    metrics: {
+      id: 'metrics'
+    }
+  },
+  jsonDataTrue = {
+    metrics: {
+      storm: {
+        nimbus: {
+          name: 'nimbus'
+        }
+      }
+    }
+  };
+
+describe('App.StormLinearTimeChartMixin', function () {
+
+  beforeEach(function () {
+    slt = Em.Object.create(App.StormLinearTimeChartMixin, {
+      stormChartDefinition: [
+        {
+          field: 'name',
+          name: 'nimbus'
+        }
+      ]
+    });
+  });
+
+  describe('#getDataForAjaxRequest', function () {
+    it('should take data from stormChartDefinition', function () {
+      template = slt.getDataForAjaxRequest().metricsTemplate;
+      expect(template).to.contain('metrics');
+      expect(template).to.contain('storm');
+      expect(template).to.contain('nimbus');
+    });
+  });
+
+  describe('#transformToSeries', function () {
+    it('should be empty', function () {
+      expect(slt.transformToSeries(jsonDataFalse)).to.be.empty;
+    });
+    it('should take one element from data', function () {
+      slt.set('transformData', function (data, name) {
+        return name + ': ' + JSON.stringify(data);
+      });
+      series = slt.transformToSeries(jsonDataTrue);
+      expect(series).to.have.length(1);
+      expect(series[0]).to.equal('nimbus: "nimbus"');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/de909721/ambari-web/test/mixins/common/localStorage_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/common/localStorage_test.js b/ambari-web/test/mixins/common/localStorage_test.js
new file mode 100644
index 0000000..e96f1fb
--- /dev/null
+++ b/ambari-web/test/mixins/common/localStorage_test.js
@@ -0,0 +1,77 @@
+/**
+ * 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('mixins/common/localStorage');
+
+var localStorage,
+  nameCases = [
+    {
+      toSet: {
+        name: 'name'
+      },
+      toExpect: 'Name'
+    },
+    {
+      toSet: {
+        name: null,
+        controller: {
+          name: 'samplecontroller'
+        }
+      },
+      toExpect: 'Samplecontroller'
+    },
+    {
+      toSet: {
+        controller: {
+          name: 'sampleController'
+        }
+      },
+      toExpect: 'Sample'
+    }
+  ];
+
+describe('App.LocalStorage', function () {
+
+  beforeEach(function () {
+    localStorage = Em.Object.create(App.LocalStorage);
+  });
+
+  after(function () {
+    App.db.cleanUp();
+  });
+
+  describe('#dbNamespace', function () {
+    nameCases.forEach(function (item) {
+      it('should be ' + item.toExpect, function () {
+        localStorage.setProperties(item.toSet);
+        expect(localStorage.get('dbNamespace')).to.equal(item.toExpect)
+      });
+    });
+  });
+
+  describe('#getDBProperty', function () {
+    it('should take value from DB', function () {
+      localStorage.set('name', 'name');
+      localStorage.setDBProperty('key', 'value');
+      expect(localStorage.getDBProperty('key')).to.equal('value');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/de909721/ambari-web/test/mixins/main/host/details/host_components/decommissionable_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/mixins/main/host/details/host_components/decommissionable_test.js b/ambari-web/test/mixins/main/host/details/host_components/decommissionable_test.js
new file mode 100644
index 0000000..c175dad
--- /dev/null
+++ b/ambari-web/test/mixins/main/host/details/host_components/decommissionable_test.js
@@ -0,0 +1,131 @@
+/**
+ * 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('mixins/main/host/details/host_components/decommissionable');
+
+var decommissionable,
+  view,
+  statusCases = [
+    {
+      status: App.HostComponentStatus.started,
+      expected: false
+    },
+    {
+      status: 'status',
+      expected: true
+    }
+  ],
+  responseCases = [
+    {
+      HostRoles: {
+        desired_admin_state: 'state'
+      },
+      ServiceComponentInfo: 'info'
+    },
+    {
+      HostRoles: {
+        desired_admin_state: null
+      },
+      ServiceComponentInfo: null
+    }
+  ],
+  textCases = [
+    {
+      available: true,
+      text: Em.I18n.t('common.decommission')
+    },
+    {
+      available: false,
+      text: Em.I18n.t('common.recommission')
+    }
+  ];
+
+describe('App.Decommissionable', function () {
+
+  beforeEach(function () {
+    decommissionable = Em.Object.create(App.Decommissionable);
+  });
+
+  describe('#isComponentDecommissionDisable', function () {
+    statusCases.forEach(function (item) {
+      it('should be ' + item.expected, function () {
+        decommissionable.set('content', {
+          service: {
+            workStatus: item.status
+          }
+        });
+        expect(decommissionable.get('isComponentDecommissionDisable')).to.equal(item.expected);
+      });
+    });
+  });
+
+  describe('#getDesiredAdminStateSuccessCallback', function () {
+    responseCases.forEach(function (item) {
+      var state = item.HostRoles.desired_admin_state;
+      it('should return ' + state, function () {
+        decommissionable.getDesiredAdminStateSuccessCallback(item);
+        expect(decommissionable.get('desiredAdminState')).to.equal(state);
+      });
+    });
+  });
+
+  describe('#getDesiredAdminStateErrorCallback', function () {
+    it('should return null', function () {
+      decommissionable.getDesiredAdminStateErrorCallback();
+      expect(decommissionable.get('desiredAdminState')).to.be.a('null');
+    });
+  });
+
+  describe('#getDecommissionStatusSuccessCallback', function () {
+    responseCases.forEach(function (item) {
+      var state = item.ServiceComponentInfo;
+      it('should return ' + state, function () {
+        decommissionable.getDecommissionStatusSuccessCallback(item);
+        expect(decommissionable.get('decommissionedStatusObject')).to.equal(state);
+      });
+    });
+  });
+
+  describe('#getDecommissionStatusErrorCallback', function () {
+    it('should return null', function () {
+      decommissionable.getDecommissionStatusErrorCallback();
+      expect(decommissionable.get('decommissionedStatusObject')).to.be.a('null');
+    });
+  });
+
+  describe('#decommissionView.text', function () {
+
+    beforeEach(function () {
+      view = decommissionable.decommissionView.create();
+      view.reopen({
+        parentView: decommissionable
+      });
+    });
+
+    textCases.forEach(function (item) {
+      it('should be ' + item.text, function () {
+        view.set('parentView.isComponentDecommissionAvailable', item.available);
+        expect(view.get('text')).to.equal(item.text);
+      });
+    });
+
+  });
+
+});


[3/4] AMBARI-5822. Add unit tests for models. (Max Shepel via akovalenko)

Posted by ak...@apache.org.
http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/user_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/user_test.js b/ambari-web/test/models/user_test.js
new file mode 100644
index 0000000..d844472
--- /dev/null
+++ b/ambari-web/test/models/user_test.js
@@ -0,0 +1,227 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/user');
+
+var user,
+  form,
+  userNameField,
+  userData = {
+    id: 'user'
+  },
+  objectData = Em.Object.create({
+    userName: 'name',
+    isLdap: true
+  });
+
+describe('App.User', function () {
+
+  beforeEach(function () {
+    user = App.User.createRecord(userData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(user);
+  });
+
+  describe('#id', function () {
+    it('should take value from userName', function () {
+      user.set('userName', 'name');
+      expect(user.get('id')).to.equal('name');
+    });
+  });
+
+  describe('#type', function () {
+    it('should be LDAP', function () {
+      user.set('isLdap', true);
+      expect(user.get('type')).to.equal('LDAP');
+    });
+    it('should be Local', function () {
+      user.set('isLdap', false);
+      expect(user.get('type')).to.equal('Local');
+    });
+  });
+
+});
+
+describe('App.EditUserForm', function () {
+
+  beforeEach(function () {
+    form = App.EditUserForm.create();
+  });
+
+  describe('#object', function () {
+
+    before(function () {
+      sinon.stub(App.router, 'get', function (k) {
+        if (k === 'mainAdminUserEditController.content') return userData;
+        return Em.get(App.router, k);
+      });
+    });
+
+    after(function () {
+      App.router.get.restore();
+    });
+
+    it('should take data from controller', function () {
+      expect(form.get('object')).to.eql(userData);
+    });
+
+  });
+
+  describe('#disableUsername', function () {
+    it('should update userName field', function () {
+      form.set('object', userData);
+      expect(form.get('field.userName.disabled')).to.equal('disabled');
+    });
+  });
+
+  describe('#disableAdminCheckbox', function () {
+
+    before(function () {
+      sinon.stub(App, 'get', function(k) {
+        switch (k) {
+          case 'router':
+            return {
+              getLoginName: Em.K
+            };
+          case 'supports.ldapGroupMapping':
+            return true;
+          default:
+            return Em.get(App, k);
+        }
+      });
+      sinon.stub(App.router, 'get', function (k) {
+        if (k === 'mainAdminUserEditController.content') return objectData;
+        return Em.get(App.router, k);
+      });
+    });
+
+    after(function () {
+      App.get.restore();
+      App.router.get.restore();
+    });
+
+    it('should not disable', function () {
+      expect(form.get('field.admin.disabled')).to.be.false;
+    });
+
+    it('should disable', function () {
+      form.set('object', objectData);
+      expect(form.get('field.admin.disabled')).to.be.true;
+    });
+
+  });
+
+  describe('#isValid', function () {
+    it('should be true as default', function () {
+      expect(form.isValid()).to.be.true;
+    });
+    it('should be false', function () {
+      form.set('field.new_password.isRequired', true);
+      expect(form.isValid()).to.be.false;
+    });
+  });
+
+  describe('#save', function () {
+
+    before(function () {
+      sinon.stub(App.router, 'get', function (k) {
+        if (k === 'mainAdminUserEditController.content') return objectData;
+        return Em.get(App.router, k);
+      });
+    });
+
+    after(function () {
+      App.router.get.restore();
+    });
+
+    it('should record form values to object', function () {
+      form.set('field.userName.value', 'name');
+      form.save();
+      expect(form.get('object.userName')).to.equal('name');
+    });
+  });
+
+});
+
+describe('App.CreateUserForm', function () {
+
+  beforeEach(function () {
+    form = App.CreateUserForm.create();
+  });
+
+  describe('#object', function () {
+
+    before(function () {
+      sinon.stub(App.router, 'get', function (k) {
+        if (k === 'mainAdminUserCreateController.content') return userData;
+        return Em.get(App, k);
+      });
+    });
+
+    after(function () {
+      App.router.get.restore();
+    });
+
+    it('should take data from controller', function () {
+      expect(form.get('object')).to.eql(userData);
+    });
+
+  });
+
+  describe('#field.userName.toLowerCase', function () {
+    it('should convert userName into lower case', function () {
+      userNameField = form.getField('userName');
+      userNameField.set('value', 'NAME');
+      expect(userNameField.get('value')).to.equal('name');
+    });
+  });
+
+  describe('#isValid', function () {
+    it('should be false as default', function () {
+      expect(form.isValid()).to.be.false;
+    });
+    it('should be true', function () {
+      form.get('fields').forEach(function (item) {
+        if (item.get('isRequired')) {
+          item.set('value', 'value');
+        }
+      });
+      expect(form.isValid()).to.be.true;
+    });
+  });
+
+  describe('#isWarn', function () {
+    it('should be false as default', function () {
+      expect(form.isWarn()).to.be.false;
+    });
+    it('should be true', function () {
+      form.getField('userName').set('value', '1');
+      expect(form.isWarn()).to.be.true;
+    });
+    it('should be false', function () {
+      form.getField('userName').set('value', 'name');
+      expect(form.isWarn()).to.be.false;
+    });
+  });
+
+});


[2/4] git commit: AMBARI-5821. Add unit tests for jobs models. (Max Shepel via akovalenko)

Posted by ak...@apache.org.
AMBARI-5821. Add unit tests for jobs models. (Max Shepel via akovalenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/6d029a22
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/6d029a22
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/6d029a22

Branch: refs/heads/trunk
Commit: 6d029a22764bb30102b9a5beb2792040f0632515
Parents: de90972
Author: Aleksandr Kovalenko <ak...@hortonworks.com>
Authored: Tue May 20 15:08:24 2014 +0300
Committer: Aleksandr Kovalenko <ak...@hortonworks.com>
Committed: Tue May 20 15:08:24 2014 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js         |   2 +
 ambari-web/test/models/jobs/job_test.js     |  98 ++++++++++++++++++++
 ambari-web/test/models/jobs/tez_dag_test.js | 111 +++++++++++++++++++++++
 3 files changed, 211 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/6d029a22/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 d820fcd..f694cb9 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -172,6 +172,8 @@ require('test/views/wizard/step8_view_test');
 require('test/views/wizard/step9_view_test');
 require('test/views/wizard/step9/hostLogPopupBody_view_test');
 require('test/views/wizard/step10_view_test');
+require('test/models/jobs/job_test');
+require('test/models/jobs/tez_dag_test');
 require('test/models/service/flume_test');
 require('test/models/service/hbase_test');
 require('test/models/service/hdfs_test');

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d029a22/ambari-web/test/models/jobs/job_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/jobs/job_test.js b/ambari-web/test/models/jobs/job_test.js
new file mode 100644
index 0000000..1ca21d8
--- /dev/null
+++ b/ambari-web/test/models/jobs/job_test.js
@@ -0,0 +1,98 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/jobs/job');
+
+var job,
+  jobData = {
+    id: 'job'
+  },
+  timeCases = [
+    {
+      toSet: 'startTime',
+      toExpect: 'startTimeDisplay'
+    },
+    {
+      toSet: 'endTime',
+      toExpect: 'endTimeDisplay'
+    }
+  ],
+  timeDataCorrect = {
+    startTime: 1000,
+    endTime: 2000
+  },
+  timeDataRunning = {
+    startTime: App.dateTime() - 1000,
+    endTime: undefined
+  },
+  timeDataIncorrect = {
+    startTime: App.dateTime() - 1000,
+    endTime: 1
+  };
+
+describe('App.AbstractJob', function () {
+
+  beforeEach(function () {
+    job = App.AbstractJob.createRecord(jobData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(job);
+  });
+
+  timeCases.forEach(function (item) {
+    var toSet = item.toSet,
+      toExpect = item.toExpect;
+    describe('#' + toExpect, function () {
+      it('should be empty', function () {
+        job.set(toSet, 0);
+        expect(job.get(toExpect)).to.be.empty;
+      });
+      it('should return formatted time', function () {
+        job.set(toSet, 1000000);
+        expect(job.get(toExpect)).to.equal('Thu, Jan 01, 1970 00:16');
+      });
+    });
+  });
+
+  describe('#duration', function () {
+    it('should calculate the difference between endTime and startTime', function () {
+      job.setProperties(timeDataCorrect);
+      expect(job.get('duration')).to.equal(1000);
+    });
+    it('should calculate the difference between current time and startTime if the job is running', function () {
+      job.setProperties(timeDataRunning);
+      expect(job.get('duration')).to.be.at.least(1000);
+    });
+    it('should calculate the difference between current time and startTime if endTime is incorrect', function () {
+      job.setProperties(timeDataIncorrect);
+      expect(job.get('duration')).to.be.at.least(1000);
+    });
+  });
+
+  describe('#durationDisplay', function () {
+    it('should return formatted string', function () {
+      job.setProperties(timeDataCorrect);
+      expect(job.get('durationDisplay')).to.equal('1.00 secs');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/6d029a22/ambari-web/test/models/jobs/tez_dag_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/jobs/tez_dag_test.js b/ambari-web/test/models/jobs/tez_dag_test.js
new file mode 100644
index 0000000..17e6c7e
--- /dev/null
+++ b/ambari-web/test/models/jobs/tez_dag_test.js
@@ -0,0 +1,111 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/jobs/tez_dag');
+
+var vertex,
+  vertexData = {
+    id: 'vertex'
+  },
+  timeData = {
+    startTime: 1000,
+    endTime: 2000
+  },
+  tasksCases = [
+    {
+      count: 5,
+      number: 5,
+      title: 'should return tasks count'
+    },
+    {
+      count: null,
+      number: 0,
+      title: 'should return 0'
+    }
+  ],
+  dataSizeCases = [
+    {
+      file: 'fileReadBytes',
+      hdfs: 'hdfsReadBytes',
+      total: 'totalReadBytes',
+      totalDisplay: 'totalReadBytesDisplay'
+    },
+    {
+      file: 'fileWriteBytes',
+      hdfs: 'hdfsWriteBytes',
+      total: 'totalWriteBytes',
+      totalDisplay: 'totalWriteBytesDisplay'
+    }
+  ],
+  setDataSize = function (vertex, fileProp, fileVal, hdfsProp, hdfsVal) {
+    vertex.set(fileProp, fileVal);
+    vertex.set(hdfsProp, hdfsVal);
+  };
+
+describe('App.TezDagVertex', function () {
+
+  beforeEach(function () {
+    vertex = App.TezDagVertex.createRecord(vertexData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(vertex);
+  });
+
+  describe('#duration', function () {
+    it('should calculate the difference between endTime and startTime', function () {
+      vertex.setProperties(timeData);
+      expect(vertex.get('duration')).to.equal(1000);
+    });
+  });
+
+  tasksCases.forEach(function(item) {
+    describe('#tasksNumber', function () {
+      it(item.title, function () {
+        vertex.set('tasksCount', item.count);
+        expect(vertex.get('tasksNumber')).to.equal(item.number);
+      });
+    });
+  });
+
+  dataSizeCases.forEach(function (item) {
+    describe('#' + item.total, function () {
+      it('should sum ' + item.file + ' and ' + item.hdfs, function () {
+        setDataSize(vertex, item.file, 1024, item.hdfs, 2048);
+        expect(vertex.get(item.total)).to.equal(3072);
+      });
+    });
+    describe('#' + item.totalDisplay, function () {
+      it('should return formatted ' + item.total, function () {
+        setDataSize(vertex, item.file, 1024, item.hdfs, 2048);
+        expect(vertex.get(item.totalDisplay)).to.equal('3 KB');
+      });
+    });
+  });
+
+  describe('#durationDisplay', function () {
+    it('should return formatted string', function () {
+      vertex.setProperties(timeData);
+      expect(vertex.get('durationDisplay')).to.equal('1.00 secs');
+    });
+  });
+
+});


[4/4] git commit: AMBARI-5822. Add unit tests for models. (Max Shepel via akovalenko)

Posted by ak...@apache.org.
AMBARI-5822. Add unit tests for models. (Max Shepel via akovalenko)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/82ffda43
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/82ffda43
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/82ffda43

Branch: refs/heads/trunk
Commit: 82ffda43687b3f792e0247925486d172289abf7f
Parents: 6d029a2
Author: Aleksandr Kovalenko <ak...@hortonworks.com>
Authored: Tue May 20 15:14:01 2014 +0300
Committer: Aleksandr Kovalenko <ak...@hortonworks.com>
Committed: Tue May 20 15:14:01 2014 +0300

----------------------------------------------------------------------
 ambari-web/app/assets/test/tests.js             |  14 +
 ambari-web/app/models/cluster_states.js         |   2 +-
 ambari-web/app/models/config_group.js           |   2 +-
 ambari-web/app/models/user.js                   |   2 +-
 ambari-web/test/models/alert_test.js            | 230 +++++++
 ambari-web/test/models/authentication_test.js   |  91 +++
 ambari-web/test/models/cluster_states_test.js   | 114 ++++
 ambari-web/test/models/config_group_test.js     | 154 +++++
 ambari-web/test/models/dataset_job_test.js      | 138 +++++
 ambari-web/test/models/dataset_test.js          | 148 +++++
 ambari-web/test/models/form_test.js             | 218 +++++++
 ambari-web/test/models/hosts_test.js            |  87 +++
 ambari-web/test/models/job_test.js              |  60 ++
 ambari-web/test/models/run_test.js              | 169 +++++
 ambari-web/test/models/service_config_test.js   | 617 +++++++++++++++++++
 ambari-web/test/models/service_test.js          | 292 +++++++++
 .../test/models/stack_service_component_test.js | 254 ++++++++
 ambari-web/test/models/user_test.js             | 227 +++++++
 18 files changed, 2816 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/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 f694cb9..2db199d 100644
--- a/ambari-web/app/assets/test/tests.js
+++ b/ambari-web/app/assets/test/tests.js
@@ -180,6 +180,20 @@ require('test/models/service/hdfs_test');
 require('test/models/service/mapreduce_test');
 require('test/models/service/mapreduce2_test');
 require('test/models/service/yarn_test');
+require('test/models/alert_test');
+require('test/models/authentication_test');
+require('test/models/cluster_states_test');
+require('test/models/config_group_test');
+require('test/models/dataset_test');
+require('test/models/dataset_job_test');
+require('test/models/form_test');
 require('test/models/host_test');
 require('test/models/host_component_test');
+require('test/models/hosts_test');
+require('test/models/job_test');
 require('test/models/rack_test');
+require('test/models/run_test');
+require('test/models/service_test');
+require('test/models/service_config_test');
+require('test/models/stack_service_component_test');
+require('test/models/user_test');

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/app/models/cluster_states.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/cluster_states.js b/ambari-web/app/models/cluster_states.js
index 0d88860..320c3f4 100644
--- a/ambari-web/app/models/cluster_states.js
+++ b/ambari-web/app/models/cluster_states.js
@@ -186,7 +186,7 @@ App.clusterStatus = Em.Object.create(App.UserPref, {
    * @return {*}
    */
   setClusterStatus: function (newValue, opt) {
-    if (App.testMode) return false;
+    if (App.get('testMode')) return false;
     var user = App.db.getUser();
     var login = App.db.getLoginName();
     var val = {clusterName: this.get('clusterName')};

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/app/models/config_group.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/config_group.js b/ambari-web/app/models/config_group.js
index 82db2a9..b1f178b 100644
--- a/ambari-web/app/models/config_group.js
+++ b/ambari-web/app/models/config_group.js
@@ -122,7 +122,7 @@ App.ConfigGroup = Ember.Object.extend({
     }
     // parentConfigGroup.hosts(hosts from default group) - are available hosts, which don't belong to any group
     this.get('parentConfigGroup.hosts').forEach(function (hostName) {
-      unusedHostsMap[hostName] = true;
+      unusedHostsMap[hostName.get('id')] = true;
     });
     sharedHosts.forEach(function (host) {
       if (unusedHostsMap[host.get('id')]) {

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/app/models/user.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/models/user.js b/ambari-web/app/models/user.js
index 46af975..d1f1b9a 100644
--- a/ambari-web/app/models/user.js
+++ b/ambari-web/app/models/user.js
@@ -58,7 +58,7 @@ App.EditUserForm = App.Form.extend({
   disableAdminCheckbox:function () {
     var object = this.get('object');
     if (object) {
-      if ((object.get('userName') == App.get('router').getLoginName()) || App.supports.ldapGroupMapping && object.get("isLdap")) {
+      if ((object.get('userName') == App.get('router').getLoginName()) || App.get('supports.ldapGroupMapping') && object.get("isLdap")) {
         this.getField("admin").set("disabled", true);
       } else {
         this.getField("admin").set("disabled", false);

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/alert_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/alert_test.js b/ambari-web/test/models/alert_test.js
new file mode 100644
index 0000000..48d06b5
--- /dev/null
+++ b/ambari-web/test/models/alert_test.js
@@ -0,0 +1,230 @@
+/**
+ * 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('models/alert');
+
+var alert,
+  sampleTime = 1399312800,
+  statusCases = [
+    {
+      status: 0,
+      property: 'isOk',
+      format: 'OK'
+    },
+    {
+      status: 1,
+      property: 'isWarning',
+      format: 'WARN'
+    },
+    {
+      status: 2,
+      property: 'isCritical',
+      format: 'CRIT'
+    },
+    {
+      status: 3,
+      property: 'isPassive',
+      format: 'MAINT'
+    },
+    {
+      status: 4,
+      property: '',
+      format: 'UNKNOWN'
+    }
+  ],
+  ignoredCases = [
+    {
+      title: 'title',
+      result: false
+    },
+    {
+      title: 'Percent',
+      result: true
+    }
+  ],
+  serviceTypeCases = [
+    {
+      type: 'MAPREDUCE',
+      name: 'MapReduce',
+      link: '#/main/services/MAPREDUCE/summary'
+    },
+    {
+      type: 'HDFS',
+      name: 'HDFS',
+      link: '#/main/services/HDFS/summary'
+    },
+    {
+      type: 'HBASE',
+      name: 'HBase',
+      link: '#/main/services/HBASE/summary'
+    },
+    {
+      type: 'ZOOKEEPER',
+      name: 'Zookeeper',
+      link: '#/main/services/ZOOKEEPER/summary'
+    },
+    {
+      type: 'OOZIE',
+      name: 'Oozie',
+      link: '#/main/services/OOZIE/summary'
+    },
+    {
+      type: 'HIVE',
+      name: 'Hive',
+      link: '#/main/services/HIVE/summary'
+    },
+    {
+      type: 'service',
+      name: null,
+      link: null
+    },
+    {
+      type: null,
+      name: null,
+      link: null
+    }
+  ],
+  titles = ['NodeManager health', 'NodeManager process', 'TaskTracker process', 'RegionServer process', 'DataNode process', 'DataNode space', 'ZooKeeper Server process', 'Supervisors process'];
+
+describe('App.Alert', function () {
+
+  beforeEach(function() {
+    alert = App.Alert.create();
+  });
+
+  describe('#date', function () {
+    it('is Mon May 05 2014', function () {
+      alert.set('lastTime', sampleTime);
+      expect(alert.get('date').toDateString()).to.equal('Mon May 05 2014');
+    });
+  });
+
+  statusCases.forEach(function (item) {
+    var status = item.status,
+      property = item.property;
+    if (property) {
+      describe('#' + property, function () {
+        it('status ' + status + ' is for ' + property, function () {
+          alert.set('status', status);
+          expect(alert.get(property)).to.be.true;
+          var falseStates = statusCases.mapProperty('property').without(property).without('');
+          var falseStatuses = [];
+          falseStates.forEach(function (state) {
+            falseStatuses.push(alert.get(state));
+          });
+          expect(falseStatuses).to.eql([false, false, false]);
+        });
+      });
+    }
+  });
+
+  describe('#ignoredForServices', function () {
+    titles.forEach(function (item) {
+      it('should be true for ' + item, function () {
+        alert.set('title', item);
+        expect(alert.get('ignoredForServices')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      alert.set('title', 'title');
+      expect(alert.get('ignoredForServices')).to.be.false;
+    });
+  });
+
+  describe('#ignoredForHosts', function () {
+    ignoredCases.forEach(function (item) {
+      it('should be ' + item.result, function () {
+        alert.set('title', item.title);
+        expect(alert.get('ignoredForHosts')).to.equal(item.result);
+      });
+    });
+  });
+
+  describe('#timeSinceAlert', function () {
+    statusCases.forEach(function (item) {
+      var format = item.format;
+      it('should indicate ' + format + ' status duration', function () {
+        alert.setProperties({
+          lastTime: sampleTime,
+          status: item.status.toString()
+        });
+        expect(alert.get('timeSinceAlert')).to.have.string(format);
+        expect(alert.get('timeSinceAlert.length')).to.be.above(format.length);
+        alert.set('lastTime', 0);
+        expect(alert.get('timeSinceAlert')).to.equal(format);
+      });
+    });
+    it('should be empty', function () {
+      alert.set('lastTime', undefined);
+      expect(alert.get('timeSinceAlert')).to.be.empty;
+    });
+  });
+
+  describe('#makeTimeAtleastMinuteAgo', function () {
+    it('should set the minute-ago time', function () {
+      var time = App.dateTime() - 50000,
+        date = new Date(time - 10000);
+      alert.set('lastTime', time);
+      expect(alert.makeTimeAtleastMinuteAgo(alert.get('date'))).to.be.at.least(date);
+    });
+    it('should return the actual time', function () {
+      var time = App.dateTime() - 70000;
+      alert.set('lastTime', time);
+      expect(alert.makeTimeAtleastMinuteAgo(alert.get('date'))).to.eql(alert.get('date'));
+    });
+  });
+
+  describe('#timeSinceAlertDetails', function () {
+    it ('should return the appropriate string', function () {
+      alert.set('lastTime', sampleTime);
+      var occurred = Em.I18n.t('services.alerts.occurredOn').format('May 05 2014', alert.get('date').toLocaleTimeString());
+      var brChecked = Em.I18n.t('services.alerts.brLastCheck').format($.timeago(sampleTime));
+      var checked = Em.I18n.t('services.alerts.lastCheck').format($.timeago(sampleTime));
+      expect(alert.get('timeSinceAlertDetails')).to.equal(occurred);
+      alert.set('lastCheck', sampleTime / 1000);
+      expect(alert.get('timeSinceAlertDetails')).to.equal(occurred + brChecked);
+      alert.set('lastTime', undefined);
+      expect(alert.get('timeSinceAlertDetails')).to.equal(checked);
+    });
+    it ('should be empty', function () {
+      alert.set('lastCheck', undefined);
+      expect(alert.get('timeSinceAlertDetails')).to.be.empty;
+    });
+  });
+
+  describe('#serviceName', function () {
+    serviceTypeCases.forEach(function (item) {
+      it('should be ' + item.name, function () {
+        alert.set('serviceType', item.type);
+        expect(alert.get('serviceName')).to.equal(item.name);
+      });
+    });
+  });
+
+  describe('#serviceLink', function () {
+    serviceTypeCases.forEach(function (item) {
+      it('should be ' + item.link, function () {
+        alert.set('serviceType', item.type);
+        expect(alert.get('serviceLink')).to.equal(item.link);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/authentication_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/authentication_test.js b/ambari-web/test/models/authentication_test.js
new file mode 100644
index 0000000..3e7f44d
--- /dev/null
+++ b/ambari-web/test/models/authentication_test.js
@@ -0,0 +1,91 @@
+/**
+ * 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('models/authentication');
+
+var form,
+  methods = [
+    {
+      name: 'method',
+      fields: ['primaryServer', 'searchBaseDn', 'usernameAttribute']
+    },
+    {
+      name: 'bindMethod',
+      fields: ['bindUser', 'password', 'passwordRetype']
+    }
+  ],
+  classCases = [
+    {
+      result: 0,
+      message: 'fail',
+      className: 'error'
+    },
+    {
+      result: 1,
+      message: 'success',
+      className: 'success'
+    }
+  ];
+
+describe('App.AuthenticationForm', function () {
+
+  beforeEach(function() {
+    form = App.AuthenticationForm.create();
+  });
+
+  methods.forEach(function (method) {
+    method.fields.forEach(function (field) {
+      describe('#' + field + '.isRequired', function () {
+        for (var i = 2; i--; ) {
+          it('should be ' + i + ' dependent on ' + method.name + ' value', function () {
+            form.getField(method.name).set('value', i);
+            expect(form.getField(field).get('isRequired')).to.equal(i);
+          });
+        }
+      });
+    });
+  });
+
+  describe('#testResult', function () {
+    it('should be 0 or 1', function () {
+      form.testConfiguration();
+      expect([0, 1]).to.include(Number(form.get('testResult')));
+    });
+  });
+
+  describe('#testConfigurationMessage', function () {
+    classCases.forEach(function (item) {
+      it('should indicate ' + item.message, function () {
+        form.set('testResult', item.result);
+        expect(form.get('testConfigurationMessage')).to.equal(Em.I18n.t('admin.authentication.form.test.' + item.message));
+      });
+    });
+  });
+
+  describe('#testConfigurationClass', function () {
+    classCases.forEach(function (item) {
+      it('should indicate ' + item.className, function () {
+        form.set('testResult', item.result);
+        expect(form.get('testConfigurationClass')).to.equal('text-' + item.className);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/cluster_states_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/cluster_states_test.js b/ambari-web/test/models/cluster_states_test.js
new file mode 100644
index 0000000..01c634e
--- /dev/null
+++ b/ambari-web/test/models/cluster_states_test.js
@@ -0,0 +1,114 @@
+/**
+ * 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('models/cluster_states');
+
+var status = App.clusterStatus,
+  notInstalledStates = ['CLUSTER_NOT_CREATED_1', 'CLUSTER_DEPLOY_PREP_2', 'CLUSTER_INSTALLING_3', 'SERVICE_STARTING_3'],
+  values = {
+    clusterName: 'name',
+    clusterState: 'STACK_UPGRADING',
+    wizardControllerName: 'wizardStep0Controller',
+    localdb: {}
+  },
+  response = {
+    clusterState: 'DEFAULT',
+    clusterName: 'cluster'
+  },
+  newValue = {
+    clusterName: 'name',
+    clusterState: 'STACK_UPGRADING',
+    wizardControllerName: 'wizardStep0Controller'
+  };
+
+describe('App.clusterStatus', function () {
+
+  describe('#isInstalled', function () {
+    notInstalledStates.forEach(function (item) {
+      it('should be false', function () {
+        status.set('clusterState', item);
+        expect(status.get('isInstalled')).to.be.false;
+      });
+    });
+    it('should be true', function () {
+      status.set('clusterState', 'DEFAULT');
+      expect(status.get('isInstalled')).to.be.true;
+    });
+  });
+
+  describe('#value', function () {
+    it('should be set from properties', function () {
+      Em.keys(values).forEach(function (key) {
+        status.set(key, values[key]);
+      });
+      expect(status.get('value')).to.eql(values);
+    });
+  });
+
+  describe('#getUserPrefSuccessCallback', function () {
+    it('should set the cluster parameters', function () {
+      status.getUserPrefSuccessCallback(response);
+      Em.keys(response).forEach(function (key) {
+        expect(status.get(key)).to.equal(response[key]);
+      });
+    });
+  });
+
+  describe('#setClusterStatus', function () {
+
+    afterEach(function () {
+      App.get.restore();
+    });
+
+    it('should return false in test mode', function () {
+      sinon.stub(App, 'get', function(k) {
+        if (k === 'testMode') return true;
+        return Em.get(App, k);
+      });
+      expect(status.setClusterStatus()).to.be.false;
+    });
+
+    it('should set cluster status in non-test mode', function () {
+      sinon.stub(App, 'get', function(k) {
+        if (k === 'testMode') return false;
+        return Em.get(App, k);
+      });
+      var clusterStatus = status.setClusterStatus(newValue);
+      expect(clusterStatus).to.eql(newValue);
+    });
+
+  });
+
+  describe('#makeRequestAsync', function () {
+    it('should be false after synchronous updateFromServer', function () {
+      status.updateFromServer();
+      expect(status.get('makeRequestAsync')).to.be.false;
+    });
+    it('should be true after asynchronous updateFromServer', function () {
+      status.updateFromServer(true);
+      expect(status.get('makeRequestAsync')).to.be.true;
+    });
+    it('should be false after synchronous setClusterStatus with no opt specified', function () {
+      status.setClusterStatus({clusterName: 'name'});
+      expect(status.get('makeRequestAsync')).to.be.false;
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/config_group_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/config_group_test.js b/ambari-web/test/models/config_group_test.js
new file mode 100644
index 0000000..9c5b09f
--- /dev/null
+++ b/ambari-web/test/models/config_group_test.js
@@ -0,0 +1,154 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/config_group');
+require('models/host');
+
+var configGroup,
+  hostRecord,
+  hosts = [
+    Em.Object.create({
+      id: 'host0',
+      hostName: 'host0'
+    }),
+    Em.Object.create({
+      id: 'host1',
+      hostName: 'host1'
+    })
+  ],
+  host = {
+    id: 'host0',
+    host_name: 'host0'
+  },
+  properties = [
+    {
+      name: 'n0',
+      value: 'v0'
+    },
+    {
+      name: 'n1',
+      value: 'v1'
+    }
+  ],
+  setParentConfigGroup = function (configGroup, hosts) {
+    configGroup.set('parentConfigGroup', App.ConfigGroup.create());
+    configGroup.set('parentConfigGroup.hosts', hosts);
+  };
+
+describe('App.ConfigGroup', function () {
+
+  beforeEach(function () {
+    configGroup = App.ConfigGroup.create();
+  });
+
+  describe('#displayName', function () {
+    it('should equal name if maximum length is not exceeded', function () {
+      configGroup.set('name', 'n');
+      expect(configGroup.get('displayName')).to.equal(configGroup.get('name'));
+    });
+    it('should be shortened if maximum length is exceeded', function () {
+      var maxLength = App.config.CONFIG_GROUP_NAME_MAX_LENGTH;
+      for (var i = maxLength + 1, name = ''; i--; ) {
+        name += 'n';
+      }
+      configGroup.set('name', name);
+      expect(configGroup.get('displayName')).to.contain('...');
+      expect(configGroup.get('displayName')).to.have.length(2 * Math.floor(maxLength / 2) + 3);
+    });
+  });
+
+  describe('#displayNameHosts', function () {
+    it('should indicate the number of hosts', function () {
+      var displayName = configGroup.get('displayName');
+      configGroup.set('hosts', []);
+      expect(configGroup.get('displayNameHosts')).to.equal(displayName + ' (0)');
+      configGroup.set('hosts', hosts);
+      expect(configGroup.get('displayNameHosts')).to.equal(displayName + ' (2)');
+    });
+  });
+
+  describe('#availableHosts', function () {
+
+    beforeEach(function () {
+      App.clusterStatus.set('clusterState', 'DEFAULT');
+      App.store.load(App.Host, host);
+      hostRecord = App.Host.find().findProperty('hostName', 'host0');
+      setParentConfigGroup(configGroup, hosts);
+    });
+
+    afterEach(function () {
+      modelSetup.deleteRecord(hostRecord);
+    });
+
+    it('should return an empty array as default', function () {
+      configGroup.set('isDefault', true);
+      expect(configGroup.get('availableHosts')).to.eql([]);
+    });
+
+    it('should return an empty array if there are no unused hosts', function () {
+      configGroup.set('parentConfigGroup', App.ConfigGroup.create());
+      expect(configGroup.get('availableHosts')).to.eql([]);
+    });
+
+    it('should take hosts from parentConfigGroup', function () {
+      setParentConfigGroup(configGroup, hosts);
+      expect(configGroup.get('availableHosts')).to.have.length(2);
+    });
+  });
+
+  describe('#isAddHostsDisabled', function () {
+
+    beforeEach(function () {
+      hostRecord = App.Host.createRecord(host);
+      setParentConfigGroup(configGroup, hosts);
+      configGroup.set('isDefault', false);
+      configGroup.set('availableHosts', []);
+    });
+
+    afterEach(function () {
+      modelSetup.deleteRecord(hostRecord);
+    });
+
+    it('should be false', function () {
+      expect(configGroup.get('isAddHostsDisabled')).to.be.false;
+    });
+    it('should be true', function () {
+      App.clusterStatus.set('clusterState', 'DEFAULT');
+      configGroup.set('isDefault', true);
+      expect(configGroup.get('isAddHostsDisabled')).to.be.true;
+      configGroup.set('availableHosts', hosts);
+      expect(configGroup.get('isAddHostsDisabled')).to.be.true;
+    });
+  });
+
+  describe('#propertiesList', function () {
+    it('should be formed from properties', function () {
+      configGroup.set('properties', properties);
+      properties.forEach(function (item) {
+        Em.keys(item).forEach(function (prop) {
+          expect(configGroup.get('propertiesList')).to.contain(item[prop]);
+        });
+      });
+      expect(configGroup.get('propertiesList')).to.have.length(24);
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/dataset_job_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/dataset_job_test.js b/ambari-web/test/models/dataset_job_test.js
new file mode 100644
index 0000000..d7e64fa
--- /dev/null
+++ b/ambari-web/test/models/dataset_job_test.js
@@ -0,0 +1,138 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/dataset_job');
+
+var dataSetJob,
+  dataSetJobData = {
+    id: 'job',
+    name: 'job'
+  },
+  timeCases = [
+    {
+      property: 'startFormatted',
+      dateProperty: 'startDate'
+    },
+    {
+      property: 'endFormatted',
+      dateProperty: 'endDate'
+    }
+  ],
+  timeTestData = [
+    {
+      title: 'should calculate time period',
+      time: function () {
+        return App.dateTime();
+      },
+      result: 'less than a minute ago'
+    },
+    {
+      title: 'should be empty',
+      time: function () {
+        return 0;
+      },
+      result: ''
+    }
+  ],
+  healthCases = [
+    {
+      status: 'SUCCEEDED',
+      className: 'icon-ok'
+    },
+    {
+      status: 'SUSPENDED',
+      className: 'icon-cog'
+    },
+    {
+      status: 'WAITING',
+      className: 'icon-time'
+    },
+    {
+      status: 'RUNNING',
+      className: 'icon-play'
+    },
+    {
+      status: 'KILLED',
+      className: 'icon-exclamation-sign'
+    },
+    {
+      status: 'FAILED',
+      className: 'icon-warning-sign'
+    },
+    {
+      status: 'ERROR',
+      className: 'icon-remove'
+    },
+    {
+      status: '',
+      className: 'icon-question-sign'
+    }
+  ];
+
+describe('App.DataSetJob', function () {
+
+  beforeEach(function () {
+    dataSetJob = App.DataSetJob.createRecord(dataSetJobData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(dataSetJob);
+  });
+
+  describe('#statusFormatted', function () {
+    it('should be in lower case and capitalized', function () {
+      dataSetJob.set('status', 'RUNNING');
+      expect(dataSetJob.get('statusFormatted')).to.equal('Running');
+    });
+  });
+
+  describe('#isSuspended', function () {
+    it('should be false', function () {
+      dataSetJob.set('status', 'RUNNING');
+      expect(dataSetJob.get('isSuspended')).to.be.false;
+    });
+    it('should be true', function () {
+      dataSetJob.set('status', 'SUSPENDED');
+      expect(dataSetJob.get('isSuspended')).to.be.true;
+    });
+  });
+
+  timeCases.forEach(function (item) {
+    describe('#' + item.property, function () {
+      timeTestData.forEach(function (test) {
+        it(test.title, function () {
+          dataSetJob.set(item.dateProperty, test.time());
+          expect(dataSetJob.get(item.property)).to.equal(test.result);
+        });
+      });
+    });
+  });
+
+  describe('#healthClass', function () {
+    healthCases.forEach(function (item) {
+      it('should be ' + item.className, function () {
+        dataSetJob.set('status', item.status);
+        expect(dataSetJob.get('healthClass')).to.equal(item.className);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/dataset_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/dataset_test.js b/ambari-web/test/models/dataset_test.js
new file mode 100644
index 0000000..1d66709
--- /dev/null
+++ b/ambari-web/test/models/dataset_test.js
@@ -0,0 +1,148 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/dataset');
+
+var dataset,
+  datasetData = {
+    id: 'dataset',
+    name: 'dataset'
+  },
+  statusCases = [
+    {
+      status: 'RUNNING',
+      property: 'isRunning'
+    },
+    {
+      status: 'SUSPENDED',
+      property: 'isSuspended'
+    },
+    {
+      status: 'SUBMITTED',
+      property: 'isSubmitted'
+    }
+  ],
+  healthCases = [
+    {
+      title: 'should be live',
+      data: {
+        datasetJobs: [
+          Em.Object.create({
+            status: 'SUCCESSFUL'
+          })
+        ]
+      },
+      className: 'health-status-LIVE',
+      icon: App.healthIconClassGreen
+    },
+    {
+      title: 'should be dead for failed first job',
+      data: {
+        datasetJobs: [
+          Em.Object.create({
+            status: 'SUSPENDED',
+            endDate: 1
+          }),
+          Em.Object.create({
+            status: 'FAILED',
+            endDate: 0
+          })
+        ]
+      },
+      className: 'health-status-DEAD-RED',
+      icon: App.healthIconClassRed
+    },
+    {
+      title: 'should be for no jobs',
+      data: {
+        datasetJobs: []
+      },
+      className: 'health-status-LIVE',
+      icon: App.healthIconClassGreen
+    }
+  ];
+
+describe('App.Dataset', function () {
+
+  beforeEach(function () {
+    dataset = App.Dataset.createRecord(datasetData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(dataset);
+  });
+
+  describe('#prefixedName', function () {
+    it('should add mirroring prefix before the name', function () {
+      dataset.set('name', 'name');
+      expect(dataset.get('prefixedName')).to.equal(App.mirroringDatasetNamePrefix + 'name');
+    });
+  });
+
+  describe('#statusFormatted', function () {
+    it('should be in lower case and capitalized', function () {
+      dataset.set('status', 'RUNNING');
+      expect(dataset.get('statusFormatted')).to.equal('Running');
+    });
+  });
+
+  statusCases.forEach(function (item) {
+    describe(item.property, function () {
+
+      beforeEach(function () {
+        dataset.set('status', item.status);
+      });
+
+      it('should be true', function () {
+        expect(dataset.get(item.property)).to.be.true;
+      });
+
+      it('others should be false', function () {
+        var falseProperties = statusCases.mapProperty('property').without(item.property);
+        var falseStates = [];
+        falseProperties.forEach(function (prop) {
+          falseStates.push(dataset.get(prop));
+        });
+        expect(falseStates).to.eql([false, false]);
+      });
+
+    });
+  });
+
+  describe('#healthClass', function () {
+    healthCases.forEach(function (item) {
+      it(item.title, function () {
+        dataset.reopen(item.data);
+        expect(dataset.get('healthClass')).to.equal(item.className);
+      });
+    });
+  });
+
+  describe('#healthIconClass', function () {
+    healthCases.forEach(function (item) {
+      it(item.title, function () {
+        dataset.reopen(item.data);
+        expect(dataset.get('healthIconClass')).to.equal(item.icon);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/form_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/form_test.js b/ambari-web/test/models/form_test.js
new file mode 100644
index 0000000..3ba7c10
--- /dev/null
+++ b/ambari-web/test/models/form_test.js
@@ -0,0 +1,218 @@
+/**
+ * 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('models/form');
+
+var form,
+  field,
+  formField,
+  resultCases = [
+    {
+      text: Em.I18n.t('form.saveError'),
+      result: -1
+    },
+    {
+      text: Em.I18n.t('form.saveSuccess'),
+      result: 1
+    },
+    {
+      text: '',
+      result: 0
+    }
+  ],
+  displayTypeCases = [
+    {
+      type: 'checkbox',
+      classString: 'Checkbox'
+    },
+    {
+      type: 'select',
+      classString: 'Select'
+    },
+    {
+      type: 'textarea',
+      classString: 'TextArea'
+    },
+    {
+      type: 'password',
+      classString: 'TextField'
+    },
+    {
+      type: 'hidden',
+      classString: 'TextField'
+    }
+  ],
+  hiddenCases = [
+    {
+      displayType: 'password',
+      type: 'hidden',
+      value: false
+    },
+    {
+      displayType: 'hidden',
+      type: 'hidden',
+      value: true
+    }
+  ],
+  expectError = function (message) {
+    formField.validate();
+    expect(formField.get('errorMessage')).to.equal(message);
+  };
+
+describe('App.Form', function () {
+
+  beforeEach(function () {
+    form = App.Form.create({
+      fieldsOptions: [
+        {
+          name: 'field0',
+          value: 'value0',
+          isRequired: false
+        }
+      ]
+    });
+    field = form.get('fields').objectAt(0);
+  });
+
+  describe('#fields', function () {
+    it('should get data from formFields', function () {
+      var fields = form.get('fields');
+      expect(fields).to.have.length(1);
+      expect(field.get('name')).to.equal('field0');
+    });
+  });
+
+  describe('#field', function () {
+    it('should get data from formFields', function () {
+      var field0 = form.get('field.field0');
+      expect(form.get('field')).to.not.be.empty;
+      expect(field0.get('name')).to.equal('field0');
+      expect(field0.get('form')).to.eql(form);
+    });
+  });
+
+  describe('#getField', function () {
+    it('should get field0', function () {
+      expect(form.getField('field0')).to.eql(form.get('field.field0'));
+    });
+    it('should be empty', function () {
+      form.set('fields', []);
+      expect(form.getField()).to.be.empty;
+    });
+  });
+
+  describe('#isValid', function () {
+    it('should be true', function () {
+      field.set('isRequired', false);
+      expect(form.isValid()).to.be.true;
+    });
+    it('should be false', function () {
+      field.setProperties({
+        isRequired: true,
+        value: ''
+      });
+      expect(form.isValid()).to.be.false;
+    });
+  });
+
+  describe('#updateValues', function () {
+    it('should update field0 value', function () {
+      form.set('object', Em.Object.create({field0: 'value0upd'}));
+      expect(field.get('value')).to.equal('value0upd');
+    });
+    it('should empty password value', function () {
+      field.set('displayType', 'password');
+      form.set('object', Em.Object.create());
+      expect(field.get('value')).to.be.empty;
+    });
+    it('should clear values', function () {
+      form.set('object', []);
+      expect(field.get('value')).to.be.empty;
+    });
+  });
+
+  describe('#clearValues', function () {
+    it('should clear values', function () {
+      var field0 = form.get('fields').objectAt(0);
+      field0.set('value', 'value0');
+      form.clearValues();
+      expect(field0.get('value')).to.be.empty;
+    });
+  });
+
+  describe('#resultText', function () {
+    resultCases.forEach(function (item) {
+      it('should be ' + item.text, function () {
+        form.set('result', item.result);
+        expect(form.get('resultText')).to.equal(item.text);
+      });
+    });
+  });
+
+});
+
+describe('App.FormField', function () {
+
+  beforeEach(function () {
+    formField = App.FormField.create();
+  });
+
+  describe('#isValid', function () {
+    it('should be true', function () {
+      expect(formField.get('isValid')).to.be.true;
+    });
+    it('should be false', function () {
+      formField.set('errorMessage', 'error');
+      expect(formField.get('isValid')).to.be.false;
+    });
+  });
+
+  describe('#viewClass', function () {
+    displayTypeCases.forEach(function (item) {
+      it('should be ' + item.classString, function () {
+        formField.set('displayType', item.type);
+        expect(formField.get('viewClass').toString()).to.contain(item.classString);
+      });
+    });
+  });
+
+  describe('#validate', function () {
+    it('should return error message', function () {
+      formField.set('isRequired', true);
+      expectError('This is required');
+    });
+    it('should return empty error message', function () {
+      formField.set('isRequired', false);
+      expectError('');
+      formField.set('value', 'value');
+      expectError('');
+    });
+  });
+
+  describe('#isHiddenField', function () {
+    hiddenCases.forEach(function (item) {
+      it('should be ' + item.value, function () {
+        formField.setProperties(item);
+        expect(formField.get('isHiddenField')).to.equal(item.value);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/hosts_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/hosts_test.js b/ambari-web/test/models/hosts_test.js
new file mode 100644
index 0000000..288d425
--- /dev/null
+++ b/ambari-web/test/models/hosts_test.js
@@ -0,0 +1,87 @@
+/**
+ * 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('models/hosts');
+
+var hostInfo,
+  statusCases = [
+    {
+      status: 'REGISTERED',
+      bootStatusForDisplay: 'Success',
+      bootBarColor: 'progress-success',
+      bootStatusColor: 'text-success',
+      isBootDone: true
+    },
+    {
+      status: 'FAILED',
+      bootStatusForDisplay: 'Failed',
+      bootBarColor: 'progress-danger',
+      bootStatusColor: 'text-error',
+      isBootDone: true
+    },
+    {
+      status: 'PENDING',
+      bootStatusForDisplay: 'Preparing',
+      bootBarColor: 'progress-info',
+      bootStatusColor: 'text-info',
+      isBootDone: false
+    },
+    {
+      status: 'RUNNING',
+      bootStatusForDisplay: 'Installing',
+      bootBarColor: 'progress-info',
+      bootStatusColor: 'text-info',
+      isBootDone: false
+    },
+    {
+      status: 'DONE',
+      bootStatusForDisplay: 'Registering',
+      bootBarColor: 'progress-info',
+      bootStatusColor: 'text-info',
+      isBootDone: false
+    },
+    {
+      status: 'REGISTERING',
+      bootStatusForDisplay: 'Registering',
+      bootBarColor: 'progress-info',
+      bootStatusColor: 'text-info',
+      isBootDone: false
+    }
+  ],
+  tests = ['bootStatusForDisplay', 'bootBarColor', 'bootStatusColor', 'isBootDone'];
+
+describe('App.HostInfo', function () {
+
+  beforeEach(function () {
+    hostInfo = App.HostInfo.create();
+  });
+
+  tests.forEach(function (property) {
+    describe('#' + property, function () {
+      statusCases.forEach(function (testCase) {
+        it('should be ' + testCase[property], function () {
+          hostInfo.set('bootStatus', testCase.status);
+          expect(hostInfo.get(property)).to.equal(testCase[property]);
+        });
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/job_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/job_test.js b/ambari-web/test/models/job_test.js
new file mode 100644
index 0000000..d49cfe9
--- /dev/null
+++ b/ambari-web/test/models/job_test.js
@@ -0,0 +1,60 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/job');
+
+var job,
+  jobData = {
+   id: 'job'
+  };
+
+describe('App.Job', function () {
+
+  beforeEach(function () {
+    job = App.Job.createRecord(jobData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(job);
+  });
+
+  describe('#duration', function () {
+    it('should convert elapsedTime into time format', function () {
+      job.set('elapsedTime', 1000);
+      expect(job.get('duration')).to.equal('1.00 secs');
+    });
+  });
+
+  describe('#inputFormatted', function () {
+    it('should convert input into bandwidth format', function () {
+      job.set('input', 1024);
+      expect(job.get('inputFormatted')).to.equal('1.0KB');
+    });
+  });
+
+  describe('#outputFormatted', function () {
+    it('should convert output into bandwidth format', function () {
+      job.set('output', 1024);
+      expect(job.get('outputFormatted')).to.equal('1.0KB');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/run_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/run_test.js b/ambari-web/test/models/run_test.js
new file mode 100644
index 0000000..0478fb0
--- /dev/null
+++ b/ambari-web/test/models/run_test.js
@@ -0,0 +1,169 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/run');
+
+var run,
+  job,
+  runData = {
+    id: 'run'
+  },
+  jobData = {
+    id: 'job'
+  },
+  cases = [
+    {
+      id: 'pig_run',
+      type: 'Pig'
+    },
+    {
+      id: 'hive_run',
+      type: 'Hive'
+    },
+    {
+      id: 'mr_run',
+      type: 'MapReduce'
+    },
+    {
+      id: 'run_pig_hive_mr_id',
+      type: ''
+    }
+  ];
+
+describe('App.Run', function () {
+
+  beforeEach(function () {
+    run = App.Run.createRecord(runData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(run);
+  });
+
+  describe('#idFormatted', function () {
+    it('should shorten id to 20 characters', function () {
+      for (var i = 21, name = ''; i--; ) {
+        name += 'n';
+      }
+      run.set('id', name);
+      expect(run.get('idFormatted')).to.have.length(20);
+    });
+  });
+
+  describe('#jobs', function () {
+
+    beforeEach(function () {
+      job = App.Job.createRecord(jobData);
+      job.reopen({
+        run: runData
+      });
+    });
+
+    afterEach(function () {
+      modelSetup.deleteRecord(job);
+    });
+
+    it('should load corresponding jobs from the store', function () {
+      run.set('loadAllJobs', true);
+      expect(run.get('jobs')).to.have.length(1);
+      expect(run.get('jobs').objectAt(0).get('run.id')).to.equal('run');
+    });
+
+  });
+
+  describe('#duration', function () {
+    it('should convert elapsedTime into time format', function () {
+      run.set('elapsedTime', 1000);
+      expect(run.get('duration')).to.equal('1.00 secs');
+    });
+  });
+
+  describe('#isRunning', function () {
+    it('should be true', function () {
+      run.setProperties({
+        numJobsTotal: 5,
+        numJobsCompleted: 0
+      });
+      expect(run.get('isRunning')).to.be.true;
+    });
+    it('should be false', function () {
+      run.setProperties({
+        numJobsTotal: 5,
+        numJobsCompleted: 5
+      });
+      expect(run.get('isRunning')).to.be.false;
+    });
+  });
+
+  describe('#inputFormatted', function () {
+    it('should convert input into bandwidth format', function () {
+      run.set('input', 1024);
+      expect(run.get('inputFormatted')).to.equal('1.0KB');
+    });
+  });
+
+  describe('#outputFormatted', function () {
+    it('should convert output into bandwidth format', function () {
+      run.set('output', 1024);
+      expect(run.get('outputFormatted')).to.equal('1.0KB');
+    });
+  });
+
+  describe('#lastUpdateTime', function () {
+    it('should sum elapsedTime and startTime', function () {
+      run.setProperties({
+        elapsedTime: 1000,
+        startTime: 2000
+      });
+      expect(run.get('lastUpdateTime')).to.equal(3000);
+    });
+  });
+
+  describe('#lastUpdateTimeFormatted', function () {
+    it('should form date from lastUpdateTime', function () {
+      run.setProperties({
+        elapsedTime: 1000,
+        startTime: 100000000000
+      });
+      expect(run.get('lastUpdateTimeFormatted')).to.equal('Sat, Mar 03, 1973 09:46');
+    });
+  });
+
+  describe('#lastUpdateTimeFormattedShort', function () {
+    it('should form date and time from lastUpdateTime', function () {
+      run.setProperties({
+        elapsedTime: 1000,
+        startTime: 100000000000
+      });
+      expect(run.get('lastUpdateTimeFormattedShort')).to.equal('Sat Mar 03 1973');
+    });
+  });
+
+  describe('#type', function () {
+    cases.forEach(function (item) {
+      it('should be ' + (item.type ? item.type : 'empty'), function () {
+        run.set('id', item.id);
+        expect(run.get('type')).to.equal(item.type);
+      });
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/service_config_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/service_config_test.js b/ambari-web/test/models/service_config_test.js
new file mode 100644
index 0000000..b422862
--- /dev/null
+++ b/ambari-web/test/models/service_config_test.js
@@ -0,0 +1,617 @@
+/**
+ * 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('models/service_config');
+
+var serviceConfig,
+  serviceConfigCategory,
+  group,
+  serviceConfigProperty,
+  serviceConfigPropertyInit,
+  configsData = [
+    {
+      overrides: [
+        {
+          error: true,
+          errorMessage: 'error'
+        },
+        {
+          error: true
+        },
+        {}
+      ]
+    },
+    {
+      isValid: false,
+      isVisible: true
+    },
+    {
+      isValid: true,
+      isVisible: true
+    },    {
+      isValid: false,
+      isVisible: false
+    }
+  ],
+  configCategoriesData = [
+    Em.Object.create({
+      slaveErrorCount: 1
+    }),
+    Em.Object.create({
+      slaveErrorCount: 2
+    })
+  ],
+  nameCases = [
+    {
+      name: 'DataNode',
+      primary: 'DATANODE'
+    },
+    {
+      name: 'TaskTracker',
+      primary: 'TASKTRACKER'
+    },
+    {
+      name: 'RegionServer',
+      primary: 'HBASE_REGIONSERVER'
+    },
+    {
+      name: 'name',
+      primary: null
+    }
+  ],
+  components = [
+    {
+      name: 'NameNode',
+      master: true
+    },
+    {
+      name: 'SNameNode',
+      master: true
+    },
+    {
+      name: 'JobTracker',
+      master: true
+    },
+    {
+      name: 'HBase Master',
+      master: true
+    },
+    {
+      name: 'Oozie Master',
+      master: true
+    },
+    {
+      name: 'Hive Metastore',
+      master: true
+    },
+    {
+      name: 'WebHCat Server',
+      master: true
+    },
+    {
+      name: 'ZooKeeper Server',
+      master: true
+    },
+    {
+      name: 'Nagios',
+      master: true
+    },
+    {
+      name: 'Ganglia',
+      master: true
+    },
+    {
+      name: 'DataNode',
+      slave: true
+    },
+    {
+      name: 'TaskTracker',
+      slave: true
+    },
+    {
+      name: 'RegionServer',
+      slave: true
+    }
+  ],
+  masters = components.filterProperty('master'),
+  slaves = components.filterProperty('slave'),
+  groupsData = {
+    groups: [
+      Em.Object.create({
+        errorCount: 1
+      }),
+      Em.Object.create({
+        errorCount: 2
+      })
+    ]
+  },
+  groupNoErrorsData = [].concat(configsData.slice(2)),
+  groupErrorsData = [configsData[1]],
+  overridableFalseData = [
+    {
+      isOverridable: false
+    },
+    {
+      isEditable: false,
+      overrides: configsData[0].overrides
+    },
+    {
+      displayType: 'masterHost'
+    }
+  ],
+  overridableTrueData = [
+    {
+      isOverridable: true,
+      isEditable: true
+    },    {
+      isOverridable: true,
+      overrides: []
+    },
+    {
+      isOverridable: true
+    }
+  ],
+  overriddenFalseData = [
+    {
+      overrides: null,
+      isOriginalSCP: true
+    },
+    {
+      overrides: [],
+      isOriginalSCP: true
+    }
+  ],
+  overriddenTrueData = [
+    {
+      overrides: configsData[0].overrides
+    },
+    {
+      isOriginalSCP: false
+    }
+  ],
+  removableFalseData = [
+    {
+      isEditable: false
+    },
+    {
+      hasOverrides: true
+    },
+    {
+      isUserProperty: false,
+      isOriginalSCP: true
+    }
+  ],
+  removableTrueData = [
+    {
+      isEditable: true,
+      hasOverrides: false,
+      isUserProperty: true
+    },
+    {
+      isEditable: true,
+      hasOverrides: false,
+      isOriginalSCP: false
+    }
+  ],
+  initPropertyData = [
+    {
+      initial: {
+        displayType: 'password',
+        value: 'value'
+      },
+      result: {
+        retypedPassword: 'value'
+      }
+    },
+    {
+      initial: {
+        id: 'puppet var',
+        value: '',
+        defaultValue: 'default'
+      },
+      result: {
+        value: 'default'
+      }
+    }
+  ],
+  notDefaultFalseData = [
+    {
+      isEditable: false
+    },
+    {
+      defaultValue: null
+    },
+    {
+      value: 'value',
+      defaultValue: 'value'
+    }
+  ],
+  notDefaultTrueData = {
+    isEditable: true,
+    value: 'value',
+    defaultValue: 'default'
+  },
+  types = ['masterHost', 'slaveHosts', 'masterHosts', 'slaveHost', 'radio button'],
+  classCases = [
+    {
+      initial: {
+        displayType: 'checkbox'
+      },
+      viewClass: App.ServiceConfigCheckbox
+    },
+    {
+      initial: {
+        displayType: 'password'
+      },
+      viewClass: App.ServiceConfigPasswordField
+    },
+    {
+      initial: {
+        displayType: 'combobox'
+      },
+      viewClass: App.ServiceConfigComboBox
+    },
+    {
+      initial: {
+        displayType: 'radio button'
+      },
+      viewClass: App.ServiceConfigRadioButtons
+    },
+    {
+      initial: {
+        displayType: 'directories'
+      },
+      viewClass: App.ServiceConfigTextArea
+    },
+    {
+      initial: {
+        displayType: 'content'
+      },
+      viewClass: App.ServiceConfigTextAreaContent
+
+    },
+    {
+      initial: {
+        displayType: 'multiLine'
+      },
+      viewClass: App.ServiceConfigTextArea
+    },
+    {
+      initial: {
+        displayType: 'custom'
+      },
+      viewClass: App.ServiceConfigBigTextArea
+    },
+    {
+      initial: {
+        displayType: 'masterHost'
+      },
+      viewClass: App.ServiceConfigMasterHostView
+    },
+    {
+      initial: {
+        displayType: 'masterHosts'
+      },
+      viewClass: App.ServiceConfigMasterHostsView
+    },
+    {
+      initial: {
+        displayType: 'slaveHosts'
+      },
+      viewClass: App.ServiceConfigSlaveHostsView
+    },
+    {
+      initial: {
+        unit: true,
+        displayType: 'type'
+      },
+      viewClass: App.ServiceConfigTextFieldWithUnit
+    },
+    {
+      initial: {
+        unit: false,
+        displayType: 'type'
+      },
+      viewClass: App.ServiceConfigTextField
+    }
+  ];
+
+
+describe('App.ServiceConfig', function () {
+
+  beforeEach(function () {
+    serviceConfig = App.ServiceConfig.create();
+  });
+
+  describe('#errorCount', function () {
+    it('should be 0', function () {
+      serviceConfig.setProperties({
+        configs: [],
+        configCategories: []
+      });
+      expect(serviceConfig.get('errorCount')).to.equal(0);
+    });
+    it('should sum counts of all errors', function () {
+      serviceConfig.setProperties({
+        configs: configsData,
+        configCategories: configCategoriesData
+      });
+      expect(serviceConfig.get('errorCount')).to.equal(6);
+    });
+  });
+
+});
+
+describe('App.ServiceConfigCategory', function () {
+
+  beforeEach(function () {
+    serviceConfigCategory = App.ServiceConfigCategory.create();
+  });
+
+  describe('#primaryName', function () {
+    nameCases.forEach(function (item) {
+      it('should return ' + item.primary, function () {
+        serviceConfigCategory.set('name', item.name);
+        expect(serviceConfigCategory.get('primaryName')).to.equal(item.primary);
+      })
+    });
+  });
+
+  describe('#isForMasterComponent', function () {
+    masters.forEach(function (item) {
+      it('should be true for ' + item.name, function () {
+        serviceConfigCategory.set('name', item.name);
+        expect(serviceConfigCategory.get('isForMasterComponent')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      serviceConfigCategory.set('name', 'name');
+      expect(serviceConfigCategory.get('isForMasterComponent')).to.be.false;
+    });
+  });
+
+  describe('#isForSlaveComponent', function () {
+    slaves.forEach(function (item) {
+      it('should be true for ' + item.name, function () {
+        serviceConfigCategory.set('name', item.name);
+        expect(serviceConfigCategory.get('isForSlaveComponent')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      serviceConfigCategory.set('name', 'name');
+      expect(serviceConfigCategory.get('isForSlaveComponent')).to.be.false;
+    });
+  });
+
+  describe('#slaveErrorCount', function () {
+    it('should be 0', function () {
+      serviceConfigCategory.set('slaveConfigs', []);
+      expect(serviceConfigCategory.get('slaveErrorCount')).to.equal(0);
+    });
+    it('should sum all errorCount values', function () {
+      serviceConfigCategory.set('slaveConfigs', groupsData);
+      expect(serviceConfigCategory.get('slaveErrorCount')).to.equal(3);
+    });
+  });
+
+  describe('#isAdvanced', function () {
+    it('should be true', function () {
+      serviceConfigCategory.set('name', 'Advanced');
+      expect(serviceConfigCategory.get('isAdvanced')).to.be.true;
+    });
+    it('should be false', function () {
+      serviceConfigCategory.set('name', 'name');
+      expect(serviceConfigCategory.get('isAdvanced')).to.be.false;
+    });
+  });
+
+});
+
+describe('App.Group', function () {
+
+  beforeEach(function () {
+    group = App.Group.create();
+  });
+
+  describe('#errorCount', function () {
+    it('should be 0', function () {
+      group.set('properties', groupNoErrorsData);
+      expect(group.get('errorCount')).to.equal(0);
+    });
+    it('should be 1', function () {
+      group.set('properties', groupErrorsData);
+      expect(group.get('errorCount')).to.equal(1);
+    });
+  });
+
+});
+
+describe('App.ServiceConfigProperty', function () {
+
+  beforeEach(function () {
+    serviceConfigProperty = App.ServiceConfigProperty.create();
+  });
+
+  describe('#overrideErrorTrigger', function () {
+    it('should be an increment', function () {
+      serviceConfigProperty.set('overrides', configsData[0].overrides);
+      expect(serviceConfigProperty.get('overrideErrorTrigger')).to.equal(1);
+      serviceConfigProperty.set('overrides', []);
+      expect(serviceConfigProperty.get('overrideErrorTrigger')).to.equal(2);
+    });
+  });
+
+  describe('#isPropertyOverridable', function () {
+    overridableFalseData.forEach(function (item) {
+      it('should be false', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isPropertyOverridable')).to.be.false;
+      });
+    });
+    overridableTrueData.forEach(function (item) {
+      it('should be true', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isPropertyOverridable')).to.be.true;
+      });
+    });
+  });
+
+  describe('#isOverridden', function () {
+    overriddenFalseData.forEach(function (item) {
+      it('should be false', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isOverridden')).to.be.false;
+      });
+    });
+    overriddenTrueData.forEach(function (item) {
+      it('should be true', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isOverridden')).to.be.true;
+      });
+    });
+  });
+
+  describe('#isRemovable', function () {
+    removableFalseData.forEach(function (item) {
+      it('should be false', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isRemovable')).to.be.false;
+      });
+    });
+    removableTrueData.forEach(function (item) {
+      it('should be true', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isRemovable')).to.be.true;
+      });
+    });
+  });
+
+  describe('#init', function () {
+    initPropertyData.forEach(function (item) {
+      it('should set initial data', function () {
+        serviceConfigPropertyInit = App.ServiceConfigProperty.create(item.initial);
+        Em.keys(item.result).forEach(function (prop) {
+          expect(serviceConfigPropertyInit.get(prop)).to.equal(item.result[prop]);
+        });
+      });
+    });
+  });
+
+  describe('#isNotDefaultValue', function () {
+    notDefaultFalseData.forEach(function (item) {
+      it('should be false', function () {
+        Em.keys(item).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item[prop]);
+        });
+        expect(serviceConfigProperty.get('isNotDefaultValue')).to.be.false;
+      });
+    });
+    it('should be true', function () {
+      Em.keys(notDefaultTrueData).forEach(function (prop) {
+        serviceConfigProperty.set(prop, notDefaultTrueData[prop]);
+      });
+      expect(serviceConfigProperty.get('isNotDefaultValue')).to.be.true;
+    });
+  });
+
+  describe('#cantBeUndone', function () {
+    types.forEach(function (item) {
+      it('should be true', function () {
+        serviceConfigProperty.set('displayType', item);
+        expect(serviceConfigProperty.get('cantBeUndone')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      serviceConfigProperty.set('displayType', 'type');
+      expect(serviceConfigProperty.get('cantBeUndone')).to.be.false;
+    });
+  });
+
+  describe('#setDefaultValue', function () {
+    it('should change the default value', function () {
+      serviceConfigProperty.set('defaultValue', 'value0');
+      serviceConfigProperty.setDefaultValue(/\d/, '1');
+      expect(serviceConfigProperty.get('defaultValue')).to.equal('value1');
+    });
+  });
+
+  describe('#isValid', function () {
+    it('should be true', function () {
+      serviceConfigProperty.set('errorMessage', '');
+      expect(serviceConfigProperty.get('isValid')).to.be.true;
+    });
+    it('should be false', function () {
+      serviceConfigProperty.set('errorMessage', 'message');
+      expect(serviceConfigProperty.get('isValid')).to.be.false;
+    });
+  });
+
+  describe('#viewClass', function () {
+    classCases.forEach(function (item) {
+      it ('should be ' + item.viewClass, function () {
+        Em.keys(item.initial).forEach(function (prop) {
+          serviceConfigProperty.set(prop, item.initial[prop]);
+        });
+        expect(serviceConfigProperty.get('viewClass')).to.eql(item.viewClass);
+      });
+    });
+  });
+
+  describe('#validate', function () {
+    it('not required', function () {
+      serviceConfigProperty.setProperties({
+        isRequired: false,
+        value: ''
+      });
+      expect(serviceConfigProperty.get('errorMessage')).to.be.empty;
+      expect(serviceConfigProperty.get('error')).to.be.false;
+    });
+    it('should validate', function () {
+      serviceConfigProperty.setProperties({
+        isRequired: true,
+        value: 'value'
+      });
+      expect(serviceConfigProperty.get('errorMessage')).to.be.empty;
+      expect(serviceConfigProperty.get('error')).to.be.false;
+    });
+    it('should fail', function () {
+      serviceConfigProperty.setProperties({
+        isRequired: true,
+        value: 'value'
+      });
+      serviceConfigProperty.set('value', '');
+      expect(serviceConfigProperty.get('errorMessage')).to.equal('This is required');
+      expect(serviceConfigProperty.get('error')).to.be.true;
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/service_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/service_test.js b/ambari-web/test/models/service_test.js
new file mode 100644
index 0000000..eb1f5ea
--- /dev/null
+++ b/ambari-web/test/models/service_test.js
@@ -0,0 +1,292 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/service');
+
+var service,
+  serviceData = {
+    id: 'service'
+  },
+  healthCases = [
+    {
+      status: 'STARTED',
+      health: 'green'
+    },
+    {
+      status: 'STARTING',
+      health: 'green-blinking'
+    },
+    {
+      status: 'INSTALLED',
+      health: 'red'
+    },
+    {
+      status: 'STOPPING',
+      health: 'red-blinking'
+    },
+    {
+      status: 'UNKNOWN',
+      health: 'yellow'
+    },
+    {
+      status: 'ANOTHER',
+      health: 'yellow'
+    }
+  ],
+  statusPropertiesCases = [
+    {
+      status: 'INSTALLED',
+      property: 'isStopped'
+    },
+    {
+      status: 'STARTED',
+      property: 'isStarted'
+    }
+  ],
+  services = [
+    {
+      name: 'HDFS',
+      configurable: true
+    },
+    {
+      name: 'YARN',
+      configurable: true
+    },
+    {
+      name: 'MAPREDUCE',
+      configurable: true
+    },
+    {
+      name: 'MAPREDUCE2',
+      configurable: true
+    },
+    {
+      name:'TEZ',
+      clientOnly: true,
+      configurable: true
+    },
+    {
+      name: 'HBASE',
+      configurable: true
+    },
+    {
+      name: 'HIVE',
+      configurable: true
+    },
+    {
+      name: 'HCATALOG',
+      clientOnly: true
+    },
+    {
+      name: 'WEBHCAT',
+      configurable: true
+    },
+    {
+      name: 'FLUME',
+      configurable: true
+    },
+    {
+      name: 'FALCON',
+      configurable: true
+    },
+    {
+      name: 'STORM',
+      configurable: true
+    },
+    {
+      name: 'OOZIE',
+      configurable: true
+    },
+    {
+      name: 'GANGLIA',
+      configurable: true
+    },
+    {
+      name: 'NAGIOS',
+      configurable: true
+    },
+    {
+      name: 'ZOOKEEPER',
+      configurable: true
+    },
+    {
+      name: 'PIG',
+      configurable: true,
+      clientOnly: true
+    },
+    {
+      name: 'SQOOP',
+      clientOnly: true
+    },
+    {
+      name: 'HUE',
+      configurable: true
+    }
+  ],
+  clientsOnly = services.filterProperty('clientOnly').mapProperty('name'),
+  configurable = services.filterProperty('configurable').mapProperty('name'),
+  hostComponentsDataFalse = [
+    [],
+    [
+      {
+        staleConfigs: false
+      }
+    ],
+    [
+      {
+        serviceName: 'HIVE',
+        staleConfigs: false
+      }
+    ]
+  ],
+  hostComponentsDataTrue = [
+    [
+      Em.Object.create({
+        staleConfigs: true,
+        displayName: 'service0'
+      })
+    ],
+    [
+      Em.Object.create({
+        host: {
+          publicHostName: 'host0'
+        },
+        staleConfigs: true,
+        displayName: 'service1'
+      })
+    ]
+  ],
+  restartData = {
+    host0: ['service0', 'service1']
+};
+
+describe('App.Service', function () {
+
+  beforeEach(function () {
+    service = App.Service.createRecord(serviceData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(service);
+  });
+
+  describe('#isInPassive', function () {
+    it('should be true', function () {
+      service.set('passiveState', 'ON');
+      expect(service.get('isInPassive')).to.be.true;
+    });
+    it('should be false', function () {
+      service.set('passiveState', 'OFF');
+      expect(service.get('isInPassive')).to.be.false;
+    });
+  });
+
+  describe('#healthStatus', function () {
+    healthCases.forEach(function (item) {
+      it('should be ' + item.health, function () {
+        service.set('workStatus', item.status);
+        expect(service.get('healthStatus')).to.equal(item.health);
+      });
+    });
+  });
+
+  statusPropertiesCases.forEach(function (item) {
+    var status = item.status,
+      property = item.property;
+    describe('#' + property, function () {
+      it('status ' + status + ' is for ' + property, function () {
+        service.set('workStatus', status);
+        expect(service.get(property)).to.be.true;
+        var falseStates = statusPropertiesCases.mapProperty('property').without(property);
+        var falseStatuses = [];
+        falseStates.forEach(function (state) {
+          falseStatuses.push(service.get(state));
+        });
+        expect(falseStatuses).to.eql([false]);
+      });
+    });
+  });
+
+  describe('#isClientsOnly', function () {
+    clientsOnly.forEach(function (item) {
+      it('should be true', function () {
+        service.set('serviceName', item);
+        expect(service.get('isClientsOnly')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      service.set('serviceName', 'HDFS');
+      expect(service.get('isClientsOnly')).to.be.false;
+    });
+  });
+
+  describe('#isConfigurable', function () {
+    configurable.forEach(function (item) {
+      it('should be true', function () {
+        service.set('serviceName', item);
+        expect(service.get('isConfigurable')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      service.set('serviceName', 'SQOOP');
+      expect(service.get('isConfigurable')).to.be.false;
+    });
+  });
+
+  describe('#displayName', function () {
+    services.forEach(function (item) {
+      var displayName = App.Service.DisplayNames[item.name];
+      it('should return ' + displayName, function () {
+        service.set('serviceName', item.name);
+        expect(service.get('displayName')).to.equal(displayName);
+      });
+    });
+  });
+
+  describe('#isRestartRequired', function () {
+    hostComponentsDataFalse.forEach(function (item) {
+      it('should be false', function () {
+        service.reopen({
+          hostComponents: item
+        });
+        expect(service.get('isRestartRequired')).to.be.false;
+      });
+    });
+    hostComponentsDataTrue.forEach(function (item) {
+      it('should be true', function () {
+        service.reopen({
+          hostComponents: item
+        });
+        expect(service.get('isRestartRequired')).to.be.true;
+      });
+    });
+  });
+
+  describe('#restartRequiredMessage', function () {
+    it('should form message for 2 services on 1 host', function () {
+      service.set('restartRequiredHostsAndComponents', restartData);
+      expect(service.get('restartRequiredMessage')).to.contain('host0');
+      expect(service.get('restartRequiredMessage')).to.contain('service0');
+      expect(service.get('restartRequiredMessage')).to.contain('service1');
+    });
+  });
+
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/82ffda43/ambari-web/test/models/stack_service_component_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/models/stack_service_component_test.js b/ambari-web/test/models/stack_service_component_test.js
new file mode 100644
index 0000000..8bb349b
--- /dev/null
+++ b/ambari-web/test/models/stack_service_component_test.js
@@ -0,0 +1,254 @@
+/**
+ * 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');
+
+var modelSetup = require('test/init_model_test');
+require('models/stack_service_component');
+
+var stackServiceComponent,
+  stackServiceComponentData = {
+    id: 'ssc'
+  },
+  components = [
+    {
+      name: 'NAMENODE',
+      isReassignable: true
+    },
+    {
+      name: 'SECONDARY_NAMENODE',
+      isReassignable: true
+    },
+    {
+      name: 'JOBTRACKER',
+      isReassignable: true
+    },
+    {
+      name: 'RESOURCEMANAGER',
+      isReassignable: true
+    },
+    {
+      name: 'SUPERVISOR',
+      isDeletable: true,
+      isRollinRestartAllowed: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'HBASE_MASTER',
+      isDeletable: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'DATANODE',
+      isDeletable: true,
+      isRollinRestartAllowed: true,
+      isDecommissionAllowed: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'TASKTRACKER',
+      isDeletable: true,
+      isRollinRestartAllowed: true,
+      isDecommissionAllowed: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'NODEMANAGER',
+      isDeletable: true,
+      isRollinRestartAllowed: true,
+      isDecommissionAllowed: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'HBASE_REGIONSERVER',
+      isDeletable: true,
+      isRollinRestartAllowed: true,
+      isDecommissionAllowed: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'GANGLIA_MONITOR',
+      isDeletable: true,
+      isAddableToHost: true
+    },
+    {
+      name: 'FLUME_HANDLER',
+      isRefreshConfigsAllowed: true
+    },
+    {
+      name: 'ZOOKEEPER_SERVER',
+      isAddableToHost: true
+    },
+    {
+      name: 'MYSQL_SERVER',
+      mastersNotShown: true
+    },
+    {
+      name: 'JOURNALNODE',
+      mastersNotShown: true
+    }
+  ],
+  reassignable = components.filterProperty('isReassignable').mapProperty('name'),
+  deletable = components.filterProperty('isDeletable').mapProperty('name'),
+  rollingRestartable = components.filterProperty('isRollinRestartAllowed').mapProperty('name'),
+  decommissionable = components.filterProperty('isDecommissionAllowed').mapProperty('name'),
+  refreshable = components.filterProperty('isRefreshConfigsAllowed').mapProperty('name'),
+  addable = components.filterProperty('isAddableToHost').mapProperty('name'),
+  mastersNotShown = components.filterProperty('mastersNotShown').mapProperty('name');
+
+describe('App.StackServiceComponent', function () {
+
+  beforeEach(function () {
+    stackServiceComponent = App.StackServiceComponent.createRecord(stackServiceComponentData);
+  });
+
+  afterEach(function () {
+    modelSetup.deleteRecord(stackServiceComponent);
+  });
+
+  describe('#displayName', function () {
+    components.forEach(function (item) {
+      var displayName = App.format.components[item.name];
+      it('should be ' + displayName, function () {
+        stackServiceComponent.set('componentName', item.name);
+        expect(stackServiceComponent.get('displayName')).to.equal(displayName);
+      });
+    });
+  });
+
+  describe('#isSlave', function () {
+    it('should be true', function () {
+      stackServiceComponent.set('componentCategory', 'SLAVE');
+      expect(stackServiceComponent.get('isSlave')).to.be.true;
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentCategory', 'cc');
+      expect(stackServiceComponent.get('isSlave')).to.be.false;
+    });
+  });
+
+  describe('#isRestartable', function () {
+    it('should be true', function () {
+      stackServiceComponent.set('isClient', false);
+      expect(stackServiceComponent.get('isRestartable')).to.be.true;
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('isClient', true);
+      expect(stackServiceComponent.get('isRestartable')).to.be.false;
+    });
+  });
+
+  describe('#isReassignable', function () {
+    reassignable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isReassignable')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isReassignable')).to.be.false;
+    });
+  });
+
+  describe('#isDeletable', function () {
+    deletable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isDeletable')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isDeletable')).to.be.false;
+    });
+  });
+
+  describe('#isRollinRestartAllowed', function () {
+    rollingRestartable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isRollinRestartAllowed')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isRollinRestartAllowed')).to.be.false;
+    });
+  });
+
+  describe('#isDecommissionAllowed', function () {
+    decommissionable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isDecommissionAllowed')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isDecommissionAllowed')).to.be.false;
+    });
+  });
+
+  describe('#isRefreshConfigsAllowed', function () {
+    refreshable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isRefreshConfigsAllowed')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isRefreshConfigsAllowed')).to.be.false;
+    });
+  });
+
+  describe('#isAddableToHost', function () {
+    addable.forEach(function (item) {
+      it('should be true', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isAddableToHost')).to.be.true;
+      });
+    });
+    it('should be false', function () {
+      stackServiceComponent.set('componentName', 'name');
+      expect(stackServiceComponent.get('isAddableToHost')).to.be.false;
+    });
+  });
+
+  describe('#isShownOnInstallerAssignMasterPage', function () {
+    mastersNotShown.forEach(function (item) {
+      it('should be false', function () {
+        stackServiceComponent.set('componentName', item);
+        expect(stackServiceComponent.get('isShownOnInstallerAssignMasterPage')).to.be.false;
+      });
+    });
+    it('should be true', function () {
+      stackServiceComponent.set('componentName', 'APP_TIMELINE_SERVER');
+      expect(stackServiceComponent.get('isShownOnInstallerAssignMasterPage')).to.be.true;
+    });
+    it('should be true', function () {
+      stackServiceComponent.setProperties({
+        componentName: 'name',
+        isMaster: true
+      });
+      expect(stackServiceComponent.get('isShownOnInstallerAssignMasterPage')).to.be.true;
+    });
+  });
+
+});