diff --git a/ui/app/adapters/pki/key.js b/ui/app/adapters/pki/key.js index 91a979509..58541ef76 100644 --- a/ui/app/adapters/pki/key.js +++ b/ui/app/adapters/pki/key.js @@ -1,9 +1,7 @@ import ApplicationAdapter from '../application'; import { encodePath } from 'vault/utils/path-encoding-helpers'; - export default class PkiKeyAdapter extends ApplicationAdapter { namespace = 'v1'; - getUrl(backend, id) { const url = `${this.buildURL()}/${encodePath(backend)}`; if (id) { @@ -21,4 +19,9 @@ export default class PkiKeyAdapter extends ApplicationAdapter { const { backend, id } = query; return this.ajax(this.getUrl(backend, id), 'GET'); } + + deleteRecord(store, type, snapshot) { + const { id, record } = snapshot; + return this.ajax(this.getUrl(record.backend, id), 'DELETE'); + } } diff --git a/ui/app/models/pki/key.js b/ui/app/models/pki/key.js index d767289e7..7c5f2bb1a 100644 --- a/ui/app/models/pki/key.js +++ b/ui/app/models/pki/key.js @@ -1,18 +1,19 @@ import Model, { attr } from '@ember-data/model'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; +import { inject as service } from '@ember/service'; +import { withFormFields } from 'vault/decorators/model-form-fields'; +@withFormFields(['keyId', 'keyName', 'keyType', 'keyBits']) export default class PkiKeyModel extends Model { - @attr('string', { readOnly: true }) backend; + @service secretMountPath; + @attr('boolean') isDefault; @attr('string', { possibleValues: ['internal', 'external'] }) type; @attr('string', { detailsLabel: 'Key ID' }) keyId; @attr('string') keyName; @attr('string') keyType; - @attr('string', { detailsLabel: 'Key bit length' }) keyBits; // TODO confirm with crypto team to remove this field from details page + @attr('string') keyBits; - // TODO refactor when field-to-attrs util is refactored as decorator - constructor() { - super(...arguments); - this.formFields = expandAttributeMeta(this, ['keyId', 'keyName', 'keyType', 'keyBits']); + get backend() { + return this.secretMountPath.currentPath; } } diff --git a/ui/app/services/flash-messages.js b/ui/app/services/flash-messages.ts similarity index 59% rename from ui/app/services/flash-messages.js rename to ui/app/services/flash-messages.ts index ae0258579..a2de0a1a7 100644 --- a/ui/app/services/flash-messages.js +++ b/ui/app/services/flash-messages.ts @@ -1,10 +1,10 @@ import FlashMessages from 'ember-cli-flash/services/flash-messages'; -export default FlashMessages.extend({ - stickyInfo(message) { +export default class FlashMessageService extends FlashMessages { + stickyInfo(message: string) { return this.info(message, { sticky: true, priority: 300, }); - }, -}); + } +} diff --git a/ui/app/utils/error-message.js b/ui/app/utils/error-message.js index 21ef2a50f..82614f5a6 100644 --- a/ui/app/utils/error-message.js +++ b/ui/app/utils/error-message.js @@ -1,6 +1,6 @@ // accepts an error and returns error.errors joined with a comma, error.message or a fallback message export default function (error, fallbackMessage = 'An error occurred, please try again') { - if (error?.errors) { + if (error instanceof Error && error?.errors) { return error.errors.join(', '); } return error?.message || fallbackMessage; diff --git a/ui/lib/pki/addon/components/pki-key-details.hbs b/ui/lib/pki/addon/components/page/pki-key-details.hbs similarity index 100% rename from ui/lib/pki/addon/components/pki-key-details.hbs rename to ui/lib/pki/addon/components/page/pki-key-details.hbs diff --git a/ui/lib/pki/addon/components/page/pki-key-details.ts b/ui/lib/pki/addon/components/page/pki-key-details.ts new file mode 100644 index 000000000..713fc426f --- /dev/null +++ b/ui/lib/pki/addon/components/page/pki-key-details.ts @@ -0,0 +1,41 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +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'; +interface Args { + key: { + rollbackAttributes: () => void; + destroyRecord: () => void; + backend: string; + keyName: string; + keyId: string; + }; +} + +export default class PkiKeyDetails extends Component { + @service declare readonly router: RouterService; + @service declare readonly flashMessages: FlashMessageService; + + get breadcrumbs() { + return [ + { label: 'secrets', route: 'secrets', linkExternal: true }, + { label: this.args.key.backend || 'pki', route: 'overview' }, + { label: 'keys', route: 'keys.index' }, + { label: this.args.key.keyId }, + ]; + } + + @action + async deleteKey() { + try { + await this.args.key.destroyRecord(); + this.flashMessages.success('Key deleted successfully'); + this.router.transitionTo('vault.cluster.secrets.backend.pki.keys.index'); + } catch (error) { + this.args.key.rollbackAttributes(); + this.flashMessages.danger(errorMessage(error)); + } + } +} diff --git a/ui/lib/pki/addon/components/pki-role-details-page.hbs b/ui/lib/pki/addon/components/page/pki-role-details.hbs similarity index 100% rename from ui/lib/pki/addon/components/pki-role-details-page.hbs rename to ui/lib/pki/addon/components/page/pki-role-details.hbs diff --git a/ui/lib/pki/addon/components/pki-role-details-page.ts b/ui/lib/pki/addon/components/page/pki-role-details.ts similarity index 100% rename from ui/lib/pki/addon/components/pki-role-details-page.ts rename to ui/lib/pki/addon/components/page/pki-role-details.ts diff --git a/ui/lib/pki/addon/components/pki-key-details.ts b/ui/lib/pki/addon/components/pki-key-details.ts deleted file mode 100644 index c9eaa3793..000000000 --- a/ui/lib/pki/addon/components/pki-key-details.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -interface Args { - key: { - backend: string; - keyName: string; - keyId: string; - }; -} - -export default class PkiKeyDetails extends Component { - @service declare secretMountPath: { currentPath: string }; - - get breadcrumbs() { - return [ - { label: 'secrets', route: 'secrets', linkExternal: true }, - { label: this.secretMountPath.currentPath || 'pki', route: 'overview' }, - { label: 'keys', route: 'keys.index' }, - { label: this.args.key.keyId }, - ]; - } - - @action deleteKey() { - // TODO handle delete - } -} diff --git a/ui/lib/pki/addon/templates/keys/key/details.hbs b/ui/lib/pki/addon/templates/keys/key/details.hbs index f371d1b43..89251f235 100644 --- a/ui/lib/pki/addon/templates/keys/key/details.hbs +++ b/ui/lib/pki/addon/templates/keys/key/details.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/lib/pki/addon/templates/roles/role/details.hbs b/ui/lib/pki/addon/templates/roles/role/details.hbs index 8e79cd382..4b5f7a632 100644 --- a/ui/lib/pki/addon/templates/roles/role/details.hbs +++ b/ui/lib/pki/addon/templates/roles/role/details.hbs @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/ui/tests/helpers/pki/keys/page-details.js b/ui/tests/helpers/pki/keys/page-details.js new file mode 100644 index 000000000..1be50fc05 --- /dev/null +++ b/ui/tests/helpers/pki/keys/page-details.js @@ -0,0 +1,11 @@ +export const SELECTORS = { + breadcrumbContainer: '[data-test-breadcrumbs]', + breadcrumbs: '[data-test-breadcrumbs] li', + title: '[data-test-key-details-title]', + keyIdValue: '[data-test-value-div="Key ID"]', + keyNameValue: '[data-test-value-div="Key name"]', + keyTypeValue: '[data-test-value-div="Key type"]', + keyBitsValue: '[data-test-value-div="Key bits"]', + keyDeleteButton: '[data-test-pki-key-delete] button', + confirmDelete: '[data-test-confirm-button]', +}; diff --git a/ui/tests/helpers/pki-engine.js b/ui/tests/helpers/pki/roles/form.js similarity index 99% rename from ui/tests/helpers/pki-engine.js rename to ui/tests/helpers/pki/roles/form.js index 07b7e6b22..9f361ea55 100644 --- a/ui/tests/helpers/pki-engine.js +++ b/ui/tests/helpers/pki/roles/form.js @@ -1,7 +1,6 @@ export const PKI_BASE_URL = `/vault/cluster/secrets/backend/pki/roles`; export const SELECTORS = { - // Pki role roleName: '[data-test-input="name"]', issuerRef: '[data-test-input="issuerRef"]', customTtl: '[data-test-field="customTtl"]', diff --git a/ui/tests/helpers/pki/page-role-details.js b/ui/tests/helpers/pki/roles/page-details.js similarity index 100% rename from ui/tests/helpers/pki/page-role-details.js rename to ui/tests/helpers/pki/roles/page-details.js diff --git a/ui/tests/integration/components/pki/keys/page-details-test.js b/ui/tests/integration/components/pki/keys/page-details-test.js new file mode 100644 index 000000000..b254ee19e --- /dev/null +++ b/ui/tests/integration/components/pki/keys/page-details-test.js @@ -0,0 +1,53 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupEngine } from 'ember-engines/test-support'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { SELECTORS } from 'vault/tests/helpers/pki/keys/page-details'; + +module('Integration | Component | pki key details page', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.owner.lookup('service:flash-messages').registerTypes(['success', 'danger']); + this.store = this.owner.lookup('service:store'); + this.secretMountPath = this.owner.lookup('service:secret-mount-path'); + this.backend = 'pki-test'; + this.secretMountPath.currentPath = this.backend; + this.store.pushPayload('pki/key', { + modelName: 'pki/key', + key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', + key_type: 'ec', + key_name: 'test-key', + }); + this.model = this.store.peekRecord('pki/key', '724862ff-6438-bad0-b598-77a6c7f4e934'); + }); + + test('it renders the page component and deletes a key', async function (assert) { + assert.expect(9); + this.server.delete(`${this.backend}/key/${this.model.keyId}`, () => { + assert.ok(true, 'confirming delete fires off destroyRecord()'); + }); + + await render( + hbs` + + `, + { owner: this.engine } + ); + + assert.dom(SELECTORS.breadcrumbContainer).exists({ count: 1 }, 'breadcrumb containers exist'); + assert.dom(SELECTORS.breadcrumbs).exists({ count: 4 }, 'Shows 4 breadcrumbs'); + assert.dom(SELECTORS.title).containsText('View key', 'title renders'); + assert.dom(SELECTORS.keyIdValue).hasText(' 724862ff-6438-bad0-b598-77a6c7f4e934', 'key id renders'); + assert.dom(SELECTORS.keyNameValue).hasText('test-key', 'key name renders'); + assert.dom(SELECTORS.keyTypeValue).hasText('ec', 'key type renders'); + assert.dom(SELECTORS.keyBitsValue).doesNotExist('does not render empty value'); + assert.dom(SELECTORS.keyDeleteButton).exists('renders delete button'); + await click(SELECTORS.keyDeleteButton); + await click(SELECTORS.confirmDelete); + }); +}); diff --git a/ui/tests/integration/components/pki/pki-key-parameters-test.js b/ui/tests/integration/components/pki/pki-key-parameters-test.js index 3aaa877de..13b1ce4c8 100644 --- a/ui/tests/integration/components/pki/pki-key-parameters-test.js +++ b/ui/tests/integration/components/pki/pki-key-parameters-test.js @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render, fillIn } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; -import { SELECTORS } from 'vault/tests/helpers/pki-engine'; +import { SELECTORS } from 'vault/tests/helpers/pki/roles/form'; module('Integration | Component | pki-key-parameters', function (hooks) { setupRenderingTest(hooks); diff --git a/ui/tests/integration/components/pki/pki-key-usage-test.js b/ui/tests/integration/components/pki/pki-key-usage-test.js index 927d3854f..e290030c7 100644 --- a/ui/tests/integration/components/pki/pki-key-usage-test.js +++ b/ui/tests/integration/components/pki/pki-key-usage-test.js @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render, click, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; -import { SELECTORS } from 'vault/tests/helpers/pki-engine'; +import { SELECTORS } from 'vault/tests/helpers/pki/roles/form'; module('Integration | Component | pki-key-usage', function (hooks) { setupRenderingTest(hooks); diff --git a/ui/tests/integration/components/pki/pki-role-form-test.js b/ui/tests/integration/components/pki/roles/form-test.js similarity index 98% rename from ui/tests/integration/components/pki/pki-role-form-test.js rename to ui/tests/integration/components/pki/roles/form-test.js index 92d51762a..2704b7fa9 100644 --- a/ui/tests/integration/components/pki/pki-role-form-test.js +++ b/ui/tests/integration/components/pki/roles/form-test.js @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render, click, fillIn, find } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; -import { SELECTORS } from 'vault/tests/helpers/pki-engine'; +import { SELECTORS } from 'vault/tests/helpers/pki/roles/form'; import { setupMirage } from 'ember-cli-mirage/test-support'; module('Integration | Component | pki-role-form', function (hooks) { diff --git a/ui/tests/integration/components/pki/page-role-detail-test.js b/ui/tests/integration/components/pki/roles/page-details-test.js similarity index 92% rename from ui/tests/integration/components/pki/page-role-detail-test.js rename to ui/tests/integration/components/pki/roles/page-details-test.js index 893fb05b2..f77a3e617 100644 --- a/ui/tests/integration/components/pki/page-role-detail-test.js +++ b/ui/tests/integration/components/pki/roles/page-details-test.js @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupEngine } from 'ember-engines/test-support'; -import { SELECTORS } from 'vault/tests/helpers/pki/page-role-details'; +import { SELECTORS } from 'vault/tests/helpers/pki/roles/page-details'; module('Integration | Component | pki role details page', function (hooks) { setupRenderingTest(hooks); @@ -24,7 +24,7 @@ module('Integration | Component | pki role details page', function (hooks) { assert.expect(7); await render( hbs` - + `, { owner: this.engine } ); diff --git a/ui/tests/unit/adapters/pki/key-test.js b/ui/tests/unit/adapters/pki/key-test.js new file mode 100644 index 000000000..2adea524e --- /dev/null +++ b/ui/tests/unit/adapters/pki/key-test.js @@ -0,0 +1,60 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; + +module('Unit | Adapter | pki/key', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + this.secretMountPath = this.owner.lookup('service:secret-mount-path'); + this.backend = 'pki-test'; + this.secretMountPath.currentPath = this.backend; + this.data = { + key_id: '724862ff-6438-bad0-b598-77a6c7f4e934', + key_type: 'ec', + key_name: 'test-key', + key_bits: '256', + }; + }); + + hooks.afterEach(function () { + this.server.shutdown(); + }); + + test('it should make request to correct endpoint on query', async function (assert) { + assert.expect(1); + const { key_id, ...otherAttrs } = this.data; // excludes key_id from key_info data + const key_info = { [key_id]: { ...otherAttrs } }; + this.server.get(`${this.backend}/keys`, (schema, req) => { + assert.strictEqual(req.queryParams.list, 'true', 'request is made to correct endpoint on query'); + return { data: { keys: [key_id], key_info } }; + }); + + this.store.query('pki/key', { backend: this.backend }); + }); + + test('it should make request to correct endpoint on queryRecord', async function (assert) { + assert.expect(1); + + this.server.get(`${this.backend}/key/${this.data.key_id}`, () => { + assert.ok(true, 'request is made to correct endpoint on query record'); + return { data: this.data }; + }); + + this.store.queryRecord('pki/key', { backend: this.backend, id: this.data.key_id }); + }); + + test('it should make request to correct endpoint on delete', async function (assert) { + assert.expect(1); + this.store.pushPayload('pki/key', { modelName: 'pki/key', ...this.data }); + this.server.get(`${this.backend}/key/${this.data.key_id}`, () => ({ data: this.data })); + this.server.delete(`${this.backend}/key/${this.data.key_id}`, () => { + assert.ok(true, 'request made to correct endpoint on delete'); + }); + + const model = await this.store.queryRecord('pki/key', { backend: this.backend, id: this.data.key_id }); + await model.destroyRecord(); + }); +}); diff --git a/ui/types/ember-cli-flash/services/flash-messages.d.ts b/ui/types/ember-cli-flash/services/flash-messages.d.ts new file mode 100644 index 000000000..ea399b463 --- /dev/null +++ b/ui/types/ember-cli-flash/services/flash-messages.d.ts @@ -0,0 +1,44 @@ +declare module 'ember-cli-flash/services/flash-messages' { + import Service from '@ember/service'; + import FlashObject from 'ember-cli-flash/flash/object'; + import { A } from '@ember/array'; + + type Partial = { [K in keyof T]?: T[K] }; + + interface MessageOptions { + type: string; + priority: number; + timeout: number; + sticky: boolean; + showProgress: boolean; + extendedTimeout: number; + destroyOnClick: boolean; + onDestroy: () => void; + [key: string]: unknown; + } + + interface CustomMessageInfo extends Partial { + message: string; + } + + interface FlashFunction { + (message: string, options?: Partial): FlashMessageService; + } + + class FlashMessageService extends Service { + queue: A; + success: FlashFunction; + warning: FlashFunction; + info: FlashFunction; + error: FlashFunction; + danger: FlashFunction; + alert: FlashFunction; + secondary: FlashFunction; + add(messageInfo: CustomMessageInfo): FlashMessageService; + clearMessages(): FlashMessageService; + registerTypes(types: string[]): FlashMessageService; + getFlashObject(): FlashObject; + } + + export default FlashMessageService; +}