Merge pull request #3035 from ansible/e2e-websockets

E2E tests for websockets (and other things)

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-01-23 02:34:19 +00:00 committed by GitHub
commit cf9dffbaf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 134 additions and 47 deletions

View File

@ -16,7 +16,7 @@ const store = {};
*
* @param endpoint - The REST API url suffix.
* @param data - Attributes used to create a new endpoint.
* @param [unique=['name']] - An array of keys used to uniquely identify previously
* @param [unique] - An array of keys used to uniquely identify previously
* created resources from the endpoint.
*
*/
@ -52,7 +52,7 @@ const getOrCreate = (endpoint, data, unique = ['name']) => {
/* Retrieves an organization, and creates it if it does not exist.
*
* @param [namespace=session] - A unique name prefix for the organization.
* @param [namespace] - A unique name prefix for the organization.
*
*/
const getOrganization = (namespace = session) => getOrCreate('/organizations/', {
@ -63,7 +63,7 @@ const getOrganization = (namespace = session) => getOrCreate('/organizations/',
/* Retrieves an inventory, and creates it if it does not exist.
* Also creates an organization with the same name prefix if needed.
*
* @param [namespace=session] - A unique name prefix for the inventory.
* @param [namespace] - A unique name prefix for the inventory.
*
*/
const getInventory = (namespace = session) => getOrganization(namespace)
@ -81,7 +81,7 @@ const getInventory = (namespace = session) => getOrganization(namespace)
/* Identical to getInventory except it provides a unique suffix,
* "*-inventory-nosource".
*
* @param[namespace=session] - A unique name prefix for the inventory.
* @param[namespace] - A unique name prefix for the inventory.
*/
const getInventoryNoSource = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate('/inventories/', {
@ -98,7 +98,7 @@ const getInventoryNoSource = (namespace = session) => getOrganization(namespace)
/* Retrieves a host with the given name prefix, and creates it if it does not exist.
* If an inventory does not exist with the same prefix, it is created as well.
*
* @param[namespace=session] - A unique name prefix for the host.
* @param[namespace] - A unique name prefix for the host.
*/
const getHost = (namespace = session) => getInventory(namespace)
.then(inventory => getOrCreate('/hosts/', {
@ -112,7 +112,7 @@ const getHost = (namespace = session) => getInventory(namespace)
* does not exist. If an organization does not exist with the same prefix, it is
* created as well.
*
* @param[namespace=session] - A unique name prefix for the host.
* @param[namespace] - A unique name prefix for the host.
*/
const getInventoryScript = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate('/inventory_scripts/', {
@ -126,7 +126,7 @@ const getInventoryScript = (namespace = session) => getOrganization(namespace)
* required dependent inventory and inventory script do not exist, they are also
* created.
*
* @param[namespace=session] - A unique name prefix for the inventory source.
* @param[namespace] - A unique name prefix for the inventory source.
*/
const getInventorySource = (namespace = session) => {
const promises = [
@ -146,7 +146,7 @@ const getInventorySource = (namespace = session) => {
/* Retrieves an AWS credential, and creates it if it does not exist.
*
* @param[namespace=session] - A unique name prefix for the AWS credential.
* @param[namespace] - A unique name prefix for the AWS credential.
*/
const getAdminAWSCredential = (namespace = session) => {
const promises = [
@ -176,7 +176,7 @@ const getAdminAWSCredential = (namespace = session) => {
/* Retrieves a machine credential, and creates it if it does not exist.
*
* @param[namespace=session] - A unique name prefix for the machine credential.
* @param[namespace] - A unique name prefix for the machine credential.
*/
const getAdminMachineCredential = (namespace = session) => {
const promises = [
@ -200,7 +200,7 @@ const getAdminMachineCredential = (namespace = session) => {
* If an organization does not exist with the same prefix, it is
* created as well.
*
* @param[namespace=session] - A unique name prefix for the team.
* @param[namespace] - A unique name prefix for the team.
*/
const getTeam = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/teams/`, {
@ -213,7 +213,7 @@ const getTeam = (namespace = session) => getOrganization(namespace)
* name prefix. If an organization does not exist with the same prefix, it is
* created as well.
*
* @param[namespace=session] - A unique name prefix for the smart inventory.
* @param[namespace] - A unique name prefix for the smart inventory.
*/
const getSmartInventory = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate('/inventories/', {
@ -228,7 +228,7 @@ const getSmartInventory = (namespace = session) => getOrganization(namespace)
* name prefix. If an organization does not exist with the same prefix, it is
* created as well.
*
* @param[namespace=session] - A unique name prefix for the notification template.
* @param[namespace] - A unique name prefix for the notification template.
*/
const getNotificationTemplate = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/notification_templates/`, {
@ -246,15 +246,21 @@ const getNotificationTemplate = (namespace = session) => getOrganization(namespa
* name prefix. If an organization does not exist with the same prefix, it is
* created as well.
*
* @param[namespace=session] - A unique name prefix for the host.
* @param[namespace] - A unique name prefix for the host.
* @param[scmUrl] - The url of the repository.
* @param[scmType] - The type of scm (git, etc.)
*/
const getProject = (namespace = session) => getOrganization(namespace)
const getProject = (
namespace = session,
scmUrl = 'https://github.com/ansible/ansible-tower-samples',
scmType = 'git'
) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/projects/`, {
name: `${namespace}-project`,
description: namespace,
organization: organization.id,
scm_url: 'https://github.com/ansible/ansible-tower-samples',
scm_type: 'git'
scm_url: `${scmUrl}`,
scm_type: `${scmType}`
}));
const waitForJob = endpoint => {
@ -297,9 +303,15 @@ const getUpdatedProject = (namespace = session) => getProject(namespace)
* name prefix. This function also runs getOrCreate for an inventory,
* credential, and project with the same prefix.
*
* @param[namespace=session] - A unique name prefix for the job template.
*/
const getJobTemplate = (namespace = session) => {
* @param [namespace] - Name prefix for associated dependencies.
* @param [playbook] - Playbook for the job template.
* @param [name] - Unique name prefix for the job template.
* */
const getJobTemplate = (
namespace = session,
playbook = 'hello_world.yml',
name = `${namespace}-job-template`
) => {
const promises = [
getInventory(namespace),
getAdminMachineCredential(namespace),
@ -308,20 +320,26 @@ const getJobTemplate = (namespace = session) => {
return Promise.all(promises)
.then(([inventory, credential, project]) => getOrCreate('/job_templates/', {
name: `${namespace}-job-template`,
name: `${name}`,
description: namespace,
inventory: inventory.id,
credential: credential.id,
project: project.id,
playbook: 'hello_world.yml',
playbook: `${playbook}`,
}));
};
/* Similar to getJobTemplate, except that it also launches the job.
*
* @param[namespace=session] - A unique name prefix for the host.
* @param[namespace] - A unique name prefix for the job and its dependencies.
* @param[playbook] - The playbook file to be run by the job template.
* @param[name] - A unique name for the job template.
*/
const getJob = (namespace = session) => getJobTemplate(namespace)
const getJob = (
namespace = session,
playbook = 'hello_world.yml',
name = `${namespace}-job-template`
) => getJobTemplate(namespace, playbook, name)
.then(template => {
const launchURL = template.related.launch;
return post(launchURL, {}).then(response => {
@ -334,7 +352,7 @@ const getJob = (namespace = session) => getJobTemplate(namespace)
* name prefix. If an organization does not exist with the same prefix, it is
* created as well. A basic workflow node setup is also created.
*
* @param[namespace=session] - A unique name prefix for the workflow template.
* @param[namespace] - A unique name prefix for the workflow template.
*/
const getWorkflowTemplate = (namespace = session) => {
const workflowTemplatePromise = getOrganization(namespace)
@ -380,7 +398,7 @@ const getWorkflowTemplate = (namespace = session) => {
* name prefix. If an organization does not exist with the same prefix,
* it is also created.
*
* @param[namespace=session] - A unique name prefix for the auditor.
* @param[namespace] - A unique name prefix for the auditor.
*/
const getAuditor = (namespace = session) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/users/`, {
@ -398,23 +416,15 @@ const getAuditor = (namespace = session) => getOrganization(namespace)
* name prefix. If an organization does not exist with the same prefix,
* it is also created.
*
* @param[namespace=session] - A unique name prefix for the user.
* @param[namespace] - A unique name prefix for the user's organization.
* @param[username] - A unique name for the user.
*/
const getUser = (namespace = session) => getOrganization(namespace)
const getUser = (
namespace = session,
username = `user-${uuid().substr(0, 8)}`
) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/users/`, {
username: `user-${uuid().substr(0, 8)}`,
organization: organization.id,
first_name: 'firstname',
last_name: 'lastname',
email: 'null@ansible.com',
is_superuser: false,
is_system_auditor: false,
password: AWX_E2E_PASSWORD
}, ['username']));
const getUserExact = (namespace = session, name) => getOrganization(namespace)
.then(organization => getOrCreate(`/organizations/${organization.id}/users/`, {
username: `${name}`,
username: `${username}`,
organization: organization.id,
first_name: 'firstname',
last_name: 'lastname',
@ -428,7 +438,7 @@ const getUserExact = (namespace = session, name) => getOrganization(namespace)
* If a job template or organization does not exist with the same
* prefix, they are also created.
*
* @param[namespace=session] - A unique name prefix for the template admin.
* @param[namespace] - A unique name prefix for the template admin.
*/
const getJobTemplateAdmin = (namespace = session) => {
const rolePromise = getJobTemplate(namespace)
@ -457,7 +467,7 @@ const getJobTemplateAdmin = (namespace = session) => {
* If a job template or organization does not exist with the same
* prefix, they are also created.
*
* @param[namespace=session] - A unique name prefix for the project admin.
* @param[namespace] - A unique name prefix for the project admin.
*/
const getProjectAdmin = (namespace = session) => {
const rolePromise = getUpdatedProject(namespace)
@ -485,7 +495,7 @@ const getProjectAdmin = (namespace = session) => {
/* Retrieves a inventory source schedule, and creates it if it does not exist.
* If an inventory source does not exist with the same prefix, it is also created.
*
* @param[namespace=session] - A unique name prefix for the schedule.
* @param[namespace] - A unique name prefix for the schedule.
*/
const getInventorySourceSchedule = (namespace = session) => getInventorySource(namespace)
.then(source => getOrCreate(source.related.schedules, {
@ -497,7 +507,7 @@ const getInventorySourceSchedule = (namespace = session) => getInventorySource(n
/* Retrieves a job template schedule, and creates it if it does not exist.
* If an job template does not exist with the same prefix, it is also created.
*
* @param[namespace=session] - A unique name prefix for the schedule.
* @param[namespace] - A unique name prefix for the schedule.
*/
const getJobTemplateSchedule = (namespace = session) => getJobTemplate(namespace)
.then(template => getOrCreate(template.related.schedules, {
@ -529,6 +539,5 @@ module.exports = {
getTeam,
getUpdatedProject,
getUser,
getUserExact,
getWorkflowTemplate,
};

View File

@ -1,6 +1,6 @@
import {
getOrganization,
getUserExact,
getUser,
getTeam,
} from '../fixtures';
@ -53,7 +53,7 @@ const readOrgPermissionResults = `//*[@id="permissions_table"]//*[text()="${name
module.exports = {
before: (client, done) => {
const resources = [
getUserExact(namespace, `${namespace}-user`),
getUser(namespace, `${namespace}-user`),
getOrganization(namespace),
getTeam(namespace),
];

View File

@ -0,0 +1,78 @@
/* Websocket tests. These tests verify that the sparkline (colored box rows which
* display job status) update correctly as the jobs progress.
*/
import {
getInventorySource,
getProject,
getJob
} from '../fixtures';
let data;
const spinny = '//*[contains(@class, "spinny")]';
const dashboard = '//at-side-nav-item[contains(@name, "DASHBOARD")]';
// UI elements for recently run job templates on the dashboard.
const successfulJt = '//a[contains(text(), "test-websockets-successful")]/../..';
const failedJt = '//a[contains(text(), "test-websockets-failed")]/../..';
const sparklineIcon = '//div[contains(@class, "SmartStatus-iconContainer")]';
// Sparkline icon statuses.
// Running is blinking green, successful is green, fail/error/cancellation is red.
const running = '//div[@ng-show="job.status === \'running\'"]';
const success = '//div[contains(@class, "SmartStatus-iconIndicator--success")]';
const fail = '//div[contains(@class, "SmartStatus-iconIndicator--failed")]';
module.exports = {
before: (client, done) => {
// Jobs only display on the dashboard if they have been run at least once.
const resources = [
getInventorySource('test-websockets'),
getProject('test-websockets', 'https://github.com/ansible/test-playbooks'),
// launch job templates once before running tests.
getJob('test-websockets', 'debug.yml', 'test-websockets-successful', done),
getJob('test-websockets', 'fail_unless.yml', 'test-websockets-failed', done)
];
Promise.all(resources)
.then(([inventory, project, jt1, jt2]) => {
data = { inventory, project, jt1, jt2 };
done();
});
client
.login()
.waitForAngular()
.resizeWindow(1200, 1000);
},
'Test job template status updates for a successful job on dashboard': client => {
client.useXpath().findThenClick(dashboard);
getJob('test-websockets', 'debug.yml', 'test-websockets-successful');
client.expect.element(spinny).to.not.be.visible.before(5000);
client.expect.element(`${sparklineIcon}[1]${running}`)
.to.be.visible.before(5000);
// Allow a maximum amount of 30 seconds for the job to complete.
client.expect.element(`${successfulJt}${sparklineIcon}[1]${success}`)
.to.be.present.after(30000);
},
'Test job template status updates for a failed job on dashboard': client => {
client.useXpath().findThenClick(dashboard);
getJob('test-websockets', 'fail_unless.yml', 'test-websockets-failed');
client.expect.element(spinny).to.not.be.visible.before(5000);
client.expect.element(`${sparklineIcon}[1]${running}`)
.to.be.visible.before(5000);
// Allow a maximum amount of 30 seconds for the job to complete.
client.expect.element(`${failedJt}${sparklineIcon}[1]${fail}`)
.to.be.present.after(30000);
},
after: client => {
client.end();
}
};