Add create health monitor action

This adds the Create Health Monitor action to the LBaaS v2 pool
details page.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: Ie4312bdb0a29b069011b6babe702f3b7699c0aa1
This commit is contained in:
Justin Pomeroy 2016-03-04 08:59:48 -06:00
parent 05f1bf8fd6
commit 9e9c74d8cc
11 changed files with 523 additions and 108 deletions

View File

@ -136,13 +136,35 @@ def create_pool(request, **kwargs):
'index': 0}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
elif data.get('monitor'):
args = (request, kwargs['loadbalancer_id'], add_monitor)
args = (request, kwargs['loadbalancer_id'], create_health_monitor)
kwargs = {'callback_kwargs': {'pool_id': pool['id']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return pool
def create_health_monitor(request, **kwargs):
"""Create a new health monitor for a pool.
"""
data = request.DATA
monitorSpec = {
'type': data['monitor']['type'],
'delay': data['monitor']['interval'],
'timeout': data['monitor']['timeout'],
'max_retries': data['monitor']['retry'],
'pool_id': kwargs['pool_id']
}
if data['monitor'].get('method'):
monitorSpec['http_method'] = data['monitor']['method']
if data['monitor'].get('path'):
monitorSpec['url_path'] = data['monitor']['path']
if data['monitor'].get('status'):
monitorSpec['expected_codes'] = data['monitor']['status']
return neutronclient(request).create_lbaas_healthmonitor(
{'healthmonitor': monitorSpec}).get('healthmonitor')
def add_member(request, **kwargs):
"""Add a member to a pool.
@ -189,7 +211,7 @@ def add_member(request, **kwargs):
'index': index}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
elif data.get('monitor'):
args = (request, loadbalancer_id, add_monitor)
args = (request, loadbalancer_id, create_health_monitor)
kwargs = {'callback_kwargs': {'pool_id': pool_id}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
@ -218,28 +240,6 @@ def remove_member(request, **kwargs):
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
def add_monitor(request, **kwargs):
"""Create a new health monitor for a pool.
"""
data = request.DATA
monitorSpec = {
'type': data['monitor']['type'],
'delay': data['monitor']['interval'],
'timeout': data['monitor']['timeout'],
'max_retries': data['monitor']['retry'],
'pool_id': kwargs['pool_id']
}
if data['monitor'].get('method'):
monitorSpec['http_method'] = data['monitor']['method']
if data['monitor'].get('path'):
monitorSpec['url_path'] = data['monitor']['path']
if data['monitor'].get('status'):
monitorSpec['expected_codes'] = data['monitor']['status']
return neutronclient(request).create_lbaas_healthmonitor(
{'healthmonitor': monitorSpec}).get('healthmonitor')
def update_loadbalancer(request, **kwargs):
"""Update a load balancer.
@ -671,6 +671,23 @@ class Member(generic.View):
return lb.get('member')
@urls.register
class HealthMonitors(generic.View):
"""API for load balancer pool health monitors.
"""
url_regex = r'lbaas/healthmonitors/$'
@rest_utils.ajax()
def post(self, request):
"""Create a new health monitor.
"""
kwargs = {'loadbalancer_id': request.DATA.get('loadbalancer_id'),
'pool_id': request.DATA.get('parentResourceId')}
return create_health_monitor(request, **kwargs)
@urls.register
class HealthMonitor(generic.View):
"""API for retrieving a single health monitor.

View File

@ -53,7 +53,8 @@
getMembers: getMembers,
getMember: getMember,
getHealthMonitor: getHealthMonitor,
deleteHealthMonitor: deleteHealthMonitor
deleteHealthMonitor: deleteHealthMonitor,
createHealthMonitor: createHealthMonitor
};
return service;
@ -342,6 +343,8 @@
});
}
// Health Monitors
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getHealthMonitor
* @description
@ -373,5 +376,20 @@
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.createHealthMonitor
* @description
* Create a new health monitor
* @param {object} spec
* Specifies the data used to create the new health monitor.
*/
function createHealthMonitor(spec) {
return apiService.post('/api/lbaas/healthmonitors/', spec)
.error(function () {
toastService.add('error', gettext('Unable to create health monitor.'));
});
}
}
}());

View File

@ -195,6 +195,14 @@
error: 'Unable to update pool.',
data: { name: 'pool-1' },
testInput: [ '1234', { name: 'pool-1' } ]
},
{
func: 'createHealthMonitor',
method: 'post',
path: '/api/lbaas/healthmonitors/',
error: 'Unable to create health monitor.',
data: { name: 'healthmonitor-1' },
testInput: [ { name: 'healthmonitor-1' } ]
}
];

View File

@ -0,0 +1,88 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed 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.
*/
(function() {
'use strict';
angular
.module('horizon.dashboard.project.lbaasv2.healthmonitors')
.factory('horizon.dashboard.project.lbaasv2.healthmonitors.actions.create', createService);
createService.$inject = [
'$q',
'$location',
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.framework.util.q.extensions'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.lbaasv2.healthmonitors.actions.createService
* @description
* Provides the service for creating a health monitor resource.
* @param $q The angular service for promises.
* @param $location The angular $location service.
* @param workflowModal The LBaaS workflow modal service.
* @param policy The horizon policy service.
* @param gettext The horizon gettext function for translation.
* @param qExtensions Horizon extensions to the $q service.
* @returns The health monitor create service.
*/
function createService($q, $location, workflowModal, policy, gettext, qExtensions) {
var loadbalancerId, listenerId, poolId, statePromise;
var create = workflowModal.init({
controller: 'CreateHealthMonitorWizardController',
message: gettext('A new health monitor is being created.'),
handle: onCreate,
allowed: allowed
});
var service = {
init: init,
create: create
};
return service;
//////////////
function init(_loadbalancerId_, _listenerId_, _statePromise_) {
loadbalancerId = _loadbalancerId_;
listenerId = _listenerId_;
statePromise = _statePromise_;
return service;
}
function allowed(pool) {
poolId = pool.id;
return $q.all([
statePromise,
qExtensions.booleanAsPromise(!pool.healthmonitor_id),
policy.ifAllowed({ rules: [['neutron', 'create_health_monitor']] })
]);
}
function onCreate(response) {
var healthMonitorId = response.data.id;
$location.path('project/ngloadbalancersv2/' + loadbalancerId + '/listeners/' +
listenerId + '/pools/' + poolId + '/healthmonitors/' + healthMonitorId);
}
}
})();

View File

@ -0,0 +1,106 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed 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.
*/
(function() {
'use strict';
describe('LBaaS v2 Create Health Monitor Action Service', function() {
var scope, $q, $location, policy, init, service, loadBalancerState;
function allowed(item) {
spyOn(policy, 'ifAllowed').and.returnValue(true);
var promise = service.create.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith(
{rules: [['neutron', 'create_health_monitor']]});
return allowed;
}
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
$provide.value('$modal', {
open: function() {
return {
result: {
then: function(func) {
func({ data: { id: 'healthmonitor1' } });
}
}
};
}
});
}));
beforeEach(inject(function ($injector) {
scope = $injector.get('$rootScope').$new();
$q = $injector.get('$q');
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
$location = $injector.get('$location');
service = $injector.get('horizon.dashboard.project.lbaasv2.healthmonitors.actions.create');
init = service.init;
loadBalancerState = $q.defer();
}));
it('should define the correct service properties', function() {
expect(service.init).toBeDefined();
expect(service.create).toBeDefined();
});
it('should have the "allowed" and "perform" functions', function() {
expect(service.create.allowed).toBeDefined();
expect(service.create.perform).toBeDefined();
});
it('should allow creating a health monitor under an ACTIVE load balancer', function() {
loadBalancerState.resolve();
init('active', '1', loadBalancerState.promise);
expect(allowed({})).toBe(true);
});
it('should not allow creating a health monitor under a NON-ACTIVE load balancer', function() {
loadBalancerState.reject();
init('non-active', '1', loadBalancerState.promise);
expect(allowed({})).toBe(false);
});
it('should not allow creating a health monitor if one already exists', function() {
loadBalancerState.resolve();
init('active', '1', loadBalancerState.promise);
expect(allowed({ healthmonitor_id: '1234' })).toBe(false);
});
it('should redirect after create', function() {
loadBalancerState.resolve();
spyOn($location, 'path').and.callThrough();
init('loadbalancer1', 'listener1', loadBalancerState.promise).create.allowed({id: 'pool1'});
service.create.perform();
expect($location.path).toHaveBeenCalledWith(
'project/ngloadbalancersv2/loadbalancer1/listeners/listener1/pools/pool1/' +
'healthmonitors/healthmonitor1');
});
});
})();

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed 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.
*/
(function () {
'use strict';
angular
.module('horizon.dashboard.project.lbaasv2.healthmonitors')
.controller('CreateHealthMonitorWizardController', CreateHealthMonitorWizardController);
CreateHealthMonitorWizardController.$inject = [
'$scope',
'$routeParams',
'horizon.dashboard.project.lbaasv2.workflow.model',
'horizon.dashboard.project.lbaasv2.workflow.workflow',
'horizon.framework.util.i18n.gettext'
];
function CreateHealthMonitorWizardController(
$scope, $routeParams, model, workflowService, gettext
) {
var loadbalancerId = $routeParams.loadbalancerId;
var scope = $scope;
scope.model = model;
scope.submit = scope.model.submit;
scope.workflow = workflowService(
gettext('Create Health Monitor'),
'fa fa-cloud-download',
['monitor']
);
scope.model.initialize('monitor', false, loadbalancerId, scope.launchContext.id);
}
})();

View File

@ -0,0 +1,63 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed 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.
*/
(function () {
'use strict';
describe('LBaaS v2 Create Health Monitor Wizard Controller', function() {
var ctrl;
var model = {
submit: function() {
return 'created';
},
initialize: angular.noop
};
var workflow = function() {
return 'foo';
};
var scope = {
launchContext: {id: 'pool1'}
};
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function ($provide) {
$provide.value('horizon.dashboard.project.lbaasv2.workflow.model', model);
$provide.value('horizon.dashboard.project.lbaasv2.workflow.workflow', workflow);
}));
beforeEach(inject(function ($controller) {
spyOn(model, 'initialize');
ctrl = $controller('CreateHealthMonitorWizardController', { $scope: scope });
}));
it('defines the controller', function() {
expect(ctrl).toBeDefined();
});
it('calls initialize on the given model', function() {
expect(model.initialize).toHaveBeenCalled();
});
it('sets scope.workflow to the given workflow', function() {
expect(scope.workflow).toBe('foo');
});
it('defines scope.submit', function() {
expect(scope.submit).toBeDefined();
expect(scope.submit()).toBe('created');
});
});
})();

View File

@ -25,7 +25,8 @@
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.pools.actions.edit',
'horizon.dashboard.project.lbaasv2.pools.actions.delete'
'horizon.dashboard.project.lbaasv2.pools.actions.delete',
'horizon.dashboard.project.lbaasv2.healthmonitors.actions.create'
];
/**
@ -39,10 +40,11 @@
* @param loadBalancersService The LBaaS v2 load balancers service.
* @param editService The LBaaS v2 pools delete service.
* @param deleteService The LBaaS v2 pools delete service.
* @param createService The LBaaS v2 health monitor create service.
* @returns Pool row actions service object.
*/
function rowActions(gettext, loadBalancersService, editService, deleteService) {
function rowActions(gettext, loadBalancersService, editService, deleteService, createService) {
var loadBalancerIsActionable, loadbalancerId, listenerId;
var service = {
@ -67,6 +69,11 @@
template: {
text: gettext('Edit Pool')
}
},{
service: createService.init(loadbalancerId, listenerId, loadBalancerIsActionable).create,
template: {
text: gettext('Create Health Monitor')
}
},{
service: deleteService.init(loadbalancerId, listenerId, loadBalancerIsActionable),
template: {

View File

@ -35,9 +35,10 @@
}));
it('should define correct table row actions', function() {
expect(actions.length).toBe(2);
expect(actions.length).toBe(3);
expect(actions[0].template.text).toBe('Edit Pool');
expect(actions[1].template.text).toBe('Delete Pool');
expect(actions[1].template.text).toBe('Create Health Monitor');
expect(actions[2].template.text).toBe('Delete Pool');
});
it('should have the "allowed" and "perform" functions', function() {

View File

@ -176,7 +176,6 @@
}
function initializeResources() {
var promise;
var type = (model.context.id ? 'edit' : 'create') + model.context.resource;
keymanagerPromise = serviceCatalog.ifTypeEnabled('key-manager');
@ -184,66 +183,15 @@
keymanagerPromise.then(angular.noop, certificatesNotSupported);
}
switch (type) {
case 'createloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers),
// The noop errback prevents this from tripping up $q.all since this is a case
// where we don't care if it fails, i.e. key-manager service doesn't exist.
keymanagerPromise.then(prepareCertificates, angular.noop)
]).then(initMemberAddresses);
model.context.submit = createLoadBalancer;
break;
case 'createlistener':
promise = $q.all([
lbaasv2API.getListeners(model.spec.loadbalancer_id).then(onGetListeners),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers),
keymanagerPromise.then(prepareCertificates, angular.noop)
]).then(initMemberAddresses);
model.context.submit = createListener;
break;
case 'createpool':
// We get the listener details here because we need to know the listener protocol
// in order to default the new pool's protocol to match.
promise = $q.all([
lbaasv2API.getListener(model.spec.parentResourceId).then(onGetListener),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = createPool;
break;
case 'editloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets)
]).then(initSubnet);
model.context.submit = editLoadBalancer;
break;
case 'editlistener':
promise = $q.all([
neutronAPI.getSubnets().then(onGetSubnets).then(getListener).then(onGetListener),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = editListener;
break;
case 'editpool':
promise = $q.all([
neutronAPI.getSubnets().then(onGetSubnets).then(getPool).then(onGetPool),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = editPool;
break;
default:
throw Error('Invalid resource context: ' + type);
}
var promise = {
'createloadbalancer': initCreateLoadBalancer,
'createlistener': initCreateListener,
'createpool': initCreatePool,
'createmonitor': initCreateMonitor,
'editloadbalancer': initEditLoadBalancer,
'editlistener': initEditListener,
'editpool': initEditPool
}[type](keymanagerPromise);
return promise.then(onInitSuccess, onInitFail);
}
@ -258,6 +206,71 @@
model.initialized = false;
}
function initCreateLoadBalancer(keymanagerPromise) {
model.context.submit = createLoadBalancer;
return $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers),
keymanagerPromise.then(prepareCertificates, angular.noop)
]).then(initMemberAddresses);
}
function initCreateListener(keymanagerPromise) {
model.context.submit = createListener;
return $q.all([
lbaasv2API.getListeners(model.spec.loadbalancer_id).then(onGetListeners),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers),
keymanagerPromise.then(prepareCertificates, angular.noop)
]).then(initMemberAddresses);
}
function initCreatePool() {
model.context.submit = createPool;
// We get the listener details here because we need to know the listener protocol
// in order to default the new pool's protocol to match.
return $q.all([
lbaasv2API.getListener(model.spec.parentResourceId).then(onGetListener),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
}
function initCreateMonitor() {
model.context.submit = createHealthMonitor;
return $q.when();
}
function initEditLoadBalancer() {
model.context.submit = editLoadBalancer;
return $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets)
]).then(initSubnet);
}
function initEditListener() {
model.context.submit = editListener;
return $q.all([
neutronAPI.getSubnets().then(onGetSubnets).then(getListener).then(onGetListener),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
}
function initEditPool() {
model.context.submit = editPool;
return $q.all([
neutronAPI.getSubnets().then(onGetSubnets).then(getPool).then(onGetPool),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
}
/**
* @ngdoc method
* @name workflowModel.submit
@ -292,6 +305,10 @@
return lbaasv2API.createPool(spec);
}
function createHealthMonitor(spec) {
return lbaasv2API.createHealthMonitor(spec);
}
function editLoadBalancer(spec) {
return lbaasv2API.editLoadBalancer(model.context.id, spec);
}
@ -325,6 +342,7 @@
if (!finalSpec.listener.protocol || !finalSpec.listener.port) {
// Listener requires protocol and port
delete finalSpec.listener;
delete finalSpec.certificates;
} else if (finalSpec.listener.protocol !== 'TERMINATED_HTTPS') {
// Remove certificate containers if not using TERMINATED_HTTPS
delete finalSpec.certificates;

View File

@ -182,6 +182,9 @@
},
editPool: function(id, spec) {
return spec;
},
createHealthMonitor: function(spec) {
return spec;
}
});
@ -463,6 +466,34 @@
});
});
describe('Post initialize model (create health monitor)', function() {
beforeEach(function() {
model.initialize('monitor', null, 'loadbalancer1', 'pool1');
scope.$apply();
});
it('should initialize model properties', function() {
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(0);
expect(model.members.length).toBe(0);
expect(model.certificates.length).toBe(0);
expect(model.listenerPorts.length).toBe(0);
expect(model.spec.loadbalancer_id).toBe('loadbalancer1');
expect(model.spec.parentResourceId).toBe('pool1');
expect(model.spec.members.length).toBe(0);
expect(model.spec.certificates).toEqual([]);
expect(model.certificatesError).toBe(false);
});
it('should initialize context properties', function() {
expect(model.context.resource).toBe('monitor');
expect(model.context.id).toBeFalsy();
expect(model.context.submit.name).toBe('createHealthMonitor');
});
});
describe('Post initialize model (edit loadbalancer)', function() {
beforeEach(function() {
@ -896,24 +927,6 @@
});
});
describe('Resource not provided', function() {
var initModelNoContext = function() {
model.initialize();
};
var initModelNoResource = function() {
model.initialize('', 'foo');
};
it('should fail to be initialized - create', function() {
expect(initModelNoContext).toThrow(Error('Invalid resource context: createundefined'));
});
it('should fail to be initialized - edit', function() {
expect(initModelNoResource).toThrow(Error('Invalid resource context: edit'));
});
});
describe('context (create loadbalancer)', function() {
beforeEach(function() {
@ -1636,6 +1649,36 @@
});
});
describe('Model submit function (create health monitor)', function() {
beforeEach(function() {
model.initialize('monitor', null, 'loadbalancer1', 'pool1');
scope.$apply();
});
it('should set final spec properties', function() {
model.spec.monitor.type = 'HTTP';
var finalSpec = model.submit();
expect(finalSpec.loadbalancer_id).toBe('loadbalancer1');
expect(finalSpec.parentResourceId).toBe('pool1');
expect(finalSpec.loadbalancer).toBeUndefined();
expect(finalSpec.listener).toBeUndefined();
expect(finalSpec.pool).toBeUndefined();
expect(finalSpec.members).toBeUndefined();
expect(finalSpec.certificates).toBeUndefined();
expect(finalSpec.monitor.type).toBe('HTTP');
expect(finalSpec.monitor.interval).toBe(5);
expect(finalSpec.monitor.retry).toBe(3);
expect(finalSpec.monitor.timeout).toBe(5);
expect(finalSpec.monitor.method).toBe('GET');
expect(finalSpec.monitor.status).toBe('200');
expect(finalSpec.monitor.path).toBe('/');
});
});
describe('Model submit function (edit listener)', function() {
beforeEach(function() {