diff --git a/ui/app/adapters/kmip/role.js b/ui/app/adapters/kmip/role.js index 9779ad67a564..693ae6327e11 100644 --- a/ui/app/adapters/kmip/role.js +++ b/ui/app/adapters/kmip/role.js @@ -6,10 +6,11 @@ import BaseAdapter from './base'; import { decamelize } from '@ember/string'; import { getProperties } from '@ember/object'; +import { nonOperationFields } from 'vault/utils/kmip-role-fields'; export default BaseAdapter.extend({ createRecord(store, type, snapshot) { - const name = snapshot.id || snapshot.attr('name'); + const name = snapshot.id || snapshot.record.role; const url = this._url( type.modelName, { @@ -18,10 +19,11 @@ export default BaseAdapter.extend({ }, name ); - return this.ajax(url, 'POST', { data: this.serialize(snapshot) }).then(() => { + const data = this.serialize(snapshot); + return this.ajax(url, 'POST', { data }).then(() => { return { id: name, - name, + role: name, backend: snapshot.record.backend, scope: snapshot.record.scope, }; @@ -29,7 +31,8 @@ export default BaseAdapter.extend({ }, deleteRecord(store, type, snapshot) { - const name = snapshot.id || snapshot.attr('name'); + // records must always have IDs + const name = snapshot.id; const url = this._url( type.modelName, { @@ -41,35 +44,35 @@ export default BaseAdapter.extend({ return this.ajax(url, 'DELETE'); }, + updateRecord() { + return this.createRecord(...arguments); + }, + serialize(snapshot) { // the endpoint here won't allow sending `operation_all` and `operation_none` at the same time or with // other operation_ values, so we manually check for them and send an abbreviated object const json = snapshot.serialize(); - const keys = snapshot.record.nonOperationFields.map(decamelize); - const nonOperationFields = getProperties(json, keys); - for (const field in nonOperationFields) { - if (nonOperationFields[field] == null) { - delete nonOperationFields[field]; + const keys = nonOperationFields(snapshot.record.editableFields).map(decamelize); + const nonOp = getProperties(json, keys); + for (const field in nonOp) { + if (nonOp[field] == null) { + delete nonOp[field]; } } if (json.operation_all) { return { operation_all: true, - ...nonOperationFields, + ...nonOp, }; } if (json.operation_none) { return { operation_none: true, - ...nonOperationFields, + ...nonOp, }; } delete json.operation_none; delete json.operation_all; return json; }, - - updateRecord() { - return this.createRecord(...arguments); - }, }); diff --git a/ui/app/models/kmip/role.js b/ui/app/models/kmip/role.js index 8083acd7b331..520efcf1b517 100644 --- a/ui/app/models/kmip/role.js +++ b/ui/app/models/kmip/role.js @@ -4,52 +4,35 @@ */ import Model, { attr } from '@ember-data/model'; -import { computed } from '@ember/object'; -import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; import apiPath from 'vault/utils/api-path'; import lazyCapabilities from 'vault/macros/lazy-capabilities'; +import { withExpandedAttributes } from 'vault/decorators/model-expanded-attributes'; +import { operationFields, operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/kmip-role-fields'; import { removeManyFromArray } from 'vault/helpers/remove-from-array'; -const COMPUTEDS = { - operationFields: computed('newFields', function () { - return this.newFields.filter((key) => key.startsWith('operation')); - }), +@withExpandedAttributes() +export default class KmipRoleModel extends Model { + @attr({ readOnly: true }) backend; + @attr({ readOnly: true }) scope; - operationFieldsWithoutSpecial: computed('operationFields', function () { - return removeManyFromArray(this.operationFields, ['operationAll', 'operationNone']); - }), + get editableFields() { + return Object.keys(this.allByKey).filter((k) => !['backend', 'scope', 'role'].includes(k)); + } - tlsFields: computed(function () { - return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl']; - }), - - // For rendering on the create/edit pages - defaultFields: computed('newFields', 'operationFields', 'tlsFields', function () { - const excludeFields = ['role'].concat(this.operationFields, this.tlsFields); - return removeManyFromArray(this.newFields, excludeFields); - }), - - // For adapter/serializer - nonOperationFields: computed('newFields', 'operationFields', function () { - return removeManyFromArray(this.newFields, this.operationFields); - }), -}; - -export default Model.extend(COMPUTEDS, { - backend: attr({ readOnly: true }), - scope: attr({ readOnly: true }), - name: attr({ readOnly: true }), - - fieldGroups: computed('fields', 'defaultFields.length', 'tlsFields', function () { - const groups = [{ TLS: this.tlsFields }]; - if (this.defaultFields.length) { - groups.unshift({ default: this.defaultFields }); + get fieldGroups() { + const tls = tlsFields(); + const groups = [{ TLS: tls }]; + // op fields are shown in OperationFieldDisplay + const opFields = operationFields(this.editableFields); + // not op fields, tls fields, or role/backend/scope + const defaultFields = this.editableFields.filter((f) => ![...opFields, ...tls].includes(f)); + if (defaultFields.length) { + groups.unshift({ default: defaultFields }); } - const ret = fieldToAttrs(this, groups); - return ret; - }), + return this._expandGroups(groups); + } - operationFormFields: computed('operationFieldsWithoutSpecial', function () { + get operationFormFields() { const objects = [ 'operationCreate', 'operationActivate', @@ -62,7 +45,7 @@ export default Model.extend(COMPUTEDS, { const attributes = ['operationAddAttribute', 'operationGetAttributes']; const server = ['operationDiscoverVersions']; - const others = removeManyFromArray(this.operationFieldsWithoutSpecial, [ + const others = removeManyFromArray(operationFieldsWithoutSpecial(this.editableFields), [ ...objects, ...attributes, ...server, @@ -77,14 +60,8 @@ export default Model.extend(COMPUTEDS, { Other: others, }); } - return fieldToAttrs(this, groups); - }), - tlsFormFields: computed('tlsFields', function () { - return expandAttributeMeta(this, this.tlsFields); - }), - fields: computed('defaultFields', function () { - return expandAttributeMeta(this, this.defaultFields); - }), + return this._expandGroups(groups); + } - updatePath: lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id'), -}); + @lazyCapabilities(apiPath`${'backend'}/scope/${'scope'}/role/${'id'}`, 'backend', 'scope', 'id') updatePath; +} diff --git a/ui/app/styles/components/kmip-role-edit.scss b/ui/app/styles/components/kmip-role-edit.scss index a517c1a54313..f4fc884be41f 100644 --- a/ui/app/styles/components/kmip-role-edit.scss +++ b/ui/app/styles/components/kmip-role-edit.scss @@ -3,11 +3,14 @@ * SPDX-License-Identifier: BUSL-1.1 */ +.kmip-role-operations { + column-count: 2; +} .kmip-role-allowed-operations { @extend .box; flex: 1 1 auto; box-shadow: none; - padding: 0; + padding: $spacing-4 0; } .kmip-role-allowed-operations .field { margin-bottom: $spacing-4; diff --git a/ui/app/utils/kmip-role-fields.js b/ui/app/utils/kmip-role-fields.js new file mode 100644 index 000000000000..3503ad58f449 --- /dev/null +++ b/ui/app/utils/kmip-role-fields.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { removeManyFromArray } from 'vault/helpers/remove-from-array'; + +export const operationFields = (fieldNames) => { + if (!Array.isArray(fieldNames)) { + throw new Error('fieldNames must be an array'); + } + return fieldNames.filter((key) => key.startsWith('operation')); +}; + +export const operationFieldsWithoutSpecial = (fieldNames) => { + const opFields = operationFields(fieldNames); + return removeManyFromArray(opFields, ['operationAll', 'operationNone']); +}; + +export const nonOperationFields = (fieldNames) => { + const opFields = operationFields(fieldNames); + return removeManyFromArray(fieldNames, opFields); +}; + +export const tlsFields = () => { + return ['tlsClientKeyBits', 'tlsClientKeyType', 'tlsClientTtl']; +}; diff --git a/ui/lib/kmip/addon/components/edit-form-kmip-role.js b/ui/lib/kmip/addon/components/edit-form-kmip-role.js deleted file mode 100644 index 43a230b80b19..000000000000 --- a/ui/lib/kmip/addon/components/edit-form-kmip-role.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import EditForm from 'core/components/edit-form'; -import { computed } from '@ember/object'; -import layout from '../templates/components/edit-form-kmip-role'; - -export default EditForm.extend({ - layout, - model: null, - - cancelLink: computed('cancelLinkParams.[]', function () { - if (!Array.isArray(this.cancelLinkParams) || !this.cancelLinkParams.length) return; - const [route, ...models] = this.cancelLinkParams; - return { route, models }; - }), - - init() { - this._super(...arguments); - - if (this.model.isNew) { - this.model.operationAll = true; - } - }, - - actions: { - toggleOperationSpecial(checked) { - this.model.operationNone = !checked; - this.model.operationAll = checked; - }, - - // when operationAll is true, we want all of the items - // to appear checked, but we don't want to override what items - // a user has selected - so this action creates an object that we - // pass to the FormField component as the model instead of the real model - placeholderOrModel(isOperationAll, attr) { - return isOperationAll ? { [attr.name]: true } : this.model; - }, - - preSave(model) { - // if we have operationAll or operationNone, we want to clear - // out the others so that display shows the right data - if (model.operationAll || model.operationNone) { - model.operationFieldsWithoutSpecial.forEach((field) => model.set(field, null)); - } - // set operationNone if user unchecks 'operationAll' instead of toggling the 'operationNone' input - // doing here instead of on the 'operationNone' input because a user might deselect all, then reselect some options - // and immediately setting operationNone will hide all of the checkboxes in the UI - this.model.operationNone = - model.operationFieldsWithoutSpecial.every((attr) => !model[attr]) && !this.model.operationAll; - }, - }, -}); diff --git a/ui/lib/kmip/addon/components/kmip/role-form.hbs b/ui/lib/kmip/addon/components/kmip/role-form.hbs new file mode 100644 index 000000000000..d10fb57add42 --- /dev/null +++ b/ui/lib/kmip/addon/components/kmip/role-form.hbs @@ -0,0 +1,82 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ +
+ + {{#if @model.isNew}} + {{! Show role name only in create mode }} + + {{/if}} +
+ + +
+ {{#unless @model.operationNone}} + +

+ Allowed Operations +

+
+
+ +
+
+ {{#each this.operationFormGroups as |group|}} +
+

{{group.name}}

+ {{#each group.fields as |attr|}} + + {{/each}} +
+ {{/each}} +
+
+ {{/unless}} +
+

+ TLS +

+ {{#each this.tlsFormFields as |attr|}} + + {{/each}} +
+ {{#each @model.fields as |attr|}} + + {{/each}} +
+ +
+ + + + +
+ \ No newline at end of file diff --git a/ui/lib/kmip/addon/components/kmip/role-form.js b/ui/lib/kmip/addon/components/kmip/role-form.js new file mode 100644 index 000000000000..496dde4106a0 --- /dev/null +++ b/ui/lib/kmip/addon/components/kmip/role-form.js @@ -0,0 +1,107 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import AdapterError from '@ember-data/adapter/error'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { task } from 'ember-concurrency'; +import { removeManyFromArray } from 'vault/helpers/remove-from-array'; +import { operationFieldsWithoutSpecial, tlsFields } from 'vault/utils/kmip-role-fields'; + +export default class KmipRoleFormComponent extends Component { + @service flashMessages; + @service store; + + // Actual attribute fields + get tlsFormFields() { + return tlsFields().map((attr) => this.args.model.allByKey[attr]); + } + get operationFormGroups() { + const objects = [ + 'operationCreate', + 'operationActivate', + 'operationGet', + 'operationLocate', + 'operationRekey', + 'operationRevoke', + 'operationDestroy', + ]; + const attributes = ['operationAddAttribute', 'operationGetAttributes']; + const server = ['operationDiscoverVersions']; + const others = removeManyFromArray(operationFieldsWithoutSpecial(this.args.model.editableFields), [ + ...objects, + ...attributes, + ...server, + ]); + const groups = [ + { name: 'Managed Cryptographic Objects', fields: objects }, + { name: 'Object Attributes', fields: attributes }, + { name: 'Server', fields: server }, + ]; + if (others.length) { + groups.push({ + name: 'Other', + fields: others, + }); + } + // expand field names to attributes + return groups.map((group) => ({ + ...group, + fields: group.fields.map((attr) => this.args.model.allByKey[attr]), + })); + } + + placeholderOrModel = (model, attrName) => { + return model.operationAll ? { [attrName]: true } : model; + }; + + preSave() { + const opFieldsWithoutSpecial = operationFieldsWithoutSpecial(this.args.model.editableFields); + // if we have operationAll or operationNone, we want to clear + // out the others so that display shows the right data + if (this.args.model.operationAll || this.args.model.operationNone) { + opFieldsWithoutSpecial.forEach((field) => (this.args.model[field] = null)); + } + // set operationNone if user unchecks 'operationAll' instead of toggling the 'operationNone' input + // doing here instead of on the 'operationNone' input because a user might deselect all, then reselect some options + // and immediately setting operationNone will hide all of the checkboxes in the UI + this.args.model.operationNone = + opFieldsWithoutSpecial.every((attr) => this.args.model[attr] !== true) && !this.args.model.operationAll; + return this.args.model; + } + + @action toggleOperationSpecial(evt) { + const { checked } = evt.target; + this.args.model.operationNone = !checked; + this.args.model.operationAll = checked; + } + + save = task(async (evt) => { + evt.preventDefault(); + const model = this.preSave(); + try { + await model.save(); + this.flashMessages.success(`Saved role ${model.role}`); + } catch (err) { + // err will display via model state + // AdapterErrors are handled by the error-message component + if (err instanceof AdapterError === false) { + throw err; + } + return; + } + this.args.onSave(); + }); + + willDestroy() { + // components are torn down after store is unloaded and will cause an error if attempt to unload record + const noTeardown = this.store && !this.store.isDestroying; + if (noTeardown && this.args?.model?.isDirty) { + this.args.model.rollbackAttributes(); + } + super.willDestroy(); + } +} diff --git a/ui/lib/kmip/addon/routes/scope/roles/create.js b/ui/lib/kmip/addon/routes/scope/roles/create.js index 82984cddcb63..7070b4c27408 100644 --- a/ui/lib/kmip/addon/routes/scope/roles/create.js +++ b/ui/lib/kmip/addon/routes/scope/roles/create.js @@ -21,6 +21,7 @@ export default Route.extend({ const model = this.store.createRecord('kmip/role', { backend: this.secretMountPath.currentPath, scope: this.scope(), + operationAll: true, }); return model; }, diff --git a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs deleted file mode 100644 index 3aea47a1d8df..000000000000 --- a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs +++ /dev/null @@ -1,107 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -~}} - -
- -
- - {{#if (eq @mode "create")}} - - {{/if}} -
- - -
- {{#unless this.model.operationNone}} - -

- Allowed Operations -

-
-
- -
-
-
- {{#each-in (get this.model.operationFormFields 0) as |groupName fieldsInGroup|}} -

{{groupName}}

- {{#each fieldsInGroup as |attr|}} - - {{/each}} - {{/each-in}} -
-
- {{#each (drop 1 (or this.model.operationFormFields (array))) as |group|}} -
- {{#each-in group as |groupName fieldsInGroup|}} -

{{groupName}}

- {{#each fieldsInGroup as |attr|}} - - {{/each}} - {{/each-in}} -
- {{/each}} -
-
-
- {{/unless}} -
-

- TLS -

- {{#each this.model.tlsFormFields as |attr|}} - - {{/each}} -
- {{#each this.model.fields as |attr|}} - - {{/each}} -
- -
- - - {{#if this.cancelLink}} - - {{/if}} - -
- \ No newline at end of file diff --git a/ui/lib/kmip/addon/templates/role/edit.hbs b/ui/lib/kmip/addon/templates/role/edit.hbs index ab2003d85783..482083290567 100644 --- a/ui/lib/kmip/addon/templates/role/edit.hbs +++ b/ui/lib/kmip/addon/templates/role/edit.hbs @@ -13,8 +13,9 @@ - \ No newline at end of file diff --git a/ui/lib/kmip/addon/templates/scope/roles/create.hbs b/ui/lib/kmip/addon/templates/scope/roles/create.hbs index 42fcc4bf4074..71af81aa1bad 100644 --- a/ui/lib/kmip/addon/templates/scope/roles/create.hbs +++ b/ui/lib/kmip/addon/templates/scope/roles/create.hbs @@ -13,9 +13,9 @@ - \ No newline at end of file diff --git a/ui/lib/kmip/index.js b/ui/lib/kmip/index.js index 2d2ddc66e948..635b19df758a 100644 --- a/ui/lib/kmip/index.js +++ b/ui/lib/kmip/index.js @@ -17,6 +17,10 @@ module.exports = EngineAddon.extend({ enabled: true, }, + babel: { + plugins: [require.resolve('ember-concurrency/async-arrow-task-transform')], + }, + isDevelopingAddon() { return true; }, diff --git a/ui/tests/acceptance/enterprise-kmip-test.js b/ui/tests/acceptance/enterprise-kmip-test.js index a2f743227640..916cc31da051 100644 --- a/ui/tests/acceptance/enterprise-kmip-test.js +++ b/ui/tests/acceptance/enterprise-kmip-test.js @@ -16,7 +16,7 @@ import { import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import authPage from 'vault/tests/pages/auth'; +import { login } from 'vault/tests/helpers/auth/auth-helpers'; import scopesPage from 'vault/tests/pages/secrets/backend/kmip/scopes'; import rolesPage from 'vault/tests/pages/secrets/backend/kmip/roles'; import credentialsPage from 'vault/tests/pages/secrets/backend/kmip/credentials'; @@ -91,7 +91,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { hooks.beforeEach(async function () { this.backend = `kmip-${uuidv4()}`; - await authPage.login(); + await login(); return; }); @@ -221,16 +221,11 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { test('it can create a role', async function (assert) { // moving create scope here to help with flaky test - const backend = await mountWithConfig(this.backend); - await settled(); const scope = `scope-for-can-create-role`; - await settled(); - const res = await runCmd([`write ${backend}/scope/${scope} -force`]); - await settled(); - if (res.includes('Error')) { - throw new Error(`Error creating scope: ${res}`); - } const role = `role-new-role`; + const backend = await mountWithConfig(this.backend); + await settled(); + await runCmd([`write ${backend}/scope/${scope} -force`], true); await rolesPage.visit({ backend, scope }); await settled(); assert.ok(rolesPage.isEmpty, 'renders the empty role page'); @@ -241,11 +236,15 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/create`, 'links to the role create form' ); + // check that the role form looks right + assert.dom(GENERAL.inputByAttr('operationNone')).isChecked('allows role to perform roles by default'); + assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default'); + assert.dom('[data-test-kmip-section]').exists({ count: 2 }); + assert.dom('[data-test-kmip-operations]').exists({ count: 4 }); await rolesPage.roleName(role); await settled(); - await rolesPage.submit(); - await settled(); + await click(GENERAL.saveButton); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kmip/scopes/${scope}/roles`, @@ -253,6 +252,13 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { ); assert.strictEqual(rolesPage.listItemLinks.length, 1, 'renders a single role'); + await rolesPage.visitDetail({ backend, scope, role }); + // check that the role details looks right + assert.dom('h2').exists({ count: 2 }, 'renders correct section headings'); + assert.dom('[data-test-inline-error-message]').hasText('This role allows all KMIP operations'); + ['Managed Cryptographic Objects', 'Object Attributes', 'Server', 'Other'].forEach((title) => { + assert.dom(`[data-test-row-label="${title}"]`).exists(`Renders allowed operations row for: ${title}`); + }); }); test('it navigates to kmip roles view using breadcrumbs', async function (assert) { @@ -304,8 +310,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/${role}/edit`, 'navigates to role edit' ); - await rolesPage.cancelLink(); - await settled(); + await click(GENERAL.cancelButton); assert.strictEqual( currentURL(), `/vault/secrets/${backend}/kmip/scopes/${scope}/roles/${role}`, @@ -364,12 +369,12 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { this.store = this.owner.lookup('service:store'); this.scope = 'my-scope'; this.name = 'my-role'; - await authPage.login(); + await login(); await runCmd(mountEngineCmd('kmip', this.backend), false); await runCmd([`write ${this.backend}/scope/${this.scope} -force`]); await rolesPage.visit({ backend: this.backend, scope: this.scope }); this.setModel = async () => { - await click('[data-test-edit-form-submit]'); + await click(GENERAL.saveButton); await visit(`/vault/secrets/${this.backend}/kmip/scopes/${this.scope}/roles/${this.name}`); this.model = this.store.peekRecord('kmip/role', this.name); }; @@ -382,7 +387,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(3); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); assert.dom(GENERAL.inputByAttr('operationAll')).isChecked('operationAll is checked by default'); await this.setModel(); assert.true(this.model.operationAll, 'operationAll is true'); @@ -393,7 +398,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(4); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationNone')); assert .dom(GENERAL.inputByAttr('operationNone')) @@ -410,9 +415,10 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(2); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationAll')); await this.setModel(); + assert.strictEqual(this.model.operationAll, undefined, 'operationAll is unset'); assert.true(this.model.operationNone, 'operationNone is true'); }); @@ -421,7 +427,7 @@ module('Acceptance | Enterprise | KMIP secrets', function (hooks) { assert.expect(6); await click('[data-test-role-create]'); - await fillIn(GENERAL.inputByAttr('name'), this.name); + await fillIn(GENERAL.inputByAttr('role'), this.name); await click(GENERAL.inputByAttr('operationAll')); await click(GENERAL.inputByAttr('operationGet')); await click(GENERAL.inputByAttr('operationGetAttributes')); diff --git a/ui/tests/pages/secrets/backend/kmip/roles.js b/ui/tests/pages/secrets/backend/kmip/roles.js index aafaeba988a0..bc06acb4a584 100644 --- a/ui/tests/pages/secrets/backend/kmip/roles.js +++ b/ui/tests/pages/secrets/backend/kmip/roles.js @@ -11,7 +11,7 @@ export default create({ visit: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles'), visitDetail: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles/:role'), create: clickable('[data-test-role-create]'), - roleName: fillable('[data-test-input="name"]'), + roleName: fillable('[data-test-input="role"]'), submit: clickable('[data-test-edit-form-submit]'), detailEditLink: clickable('[data-test-kmip-link-edit-role]'), cancelLink: clickable('[data-test-edit-form-cancel]'), diff --git a/ui/tests/unit/adapters/kmip/role-test.js b/ui/tests/unit/adapters/kmip/role-test.js index a9d20d900459..36dcf9a883de 100644 --- a/ui/tests/unit/adapters/kmip/role-test.js +++ b/ui/tests/unit/adapters/kmip/role-test.js @@ -9,6 +9,8 @@ import { setupTest } from 'ember-qunit'; module('Unit | Adapter | kmip/role', function (hooks) { setupTest(hooks); + // these are only some of the actual editable fields + const editableFields = ['tlsTtl', 'operationAll', 'operationNone', 'operationGet', 'operationCreate']; const serializeTests = [ [ 'operation_all is the only operation item present after serialization', @@ -17,7 +19,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_all: true, operation_get: true, operation_create: true, tls_ttl: '10s' }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -32,7 +34,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_all: true, operation_get: true, operation_create: true }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -46,7 +48,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { return { operation_none: true, operation_get: true, operation_add_attribute: true, tls_ttl: '10s' }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { @@ -67,7 +69,7 @@ module('Unit | Adapter | kmip/role', function (hooks) { }; }, record: { - nonOperationFields: ['tlsTtl'], + editableFields, }, }, { diff --git a/ui/tests/unit/utils/kmip-role-fields-test.js b/ui/tests/unit/utils/kmip-role-fields-test.js new file mode 100644 index 000000000000..effbda02fb5c --- /dev/null +++ b/ui/tests/unit/utils/kmip-role-fields-test.js @@ -0,0 +1,52 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { + nonOperationFields, + operationFields, + operationFieldsWithoutSpecial, +} from 'vault/utils/kmip-role-fields'; + +module('Unit | Util | kmip role fields', function (hooks) { + setupTest(hooks); + + [ + { + name: 'when fields is empty', + fields: [], + opFields: [], + nonOpFields: [], + opWithoutSpecial: [], + }, + { + name: 'when no op fields', + fields: ['foo', 'bar'], + opFields: [], + nonOpFields: ['foo', 'bar'], + opWithoutSpecial: [], + }, + { + name: 'when op fields', + fields: ['foo', 'bar', 'operationFoo', 'operationBar', 'operationAll'], + opFields: ['operationFoo', 'operationBar', 'operationAll'], + nonOpFields: ['foo', 'bar'], + opWithoutSpecial: ['operationFoo', 'operationBar'], + }, + ].forEach(({ name, fields, opFields, nonOpFields, opWithoutSpecial }) => { + test(`${name}`, function (assert) { + const originalFields = JSON.parse(JSON.stringify(fields)); + assert.deepEqual(operationFields(fields), opFields, 'operation fields correct'); + assert.deepEqual(nonOperationFields(fields), nonOpFields, 'non operation fields'); + assert.deepEqual( + operationFieldsWithoutSpecial(fields), + opWithoutSpecial, + 'operation fields without special' + ); + assert.deepEqual(fields, originalFields, 'does not mutate the original'); + }); + }); +});