add e2e tests

This commit is contained in:
Jake McDermott 2017-08-31 15:12:25 -04:00
parent 05b4b875e2
commit 19f96b1f6f
No known key found for this signature in database
GPG Key ID: 3B02CAD476EECB35
39 changed files with 2669 additions and 2 deletions

View File

@ -0,0 +1,22 @@
## AWX E2E
```shell
# setup
docker exec -i tools_awx_1 sh <<-EOSH
awx-manage createsuperuser --noinput --username=awx-e2e --email=null@ansible.com
awx-manage update_password --username=awx-e2e --password=password
EOSH
# run with with a live browser
npm --prefix awx/ui run e2e -- --env=debug
# setup a local webdriver cluster for test development
docker-compose \
-f awx/ui/client/test/e2e/cluster/docker-compose.yml \
-f awx/ui/client/test/e2e/cluster/devel-override.yml \
up --scale chrome=2 --scale firefox=0
# run headlessly with multiple workers on the cluster
AWX_E2E_URL='https://awx:8043' AWX_E2E_WORKERS=2 npm --prefix awx/ui run e2e
```
**Note:** Unless overridden in [settings](settings.js), tests will run against `localhost:8043`.

View File

@ -0,0 +1,13 @@
---
version: '2'
networks:
default:
external:
name: tools_default
services:
chrome:
external_links:
- 'tools_awx_1:awx'
firefox:
external_links:
- 'tools_awx_1:awx'

View File

@ -0,0 +1,23 @@
---
version: '2'
services:
hub:
image: selenium/hub
ports:
- '4444:4444'
chrome:
image: selenium/node-chrome
links:
- hub
volumes:
- /dev/shm:/dev/shm
environment:
HUB_PORT_4444_TCP_ADDR: hub
HUB_PORT_4444_TCP_PORT: 4444
firefox:
image: selenium/node-firefox
links:
- hub
environment:
HUB_PORT_4444_TCP_ADDR: hub
HUB_PORT_4444_TCP_PORT: 4444

View File

@ -0,0 +1,21 @@
exports.command = function(deps, script, callback) {
this.executeAsync(`let args = Array.prototype.slice.call(arguments,0);
return function(deps, done) {
let injector = angular.element('body').injector();
let loaded = deps.map(d => {
if (typeof(d) === "string") {
return injector.get(d);
} else {
return d;
}
});
(${script.toString()}).apply(this, loaded).then(done);
}.apply(this, args);`,
[deps],
function(result) {
if(typeof(callback) === "function") {
callback.call(this, result.value);
}
});
return this;
};

View File

@ -0,0 +1,48 @@
import { EventEmitter } from 'events';
import { inherits } from 'util';
const Login = function() {
EventEmitter.call(this);
}
inherits(Login, EventEmitter);
Login.prototype.command = function(username, password) {
username = username || this.api.globals.awxUsername;
password = password || this.api.globals.awxPassword;
const loginPage = this.api.page.login();
loginPage
.navigate()
.waitForElementVisible('@submit', this.api.globals.longWait)
.waitForElementNotVisible('div.spinny')
.setValue('@username', username)
.setValue('@password', password)
.click('@submit')
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
// tempoary hack while login issue is resolved
this.api.elements('css selector', '.LoginModal-alert', result => {
let alertVisible = false;
result.value.map(i => i.ELEMENT).forEach(id => {
this.api.elementIdDisplayed(id, ({ value }) => {
if (!alertVisible && value) {
alertVisible = true;
loginPage.setValue('@username', username)
loginPage.setValue('@password', password)
loginPage.click('@submit')
loginPage.waitForElementVisible('div.spinny')
loginPage.waitForElementNotVisible('div.spinny');
}
})
})
this.emit('complete');
})
};
module.exports = Login;

View File

@ -0,0 +1,20 @@
exports.command = function(callback) {
let self = this;
this.timeoutsAsyncScript(this.globals.asyncHookTimeout, function() {
this.executeAsync(function(done) {
if(angular && angular.getTestability) {
angular.getTestability(document.body).whenStable(done);
}
else {
done();
}
},
[],
function(result) {
if(typeof(callback) === "function") {
callback.call(self, result);
}
});
});
return this;
};

View File

@ -0,0 +1,42 @@
import path from 'path';
import chromedriver from 'chromedriver';
import { test_workers } from './settings.js';
const resolve = location => path.resolve(__dirname, location);
module.exports = {
src_folders: [resolve('tests')],
output_folder: resolve('reports'),
custom_commands_path: resolve('commands'),
page_objects_path: resolve('objects'),
globals_path: resolve('settings.js'),
test_settings: {
default: {
test_workers,
skip_testcases_on_fail: false,
desiredCapabilities: {
browserName: 'chrome'
}
},
debug: {
selenium_port: 9515,
selenium_host: 'localhost',
default_path_prefix: '',
test_workers: { enabled: false },
globals: {
before(done) {
chromedriver.start();
done();
},
after(done) {
chromedriver.stop();
done();
}
}
}
}
};

View File

@ -0,0 +1,10 @@
module.exports = {
url() {
return `${this.api.globals.awxURL}/#/activity_stream`
},
elements: {
title: '.List-titleText',
subtitle: '.List-titleLockup',
category: '#stream-dropdown-nav'
}
};

View File

@ -0,0 +1,65 @@
import actions from './sections/actions.js';
import breadcrumb from './sections/breadcrumb.js';
import createFormSection from './sections/createFormSection.js';
import createTableSection from './sections/createTableSection.js';
import header from './sections/header.js';
import search from './sections/search.js';
import pagination from './sections/pagination.js';
const addEditPanel = {
selector: 'div[ui-view="form"]',
elements: {
title: 'div[class="Form-title"]',
},
sections: {
details: createFormSection({
selector: '#credential_type_form',
labels: {
name: "Name",
description: "Description",
inputConfiguration: "Input Configuration",
injectorConfiguration: "Injector Configuration"
},
strategy: 'legacy'
})
}
};
const listPanel = {
selector: 'div[ui-view="list"]',
elements: {
add: '.List-buttonSubmit',
badge: 'div[class="List-titleBadge]',
titleText: 'div[class="List-titleText"]',
noitems: 'div[class="List-noItems"]'
},
sections: {
search,
pagination,
table: createTableSection({
elements: {
name: 'td[class~="name-column"]',
kind: 'td[class~="kind-column"]',
},
sections: {
actions
}
})
}
};
module.exports = {
url() {
return `${this.api.globals.awxURL}/#/credential_types`
},
sections: {
header,
breadcrumb,
add: addEditPanel,
edit: addEditPanel,
list: listPanel
}
};

View File

@ -0,0 +1,280 @@
import _ from 'lodash';
import actions from './sections/actions.js';
import breadcrumb from './sections/breadcrumb.js';
import createFormSection from './sections/createFormSection.js';
import createTableSection from './sections/createTableSection.js';
import dynamicSection from './sections/dynamicSection.js';
import header from './sections/header.js';
import lookupModal from './sections/lookupModal.js';
import navigation from './sections/navigation.js';
import pagination from './sections/pagination.js';
import permissions from './sections/permissions.js';
import search from './sections/search.js';
const common = createFormSection({
selector: 'form',
labels: {
name: "Name",
description: "Description",
organization: "Organization",
type: "Credential type"
}
});
const machine = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
username: "Username",
password: "Password",
sshKeyData: "SSH Private Key",
sshKeyUnlock: "Private Key Passphrase",
becomeMethod: "Privilege Escalation Method",
becomeUsername: "Privilege Escalation Username",
becomePassword: "Privilege Escalation Password"
}
});
const vault = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
vaultPassword: "Vault Password",
}
});
const scm = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
username: "Username",
password: "Password",
sshKeyData: "SCM Private Key",
sshKeyUnlock: "Private Key Passphrase",
}
});
const aws = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
accessKey: "Access Key",
secretKey: "Secret Key",
securityToken: "STS Token",
}
});
const gce = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
email: "Service Account Email Address",
project: "Project",
sshKeyData: "RSA Private Key",
}
});
const vmware = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
host: "VCenter Host",
username: "Username",
password: "Password",
}
});
const azureClassic = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
subscription: "Subscription ID",
sshKeyData: "Management Certificate",
}
});
const azure = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
subscription: "Subscription ID",
username: "Username",
password: "Password",
client: "Client ID",
secret: "Client Secret",
tenant: "Tenant ID",
}
});
const openStack = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
username: "Username",
password: "Password (API Key)",
host: "Host (Authentication URL)",
project: "Project (Tenant Name)",
domain: "Domain Name",
}
});
const rackspace = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
username: "Username",
password: "Password",
}
});
const cloudForms = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
host: "Cloudforms URL",
username: "Username",
password: "Password",
}
});
const network = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
sshKeyData: "SSH Private Key",
sshKeyUnlock: "Private Key Passphrase",
username: "Username",
password: "Password",
authorizePassword: "Authorize Password",
}
});
network.elements.authorize = {
locateStrategy: 'xpath',
selector: '//input[../p/text() = "Authorize"]'
};
const sat6 = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
host: "Satellite 6 URL",
username: "Username",
password: "Password",
}
});
const insights = createFormSection({
selector: '.at-InputGroup-inset',
labels: {
username: "Username",
password: "Password",
},
});
const details = _.merge({}, common, {
elements: {
cancel: '.btn[type="cancel"]',
save: '.btn[type="save"]',
},
sections: {
aws,
azure,
azureClassic,
cloudForms,
dynamicSection,
gce,
insights,
machine,
network,
rackspace,
sat6,
scm,
openStack,
vault,
vmware
},
commands: [{
custom({ name, inputs }) {
let labels = {};
inputs.fields.map(f => labels[f.id] = f.label);
let selector = '.at-InputGroup-inset';
let generated = createFormSection({ selector, labels });
let params = _.merge({ name }, generated);
return this.section.dynamicSection.create(params);
},
clear() {
this.clearValue('@name');
this.clearValue('@organization');
this.clearValue('@description');
this.clearValue('@type');
this.waitForElementNotVisible('.at-InputGroup-inset');
return this;
},
clearAndSelectType(type) {
this.clear();
this.setValue('@type', type);
this.waitForElementVisible('.at-InputGroup-inset');
return this;
}
}]
});
module.exports = {
url() {
return `${this.api.globals.awxURL}/#/credentials`
},
sections: {
header,
navigation,
breadcrumb,
lookupModal,
add: {
selector: 'div[ui-view="add"]',
sections: {
details
},
elements: {
title: 'h3[class="at-Panel-headingTitle"] span'
}
},
edit: {
selector: 'div[ui-view="edit"]',
sections: {
details,
permissions
},
elements: {
title: 'h3[class="at-Panel-headingTitle"] span'
}
},
list: {
selector: 'div[ui-view="list"]',
elements: {
badge: 'span[class~="badge"]',
title: 'div[class="List-titleText"]',
add: 'button[class~="List-buttonSubmit"]'
},
sections: {
search,
pagination,
table: createTableSection({
elements: {
name: 'td[class~="name-column"]',
kind: 'td[class~="kind-column"]'
},
sections: {
actions
}
})
}
},
}
};

View File

@ -0,0 +1,11 @@
module.exports = {
url() {
return `${this.api.globals.awxURL}/#/login`
},
elements: {
username: '#login-username',
password: '#login-password',
submit: '#login-button',
logo: '#main_menu_logo'
}
};

View File

@ -0,0 +1,16 @@
const actions = {
selector: 'td[class="List-actionsContainer"]',
elements: {
launch: 'i[class="fa icon-launch"]',
schedule: 'i[class="fa icon-schedule"]',
copy: 'i[class="fa icon-copy"]',
edit: 'i[class="fa icon-pencil"]',
delete: 'i[class="fa icon-trash-o"]',
view: 'i[class="fa fa-search-plus"]',
sync: 'i[class="fa fa-cloud-download"]',
test: 'i[class="fa fa-bell-o'
}
};
module.exports = actions;

View File

@ -0,0 +1,8 @@
const breadcrumb = {
selector: 'bread-crumb > div',
elements: {
activity: 'i[class$="icon-activity-stream"]'
}
};
module.exports = breadcrumb;

View File

@ -0,0 +1,103 @@
import { merge } from 'lodash';
const translated = "translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')";
const normalized = `normalize-space(${translated})`;
const inputContainerElements = {
lookup: 'button > i[class="fa fa-search"]',
error: '.at-InputMessage--rejected',
help: 'i[class$="fa-question-circle"]',
hint: '.at-InputLabel-hint',
label: 'label',
popover: '.at-Popover-container',
yaml: 'input[type="radio", value="yaml"]',
json: 'input[type="radio", value="json"]',
revert: 'a[class~="reset"]',
down: 'span[class^="fa-angle-down"]',
up: 'span[class^="fa-angle-up"]',
prompt: {
locateStrategy: 'xpath',
selector: `.//p[${normalized}='prompt on launch']/preceding-sibling::input`
},
show: {
locateStrategy: 'xpath',
selector: `.//button[${normalized}='show']`
},
hide: {
locateStrategy: 'xpath',
selector: `.//button[${normalized}='hide']`
},
on: {
locateStrategy: 'xpath',
selector: `.//button[${normalized}='on']`
},
off: {
locateStrategy: 'xpath',
selector: `.//button[${normalized}='off']`
}
};
const legacyContainerElements = merge({}, inputContainerElements, {
prompt: {
locateStrategy: 'xpath',
selector: `.//label[${normalized}='prompt on launch']/input`
},
error: 'div[class~="error"]',
popover: ':root div[id^="popover"]',
});
const generateInputSelectors = function(label, containerElements) {
// descend until span with matching text attribute is encountered
let span = `.//span[text()="${label}"]`;
// recurse upward until div with form-group in class attribute is encountered
let container = `${span}/ancestor::div[contains(@class, 'form-group')]`;
// descend until element with form-control in class attribute is encountered
let input = `${container}//*[contains(@class, 'form-control')]`;
let inputContainer = {
locateStrategy: 'xpath',
selector: container,
elements: containerElements
};
let inputElement = {
locateStrategy: 'xpath',
selector: input
};
return { inputElement, inputContainer };
};
const generatorOptions = {
default: inputContainerElements,
legacy: legacyContainerElements
};
const createFormSection = function({ selector, labels, strategy }) {
let options = generatorOptions[strategy || 'default'];
let formSection = {
selector,
sections: {},
elements: {}
};
for (let key in labels) {
let label = labels[key];
let { inputElement, inputContainer } = generateInputSelectors(label, options);
formSection.elements[key] = inputElement;
formSection.sections[key] = inputContainer;
};
return formSection;
};
module.exports = createFormSection;

View File

@ -0,0 +1,63 @@
import dynamicSection from './dynamicSection.js';
const header = {
selector: 'thead',
sections: {
dynamicSection
},
commands: [{
findColumnByText(text) {
return this.section.dynamicSection.create({
name: `column[${text}]`,
locateStrategy: 'xpath',
selector: `.//*[normalize-space(text())='${text}']/ancestor-or-self::th`,
elements: {
sortable: {
locateStrategy: 'xpath',
selector: './/*[contains(@class, "fa-sort")]'
},
sorted: {
locateStrategy: 'xpath',
selector: './/*[contains(@class, "fa-sort-")]'
},
}
});
}
}]
};
const createTableSection = function({ elements, sections, commands }) {
return {
selector: 'table',
sections: {
header,
dynamicSection
},
commands: [{
findRowByText(text) {
return this.section.dynamicSection.create({
elements,
sections,
commands,
name: `row[${text}]`,
locateStrategy: 'xpath',
selector: `.//tbody/tr/td//*[normalize-space(text())='${text}']/ancestor::tr`
});
},
waitForRowCount(count) {
let countReached = `tbody tr:nth-of-type(${count})`;
this.waitForElementPresent(countReached);
let countExceeded = `tbody tr:nth-of-type(${count + 1})`;
this.waitForElementNotPresent(countExceeded);
return this;
}
}]
};
};
module.exports = createTableSection;

View File

@ -0,0 +1,26 @@
const dynamicSection = {
selector: '.',
commands: [{
create({ name, locateStrategy, selector, elements, sections, commands }) {
let Section = this.constructor;
let options = Object.assign(Object.create(this), {
name,
locateStrategy,
elements,
selector,
sections,
commands
});
options.elements.self = {
locateStrategy: 'xpath',
selector: '.'
};
return new Section(options);
}
}]
};
module.exports = dynamicSection;

View File

@ -0,0 +1,12 @@
const header = {
selector: 'div[class="at-Layout-topNav"]',
elements: {
logo: 'div[class$="logo"] img',
user: 'i[class="fa fa-user"] + span',
documentation: 'i[class="fa fa-book"]',
logout: 'i[class="fa fa-power-off"]',
}
};
module.exports = header;

View File

@ -0,0 +1,27 @@
import createTableSection from './createTableSection.js';
import pagination from './pagination.js';
import search from './search.js';
const lookupModal = {
selector: '#form-modal',
elements: {
close: 'i[class="fa fa-times-circle"]',
title: 'div[class^="Form-title"]',
select: 'button[class*="save"]',
cancel: 'button[class*="cancel"]'
},
sections: {
search,
pagination,
table: createTableSection({
elements: {
name: 'td[class~="name-column"]',
selected: 'input[type="radio", value="1"]',
}
})
}
};
module.exports = lookupModal;

View File

@ -0,0 +1,25 @@
const navigation = {
selector: 'div[class^="at-Layout-side"]',
elements: {
expand: 'i[class="fa fa-bars"]',
dashboard: 'i[class="fa fa-tachometer"]',
jobs: 'i[class="fa fa-spinner"]',
schedules: 'i[class="fa fa-calendar"]',
portal: 'i[class="fa fa-columns"]',
projects: 'i[class="fa fa-folder-open"]',
credentials: 'i[class="fa fa-key"]',
credentialTypes: 'i[class="fa fa-list-alt"]',
inventories: 'i[class="fa fa-sitemap"]',
templates: 'i[class="fa fa-pencil-square-o"]',
organizations: 'i[class="fa fa-building"]',
users: 'i[class="fa fa-user"]',
teams: 'i[class="fa fa-users"]',
inventoryScripts: 'i[class="fa fa-code"]',
notifications: 'i[class="fa fa-bell"]',
managementJobs: 'i[class="fa fa-wrench"]',
instanceGroups: 'i[class="fa fa-server"]',
}
};
module.exports = navigation;

View File

@ -0,0 +1,13 @@
const pagination = {
selector: 'paginate div',
elements: {
first: 'i[class="fa fa-angle-double-left"]',
previous: 'i[class="fa fa-angle-left"]',
next: 'i[class="fa fa-angle-right"]',
last: 'i[class="fa fa-angle-double-right"]',
pageCount: 'span[class~="pageof"]',
itemCount: 'span[class~="itemsOf"]',
}
};
module.exports = pagination;

View File

@ -0,0 +1,30 @@
import actions from './actions.js';
import createTableSection from './createTableSection.js';
import pagination from './pagination.js';
import search from './search.js';
const permissions = {
selector: 'div[ui-view="related"]',
elements: {
add: 'button[class="btn List-buttonSubmit"]',
badge: 'div[class="List-titleBadge]',
titleText: 'div[class="List-titleText"]',
noitems: 'div[class="List-noItems"]'
},
sections: {
search,
pagination,
table: createTableSection({
elements: {
username: 'td[class~="username"]',
roles: 'td role-list:nth-of-type(1)',
teamRoles: 'td role-list:nth-of-type(2)'
},
sections: { actions }
})
}
};
module.exports = permissions;

View File

@ -0,0 +1,12 @@
const search = {
selector: 'smart-search',
elements: {
clearAll: '.SmartSearch-clearAll',
searchButton: '.SmartSearch-searchButton',
input: '.SmartSearch-input',
tags: '.SmartSearch-tagContainer'
}
};
module.exports = search;

View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('babel-register');
require('nightwatch/bin/runner.js');

View File

@ -0,0 +1,29 @@
const AWX_E2E_URL = process.env.AWX_E2E_URL || 'https://localhost:8043';
const AWX_E2E_USERNAME = process.env.AWX_E2E_USERNAME || 'awx-e2e';
const AWX_E2E_PASSWORD = process.env.AWX_E2E_PASSWORD || 'password';
const AWX_E2E_SELENIUM_HOST = process.env.AWX_E2E_SELENIUM_HOST || 'localhost';
const AWX_E2E_SELENIUM_PORT = process.env.AWX_E2E_SELENIUM_PORT || 4444;
const AWX_E2E_TIMEOUT_SHORT = process.env.AWX_E2E_TIMEOUT_SHORT || 1000;
const AWX_E2E_TIMEOUT_MEDIUM = process.env.AWX_E2E_TIMEOUT_MEDIUM || 5000;
const AWX_E2E_TIMEOUT_LONG = process.env.AWX_E2E_TIMEOUT_LONG || 10000;
const AWX_E2E_TIMEOUT_ASYNC = process.env.AWX_E2E_TIMEOUT_ASYNC || 30000;
const AWX_E2E_WORKERS = process.env.AWX_E2E_WORKERS || 0;
module.exports = {
awxURL: AWX_E2E_URL,
awxUsername: AWX_E2E_USERNAME,
awxPassword: AWX_E2E_PASSWORD,
asyncHookTimeout: AWX_E2E_TIMEOUT_ASYNC,
longTmeout: AWX_E2E_TIMEOUT_LONG,
mediumTimeout: AWX_E2E_TIMEOUT_MEDIUM,
retryAssertionTimeout: AWX_E2E_TIMEOUT_MEDIUM,
selenium_host: AWX_E2E_SELENIUM_HOST,
selenium_port: AWX_E2E_SELENIUM_PORT,
shortTimeout: AWX_E2E_TIMEOUT_SHORT,
waitForConditionTimeout: AWX_E2E_TIMEOUT_MEDIUM,
test_workers: {
enabled: (AWX_E2E_WORKERS > 0),
workers: AWX_E2E_WORKERS
}
};

View File

@ -0,0 +1,32 @@
module.exports = {
before: function(client, done) {
const credentialTypes = client.page.credentialTypes();
client.login();
client.waitForAngular();
credentialTypes
.navigate(`${credentialTypes.url()}/add/`)
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentialTypes.section.add
.waitForElementVisible('@title', done);
},
'expected fields are present and enabled': function(client) {
const credentialTypes = client.page.credentialTypes();
const details = credentialTypes.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.section.inputConfiguration.expect.element('.CodeMirror').visible;
details.section.injectorConfiguration.expect.element('.CodeMirror').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@inputConfiguration').enabled;
details.expect.element('@injectorConfiguration').enabled;
client.end();
}
};

View File

@ -0,0 +1,135 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
credentials.section.add.section.details
.waitForElementVisible('@save')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Amazon Web Services', done);
},
'expected fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.section.aws.expect.element('@accessKey').visible;
details.section.aws.expect.element('@secretKey').visible;
details.section.aws.expect.element('@securityToken').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
details.section.aws.expect.element('@accessKey').enabled;
details.section.aws.expect.element('@secretKey').enabled;
details.section.aws.expect.element('@securityToken').enabled;
},
'required fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const required = [
details.section.name,
details.section.type,
details.section.aws.section.accessKey,
details.section.aws.section.secretKey
]
required.map(s => s.expect.element('@label').text.to.contain('*'));
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details
.clearAndSelectType('Amazon Web Services')
.setValue('@name', store.credential.name);
details.expect.element('@save').not.enabled;
details.section.aws.setValue('@accessKey', 'AAAAAAAAAAAAA');
details.section.aws.setValue('@secretKey', 'AAAAAAAAAAAAA');
details.expect.element('@save').enabled;
},
'create aws credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Amazon Web Services')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.aws
.setValue('@accessKey', 'ABCD123456789')
.setValue('@secretKey', '987654321DCBA');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const search = credentials.section.list.section.search;
const table = credentials.section.list.section.table;
search
.waitForElementVisible('@input')
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
table.waitForRowCount(1);
table.findRowByText(store.credential.name)
.waitForElementVisible('@self');
client.end();
}
};

View File

@ -0,0 +1,176 @@
import uuid from 'uuid';
let store = {
credentialType: {
name: `credentialType-${uuid().substr(0,8)}`,
description: "custom cloud credential",
kind: "cloud",
inputs: {
fields: [
{
id: "project",
label: "Project",
type: "string",
help_text: "Name of your project"
},
{
id: "token",
label: "Token",
secret: true,
type: "string",
help_text: "help"
},
{
id: "secret_key_data",
label: "Secret Key",
type: "string",
secret: true,
multiline: true,
help_text: "help",
},
{
id: "public_key_data",
label: "Public Key",
type: "string",
secret: true,
multiline: true,
help_text: "help",
},
{
id: "secret_key_unlock",
label: "Private Key Passphrase",
type: "string",
secret: true,
//help_text: "help"
},
{
id: "color",
label: "Favorite Color",
choices: [
"",
"red",
"orange",
"yellow",
"green",
"blue",
"indigo",
"violet"
],
help_text: "help",
},
],
required: ['project', 'token']
},
injectors: {
env: {
CUSTOM_CREDENTIAL_TOKEN: "{{ token }}"
}
}
}
};
const inputs = store.credentialType.inputs;
const fields = store.credentialType.inputs.fields;
const help = fields.filter(f => f.help_text);
const required = fields.filter(f => inputs.required.indexOf(f.id) > -1);
const strings = fields.filter(f => f.type === undefined || f.type === 'string');
const getObjects = function(client) {
let credentials = client.page.credentials();
let details = credentials.section.add.section.details;
let type = details.custom(store.credentialType);
return { credentials, details, type };
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
client.login();
client.waitForAngular();
client.inject([store.credentialType, 'CredentialTypeModel'], (data, model) => {
return new model().http.post(data);
},
({ data }) => {
store.credentialType.response = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
credentials.section.add.section.details
.waitForElementVisible('@save')
.setValue('@name', `cred-${uuid()}`)
.setValue('@type', store.credentialType.name, done);
},
'all fields are visible': function(client) {
let { type } = getObjects(client);
fields.map(f => type.expect.element(`@${f.id}`).visible);
},
'helplinks open popovers showing expected content': function(client) {
let { type } = getObjects(client);
help.map(f => {
let group = type.section[f.id];
group.expect.element('@popover').not.visible;
group.click('@help');
group.expect.element('@popover').visible;
group.expect.element('@popover').text.to.contain(f.help_text);
group.click('@help');
});
help.map(f => {
let group = type.section[f.id];
group.expect.element('@popover').not.visible;
});
},
'secret field buttons hide and unhide input': function(client) {
let { type } = getObjects(client);
let secrets = strings.filter(f => f.secret && !f.multiline);
secrets.map(f => {
let group = type.section[f.id];
let input = `@${f.id}`;
group.expect.element('@show').visible;
group.expect.element('@hide').not.present;
type.setValue(input, 'SECRET');
type.expect.element(input).text.equal('');
group.click('@show');
group.expect.element('@show').not.present;
group.expect.element('@hide').visible;
type.expect.element(input).value.contain('SECRET');
group.click('@hide');
group.expect.element('@show').visible;
group.expect.element('@hide').not.present;
type.expect.element(input).text.equal('');
})
},
'required fields show * symbol': function(client) {
let { type } = getObjects(client);
required.map(f => {
let group = type.section[f.id];
group.expect.element('@label').text.to.contain('*');
});
client.end();
}
};

View File

@ -0,0 +1,172 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details
.waitForElementVisible('@save')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Google Compute Engine', done);
},
'expected fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.section.gce.expect.element('@email').visible;
details.section.gce.expect.element('@sshKeyData').visible;
details.section.gce.expect.element('@project').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
details.section.gce.expect.element('@email').enabled;
details.section.gce.expect.element('@sshKeyData').enabled;
details.section.gce.expect.element('@project').enabled;
details.section.organization.expect.element('@lookup').visible;
},
'required fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const required = [
details.section.name,
details.section.type,
details.section.gce.section.email,
details.section.gce.section.sshKeyData
]
required.map(s => s.expect.element('@label').text.to.contain('*'));
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details
.clearAndSelectType('Google Compute Engine')
.setValue('@name', store.credential.name);
details.expect.element('@save').not.enabled;
details.section.gce
.setValue('@email', 'abc@123.com')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
details.expect.element('@save').enabled;
},
'error displayed for invalid ssh key data': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyData = details.section.gce.section.sshKeyData;
details
.clearAndSelectType('Google Compute Engine')
.setValue('@name', store.credential.name);
details.section.gce
.setValue('@email', 'abc@123.com')
.setValue('@sshKeyData', 'invalid');
details.click('@save');
sshKeyData.expect.element('@error').visible;
sshKeyData.expect.element('@error').text.to.contain('Invalid certificate or key');
details.section.gce.clearValue('@sshKeyData');
sshKeyData.expect.element('@error').visible;
sshKeyData.expect.element('@error').text.to.contain('Please enter a value');
details.section.gce.setValue('@sshKeyData', 'AAAA');
sshKeyData.expect.element('@error').not.present;
},
'create gce credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Google Compute Engine')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.gce
.setValue('@email', 'abc@123.com')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,134 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details
.waitForElementVisible('@save')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Insights', done);
},
'expected fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.section.insights.expect.element('@username').visible;
details.section.insights.expect.element('@password').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
details.section.insights.expect.element('@username').enabled;
details.section.insights.expect.element('@password').enabled;
},
'required fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const required = [
details.section.name,
details.section.type,
details.section.insights.section.username,
details.section.insights.section.password
]
required.map(s => s.expect.element('@label').text.to.contain('*'));
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details
.clearAndSelectType('Insights')
.setValue('@name', store.credential.name);
details.expect.element('@save').not.enabled;
details.section.insights
.setValue('@username', 'wrosellini')
.setValue('@password', 'quintus');
details.expect.element('@save').enabled;
},
'create insights credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Insights')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.insights
.setValue('@username', 'wrosellini')
.setValue('@password', 'quintus');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,183 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details.waitForElementVisible('@save', done);
},
'common fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
},
'required common fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.section.name.expect.element('@label').text.to.contain('*');
details.section.type.expect.element('@label').text.to.contain('*');
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@save').not.enabled;
details
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Machine');
details.expect.element('@save').enabled;
},
'machine credential fields are visible after choosing type': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const machine = details.section.machine;
machine.expect.element('@username').visible;
machine.expect.element('@password').visible;
machine.expect.element('@becomeUsername').visible;
machine.expect.element('@becomePassword').visible;
machine.expect.element('@sshKeyData').visible;
machine.expect.element('@sshKeyUnlock').visible;
},
'error displayed for invalid ssh key data': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyData = details.section.machine.section.sshKeyData;
details
.clearAndSelectType('Machine')
.setValue('@name', store.credential.name);
details.section.machine.setValue('@sshKeyData', 'invalid');
details.click('@save');
sshKeyData.expect.element('@error').visible;
sshKeyData.expect.element('@error').text.to.contain('Invalid certificate or key');
details.section.machine.clearValue('@sshKeyData');
sshKeyData.expect.element('@error').not.present;
},
'error displayed for unencrypted ssh key with passphrase': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyUnlock = details.section.machine.section.sshKeyUnlock;
details
.clearAndSelectType('Machine')
.setValue('@name', store.credential.name);
details.section.machine
.setValue('@sshKeyUnlock', 'password')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
details.click('@save');
sshKeyUnlock.expect.element('@error').visible;
sshKeyUnlock.expect.element('@error').text.to.contain('not encrypted');
details.section.machine.clearValue('@sshKeyUnlock');
sshKeyUnlock.expect.element('@error').not.present;
},
'create machine credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Machine')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.machine
.setValue('@username', 'dsarif')
.setValue('@password', 'freneticpny')
.setValue('@becomeMethod', 'sudo')
.setValue('@becomeUsername', 'dsarif')
.setValue('@becomePassword', 'freneticpny')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,208 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details.waitForElementVisible('@save', done);
},
'common fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
},
'required common fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.section.name.expect.element('@label').text.to.contain('*');
details.section.type.expect.element('@label').text.to.contain('*');
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@save').not.enabled;
details
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Network');
details.section.network
.waitForElementVisible('@username')
.setValue('@username', 'sgrimes');
details.expect.element('@save').enabled;
},
'network credential fields are visible after choosing type': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const network = details.section.network;
network.expect.element('@username').visible;
network.expect.element('@password').visible;
network.expect.element('@authorizePassword').visible;
network.expect.element('@sshKeyData').visible;
network.expect.element('@sshKeyUnlock').visible;
},
'error displayed for invalid ssh key data': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyData = details.section.network.section.sshKeyData;
details
.clearAndSelectType('Network')
.setValue('@name', store.credential.name);
details.section.network
.setValue('@username', 'sgrimes')
.setValue('@sshKeyData', 'invalid');
details.click('@save');
sshKeyData.expect.element('@error').visible;
sshKeyData.expect.element('@error').text.to.contain('Invalid certificate or key');
details.section.network.clearValue('@sshKeyData');
sshKeyData.expect.element('@error').not.present;
},
'error displayed for unencrypted ssh key with passphrase': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyUnlock = details.section.network.section.sshKeyUnlock;
details
.clearAndSelectType('Network')
.setValue('@name', store.credential.name);
details.section.network
.setValue('@username', 'sgrimes')
.setValue('@sshKeyUnlock', 'password')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
details.click('@save');
sshKeyUnlock.expect.element('@error').visible;
sshKeyUnlock.expect.element('@error').text.to.contain('not encrypted');
details.section.network.clearValue('@sshKeyUnlock');
sshKeyUnlock.expect.element('@error').not.present;
},
'error displayed for authorize password without authorize enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const authorizePassword = details.section.network.section.authorizePassword;
details
.clearAndSelectType('Network')
.setValue('@name', store.credential.name);
details.section.network
.setValue('@username', 'sgrimes')
.setValue('@authorizePassword', 'ovid');
details.click('@save');
let expected = 'cannot be set unless "Authorize" is set';
authorizePassword.expect.element('@error').visible;
authorizePassword.expect.element('@error').text.to.equal(expected);
details.section.network.clearValue('@authorizePassword');
authorizePassword.expect.element('@error').not.present;
},
'create network credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Network')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.network
.setValue('@username', 'sgrimes')
.setValue('@password', 'ovid')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,177 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details.waitForElementVisible('@save', done);
},
'common fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
},
'required common fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.section.name.expect.element('@label').text.to.contain('*');
details.section.type.expect.element('@label').text.to.contain('*');
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@save').not.enabled;
details
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Source Control');
details.expect.element('@save').enabled;
},
'scm credential fields are visible after choosing type': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.section.scm.expect.element('@username').visible;
details.section.scm.expect.element('@password').visible;
details.section.scm.expect.element('@sshKeyData').visible;
details.section.scm.expect.element('@sshKeyUnlock').visible;
},
'error displayed for invalid ssh key data': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyData = details.section.scm.section.sshKeyData;
details
.clearAndSelectType('Source Control')
.setValue('@name', store.credential.name);
details.section.scm.setValue('@sshKeyData', 'invalid');
details.click('@save');
sshKeyData.expect.element('@error').visible;
sshKeyData.expect.element('@error').text.to.contain('Invalid certificate or key');
details.section.scm.clearValue('@sshKeyData');
sshKeyData.expect.element('@error').not.present;
},
'error displayed for unencrypted ssh key with passphrase': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const sshKeyUnlock = details.section.scm.section.sshKeyUnlock;
details
.clearAndSelectType('Source Control')
.setValue('@name', store.credential.name);
details.section.scm
.setValue('@sshKeyUnlock', 'password')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
details.click('@save');
sshKeyUnlock.expect.element('@error').visible;
sshKeyUnlock.expect.element('@error').text.to.contain('not encrypted');
details.section.scm.clearValue('@sshKeyUnlock');
sshKeyUnlock.expect.element('@error').not.present;
},
'create SCM credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Source Control')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.scm
.setValue('@username', 'gthorpe')
.setValue('@password', 'hydro')
.sendKeys('@sshKeyData', '-----BEGIN RSA PRIVATE KEY-----')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', 'AAAA')
.sendKeys('@sshKeyData', client.Keys.ENTER)
.sendKeys('@sshKeyData', '-----END RSA PRIVATE KEY-----');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.to.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,137 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
organization: {
name: `org-${testID}`
},
credential: {
name: `cred-${testID}`
},
};
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
client.inject([store, 'OrganizationModel'], (store, model) => {
return new model().http.post(store.organization);
},
({ data }) => {
store.organization = data;
});
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details
.waitForElementVisible('@save')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name)
.setValue('@type', 'Vault', done);
},
'expected fields are visible and enabled': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details.expect.element('@name').visible;
details.expect.element('@description').visible;
details.expect.element('@organization').visible;
details.expect.element('@type').visible;
details.section.vault.expect.element('@vaultPassword').visible;
details.expect.element('@name').enabled;
details.expect.element('@description').enabled;
details.expect.element('@organization').enabled;
details.expect.element('@type').enabled;
details.section.vault.expect.element('@vaultPassword').enabled;
},
'required fields display \'*\'': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const required = [
details.section.name,
details.section.type,
details.section.vault.section.vaultPassword,
]
required.map(s => s.expect.element('@label').text.to.contain('*'));
},
'save button becomes enabled after providing required fields': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details
.clearAndSelectType('Vault')
.setValue('@name', store.credential.name);
details.expect.element('@save').not.enabled;
details.section.vault.setValue('@vaultPassword', 'ch@ng3m3');
details.expect.element('@save').enabled;
},
'vault password field is disabled when prompt on launch is selected': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
details
.clearAndSelectType('Vault')
.setValue('@name', store.credential.name);
details.section.vault.expect.element('@vaultPassword').enabled;
details.section.vault.section.vaultPassword.click('@prompt');
details.section.vault.expect.element('@vaultPassword').not.enabled;
},
'create vault credential': function(client) {
const credentials = client.page.credentials();
const add = credentials.section.add;
const edit = credentials.section.edit;
add.section.details
.clearAndSelectType('Vault')
.setValue('@name', store.credential.name)
.setValue('@organization', store.organization.name);
add.section.details.section.vault.setValue('@vaultPassword', 'ch@ng3m3');
add.section.details.click('@save');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
edit.expect.element('@title').text.equal(store.credential.name);
},
'edit details panel remains open after saving': function(client) {
const credentials = client.page.credentials();
credentials.section.edit.expect.section('@details').visible;
},
'credential is searchable after saving': function(client) {
const credentials = client.page.credentials();
const row = '#credentials_table tbody tr';
credentials.section.list.section.search
.waitForElementVisible('@input', client.globals.longWait)
.setValue('@input', `name:${store.credential.name}`)
.click('@searchButton');
credentials.waitForElementNotPresent(`${row}:nth-of-type(2)`);
credentials.expect.element(row).text.contain(store.credential.name);
client.end();
}
};

View File

@ -0,0 +1,47 @@
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details
.waitForElementVisible('@save', done)
},
'open the lookup modal': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const modal = 'div[class="modal-body"]';
const title = 'div[class^="Form-title"]';
details.expect.element('@type').visible;
details.expect.element('@type').enabled;
details.section.type.expect.element('@lookup').visible;
details.section.type.expect.element('@lookup').enabled;
details.section.type.click('@lookup');
client.expect.element(modal).present;
let expected = 'SELECT CREDENTIAL TYPE';
client.expect.element(title).visible;
client.expect.element(title).text.equal(expected);
client.end();
}
};

View File

@ -0,0 +1,155 @@
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
client.login();
client.waitForAngular();
credentials.section.navigation
.waitForElementVisible('@credentials')
.click('@credentials');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.section.list
.waitForElementVisible('@add')
.click('@add');
details
.waitForElementVisible('@save', done);
},
'open the lookup modal': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const lookupModal = credentials.section.lookupModal;
details.expect.element('@organization').visible;
details.expect.element('@organization').enabled;
details.section.organization.expect.element('@lookup').visible;
details.section.organization.expect.element('@lookup').enabled;
details.section.organization.click('@lookup');
credentials.expect.section('@lookupModal').present;
let expected = 'SELECT ORGANIZATION';
lookupModal.expect.element('@title').visible;
lookupModal.expect.element('@title').text.equal(expected);
},
'select button is disabled until item is selected': function(client) {
const credentials = client.page.credentials();
const details = credentials.section.add.section.details;
const lookupModal = credentials.section.lookupModal;
const table = lookupModal.section.table;
details.section.organization.expect.element('@lookup').visible;
details.section.organization.expect.element('@lookup').enabled;
details.section.organization.click('@lookup');
credentials.expect.section('@lookupModal').present;
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(6) input[type="radio"]').not.present;
lookupModal.expect.element('@select').visible;
lookupModal.expect.element('@select').not.enabled;
table.click('tbody tr:nth-child(2)');
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').selected;
lookupModal.expect.element('@select').visible;
lookupModal.expect.element('@select').enabled;
},
'sort and unsort the table by name with an item selected': function(client) {
const credentials = client.page.credentials();
const lookupModal = credentials.section.lookupModal;
const table = lookupModal.section.table;
let column = table.section.header.findColumnByText('Name');
column.expect.element('@self').visible;
column.expect.element('@sortable').visible;
column.click('@self');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
column.click('@self');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
},
'use the pagination controls with an item selected': function(client) {
const credentials = client.page.credentials();
const lookupModal = credentials.section.lookupModal;
const table = lookupModal.section.table;
const pagination = lookupModal.section.pagination;
pagination.click('@next');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
pagination.click('@previous');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
pagination.click('@last');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
pagination.click('@previous');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
pagination.click('@first');
credentials.waitForElementVisible('div.spinny');
credentials.waitForElementNotVisible('div.spinny');
table.expect.element('tbody tr:nth-child(1) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(2) input[type="radio"]').selected;
table.expect.element('tbody tr:nth-child(3) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(4) input[type="radio"]').not.selected;
table.expect.element('tbody tr:nth-child(5) input[type="radio"]').not.selected;
client.end();
}
};

View File

@ -0,0 +1,32 @@
module.exports = {
beforeEach: function(client, done) {
const credentials = client.useCss().page.credentials();
client.login();
client.waitForAngular();
credentials
.navigate()
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny', done);
},
'activity link is visible and takes user to activity stream': function(client) {
const credentials = client.page.credentials();
const activityStream = client.page.activityStream();
credentials.expect.section('@breadcrumb').visible;
credentials.section.breadcrumb.expect.element('@activity').visible;
credentials.section.breadcrumb.click('@activity');
activityStream
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny')
.waitForElementVisible('@title')
.waitForElementVisible('@category');
activityStream.expect.element('@title').text.contain('CREDENTIALS');
activityStream.expect.element('@category').value.contain('credential');
client.end();
}
};

View File

@ -0,0 +1,97 @@
import uuid from 'uuid';
let testID = uuid().substr(0,8);
let store = {
auditor: {
username: `auditor-${testID}`,
first_name: 'auditor',
last_name: 'last',
email: 'null@ansible.com',
is_superuser: false,
is_system_auditor: true,
password: 'password'
},
adminCredential: {
name: `adminCredential-${testID}`,
description: `adminCredential-description-${testID}`,
inputs: {
username: 'username',
password: 'password',
security_token: 'AAAAAAAAAAAAAAAAAAAAAAAAAA'
}
},
created: {}
};
module.exports = {
before: function (client, done) {
const credentials = client.page.credentials();
client.login();
client.waitForAngular();
client.inject([store, '$http'], (store, $http) => {
let { adminCredential, auditor } = store;
return $http.get('/api/v2/me')
.then(({ data }) => {
let resource = 'Amazon%20Web%20Services+cloud';
adminCredential.user = data.results[0].id;
return $http.get(`/api/v2/credential_types/${resource}`);
})
.then(({ data }) => {
adminCredential.credential_type = data.id;
return $http.post('/api/v2/credentials/', adminCredential);
})
.then(({ data }) => {
adminCredential = data;
return $http.post('/api/v2/users/', auditor);
})
.then(({ data }) => {
auditor = data;
return { adminCredential, auditor };
});
},
({ adminCredential, auditor }) => {
store.created = { adminCredential, auditor };
done();
})
},
beforeEach: function (client) {
const credentials = client.useCss().page.credentials();
credentials
.login(store.auditor.username, store.auditor.password)
.navigate(`${credentials.url()}/${store.created.adminCredential.id}/`)
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
},
'verify an auditor\'s inputs are read-only': function (client) {
const credentials = client.useCss().page.credentials()
const details = credentials.section.edit.section.details;
let expected = store.created.adminCredential.name;
credentials.section.edit
.expect.element('@title').text.contain(expected);
client.elements('css selector', '.at-Input', inputs => {
inputs.value.map(o => o.ELEMENT).forEach(id => {
client.elementIdAttribute(id, 'disabled', ({ value }) => {
client.assert.equal(value, 'true');
});
});
});
client.end();
}
};

View File

@ -0,0 +1,57 @@
const columns = ['Name', 'Kind', 'Owners', 'Actions'];
const sortable = ['Name'];
const defaultSorted = ['Name'];
module.exports = {
before: function(client, done) {
const credentials = client.page.credentials();
client.login();
client.resizeWindow(1200, 800);
client.waitForAngular();
credentials
.navigate()
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
credentials.waitForElementVisible('#credentials_table', done);
},
'expected table columns are visible': function(client) {
const credentials = client.page.credentials();
const table = credentials.section.list.section.table;
columns.map(label => {
table.section.header.findColumnByText(label)
.expect.element('@self').visible;
});
},
'only fields expected to be sortable show sort icon': function(client) {
const credentials = client.page.credentials();
const table = credentials.section.list.section.table;
sortable.map(label => {
table.section.header.findColumnByText(label)
.expect.element('@sortable').visible;
});
},
'sort all columns expected to be sortable': function(client) {
const credentials = client.page.credentials();
const table = credentials.section.list.section.table;
sortable.map(label => {
let column = table.section.header.findColumnByText(label);
column.click('@self');
credentials
.waitForElementVisible('div.spinny')
.waitForElementNotVisible('div.spinny');
column.expect.element('@sorted').visible;
});
client.end();
}
};

View File

@ -29,7 +29,8 @@
"lint-dev": "./node_modules/.bin/nodemon --exec \"./node_modules/.bin/eslint -c .eslintrc.js .\" --watch \"client/components/**/*.js\"",
"dev": "./node_modules/.bin/webpack --config build/webpack.development.js --progress",
"watch": "./node_modules/.bin/webpack-dev-server --config build/webpack.watch.js --progress",
"production": "./node_modules/.bin/webpack --config build/webpack.production.js"
"production": "./node_modules/.bin/webpack --config build/webpack.production.js",
"e2e": "./client/test/e2e/runner.js --config ./client/test/e2e/nightwatch.conf.js"
},
"devDependencies": {
"angular-mocks": "~1.4.14",
@ -38,6 +39,7 @@
"babel-loader": "^7.1.2",
"babel-plugin-istanbul": "^2.0.0",
"babel-preset-es2015": "^6.24.1",
"chromedriver": "^2.31.0",
"clean-webpack-plugin": "^0.1.16",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.5",
@ -79,6 +81,7 @@
"load-grunt-configs": "^1.0.0",
"load-grunt-tasks": "^3.5.0",
"minimist": "^1.2.0",
"nightwatch": "^0.9.16",
"ngtemplate-loader": "^2.0.1",
"phantomjs-prebuilt": "^2.1.12",
"script-loader": "^0.7.0",
@ -123,4 +126,4 @@
"select2": "^4.0.2",
"sprintf-js": "^1.0.3"
}
}
}