ui: Initial Intention Permission Integration and acceptance testing (#9003)

This commit is contained in:
John Cowen 2020-10-23 17:26:06 +01:00 committed by GitHub
parent 5f0f8baef9
commit 0364f3abac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 412 additions and 93 deletions

View File

@ -144,7 +144,13 @@
</fieldset> </fieldset>
{{#if (eq (or item.Action '') '')}} {{#if (eq (or item.Action '') '')}}
<fieldset class="permissions"> <fieldset class="permissions">
<button type="button" onclick={{action (mut shouldShowPermissionForm) true}}>Add permission</button> <button
data-test-create-permission
type="button"
onclick={{action (mut shouldShowPermissionForm) true}}
>
Add permission
</button>
<h2>Permissions</h2> <h2>Permissions</h2>
{{#if (gt item.Permissions.length 0) }} {{#if (gt item.Permissions.length 0) }}
<div class="notice info"> <div class="notice info">
@ -210,6 +216,7 @@
<BlockSlot @name="actions"> <BlockSlot @name="actions">
<button <button
type="button" type="button"
data-test-intention-permission-submit
class="type-submit" class="type-submit"
disabled={{if (not this.permissionForm.isDirty) 'disabled'}} disabled={{if (not this.permissionForm.isDirty) 'disabled'}}
onclick={{queue (action this.permissionForm.submit) (action modal.close)}} onclick={{queue (action this.permissionForm.submit) (action modal.close)}}

View File

@ -15,22 +15,24 @@ as |group|>
)}} )}}
<fieldset> <fieldset>
<span class="label"> <div data-property="action">
Should this permission allow the source connect to the destination? <span class="label">
</span> Should this permission allow the source connect to the destination?
<div role="radiogroup" class={{if changeset.error.Action ' has-error'}}> </span>
{{#each intents as |intent|}} <div role="radiogroup" class={{if changeset.error.Action ' has-error'}}>
<label> {{#each intents as |intent|}}
<span>{{capitalize intent}}</span> <label>
<input <span>{{capitalize intent}}</span>
type="radio" <input
name="Action" type="radio"
value={{intent}} name="Action"
checked={{if (eq changeset.Action intent) 'checked'}} value={{intent}}
onchange={{action (changeset-set changeset 'Action') value="target.value"}} checked={{if (eq changeset.Action intent) 'checked'}}
/> onchange={{action (changeset-set changeset 'Action') value="target.value"}}
</label> />
{{/each}} </label>
{{/each}}
</div>
</div> </div>
</fieldset> </fieldset>
@ -133,6 +135,7 @@ as |group|>
</Consul::Intention::Permission::Header::Form> </Consul::Intention::Permission::Header::Form>
<button <button
data-test-add-header
type="button" type="button"
class="type-submit" class="type-submit"
disabled={{if (not this.headerForm.isDirty) 'disabled'}} disabled={{if (not this.headerForm.isDirty) 'disabled'}}

View File

@ -0,0 +1,46 @@
import { clickable } from 'ember-cli-page-object';
import { input, options, click, button } from 'consul-ui/tests/lib/page-object';
import powerSelect from 'consul-ui/components/power-select/pageobject';
import headersForm from 'consul-ui/components/consul/intention/permission/header/form/pageobject';
import headersList from 'consul-ui/components/consul/intention/permission/header/list/pageobject';
export default (scope = '.consul-intention-permission-form') => {
return {
scope: scope,
resetScope: true, // where we use the form it is in a modal layer
submit: {
resetScope: true,
scope: '.consul-intention-permission-modal [data-test-intention-permission-submit]',
click: clickable(),
},
Action: {
scope: '[data-property="action"]',
...options(['Allow', 'Deny']),
},
PathType: {
scope: '[data-property="pathtype"]',
...powerSelect(['NoPath', 'PrefixedBy', 'Exact', 'RegEx']),
},
Path: {
scope: '[data-property="path"] input',
...input(),
},
AllMethods: {
scope: '[data-property="allmethods"]',
...click(),
},
Headers: {
form: {
...headersForm(),
submit: {
resetScope: true,
scope: '[data-test-add-header]',
...button(),
},
},
list: {
...headersList(),
},
},
};
};

View File

@ -0,0 +1,20 @@
import { input } from 'consul-ui/tests/lib/page-object';
import powerSelect from 'consul-ui/components/power-select/pageobject';
export default (scope = '.consul-intention-permission-header-form') => {
return {
scope: scope,
HeaderType: {
scope: '[data-property="headertype"]',
...powerSelect(['ExactlyMatching', 'PrefixedBy', 'SuffixedBy', 'RegEx', 'IsPresent']),
},
Name: {
scope: '[data-property="name"] input',
...input(),
},
Value: {
scope: '[data-property="value"] input',
...input(),
},
};
};

View File

@ -0,0 +1,8 @@
import { collection } from 'ember-cli-page-object';
export default (scope = '.consul-intention-permission-header-list') => {
return {
scope: scope,
intentionPermissionHeaders: collection('[data-test-list-row]', {}),
};
};

View File

@ -0,0 +1,8 @@
import { collection } from 'ember-cli-page-object';
export default (scope = '.consul-intention-permission-list') => {
return {
scope: scope,
intentionPermissions: collection('[data-test-list-row]', {}),
};
};

View File

@ -0,0 +1,17 @@
import { clickable, isPresent } from 'ember-cli-page-object';
export default options => {
return {
present: isPresent('.ember-power-select-trigger'),
click: clickable('.ember-power-select-trigger'),
option: {
resetScope: true,
...options.reduce((prev, item, i) => {
prev[item] = {
click: clickable(`[data-option-index='${i}']`),
};
return prev;
}, {}),
},
};
};

View File

@ -0,0 +1,24 @@
@setupApplicationTest
Feature: dc / intentions / permissions / create: Intention Permission Create
Scenario:
Given 1 datacenter model with the value "datacenter"
When I visit the intention page for yaml
---
dc: datacenter
---
Then the url should be /datacenter/intentions/create
And the title should be "New Intention - Consul"
# Specifically set L7
And I click "[value='']"
And I click the permissions.create object
And I click the permissions.form.Action.option.Deny object
And I click the permissions.form.PathType object
And I click the permissions.form.PathType.option.PrefixedBy object
And I fillIn the permissions.form.Path object with value "/path"
And I fillIn the permissions.form.Headers.form.Name object with value "Name"
And I fillIn the permissions.form.Headers.form.Value object with value "/path/name"
And I click the permissions.form.Headers.form.submit object
And I see 1 of the permissions.form.Headers.list.intentionPermissionHeaders objects
And I click the permissions.form.submit object
And I see 1 of the permissions.list.intentionPermissions objects

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -1,17 +1,19 @@
import Inflector from 'ember-inflector';
import helpers from '@ember/test-helpers';
import $ from '-jquery';
import steps from 'consul-ui/tests/steps'; import steps from 'consul-ui/tests/steps';
import pages from 'consul-ui/tests/pages'; import pages from 'consul-ui/tests/pages';
import Inflector from 'ember-inflector';
import utils from '@ember/test-helpers';
import $ from '-jquery';
import api from 'consul-ui/tests/helpers/api'; import api from 'consul-ui/tests/helpers/api';
export default function({ assert, library }) { export default function({ assert, utils, library }) {
return steps({ return steps({
assert, assert,
utils,
library, library,
pages, pages,
utils, helpers,
api, api,
Inflector, Inflector,
$, $,

View File

@ -2,8 +2,12 @@
import Yadda from 'yadda'; import Yadda from 'yadda';
import YAML from 'js-yaml'; import YAML from 'js-yaml';
import { env } from '../env'; import { env } from '../env';
export default function(annotations, nspace, dict = new Yadda.Dictionary()) { export default utils => (annotations, nspace, dict = new Yadda.Dictionary()) => {
dict dict
.define('pageObject', /(\S+)/, function(path, cb) {
const $el = utils.find(path);
cb(null, $el);
})
.define('model', /(\w+)/, function(model, cb) { .define('model', /(\w+)/, function(model, cb) {
switch (model) { switch (model) {
case 'datacenter': case 'datacenter':
@ -89,4 +93,4 @@ export default function(annotations, nspace, dict = new Yadda.Dictionary()) {
}); });
} }
return dict; return dict;
} };

View File

@ -0,0 +1,39 @@
const mb = function(path) {
return function(obj) {
return (
path.map(function(prop) {
obj = obj || {};
if (isNaN(parseInt(prop))) {
return (obj = obj[prop]);
} else {
return (obj = obj.objectAt(parseInt(prop)));
}
}) && obj
);
};
};
let currentPage;
export const getCurrentPage = function() {
return currentPage;
};
export const setCurrentPage = function(page) {
currentPage = page;
return page;
};
export const find = function(path, page = currentPage) {
const parts = path.split('.');
const last = parts.pop();
let obj;
let parent = mb(parts)(page) || page;
if (typeof parent.objectAt === 'function') {
parent = parent.objectAt(0);
}
obj = parent[last];
if (typeof obj === 'undefined') {
throw new Error(`PageObject not found: The '${path}' object doesn't exist`);
}
if (typeof obj === 'function') {
obj = obj.bind(parent);
}
return obj;
};

View File

@ -4,7 +4,9 @@ import Yadda from 'yadda';
import { env } from '../../env'; import { env } from '../../env';
import api from './api'; import api from './api';
import getDictionary from '../dictionary'; import utils from './page';
import dictionary from '../dictionary';
const getDictionary = dictionary(utils);
const staticClassList = [...document.documentElement.classList]; const staticClassList = [...document.documentElement.classList];
const reset = function() { const reset = function() {
@ -46,21 +48,22 @@ const checkAnnotations = function(annotations, isScenario) {
if (env('CONSUL_NSPACES_ENABLED')) { if (env('CONSUL_NSPACES_ENABLED')) {
if (!annotations.notnamespaceable) { if (!annotations.notnamespaceable) {
return function(scenario, feature, yadda, yaddaAnnotations, library) { return function(scenario, feature, yadda, yaddaAnnotations, library) {
const stepDefinitions = library.default;
['', 'default', 'team-1', undefined].forEach(function(item) { ['', 'default', 'team-1', undefined].forEach(function(item) {
test(`Scenario: ${ test(`Scenario: ${
scenario.title scenario.title
} with the ${item === '' ? 'empty' : typeof item === 'undefined' ? 'undefined' : item} namespace set`, function(assert) { } with the ${item === '' ? 'empty' : typeof item === 'undefined' ? 'undefined' : item} namespace set`, function(assert) {
const libraries = library.default({
assert: assert,
library: Yadda.localisation.English.library(getDictionary(annotations, item)),
});
const scenarioContext = { const scenarioContext = {
ctx: { ctx: {
nspace: item, nspace: item,
}, },
}; };
const result = runTest(this, libraries, scenario.steps, scenarioContext); const libraries = stepDefinitions({
return result; assert: assert,
utils: utils,
library: Yadda.localisation.English.library(getDictionary(annotations, item)),
});
return runTest(this, libraries, scenario.steps, scenarioContext);
}); });
}); });
}; };
@ -70,14 +73,16 @@ const checkAnnotations = function(annotations, isScenario) {
} else { } else {
if (!annotations.onlynamespaceable) { if (!annotations.onlynamespaceable) {
return function(scenario, feature, yadda, yaddaAnnotations, library) { return function(scenario, feature, yadda, yaddaAnnotations, library) {
const stepDefinitions = library.default;
test(`Scenario: ${scenario.title}`, function(assert) { test(`Scenario: ${scenario.title}`, function(assert) {
const libraries = library.default({
assert: assert,
library: Yadda.localisation.English.library(getDictionary(annotations)),
});
const scenarioContext = { const scenarioContext = {
ctx: {}, ctx: {},
}; };
const libraries = stepDefinitions({
assert: assert,
utils: utils,
library: Yadda.localisation.English.library(getDictionary(annotations)),
});
return runTest(this, libraries, scenario.steps, scenarioContext); return runTest(this, libraries, scenario.steps, scenarioContext);
}); });
}; };

View File

@ -0,0 +1,32 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { create } from 'ember-cli-page-object';
import obj from 'consul-ui/components/consul/intention/permission/form/pageobject';
const PermissionForm = create(obj());
module('Integration | Component | consul/intention/permission/form', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`
<Consul::Intention::Permission::Form
as |api|>
</Consul::Intention::Permission::Form>
`);
await PermissionForm.Action.option.Deny.click();
await PermissionForm.PathType.click();
await PermissionForm.PathType.option.PrefixedBy.click();
assert.ok(PermissionForm.Path.present);
await PermissionForm.Path.fillIn('/path');
await PermissionForm.AllMethods.click();
});
});

View File

@ -0,0 +1,42 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { create } from 'ember-cli-page-object';
import obj from 'consul-ui/components/consul/intention/permission/header/form/pageobject';
const PermissionHeaderForm = create(obj());
module('Integration | Component | consul/intention/permission/header/form', function(hooks) {
setupRenderingTest(hooks);
test('when IsPresent is selected we only show validate the header name', async function(assert) {
this.set('PermissionHeaderForm', PermissionHeaderForm);
// Handle any actions with this.set('myAction', function(val) { ... });
await render(hbs`
<Consul::Intention::Permission::Header::Form
as |api|>
<Ref @target={{PermissionHeaderForm}} @name="api" @value={{api}} />
</Consul::Intention::Permission::Header::Form>
`);
assert.ok(PermissionHeaderForm.Name.present);
assert.ok(PermissionHeaderForm.Value.present);
await PermissionHeaderForm.HeaderType.click();
await PermissionHeaderForm.HeaderType.option.IsPresent.click();
assert.notOk(
PermissionHeaderForm.Value.present,
`Value isn't present when IsPresent is selected`
);
await PermissionHeaderForm.Name.fillIn('header');
assert.ok(PermissionHeaderForm.api.isDirty);
});
});

View File

@ -0,0 +1,32 @@
import { isPresent as present, fillable, clickable, property } from 'ember-cli-page-object';
export const input = function() {
return {
present: present(),
fillIn: fillable(),
};
};
export const button = function() {
return {
disabled: property('disabled'),
present: present(),
click: clickable(),
};
};
export const click = function() {
return {
present: present(),
click: clickable(),
};
};
export const options = function(options, selector = `input`) {
return {
option: options.reduce((prev, item, i) => {
prev[item] = {
present: present(),
click: clickable(selector, { at: i }),
};
return prev;
}, {}),
};
};

View File

@ -19,6 +19,8 @@ import createCreatable from 'consul-ui/tests/lib/page-object/createCreatable';
import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable'; import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable';
// components // components
import intentionPermissionForm from 'consul-ui/components/consul/intention/permission/form/pageobject';
import intentionPermissionList from 'consul-ui/components/consul/intention/permission/list/pageobject';
import pageFactory from 'consul-ui/components/hashicorp-consul/pageobject'; import pageFactory from 'consul-ui/components/hashicorp-consul/pageobject';
import radiogroup from 'consul-ui/components/radio-group/pageobject'; import radiogroup from 'consul-ui/components/radio-group/pageobject';
@ -170,7 +172,17 @@ export default {
intentions: create( intentions: create(
intentions(visitable, creatable, clickable, consulIntentionList, popoverSelect) intentions(visitable, creatable, clickable, consulIntentionList, popoverSelect)
), ),
intention: create(intention(visitable, submitable, deletable, cancelable)), intention: create(
intention(
visitable,
clickable,
submitable,
deletable,
cancelable,
intentionPermissionForm,
intentionPermissionList
)
),
nspaces: create(nspaces(visitable, creatable, consulNspaceList, popoverSelect)), nspaces: create(nspaces(visitable, creatable, consulNspaceList, popoverSelect)),
nspace: create( nspace: create(
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector) nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)

View File

@ -1,10 +1,25 @@
export default function(visitable, submitable, deletable, cancelable) { export default function(
return submitable( visitable,
cancelable( clickable,
deletable({ submitable,
visit: visitable(['/:dc/intentions/:intention', '/:dc/intentions/create']), deletable,
}) cancelable,
), permissionsForm,
'main' permissionsList
); ) {
return {
scope: 'main',
visit: visitable(['/:dc/intentions/:intention', '/:dc/intentions/create']),
permissions: {
create: {
scope: '[data-test-create-permission]',
click: clickable(),
},
form: permissionsForm(),
list: permissionsList(),
},
...submitable(),
...cancelable(),
...deletable(),
};
} }

View File

@ -18,9 +18,10 @@ import assertForm from './steps/assertions/form';
export default function({ export default function({
assert, assert,
utils,
library, library,
pages = {}, pages = {},
utils = {}, helpers = {},
api = {}, api = {},
Inflector = {}, Inflector = {},
$ = {}, $ = {},
@ -44,20 +45,6 @@ export default function({
return requests[n]; return requests[n];
}; };
}; };
const mb = function(path) {
return function(obj) {
return (
path.map(function(prop) {
obj = obj || {};
if (isNaN(parseInt(prop))) {
return (obj = obj[prop]);
} else {
return (obj = obj.objectAt(parseInt(prop)));
}
}) && obj
);
};
};
const pauseUntil = function(run, message = 'assertion timed out') { const pauseUntil = function(run, message = 'assertion timed out') {
return new Promise(function(r) { return new Promise(function(r) {
let count = 0; let count = 0;
@ -102,48 +89,25 @@ export default function({
const setCookie = function(key, value) { const setCookie = function(key, value) {
api.server.setCookie(key, value); api.server.setCookie(key, value);
}; };
let currentPage;
const getCurrentPage = function() { const reset = function() {
return currentPage;
};
const setCurrentPage = function(page) {
api.server.clearHistory(); api.server.clearHistory();
currentPage = page;
return page;
}; };
const find = function(path) {
const page = getCurrentPage();
const parts = path.split('.');
const last = parts.pop();
let obj;
let parent = mb(parts)(page) || page;
if (typeof parent.objectAt === 'function') {
parent = parent.objectAt(0);
}
obj = parent[last];
if (typeof obj === 'undefined') {
throw new Error(`PageObject not found: The '${path}' object doesn't exist`);
}
if (typeof obj === 'function') {
obj = obj.bind(parent);
}
return obj;
};
const clipboard = function() { const clipboard = function() {
return window.localStorage.getItem('clipboard'); return window.localStorage.getItem('clipboard');
}; };
models(library, create); models(library, create);
http(library, respondWith, setCookie); http(library, respondWith, setCookie);
visit(library, pages, setCurrentPage); visit(library, pages, utils.setCurrentPage, reset);
click(library, find, utils.click); click(library, utils.find, helpers.click);
form(library, find, utils.fillIn, utils.triggerKeyEvent, getCurrentPage); form(library, utils.find, helpers.fillIn, helpers.triggerKeyEvent, utils.getCurrentPage);
debug(library, assert, utils.currentURL); debug(library, assert, utils.currentURL);
assertHttp(library, assert, lastNthRequest); assertHttp(library, assert, lastNthRequest);
assertModel(library, assert, find, getCurrentPage, pauseUntil, pluralize); assertModel(library, assert, utils.find, utils.getCurrentPage, pauseUntil, pluralize);
assertPage(library, assert, find, getCurrentPage, $); assertPage(library, assert, utils.find, utils.getCurrentPage, $);
assertDom(library, assert, pauseUntil, utils.find, utils.currentURL, clipboard); assertDom(library, assert, pauseUntil, helpers.find, helpers.currentURL, clipboard);
assertForm(library, assert, find, getCurrentPage); assertForm(library, assert, utils.find, utils.getCurrentPage);
return library.given(["I'm using a legacy token"], function(number, model, data) { return library.given(["I'm using a legacy token"], function(number, model, data) {
window.localStorage['consul:token'] = JSON.stringify({ window.localStorage['consul:token'] = JSON.stringify({

View File

@ -33,6 +33,32 @@ const isExpectedError = function(e) {
const dont = `( don't| shouldn't| can't)?`; const dont = `( don't| shouldn't| can't)?`;
export default function(scenario, assert, find, currentPage, $) { export default function(scenario, assert, find, currentPage, $) {
scenario scenario
.then([`I${dont} $verb the $pageObject object`], function(negative, verb, element, next) {
assert[negative ? 'notOk' : 'ok'](element[verb]());
setTimeout(() => next());
})
.then(
[
`I${dont} $verb the $pageObject object with value "$value"`,
`I${dont} $verb the $pageObject object from $yaml`,
],
function(negative, verb, element, data, next) {
assert[negative ? 'notOk' : 'ok'](element[verb](data));
setTimeout(() => next());
}
)
.then(`the $pageObject object is(n't) $state`, function(element, negative, state, next) {
assert[negative ? 'notOk' : 'ok'](element[state]);
setTimeout(() => next());
})
.then(`I${dont} see $num of the $pageObject objects`, function(negative, num, element, next) {
assert[negative ? 'notEqual' : 'equal'](
element.length,
num,
`Expected to${negative ? ' not' : ''} see ${num} ${element.key}`
);
setTimeout(() => next());
})
.then(['I see $num of the $component object'], function(num, component) { .then(['I see $num of the $component object'], function(num, component) {
assert.equal( assert.equal(
currentPage()[component].length, currentPage()[component].length,

View File

@ -1,9 +1,11 @@
export default function(scenario, pages, set) { export default function(scenario, pages, set, reset) {
scenario scenario
.when('I visit the $name page', function(name) { .when('I visit the $name page', function(name) {
reset();
return set(pages[name]).visit(); return set(pages[name]).visit();
}) })
.when('I visit the $name page for the "$id" $model', function(name, id, model) { .when('I visit the $name page for the "$id" $model', function(name, id, model) {
reset();
return set(pages[name]).visit({ return set(pages[name]).visit({
[model]: id, [model]: id,
}); });
@ -15,6 +17,7 @@ export default function(scenario, pages, set) {
if (nspace !== '' && typeof nspace !== 'undefined') { if (nspace !== '' && typeof nspace !== 'undefined') {
data.nspace = `~${nspace}`; data.nspace = `~${nspace}`;
} }
reset();
// TODO: Consider putting an assertion here for testing the current url // TODO: Consider putting an assertion here for testing the current url
// do I absolutely definitely need that all the time? // do I absolutely definitely need that all the time?
return set(pages[name]).visit(data); return set(pages[name]).visit(data);