Merge "Unit test framework for Ironic-UI API service"

This commit is contained in:
Jenkins 2017-06-20 18:22:08 +00:00 committed by Gerrit Code Review
commit 911671d540
12 changed files with 1186 additions and 72 deletions

View File

@ -58,6 +58,7 @@
toxPath + 'xstatic/pkg/rickshaw/data/rickshaw.js',
toxPath + 'xstatic/pkg/angular_smart_table/data/smart-table.js',
toxPath + 'xstatic/pkg/angular_lrdragndrop/data/lrdragndrop.js',
toxPath + 'xstatic/pkg/angular_fileupload/data/ng-file-upload-all.js',
toxPath + 'xstatic/pkg/spin/data/spin.js',
toxPath + 'xstatic/pkg/spin/data/spin.jquery.js',
toxPath + 'xstatic/pkg/tv4/data/tv4.js',

View File

@ -116,70 +116,6 @@
});
};
/**
* @description Check whether a group contains required properties
*
* @param {DriverProperty[]} group - Property group
* @return {boolean} Return true if the group contains required
* properties, false otherwise
*/
function driverPropertyGroupHasRequired(group) {
var hasRequired = false;
for (var i = 0; i < group.length; i++) {
if (group[i].required) {
hasRequired = true;
break;
}
}
return hasRequired;
}
/**
* @description Convert array of driver property groups to a string
*
* @param {array[]} groups - Array for driver property groups
* @return {string} Output string
*/
function driverPropertyGroupsToString(groups) {
var output = [];
angular.forEach(groups, function(group) {
var groupStr = [];
angular.forEach(group, function(property) {
groupStr.push(property.name);
});
groupStr = groupStr.join(", ");
output.push(['[', groupStr, ']'].join(""));
});
output = output.join(", ");
return ['[', output, ']'].join("");
}
/**
* @description Comaprison function used to sort driver property groups
*
* @param {DriverProperty[]} group1 - First group
* @param {DriverProperty[]} group2 - Second group
* @return {integer} Return:
* < 0 if group1 should precede group2 in an ascending ordering
* > 0 if group2 should precede group1
* 0 if group1 and group2 are considered equal from ordering perpsective
*/
function compareDriverPropertyGroups(group1, group2) {
var group1HasRequired = driverPropertyGroupHasRequired(group1);
var group2HasRequired = driverPropertyGroupHasRequired(group2);
if (group1HasRequired === group2HasRequired) {
if (group1.length === group2.length) {
return group1[0].name.localeCompare(group2[0].name);
} else {
return group1.length - group2.length;
}
} else {
return group1HasRequired ? -1 : 1;
}
return 0;
}
/**
* @description Order driver properties in the form using the following
* rules:
@ -190,7 +126,7 @@
* (2) Required properties with no dependents should be located at the
* top of the form
*
* @return {void}
* @return {[]} Ordered list of groups of strongly related properties
*/
ctrl._sortDriverProperties = function() {
// Build dependency graph between driver properties
@ -235,10 +171,10 @@
components.push(component);
},
groups);
groups.sort(compareDriverPropertyGroups);
groups.sort(baseNodeService.compareDriverPropertyGroups);
$log.debug("Found the following property groups: " +
driverPropertyGroupsToString(groups));
baseNodeService.driverPropertyGroupsToString(groups));
return groups;
};

View File

@ -0,0 +1,106 @@
/*
* Copyright 2017 Cray Inc.
*
* 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('horizon.dashboard.admin.ironic.base-node', function () {
var ironicBackendMockService, uibModalInstance;
var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
uibModalInstance = {
dismiss: jasmine.createSpy()
};
$provide.value('$uibModalInstance', uibModalInstance);
}));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service',
{});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
var controller = $injector.get('$controller');
controller('BaseNodeController', {ctrl: ctrl});
}));
afterEach(function() {
ironicBackendMockService.postTest();
});
it('controller should be defined', function () {
expect(ctrl).toBeDefined();
});
it('base construction', function () {
expect(ctrl.drivers).toBeNull();
expect(ctrl.images).toBeNull();
expect(ctrl.loadingDriverProperties).toBe(false);
expect(ctrl.driverProperties).toBeNull();
expect(ctrl.driverPropertyGroups).toBeNull();
expect(ctrl.modalTitle).toBeDefined();
angular.forEach(ctrl.propertyCollections, function(collection) {
expect(Object.getOwnPropertyNames(collection).sort()).toEqual(
PROPERTY_COLLECTION_PROPERTIES.sort());
});
expect(ctrl.propertyCollections)
.toContain(jasmine.objectContaining({id: "properties"}));
expect(ctrl.propertyCollections)
.toContain(jasmine.objectContaining({id: "extra"}));
expect(ctrl.node).toEqual({
name: null,
driver: null,
driver_info: {},
properties: {},
extra: {},
network_interface: null});
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
BASE_NODE_CONTROLLER_PROPERTIES.sort());
});
it('_loadDrivers', function () {
ctrl._loadDrivers();
ironicBackendMockService.flush();
expect(ctrl.drivers).toEqual(ironicBackendMockService.getDrivers());
});
it('_getImages', function () {
ctrl._getImages();
ironicBackendMockService.flush();
expect(ctrl.images).toEqual(ironicBackendMockService.getImages());
});
it('cancel', function () {
ctrl.cancel();
expect(uibModalInstance.dismiss).toHaveBeenCalledWith('cancel');
});
});
})();

View File

@ -64,7 +64,10 @@
var service = {
DriverProperty: DriverProperty,
PostfixExpr: PostfixExpr,
Graph: Graph
Graph: Graph,
driverPropertyGroupHasRequired: driverPropertyGroupHasRequired,
driverPropertyGroupsToString: driverPropertyGroupsToString,
compareDriverPropertyGroups: compareDriverPropertyGroups
};
var VALID_ADDRESS_HOSTNAME_REGEX = new RegExp(VALID_IPV4_ADDRESS + "|" +
@ -669,6 +672,70 @@
});
};
/**
* @description Check whether a group contains required properties
*
* @param {DriverProperty[]} group - Property group
* @return {boolean} Return true if the group contains required
* properties, false otherwise
*/
function driverPropertyGroupHasRequired(group) {
var hasRequired = false;
for (var i = 0; i < group.length; i++) {
if (group[i].required) {
hasRequired = true;
break;
}
}
return hasRequired;
}
/**
* @description Convert array of driver property groups to a string
*
* @param {array[]} groups - Array of driver property groups
* @return {string} Output string
*/
function driverPropertyGroupsToString(groups) {
var output = [];
angular.forEach(groups, function(group) {
var groupStr = [];
angular.forEach(group, function(property) {
groupStr.push(property.name);
});
groupStr = groupStr.join(", ");
output.push(['[', groupStr, ']'].join(""));
});
output = output.join(", ");
return ['[', output, ']'].join("");
}
/**
* @description Comaprison function used to sort driver property groups
*
* @param {DriverProperty[]} group1 - First group
* @param {DriverProperty[]} group2 - Second group
* @return {integer} Return:
* < 0 if group1 should precede group2 in an ascending ordering
* > 0 if group2 should precede group1
* 0 if group1 and group2 are considered equal from ordering perpsective
*/
function compareDriverPropertyGroups(group1, group2) {
var group1HasRequired = driverPropertyGroupHasRequired(group1);
var group2HasRequired = driverPropertyGroupHasRequired(group2);
if (group1HasRequired === group2HasRequired) {
if (group1.length === group2.length) {
return group1[0].name.localeCompare(group2[0].name);
} else {
return group1.length - group2.length;
}
} else {
return group1HasRequired ? -1 : 1;
}
return 0;
}
return service;
}
})();

View File

@ -224,5 +224,47 @@
expect(ret[1]).toBe(false);
});
});
describe('DriverPropertyGroup', function() {
it('driverPropertyGroupHasRequired', function () {
var dp1 = new service.DriverProperty("dp-1", " Required.", []);
var dp2 = new service.DriverProperty("dp-2", " ", []);
expect(service.driverPropertyGroupHasRequired).toBeDefined();
expect(service.driverPropertyGroupHasRequired([])).toBe(false);
expect(service.driverPropertyGroupHasRequired([dp1])).toBe(true);
expect(service.driverPropertyGroupHasRequired([dp2])).toBe(false);
expect(service.driverPropertyGroupHasRequired([dp1, dp2])).toBe(true);
});
it('driverPropertyGroupsToString', function () {
var dp1 = new service.DriverProperty("dp-1", " Required.", []);
var dp2 = new service.DriverProperty("dp-2", " ", []);
expect(service.driverPropertyGroupsToString).toBeDefined();
expect(service.driverPropertyGroupsToString([])).toBe("[]");
expect(service.driverPropertyGroupsToString([[dp1]]))
.toBe("[[dp-1]]");
expect(service.driverPropertyGroupsToString([[dp1], [dp2]]))
.toBe("[[dp-1], [dp-2]]");
});
it('compareDriverPropertyGroups', function () {
var dp1 = new service.DriverProperty("dp-1", " Required.", []);
var dp2 = new service.DriverProperty("dp-2", " ", []);
expect(service.compareDriverPropertyGroups).toBeDefined();
expect(service.compareDriverPropertyGroups([dp1], [dp1])).toBe(0);
expect(service.compareDriverPropertyGroups([dp1], [dp2])).toBe(-1);
expect(service.compareDriverPropertyGroups([dp2], [dp1])).toBe(1);
// smaller group precedes larger group
expect(service.compareDriverPropertyGroups([dp1], [dp1, dp2]))
.toBe(-1);
// group order decided on lexographic comparison of names of first
// property
expect(service.compareDriverPropertyGroups([dp2, dp1], [dp1, dp2]))
.toBe(1);
});
});
});
})();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 Cray Inc.
* Copyright 2017 Cray Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -67,6 +67,8 @@
ctrl.node[instanceInfoId] = {};
ctrl.node[instanceInfoId] = {};
init(node);
function init(node) {
@ -112,7 +114,7 @@
* @param {object} targetNode - Target node
* @return {object[]} Array of patch instructions
*/
function buildPatch(sourceNode, targetNode) {
ctrl.buildPatch = function(sourceNode, targetNode) {
var patcher = new updatePatchService.UpdatePatch();
var PatchItem = function PatchItem(id, path) {
this.id = id;
@ -132,7 +134,7 @@
});
return patcher.getPatch();
}
};
ctrl.submit = function() {
angular.forEach(ctrl.driverProperties, function(property, name) {
@ -153,7 +155,7 @@
$log.info("Updating node " + JSON.stringify(ctrl.baseNode));
$log.info("to " + JSON.stringify(ctrl.node));
var patch = buildPatch(ctrl.baseNode, ctrl.node);
var patch = ctrl.buildPatch(ctrl.baseNode, ctrl.node);
$log.info("patch = " + JSON.stringify(patch.patch));
if (patch.status === updatePatchService.UpdatePatch.status.OK) {
ironic.updateNode(ctrl.baseNode.uuid, patch.patch).then(function(node) {

View File

@ -0,0 +1,101 @@
/*
* Copyright 2015 Hewlett Packard Enterprise Development Company LP
* Copyright 2016 Cray Inc.
*
* 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('horizon.dashboard.admin.ironic.edit-node', function () {
var ironicBackendMockService, ctrl, editNode, updatePatchService;
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
$provide.value('$uibModalInstance', {});
}));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service',
{});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
updatePatchService =
$injector.get('horizon.dashboard.admin.ironic.update-patch.service');
var ironicAPI =
$injector.get('horizon.app.core.openstack-service-api.ironic');
ironicAPI.createNode(
{driver: ironicBackendMockService.params.defaultDriver})
.then(function(response) {
editNode = response.data;
var controller = $injector.get('$controller');
ctrl = controller('EditNodeController', {node: editNode});
});
ironicBackendMockService.flush();
}));
afterEach(function() {
ironicBackendMockService.postTest();
});
it('controller should be defined', function () {
expect(ctrl).toBeDefined();
});
it('controller base construction', function () {
expect(ctrl.baseNode).toEqual(
ironicBackendMockService.getNode(editNode.uuid));
expect(ctrl.propertyCollections)
.toContain(jasmine.objectContaining({id: "instance_info"}));
angular.forEach(ctrl.propertyCollections, function(collection) {
expect(Object.getOwnPropertyNames(collection).sort()).toEqual(
PROPERTY_COLLECTION_PROPERTIES.sort());
});
expect(ctrl.node.name).toEqual(editNode.name);
expect(ctrl.node.network_interface).toEqual(editNode.network_interface);
expect(ctrl.node.properties).toEqual(editNode.properties);
expect(ctrl.node.extra).toEqual(editNode.extra);
expect(ctrl.node.instance_info).toEqual(editNode.instance_info);
expect(ctrl.node.uuid).toEqual(editNode.uuid);
var properties = angular.copy(BASE_NODE_CONTROLLER_PROPERTIES);
properties.push('baseNode',
'buildPatch',
'selectedDriver',
'submit');
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
properties.sort());
});
it('buildPatch', function () {
var patch = ctrl.buildPatch(editNode, editNode);
expect(patch.patch).toEqual([]);
expect(patch.status).toEqual(updatePatchService.UpdatePatch.status.OK);
});
});
})();

View File

@ -0,0 +1,88 @@
/*
* Copyright 2017 Cray Inc.
*
* 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('horizon.dashboard.admin.ironic.enroll-node', function () {
var ironicBackendMockService, rootScope, ironicEvents, uibModalInstance;
var ctrl = {};
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
}));
beforeEach(module(function($provide) {
uibModalInstance = {
close: jasmine.createSpy()
};
$provide.value('$uibModalInstance', uibModalInstance);
}));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service',
{});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
rootScope = $injector.get('$rootScope');
ironicEvents = $injector.get('horizon.dashboard.admin.ironic.events');
}));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
var controller = $injector.get('$controller');
ctrl = controller('EnrollNodeController');
ironicBackendMockService.flush();
}));
afterEach(function() {
ironicBackendMockService.postTest();
});
it('controller should be defined', function () {
expect(ctrl).toBeDefined();
});
it('base construction', function () {
var properties = angular.copy(BASE_NODE_CONTROLLER_PROPERTIES);
properties.push('submit');
expect(Object.getOwnPropertyNames(ctrl).sort()).toEqual(
properties.sort());
});
it('submit - success', function () {
spyOn(rootScope, '$emit');
var nodeName = "node_" + Date.now();
ctrl.node.name = nodeName;
ctrl.node.driver = ironicBackendMockService.params.defaultDriver;
ctrl.submit();
ironicBackendMockService.flush();
expect(rootScope.$emit)
.toHaveBeenCalledWith(ironicEvents.ENROLL_NODE_SUCCESS);
expect(uibModalInstance.close)
.toHaveBeenCalledWith(ironicBackendMockService.getNode(nodeName));
});
});
})();

View File

@ -0,0 +1,383 @@
/*
* © Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP
* © Copyright 2016 Cray Inc.
*
* 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';
/**
* @description Service that provides a mock for the Ironic backend.
*/
angular
.module('horizon.dashboard.admin.ironic')
.factory('horizon.dashboard.admin.ironic.backend-mock.service',
ironicBackendMockService);
ironicBackendMockService.$inject = [
'$httpBackend',
'horizon.framework.util.uuid.service'
];
function ironicBackendMockService($httpBackend, uuidService) {
// Default node object.
var defaultNode = {
chassis_uuid: null,
clean_step: {},
console_enabled: false,
driver: undefined,
driver_info: {},
driver_internal_info: {},
extra: {},
inspection_finished_at: null,
inspection_started_at: null,
instance_info: {},
instance_uuid: null,
last_error: null,
maintenance: false,
maintenance_reason: null,
name: null,
network_interface: "flat",
power_state: null,
properties: {},
provision_state: "enroll",
provision_updated_at: null,
raid_config: {},
reservation: null,
resource_class: null,
target_power_state: null,
target_provision_state: null,
target_raid_config: {},
updated_at: null,
uuid: undefined
};
// Value of the next available system port
var nextAvailableSystemPort = 1024;
// Additional service parameters
var params = {
// Currently, all nodes have the same boot device.
bootDevice: {boot_device: 'pxe', persistent: true},
// Console info
consoleType: "shellinabox",
consoleUrl: "http://localhost:",
defaultDriver: "agent_ipmitool"
};
// List of supported drivers
var drivers = [{name: params.defaultDriver}];
// List of images
var images = [];
var service = {
params: params,
init: init,
flush: flush,
postTest: postTest,
getNode: getNode,
nodeGetConsoleUrl: nodeGetConsoleUrl,
getDrivers: getDrivers,
getImages: getImages
};
// Dictionary of active nodes indexed by node-id (uuid and name)
var nodes = {};
return service;
/**
* @description Get and reserve the next available system port.
*
* @return {int} Port number.
*/
function getNextAvailableSystemPort() {
return nextAvailableSystemPort++;
}
/**
* @description Create a backend managed node.
*
* @param {object} params - Dictionary of parameters that define
* the node to be created.
* @return {object | null} Node object, or null if the nde could
* not be created.
*/
function createNode(params) {
var node = null;
if (angular.isDefined(params.driver)) {
node = angular.copy(defaultNode);
angular.forEach(params, function(value, key) {
node[key] = value;
});
if (angular.isUndefined(node.uuid)) {
node.uuid = uuidService.generate();
}
var backendNode = {
base: node,
consolePort: getNextAvailableSystemPort()
};
nodes[node.uuid] = backendNode;
if (node.name !== null) {
nodes[node.name] = backendNode;
}
}
return node;
}
/**
* description Get a specified node.
*
* @param {string} nodeId - Uuid or name of the requested node.
* @return {object} Base node object.
*/
function getNode(nodeId) {
return angular.isDefined(nodes[nodeId]) ? nodes[nodeId].base : undefined;
}
/*
* @description Get the console-url for a specified node.
*
* @param {string} nodeId - Uuid or name of the node.
* @return {string} Console url if the console is enabled, null otherwise.
*/
function nodeGetConsoleUrl(nodeId) {
return nodes[nodeId].base.console_enabled
? service.params.consoleUrl + nodes[nodeId].consolePort
: undefined;
}
/**
* @description Initialize the Backend-Mock service.
* Create the handlers that intercept http requests.
*
* @return {void}
*/
function init() {
// Create node
$httpBackend.whenPOST(/\/api\/ironic\/nodes\/$/)
.respond(function(method, url, data) {
var node = createNode(JSON.parse(data).node);
return [node ? 200 : 400, node];
});
// Delete node
$httpBackend.whenDELETE(/\/api\/ironic\/nodes\/$/)
.respond(function(method, url, data) {
var nodeId = JSON.parse(data).node;
var status = 400;
if (angular.isDefined(nodes[nodeId])) {
var node = nodes[nodeId].base;
if (node.name !== null) {
delete nodes[node.name];
delete nodes[node.uuid];
} else {
delete nodes[nodeId];
}
status = 204;
}
return [status, ""];
});
function _addItem(node, path, value) {
var parts = path.substring(1).split("/");
var leaf = parts.pop();
var obj = node;
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
if (angular.isUndefined(obj[part])) {
obj[part] = {};
}
obj = obj[part];
}
obj[leaf] = value;
}
function _removeItem(node, path) {
var parts = path.substring(1).split("/");
var leaf = parts.pop();
var obj = node;
for (var i = 0; i < parts.length; i++) {
obj = obj[parts[i]];
}
delete obj[leaf];
}
function _replaceItem(node, path, value) {
if (path === "/name" &&
node.name !== null) {
delete nodes[name];
if (value !== null) {
nodes[value] = node;
}
}
var parts = path.substring(1).split("/");
var leaf = parts.pop();
var obj = node;
for (var i = 0; i < parts.length; i++) {
obj = obj[parts[i]];
}
obj[leaf] = value;
}
// Update node
$httpBackend.whenPATCH(/\/api\/ironic\/nodes\/([^\/]+)$/,
undefined,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
var status = 400;
var node = service.getNode(params.nodeId);
if (angular.isDefined(node)) {
var patch = JSON.parse(data).patch;
angular.forEach(patch, function(operation) {
switch (operation.op) {
case "add":
_addItem(node, operation.path, operation.value);
break;
case "remove":
_removeItem(node, operation.path);
break;
case "replace":
_replaceItem(node, operation.path, operation.value);
break;
default:
}
});
status = 200;
}
return [status, node];
});
// Get node
$httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)$/,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
if (angular.isDefined(nodes[params.nodeId])) {
return [200, nodes[params.nodeId].base];
} else {
return [400, null];
}
});
// Get console
$httpBackend.whenGET(/\/api\/ironic\/nodes\/(.+)\/states\/console/,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
var node = nodes[params.nodeId];
var consoleEnabled = node.base.console_enabled;
var consoleInfo = consoleEnabled
? {console_type: service.params.consoleType,
url: service.params.consoleUrl + node.consolePort}
: null;
var info = {
console_enabled: consoleEnabled,
console_info: consoleInfo};
return [200, info];
});
// Set console
$httpBackend.whenPUT(/\/api\/ironic\/nodes\/(.+)\/states\/console/,
undefined,
undefined,
['nodeId'])
.respond(function(method, url, data, headers, params) {
data = JSON.parse(data);
nodes[params.nodeId].base.console_enabled = data.enabled;
return [200, {}];
});
// Get the ports belonging to a specified node
$httpBackend.whenGET(/\/api\/ironic\/ports/)
.respond(200, []);
// Get boot device
$httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/boot_device$/,
undefined,
['nodeId'])
.respond(200, service.params.bootDevice);
// Validate the interfaces associated with a specified node
$httpBackend.whenGET(/\/api\/ironic\/nodes\/([^\/]+)\/validate$/,
undefined,
['nodeId'])
.respond(200, []);
// Get the currently available drivers
$httpBackend.whenGET(/\/api\/ironic\/drivers\/$/)
.respond(200, {drivers: drivers});
// Get driver properties
$httpBackend.whenGET(/\/api\/ironic\/drivers\/([^\/]+)\/properties$/,
undefined,
['driverName'])
.respond(200, []);
// Get glance images
$httpBackend.whenGET(/\/api\/glance\/images/)
.respond(200, {items: images});
}
/**
* @description Get the list of supported drivers
*
* @return {[]} Array of driver objects
*/
function getDrivers() {
return drivers;
}
/**
* @description Get the list of images
*
* @return {[]} Array of image objects
*/
function getImages() {
return images;
}
/**
* @description Flush pending requests
*
* @return {void}
*/
function flush() {
$httpBackend.flush();
}
/**
* @description Post test verifications.
* This function should be called after completion of a unit test.
*
* @return {void}
*/
function postTest() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
}
}
}());

View File

@ -0,0 +1,322 @@
/**
* Copyright 2016 Cray Inc
*
* 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";
var IRONIC_API_PROPERTIES = [
'createNode',
'createPort',
'deleteNode',
'deletePort',
'getDrivers',
'getDriverProperties',
'getNode',
'getNodes',
'getPortsWithNode',
'getBootDevice',
'nodeGetConsole',
'nodeSetConsoleMode',
'nodeSetPowerState',
'nodeSetMaintenance',
'setNodeProvisionState',
'updateNode',
'updatePort',
'validateNode'
];
/**
* @description Unit tests for the Ironic-UI API service
*/
describe(
'horizon.dashboard.admin.ironic.service',
function() {
// Name of default driver used to create nodes.
var ironicAPI, ironicBackendMockService, defaultDriver;
/**
* @description Create a node.
*
* @param {object} params - Dictionary of parameters that define the node.
* @return {promise} - Promise containing the newly created node.
*/
function createNode(params) {
return ironicAPI.createNode(params)
.then(function(response) {
return response.data; // node
});
}
/**
* @description Fail the current test
*
* @return {void}
*/
function failTest() {
fail();
}
beforeEach(module('horizon.dashboard.admin.ironic'));
beforeEach(module('horizon.framework.util'));
beforeEach(module(function($provide) {
$provide.value('horizon.framework.widgets.toast.service', {
add: function() {}
});
}));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(inject(function($injector) {
ironicBackendMockService =
$injector.get('horizon.dashboard.admin.ironic.backend-mock.service');
ironicBackendMockService.init();
defaultDriver = ironicBackendMockService.params.defaultDriver;
}));
beforeEach(inject(function($injector) {
ironicAPI =
$injector.get('horizon.app.core.openstack-service-api.ironic');
}));
it('defines the ironicAPI', function() {
expect(ironicAPI).toBeDefined();
});
afterEach(function() {
ironicBackendMockService.postTest();
});
describe('ironicAPI', function() {
it('service API', function() {
expect(Object.getOwnPropertyNames(ironicAPI).sort())
.toEqual(IRONIC_API_PROPERTIES.sort());
});
it('getDrivers', function() {
ironicAPI.getDrivers()
.then(function(drivers) {
expect(drivers.length).toBeGreaterThan(0);
angular.forEach(drivers, function(driver) {
expect(driver.name).toBeDefined();
});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('createNode - Minimal input data', function() {
createNode({driver: defaultDriver})
.then(function(node) {
expect(node.driver).toEqual(defaultDriver);
expect(node).toEqual(ironicBackendMockService.getNode(node.uuid));
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('createNode - Missing input data', function() {
createNode({})
.then(failTest);
ironicBackendMockService.flush();
});
it('getNode', function() {
createNode({driver: defaultDriver})
.then(function(node1) {
ironicAPI.getNode(node1.uuid).then(function(node2) {
expect(node2).toEqual(node1);
});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('deleteNode', function() {
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.deleteNode(node.uuid).then(function() {
return node;
});
})
.then(function(node) {
expect(
ironicBackendMockService.getNode(node.uuid)).toBe(undefined);
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('deleteNode - nonexistent node', function() {
ironicAPI.deleteNode(0)
.then(failTest);
ironicBackendMockService.flush();
});
it('updateNode - resource_class', function() {
createNode({driver: defaultDriver})
.then(function(node) {
return ironicAPI.updateNode(
node.uuid,
[{op: "replace",
path: "/resource_class",
value: "some-resource-class"}]).then(
function(node) {
return node;
});
})
.then(function(node) {
expect(node.resource_class).toEqual("some-resource-class");
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeGetConsole - console enabled', function() {
createNode({driver: defaultDriver,
console_enabled: true})
.then(function(node) {
expect(node.console_enabled).toEqual(true);
return node;
})
.then(function(node) {
return ironicAPI.nodeGetConsole(node.uuid).then(
function(consoleData) {
return {node: node, consoleData: consoleData};
});
})
.then(function(data) {
expect(data.consoleData.console_enabled).toEqual(true);
expect(data.consoleData.console_info.console_type)
.toEqual(ironicBackendMockService.params.consoleType);
expect(data.consoleData.console_info.url)
.toEqual(ironicBackendMockService.nodeGetConsoleUrl(
data.node.uuid));
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeGetConsole - console not enabled', function() {
createNode({driver: defaultDriver,
console_enabled: false})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.then(function(node) {
return ironicAPI.nodeGetConsole(node.uuid);
})
.then(function(consoleData) {
expect(consoleData).toEqual(
{console_enabled: false,
console_info: null});
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeSetConsoleMode - Toggle console mode', function() {
createNode({driver: defaultDriver,
console_enabled: false})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.then(function(node) {
ironicAPI.nodeSetConsoleMode(node.uuid, true);
return node;
})
.then(function(node) {
return ironicAPI.getNode(node.uuid);
})
.then(function(node) {
expect(node.console_enabled).toEqual(true);
return node;
})
// Toggle back
.then(function(node) {
ironicAPI.nodeSetConsoleMode(node.uuid, false);
return node;
})
.then(function(node) {
return ironicAPI.getNode(node.uuid);
})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.catch(failTest);
ironicBackendMockService.flush();
});
it('nodeSetConsoleMode - Redundant console set', function() {
createNode({driver: defaultDriver,
console_enabled: false})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.then(function(node) {
ironicAPI.nodeSetConsoleMode(node.uuid, false);
return node;
})
.then(function(node) {
return ironicAPI.getNode(node.uuid);
})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
});
ironicBackendMockService.flush();
});
it('getBootDevice', function() {
createNode({driver: defaultDriver})
.then(function(node) {
expect(node.console_enabled).toEqual(false);
return node;
})
.then(function(node) {
return ironicAPI.getBootDevice(node.uuid)
.then(function(bootDevice) {
return bootDevice;
});
})
.then(function(bootDevice) {
expect(bootDevice).toEqual(
ironicBackendMockService.params.bootDevice);
});
ironicBackendMockService.flush();
});
});
});
})();

View File

@ -0,0 +1,52 @@
/*
* Copyright 2017 Cray Inc.
*
* 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.
*/
/**
@description Global data used by unit tests.
*/
/* exported BASE_NODE_CONTROLLER_PROPERTIES */
var BASE_NODE_CONTROLLER_PROPERTIES = [
'_getImages',
'_loadDrivers',
'_sortDriverProperties',
'cancel',
'collectionCheckPropertyUnique',
'collectionDeleteProperty',
'driverProperties',
'driverPropertyGroups',
'drivers',
'images',
'isDriverPropertyActive',
'loadDriverProperties',
'loadingDriverProperties',
'modalTitle',
'node',
'propertyCollections',
'readyToSubmit',
'submitButtonTitle',
'validHostNameRegex'];
/* exported PROPERTY_COLLECTION_PROPERTIES */
var PROPERTY_COLLECTION_PROPERTIES = [
'id',
'formId',
'title',
'addPrompt',
'placeholder'
];

View File

@ -0,0 +1,14 @@
---
features:
- |
A backend mock has been added that enables better unit testing of the
Ironic API service and other Ironic-UI components. The mock utilizes
Angular $httpbackend handlers to intercept requests targeted at the
Ironic-UI server-side REST endpoints, and returns simulated responses.
- |
A number of unit tests have been developed that illustrate the use
of the backend mock.
- |
Although the backend mock is a work in progress, enough
functionality already exists to support test development for
the current set of in-progress features.