UI: PKI Clean up dirty model on leave (#19058)

This commit is contained in:
Chelsea Shaw
2023-02-08 10:42:02 -06:00
committed by GitHub
parent 5d9e92acca
commit aaa50f15e1
27 changed files with 546 additions and 136 deletions

View File

@@ -57,7 +57,7 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
@attr({
label: 'Usage',
subText: 'Allowed usages for this issuer. It can always be read',
subText: 'Allowed usages for this issuer. It can always be read.',
editType: 'yield',
valueOptions: [
{ label: 'Issuing certificates', value: 'issuing-certificates' },

View File

@@ -4,11 +4,39 @@ import { inject as service } from '@ember/service';
import Ember from 'ember';
/**
* Confirm that the user wants to discard unsaved changes before leaving the page.
* This decorator hooks into the willTransition action. If you override setupController,
* be sure to set 'model' on the controller to store data or this won't work.
* Confirm that the user wants to discard unsaved changes before leaving the page. This decorator hooks into
* the willTransition action. If you override setupController, be sure to set 'model' on the controller to
* store data or this won't work.
*
* By default it will check if the route's model is dirty and prompt when leaving. Usage for this is simple:
*
* @withConfirmLeave()
* export default class MyRoute extends Route {
* @service store;
* model() {
* return this.store.createRecord('some-model')
* }
* }
*
* If the route has ember-data models at multiple paths, you can pass an array of secondary modelPaths which
* will rollback on exit after the prompt for the first model is confirmed. In the example below, the window
* will only prompt on leave if `model.main` is dirty. Either way, `model.secondary` and `model.optional`
* will be cleaned up from the data store.
*
* @withConfirmLeave('model.main', ['model.secondary', 'model.optional'])
* export default class MyRoute extends Route {
* @service store;
* model() {
* return {
* main: this.store.peekRecord('some-model', 'abc1')
* secondary: this.store.createRecord('some-other')
* optional: this.store.createRecord('optional')
* }
* }
* }
*
*/
export function withConfirmLeave() {
export function withConfirmLeave(modelPath = 'model', silentCleanupPaths) {
return function decorator(SuperClass) {
if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) {
// eslint-disable-next-line
@@ -20,34 +48,42 @@ export function withConfirmLeave() {
return class ConfirmLeave extends SuperClass {
@service store;
_rollbackModel(modelPath) {
const model = this.controller.get(modelPath);
// we only want to complete rollback if the model is dirty and not saving
if (model && model.hasDirtyAttributes && !model.isSaving) {
const method = model.isNew ? 'unloadRecord' : 'rollbackAttributes';
model[method]();
}
}
@action
willTransition(transition) {
try {
super.willTransition(...arguments);
} catch (e) {
// if the SuperClass doesn't have willTransition
// defined it will throw an error.
// defined calling it will throw an error.
}
const model = this.controller.get('model');
if (model && model.hasDirtyAttributes) {
const model = this.controller.get(modelPath);
if (model && model.hasDirtyAttributes && !model.isSaving) {
if (
Ember.testing ||
window.confirm(
'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
)
) {
// error is thrown when you attempt to unload a record that is inFlight (isSaving)
if (!model || !model.unloadRecord || model.isSaving) {
return;
}
model.rollbackAttributes();
model.destroy();
return true;
this._rollbackModel(modelPath);
} else {
transition.abort();
return false;
}
}
silentCleanupPaths?.forEach((pathToModel) => {
this._rollbackModel(pathToModel);
});
return true;
}
};
};

View File

@@ -4,14 +4,9 @@ import RouterService from '@ember/routing/router-service';
import FlashMessageService from 'vault/services/flash-messages';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
import PkiKeyModel from 'vault/models/pki/key';
interface Args {
key: {
rollbackAttributes: () => void;
destroyRecord: () => void;
backend: string;
keyName: string;
keyId: string;
};
key: PkiKeyModel;
}
export default class PkiKeyDetails extends Component<Args> {

View File

@@ -5,14 +5,10 @@ import FlashMessageService from 'vault/services/flash-messages';
import SecretMountPath from 'vault/services/secret-mount-path';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
import PkiRoleModel from 'vault/models/pki/role';
// TODO: pull this in from route model once it's TS
interface Args {
role: {
id: string;
rollbackAttributes: () => void;
destroyRecord: () => void;
};
role: PkiRoleModel;
}
export default class DetailsPage extends Component<Args> {

View File

@@ -38,14 +38,14 @@
<PkiGenerateRoot
@model={{@config}}
@urls={{@urls}}
@onCancel={{this.cancel}}
@onCancel={{@onCancel}}
@adapterOptions={{hash actionType=@config.actionType useIssuer=@config.canGenerateIssuerRoot}}
/>
{{else if (eq @config.actionType "generate-csr")}}
<PkiGenerateCsr
@model={{@config}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.issuers"}}
@onCancel={{this.cancel}}
@onCancel={{@onCancel}}
/>
{{else}}
<EmptyState
@@ -58,7 +58,7 @@
<button type="button" class="button is-primary" disabled={{true}} data-test-pki-config-save>
Done
</button>
<button type="button" class="button has-left-margin-s" {{on "click" this.cancel}} data-test-pki-config-cancel>
<button type="button" class="button has-left-margin-s" {{on "click" @onCancel}} data-test-pki-config-cancel>
Cancel
</button>
</div>

View File

@@ -4,7 +4,6 @@ import { inject as service } from '@ember/service';
import Store from '@ember-data/store';
import Router from '@ember/routing/router';
import FlashMessageService from 'vault/services/flash-messages';
import { action } from '@ember/object';
import PkiActionModel from 'vault/models/pki/action';
interface Args {
@@ -48,9 +47,4 @@ export default class PkiConfigureForm extends Component<Args> {
},
];
}
@action cancel() {
this.args.config.rollbackAttributes();
this.router.transitionTo('vault.cluster.secrets.backend.pki.overview');
}
}

View File

@@ -50,7 +50,7 @@
<button type="submit" class="button is-primary" data-test-pki-generate-root-save>
Done
</button>
<button {{on "click" this.cancel}} type="button" class="button has-left-margin-s" data-test-pki-generate-root-cancel>
<button {{on "click" @onCancel}} type="button" class="button has-left-margin-s" data-test-pki-generate-root-cancel>
Cancel
</button>
</div>

View File

@@ -43,7 +43,7 @@
type="button"
class="button has-left-margin-s"
disabled={{this.save.isRunning}}
{{on "click" this.cancel}}
{{on "click" @onCancel}}
data-test-pki-key-cancel
>
Cancel

View File

@@ -1,5 +1,4 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
@@ -12,7 +11,7 @@ import { waitFor } from '@ember/test-waiters';
*
* @example
* ```js
* <PkiKeyForm @model={{this.model}}/>
* <PkiKeyForm @model={{this.model}} @onCancel={{transition-to "vault.cluster"}} @onSave={{transition-to "vault.cluster"}} />
* ```
*
* @param {Object} model - pki/key model.
@@ -49,11 +48,4 @@ export default class PkiKeyForm extends Component {
this.invalidFormAlert = 'There was an error submitting this form.';
}
}
@action
cancel() {
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
this.args.model[method]();
this.args.onCancel();
}
}

View File

@@ -93,7 +93,7 @@
type="button"
class="button has-left-margin-s"
disabled={{this.save.isRunning}}
{{on "click" this.cancel}}
{{on "click" @onCancel}}
data-test-pki-role-cancel
>
Cancel

View File

@@ -1,5 +1,4 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import { tracked } from '@glimmer/tracking';
@@ -59,11 +58,4 @@ export default class PkiRoleForm extends Component {
this.invalidFormAlert = 'There was an error submitting this form.';
}
}
@action
cancel() {
const method = this.args.model.isNew ? 'unloadRecord' : 'rollbackAttributes';
this.args.model[method]();
this.args.onCancel();
}
}

View File

@@ -1,7 +1,9 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
import { hash } from 'rsvp';
@withConfirmLeave('model.config', ['model.urls'])
export default class PkiConfigurationCreateRoute extends Route {
@service secretMountPath;
@service store;

View File

@@ -1,5 +1,7 @@
import { withConfirmLeave } from 'core/decorators/confirm-leave';
import PkiIssuersIndexRoute from '.';
@withConfirmLeave()
export default class PkiIssuersGenerateIntermediateRoute extends PkiIssuersIndexRoute {
model() {
return this.store.createRecord('pki/action', { actionType: 'generate-csr' });

View File

@@ -1,12 +1,14 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
export default class PkiIssuersGenerateRootRoute extends Route {
@service secretMountPath;
@service store;
model() {
return this.store.createRecord('pki/action');
return this.store.createRecord('pki/action', { actionType: 'generate-root' });
}
setupController(controller, resolvedModel) {

View File

@@ -1,3 +1,34 @@
import PkiIssuerDetailsRoute from './details';
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
export default class PkiIssuerEditRoute extends PkiIssuerDetailsRoute {}
@withConfirmLeave()
export default class PkiIssuerDetailRoute extends Route {
@service store;
@service secretMountPath;
@service pathHelp;
beforeModel() {
// Must call this promise before the model hook otherwise it doesn't add OpenApi to record.
return this.pathHelp.getNewModel('pki/issuer', this.secretMountPath.currentPath);
}
model() {
const { issuer_ref } = this.paramsFor('issuers/issuer');
return this.store.queryRecord('pki/issuer', {
backend: this.secretMountPath.currentPath,
id: issuer_ref,
});
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: this.secretMountPath.currentPath, route: 'overview' },
{ label: 'issuers', route: 'issuers.index' },
{ label: resolvedModel.id, route: 'issuers.issuer.details' },
{ label: 'update' },
];
}
}

View File

@@ -1,6 +1,8 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
export default class PkiIssuerSignRoute extends Route {
@service store;
@service secretMountPath;

View File

@@ -1,6 +1,6 @@
import PkiKeysIndexRoute from '.';
import PkiKeyRoute from '../key';
export default class PkiKeyDetailsRoute extends PkiKeysIndexRoute {
export default class PkiKeyDetailsRoute extends PkiKeyRoute {
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs.push({ label: resolvedModel.id });

View File

@@ -1,6 +1,8 @@
import PkiKeysIndexRoute from '.';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
import PkiKeyRoute from '../key';
export default class PkiKeyEditRoute extends PkiKeysIndexRoute {
@withConfirmLeave()
export default class PkiKeyEditRoute extends PkiKeyRoute {
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs.push({ label: resolvedModel.id, route: 'keys.key.details' }, { label: 'edit' });

View File

@@ -1,5 +1,8 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
withConfirmLeave();
export default class PkiRoleGenerateRoute extends Route {
@service store;
@service secretMountPath;

View File

@@ -1,6 +1,8 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
withConfirmLeave();
export default class PkiRoleSignRoute extends Route {
@service store;
@service secretMountPath;

View File

@@ -9,4 +9,8 @@
</p.levelLeft>
</PageHeader>
<PkiConfigureForm @config={{this.model.config}} @urls={{this.model.urls}} />
<PkiConfigureForm
@config={{this.model.config}}
@urls={{this.model.urls}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.overview"}}
/>

View File

@@ -10,11 +10,15 @@
/>
<Toolbar>
<ToolbarActions>
<ToolbarLink @route="issuers.import">
<ToolbarLink @route="issuers.import" data-test-generate-issuer="import">
Import
</ToolbarLink>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger class={{concat "toolbar-link" (if D.isOpen " is-active")}} @htmlTag="button">
<D.Trigger
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
data-test-issuer-generate-dropdown
>
Generate
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
@@ -22,12 +26,16 @@
<nav class="box menu" aria-label="generate options">
<ul class="menu-list">
<li class="action">
<LinkTo @route="issuers.generate-root" {{on "click" (fn this.onLinkClick D)}}>
<LinkTo @route="issuers.generate-root" {{on "click" (fn this.onLinkClick D)}} data-test-generate-issuer="root">
Root
</LinkTo>
</li>
<li class="action">
<LinkTo @route="issuers.generate-intermediate" {{on "click" (fn this.onLinkClick D)}}>
<LinkTo
@route="issuers.generate-intermediate"
{{on "click" (fn this.onLinkClick D)}}
data-test-generate-issuer="intermediate"
>
Intermediate CSR
</LinkTo>
</li>

View File

@@ -0,0 +1,398 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import authPage from 'vault/tests/pages/auth';
import logout from 'vault/tests/pages/logout';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { click, currentURL, fillIn, visit } from '@ember/test-helpers';
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
import { SELECTORS } from '../helpers/pki/workflow';
/**
* This test module should test that dirty route models are cleaned up when the user leaves the page
*/
module('Acceptance | pki engine route cleanup test', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(async function () {
this.store = this.owner.lookup('service:store');
await authPage.login();
// Setup PKI engine
const mountPath = `pki-workflow-${new Date().getTime()}`;
await enablePage.enable('pki', mountPath);
this.mountPath = mountPath;
await logout.visit();
});
hooks.afterEach(async function () {
await logout.visit();
await authPage.login();
// Cleanup engine
await runCommands([`delete sys/mounts/${this.mountPath}`]);
await logout.visit();
});
module('configuration', function () {
test('create config', async function (assert) {
let configs, urls, config;
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
configs = this.store.peekAll('pki/action');
urls = this.store.peekRecord('pki/urls', this.mountPath);
config = configs.objectAt(0);
assert.strictEqual(configs.length, 1, 'One config model present');
assert.false(urls.hasDirtyAttributes, 'URLs is loaded from endpoint');
assert.true(config.hasDirtyAttributes, 'Config model is dirty');
// Cancel button rolls it back
await click(SELECTORS.configuration.cancelButton);
configs = this.store.peekAll('pki/action');
urls = this.store.peekRecord('pki/urls', this.mountPath);
assert.strictEqual(configs.length, 0, 'config model is rolled back on cancel');
assert.strictEqual(urls.id, this.mountPath, 'Urls still exists on exit');
await click(SELECTORS.emptyStateLink);
configs = this.store.peekAll('pki/action');
urls = this.store.peekRecord('pki/urls', this.mountPath);
config = configs.objectAt(0);
assert.strictEqual(configs.length, 1, 'One config model present');
assert.false(urls.hasDirtyAttributes, 'URLs is loaded from endpoint');
assert.true(config.hasDirtyAttributes, 'Config model is dirty');
// Exit page via link rolls it back
await click(SELECTORS.overviewBreadcrumb);
configs = this.store.peekAll('pki/action');
urls = this.store.peekRecord('pki/urls', this.mountPath);
assert.strictEqual(configs.length, 0, 'config model is rolled back on cancel');
assert.strictEqual(urls.id, this.mountPath, 'Urls still exists on exit');
});
});
module('role routes', function (hooks) {
hooks.beforeEach(async function () {
await authPage.login();
// Configure PKI
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
await click(SELECTORS.configuration.optionByKey('generate-root'));
await fillIn(SELECTORS.configuration.typeField, 'internal');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert');
await click(SELECTORS.configuration.generateRootSave);
await logout.visit();
});
test('create role exit via cancel', async function (assert) {
let roles;
await authPage.login();
// Create PKI
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.rolesTab);
roles = this.store.peekAll('pki/role');
assert.strictEqual(roles.length, 0, 'No roles exist yet');
await click(SELECTORS.createRoleLink);
roles = this.store.peekAll('pki/role');
const role = roles.objectAt(0);
assert.strictEqual(roles.length, 1, 'New role exists');
assert.true(role.isNew, 'Role is new model');
await click(SELECTORS.roleForm.roleCancelButton);
roles = this.store.peekAll('pki/role');
assert.strictEqual(roles.length, 0, 'Role is removed from store');
});
test('create role exit via breadcrumb', async function (assert) {
let roles;
await authPage.login();
// Create PKI
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.rolesTab);
roles = this.store.peekAll('pki/role');
assert.strictEqual(roles.length, 0, 'No roles exist yet');
await click(SELECTORS.createRoleLink);
roles = this.store.peekAll('pki/role');
const role = roles.objectAt(0);
assert.strictEqual(roles.length, 1, 'New role exists');
assert.true(role.isNew, 'Role is new model');
await click(SELECTORS.overviewBreadcrumb);
roles = this.store.peekAll('pki/role');
assert.strictEqual(roles.length, 0, 'Role is removed from store');
});
test('edit role', async function (assert) {
let roles, role;
const roleId = 'workflow-edit-role';
await authPage.login();
// Create PKI
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.rolesTab);
roles = this.store.peekAll('pki/role');
assert.strictEqual(roles.length, 0, 'No roles exist yet');
await click(SELECTORS.createRoleLink);
await fillIn(SELECTORS.roleForm.roleName, roleId);
await click(SELECTORS.roleForm.roleCreateButton);
assert.dom('[data-test-value-div="Role name"]').hasText(roleId, 'Shows correct role after create');
roles = this.store.peekAll('pki/role');
role = roles.objectAt(0);
assert.strictEqual(roles.length, 1, 'Role is created');
assert.false(role.hasDirtyAttributes, 'Role no longer has dirty attributes');
// Edit role
await click(SELECTORS.editRoleLink);
await fillIn(SELECTORS.roleForm.issuerRef, 'foobar');
role = this.store.peekRecord('pki/role', roleId);
assert.true(role.hasDirtyAttributes, 'Role has dirty attrs');
// Exit page via cancel button
await click(SELECTORS.roleForm.roleCancelButton);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles/${roleId}/details`);
role = this.store.peekRecord('pki/role', roleId);
assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back');
// Edit again
await click(SELECTORS.editRoleLink);
await fillIn(SELECTORS.roleForm.issuerRef, 'foobar2');
role = this.store.peekRecord('pki/role', roleId);
assert.true(role.hasDirtyAttributes, 'Role has dirty attrs');
// Exit page via breadcrumbs
await click(SELECTORS.overviewBreadcrumb);
role = this.store.peekRecord('pki/role', roleId);
assert.false(role.hasDirtyAttributes, 'Role dirty attrs have been rolled back');
});
});
module('issuer routes', function () {
test('import issuer exit via cancel', async function (assert) {
let issuers;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 0, 'No issuers exist yet');
await click(SELECTORS.importIssuerLink);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 1, 'Issuer model created');
const issuer = issuers.objectAt(0);
assert.true(issuer.hasDirtyAttributes, 'Issuer has dirty attrs');
assert.true(issuer.isNew, 'Issuer is new');
// Exit
await click('[data-test-pki-ca-cert-cancel]');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 0, 'Issuer is removed from store');
});
test('import issuer exit via breadcrumb', async function (assert) {
let issuers;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 0, 'No issuers exist yet');
await click(SELECTORS.importIssuerLink);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 1, 'Issuer model created');
const issuer = issuers.objectAt(0);
assert.true(issuer.hasDirtyAttributes, 'Issuer has dirty attrs');
assert.true(issuer.isNew, 'Issuer is new');
// Exit
await click(SELECTORS.overviewBreadcrumb);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 0, 'Issuer is removed from store');
});
test('generate root exit via cancel', async function (assert) {
let actions;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'No actions exist yet');
await click(SELECTORS.generateIssuerDropdown);
await click(SELECTORS.generateIssuerRoot);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 1, 'Action model for generate-root created');
const action = actions.objectAt(0);
assert.true(action.hasDirtyAttributes, 'Action has dirty attrs');
assert.true(action.isNew, 'Action is new');
assert.strictEqual(action.actionType, 'generate-root', 'Action type is correct');
// Exit
await click(SELECTORS.configuration.generateRootCancel);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'Action is removed from store');
});
test('generate root exit via breadcrumb', async function (assert) {
let actions;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'No actions exist yet');
await click(SELECTORS.generateIssuerDropdown);
await click(SELECTORS.generateIssuerRoot);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 1, 'Action model for generate-root created');
const action = actions.objectAt(0);
assert.true(action.hasDirtyAttributes, 'Action has dirty attrs');
assert.true(action.isNew, 'Action is new');
assert.strictEqual(action.actionType, 'generate-root');
// Exit
await click(SELECTORS.overviewBreadcrumb);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'Action is removed from store');
});
test('generate intermediate csr exit via cancel', async function (assert) {
let actions;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'No actions exist yet');
await await click(SELECTORS.generateIssuerDropdown);
await click(SELECTORS.generateIssuerIntermediate);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 1, 'Action model for generate-csr created');
const action = actions.objectAt(0);
assert.true(action.hasDirtyAttributes, 'Action has dirty attrs');
assert.true(action.isNew, 'Action is new');
assert.strictEqual(action.actionType, 'generate-csr');
// Exit
await click('[data-test-cancel]');
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'Action is removed from store');
});
test('generate intermediate csr exit via breadcrumb', async function (assert) {
let actions;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'No actions exist yet');
await click(SELECTORS.generateIssuerDropdown);
await click(SELECTORS.generateIssuerIntermediate);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 1, 'Action model for generate-csr created');
const action = actions.objectAt(0);
assert.true(action.hasDirtyAttributes, 'Action has dirty attrs');
assert.true(action.isNew, 'Action is new');
assert.strictEqual(action.actionType, 'generate-csr');
// Exit
await click(SELECTORS.overviewBreadcrumb);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`);
actions = this.store.peekAll('pki/action');
assert.strictEqual(actions.length, 0, 'Action is removed from store');
});
test('edit issuer exit', async function (assert) {
let issuers, issuer;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
await click(SELECTORS.configuration.optionByKey('generate-root'));
await fillIn(SELECTORS.configuration.typeField, 'internal');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert');
await click(SELECTORS.configuration.generateRootSave);
issuers = this.store.peekAll('pki/issuer');
const issuerId = issuers.objectAt(0).id;
assert.strictEqual(issuers.length, 1, 'Issuer exists on model');
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`,
'url is correct'
);
await click(SELECTORS.issuerDetails.configure);
issuer = this.store.peekRecord('pki/issuer', issuerId);
assert.false(issuer.hasDirtyAttributes, 'Model not dirty');
await fillIn('[data-test-input="issuerName"]', 'foobar');
assert.true(issuer.hasDirtyAttributes, 'Model is dirty');
await click(SELECTORS.overviewBreadcrumb);
issuers = this.store.peekAll('pki/issuer');
assert.strictEqual(issuers.length, 1, 'Issuer exists on model');
issuer = this.store.peekRecord('pki/issuer', issuerId);
assert.false(issuer.hasDirtyAttributes, 'Dirty attrs were rolled back');
});
});
module('key routes', function (hooks) {
hooks.beforeEach(async function () {
await authPage.login();
// Configure PKI -- key creation not allowed unless configured
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.emptyStateLink);
await click(SELECTORS.configuration.optionByKey('generate-root'));
await fillIn(SELECTORS.configuration.typeField, 'internal');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-root-cert');
await click(SELECTORS.configuration.generateRootSave);
await logout.visit();
});
test('create key exit', async function (assert) {
let keys, key;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.keysTab);
keys = this.store.peekAll('pki/key');
const configKeyId = keys.objectAt(0).id;
assert.strictEqual(keys.length, 1, 'One key exists from config');
// Create key
await click(SELECTORS.keyPages.generateKey);
keys = this.store.peekAll('pki/key');
key = keys.objectAt(1);
assert.strictEqual(keys.length, 2, 'New key exists');
assert.true(key.isNew, 'Role is new model');
// Exit
await click(SELECTORS.keyForm.keyCancelButton);
keys = this.store.peekAll('pki/key');
assert.strictEqual(keys.length, 1, 'Second key is removed from store');
assert.strictEqual(keys.objectAt(0).id, configKeyId);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/keys`, 'url is correct');
// Create again
await click(SELECTORS.keyPages.generateKey);
assert.strictEqual(keys.length, 2, 'New key exists');
keys = this.store.peekAll('pki/key');
key = keys.objectAt(1);
assert.true(key.isNew, 'Key is new model');
// Exit
await click(SELECTORS.overviewBreadcrumb);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`, 'url is correct');
keys = this.store.peekAll('pki/key');
assert.strictEqual(keys.length, 1, 'Key is removed from store');
});
test('edit key exit', async function (assert) {
let keys, key;
await authPage.login();
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.keysTab);
keys = this.store.peekAll('pki/key');
assert.strictEqual(keys.length, 1, 'One key from config exists');
assert.dom('.list-item-row').exists({ count: 1 }, 'single row for key');
await click('.list-item-row');
// Edit
await click(SELECTORS.keyPages.keyEditLink);
await fillIn(SELECTORS.keyForm.keyNameInput, 'foobar');
keys = this.store.peekAll('pki/key');
key = keys.objectAt(0);
assert.true(key.hasDirtyAttributes, 'Key model is dirty');
// Exit
await click(SELECTORS.keyForm.keyCancelButton);
assert.strictEqual(
currentURL(),
`/vault/secrets/${this.mountPath}/pki/keys/${key.id}/details`,
'url is correct'
);
keys = this.store.peekAll('pki/key');
assert.strictEqual(keys.length, 1, 'Key list has 1');
assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back');
// Edit again
await click(SELECTORS.keyPages.keyEditLink);
await fillIn(SELECTORS.keyForm.keyNameInput, 'foobar');
keys = this.store.peekAll('pki/key');
key = keys.objectAt(0);
assert.true(key.hasDirtyAttributes, 'Key model is dirty');
// Exit via breadcrumb
await click(SELECTORS.overviewBreadcrumb);
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/overview`, 'url is correct');
keys = this.store.peekAll('pki/key');
assert.strictEqual(keys.length, 1, 'Key list has 1');
assert.false(key.hasDirtyAttributes, 'Key dirty attrs have been rolled back');
});
});
});

View File

@@ -8,6 +8,7 @@ import { SELECTORS as CONFIGURATION } from './pki-configure-form';
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
breadcrumbs: '[data-test-breadcrumbs] li',
overviewBreadcrumb: '[data-test-breadcrumbs] li:nth-of-type(2) > a',
pageTitle: '[data-test-pki-role-page-title]',
alertBanner: '[data-test-alert-banner="alert"]',
emptyStateTitle: '[data-test-empty-state-title]',
@@ -40,6 +41,10 @@ export const SELECTORS = {
...KEYPAGES,
},
// ISSUERS
importIssuerLink: '[data-test-generate-issuer="import"]',
generateIssuerDropdown: '[data-test-issuer-generate-dropdown]',
generateIssuerRoot: '[data-test-generate-issuer="root"]',
generateIssuerIntermediate: '[data-test-generate-issuer="intermediate"]',
issuerDetails: {
title: '[data-test-pki-issuer-page-title]',
...ISSUERDETAILS,

View File

@@ -4,13 +4,19 @@ import { click, render } from '@ember/test-helpers';
import { setupEngine } from 'ember-engines/test-support';
import { hbs } from 'ember-cli-htmlbars';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-configure-form';
import sinon from 'sinon';
module('Integration | Component | pki-configure-form', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
hooks.beforeEach(function () {
this.cancelSpy = sinon.spy();
});
test('it renders', async function (assert) {
await render(hbs`<PkiConfigureForm @config={{this.config}} />`, { owner: this.engine });
await render(hbs`<PkiConfigureForm @onCancel={{this.cancelSpy}} @config={{this.config}} />`, {
owner: this.engine,
});
assert.dom(SELECTORS.option).exists({ count: 3 }, 'Three configuration options are shown');
assert.dom(SELECTORS.cancelButton).exists('Cancel link is shown');

View File

@@ -5,6 +5,7 @@ import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-key-form';
import { setupMirage } from 'ember-cli-mirage/test-support';
import sinon from 'sinon';
module('Integration | Component | pki key form', function (hooks) {
setupRenderingTest(hooks);
@@ -17,6 +18,7 @@ module('Integration | Component | pki key form', function (hooks) {
this.backend = 'pki-test';
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
this.secretMountPath.currentPath = this.backend;
this.onCancel = sinon.spy();
});
test('it should render fields and show validation messages', async function (assert) {
@@ -120,50 +122,4 @@ module('Integration | Component | pki key form', function (hooks) {
await fillIn(SELECTORS.keyTypeInput, 'rsa');
await click(SELECTORS.keyCreateButton);
});
test('it should rollback attributes or unload record on cancel', async function (assert) {
assert.expect(5);
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
await render(
hbs`
<PkiKeyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);
await click(SELECTORS.keyCancelButton);
assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
this.store.pushPayload('pki/key', {
modelName: 'pki/key',
key_name: 'test-key',
type: 'exported',
key_id: 'some-key-id',
key_type: 'rsa',
key_bits: '2048',
});
this.model = this.store.peekRecord('pki/key', 'some-key-id');
await render(
hbs`
<PkiKeyForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);
await fillIn(SELECTORS.keyNameInput, 'new-name');
await click(SELECTORS.keyCancelButton);
assert.strictEqual(this.model.keyName, 'test-key', 'Model name rolled back on cancel');
await fillIn(SELECTORS.keyNameInput, 'new-name');
await click(SELECTORS.keyCreateButton);
assert.strictEqual(this.model.keyName, 'new-name', 'Model name correctly save on create');
});
});

View File

@@ -5,6 +5,7 @@ import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-role-form';
import { setupMirage } from 'ember-cli-mirage/test-support';
import sinon from 'sinon';
module('Integration | Component | pki-role-form', function (hooks) {
setupRenderingTest(hooks);
@@ -15,6 +16,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role');
this.model.backend = 'pki';
this.onCancel = sinon.spy();
});
test('it should render default fields and toggle groups', async function (assert) {
@@ -111,26 +113,6 @@ module('Integration | Component | pki-role-form', function (hooks) {
await click(SELECTORS.roleCreateButton);
});
test('it should unload model on cancel', async function (assert) {
assert.expect(3);
this.onCancel = () => assert.ok(true, 'onCancel callback fires');
await render(
hbs`
<PkiRoleForm
@model={{this.model}}
@onCancel={{this.onCancel}}
@onSave={{this.onSave}}
/>
`,
{ owner: this.engine }
);
await fillIn(SELECTORS.roleName, 'dont-save-me');
await click(SELECTORS.roleCancelButton);
assert.notEqual(this.model.roleName, 'dont-save-me');
assert.true(this.model.isDestroyed, 'new model is unloaded on cancel');
});
test('it should update attributes on the model on update', async function (assert) {
assert.expect(1);
this.store.pushPayload('pki/role', {