From b18313b4ebdf96d5f7bcf0017f413ae3355c1185 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:23:15 -0500 Subject: [PATCH] UI: kv-v2 version history & path tests (#22593) --- changelog/22593.txt | 3 + ui/app/components/diff-version-selector.js | 1 + .../addon/components/kv-version-dropdown.hbs | 9 - .../kv/addon/components/page/secret/edit.hbs | 23 ++ .../kv/addon/components/page/secret/edit.js | 24 ++ .../page/secret/metadata/version-diff.hbs | 91 ----- .../page/secret/metadata/version-diff.js | 79 ----- ui/lib/kv/addon/routes.js | 1 - ui/lib/kv/addon/routes/secret/metadata.js | 37 +++ .../kv/addon/routes/secret/metadata/diff.js | 22 -- .../addon/templates/secret/metadata/diff.hbs | 6 - .../secrets/backend/kv/diff-test.js | 74 ----- .../backend/kv/kv-create-new-version-test.js | 64 ---- .../backend/kv/kv-engine-permissions-test.js | 110 ------ .../backend/kv/kv-v2-workflow-create-test.js | 34 +- .../backend/kv/kv-v2-workflow-delete-test.js | 19 +- .../kv/kv-v2-workflow-navigation-test.js | 95 +++--- ...v-v2-workflow-version-history-diff-test.js | 171 ---------- ...-v2-workflow-version-history-paths-test.js | 314 ++++++++++++++++++ ui/tests/helpers/kv/kv-run-commands.js | 11 + ui/tests/helpers/kv/kv-selectors.js | 12 +- .../kv/page/kv-page-secret-edit-test.js | 35 +- .../kv/page/kv-page-version-diff-test.js | 125 ------- 23 files changed, 540 insertions(+), 820 deletions(-) create mode 100644 changelog/22593.txt delete mode 100644 ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs delete mode 100644 ui/lib/kv/addon/components/page/secret/metadata/version-diff.js create mode 100644 ui/lib/kv/addon/routes/secret/metadata.js delete mode 100644 ui/lib/kv/addon/routes/secret/metadata/diff.js delete mode 100644 ui/lib/kv/addon/templates/secret/metadata/diff.hbs delete mode 100644 ui/tests/acceptance/secrets/backend/kv/diff-test.js delete mode 100644 ui/tests/acceptance/secrets/backend/kv/kv-create-new-version-test.js delete mode 100644 ui/tests/acceptance/secrets/backend/kv/kv-engine-permissions-test.js delete mode 100644 ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-diff-test.js create mode 100644 ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-paths-test.js delete mode 100644 ui/tests/integration/components/kv/page/kv-page-version-diff-test.js diff --git a/changelog/22593.txt b/changelog/22593.txt new file mode 100644 index 0000000000..8f5ee5f76d --- /dev/null +++ b/changelog/22593.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: JSON diff view available in "Create New Version" form for KV v2 +``` diff --git a/ui/app/components/diff-version-selector.js b/ui/app/components/diff-version-selector.js index 610f737905..6b52533261 100644 --- a/ui/app/components/diff-version-selector.js +++ b/ui/app/components/diff-version-selector.js @@ -12,6 +12,7 @@ import { tracked } from '@glimmer/tracking'; /** * @module DiffVersionSelector * DiffVersionSelector component includes a toolbar and diff view between KV 2 versions. It uses the library jsondiffpatch. + * TODO kv engine cleanup * * @example * ```js diff --git a/ui/lib/kv/addon/components/kv-version-dropdown.hbs b/ui/lib/kv/addon/components/kv-version-dropdown.hbs index b6f450ee91..ef703fb952 100644 --- a/ui/lib/kv/addon/components/kv-version-dropdown.hbs +++ b/ui/lib/kv/addon/components/kv-version-dropdown.hbs @@ -22,15 +22,6 @@ {{/each}} - {{! version diff }} - {{#if (gt @metadata.sortedVersions.length 1)}} -
-
  • - - Version Diff - -
  • - {{/if}} diff --git a/ui/lib/kv/addon/components/page/secret/edit.hbs b/ui/lib/kv/addon/components/page/secret/edit.hbs index 298e942f67..80b20b8022 100644 --- a/ui/lib/kv/addon/components/page/secret/edit.hbs +++ b/ui/lib/kv/addon/components/page/secret/edit.hbs @@ -38,6 +38,29 @@ @modelValidations={{this.modelValidations}} @isEdit={{true}} /> + +
    + + Show diff
    +
    {{if + this.diffDelta + "Showing the diff will reveal secret values" + "No changes to show. Update secret to view diff" + }}
    + {{#if this.showDiff}} +
    +
    {{sanitized-html this.visualDiff}}
    +
    + {{/if}} +
    +
    diff --git a/ui/lib/kv/addon/components/page/secret/edit.js b/ui/lib/kv/addon/components/page/secret/edit.js index c8888a5594..d56c20326b 100644 --- a/ui/lib/kv/addon/components/page/secret/edit.js +++ b/ui/lib/kv/addon/components/page/secret/edit.js @@ -26,15 +26,23 @@ import errorMessage from 'vault/utils/error-message'; * @param {array} breadcrumbs - breadcrumb objects to render in page header */ +/* eslint-disable no-undef */ export default class KvSecretEdit extends Component { @service controlGroup; @service flashMessages; @service router; @tracked showJsonView = false; + @tracked showDiff = false; @tracked errorMessage; @tracked modelValidations; @tracked invalidFormAlert; + originalSecret; + + constructor() { + super(...arguments); + this.originalSecret = JSON.stringify(this.args.secret.secretData || {}); + } get showOldVersionAlert() { const { currentVersion, previousVersion } = this.args; @@ -44,6 +52,22 @@ export default class KvSecretEdit extends Component { return false; } + get diffDelta() { + const oldData = JSON.parse(this.originalSecret); + const newData = this.args.secret.secretData; + + const diffpatcher = jsondiffpatch.create({}); + return diffpatcher.diff(oldData, newData); + } + + get visualDiff() { + if (!this.showDiff) return null; + const newData = this.args.secret.secretData; + return this.diffDelta + ? jsondiffpatch.formatters.html.format(this.diffDelta, newData) + : JSON.stringify(newData, undefined, 2); + } + @action toggleJsonView() { this.showJsonView = !this.showJsonView; diff --git a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs b/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs deleted file mode 100644 index 81a8dc3dd7..0000000000 --- a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.hbs +++ /dev/null @@ -1,91 +0,0 @@ - - <:toolbarFilters> - {{! left side version selector }} - - - Version - {{this.leftSideVersion}} - - - - - - - {{! right side version selector }} - - - Version - {{this.rightSideVersion}} - - - - - - - {{! status icon }} - {{#if this.statesMatch}} -
    - - States match -
    - {{/if}} - -
    -{{! Show an empty state if the current version of the secret is deleted or destroyed. This would only happen on init. }} -{{#if (and (loose-equal this.leftSideVersion @metadata.currentVersion) @metadata.currentSecret.isDeactivated)}} - -{{else}} -
    -
    {{sanitized-html this.visualDiff}}
    -
    -{{/if}} \ No newline at end of file diff --git a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.js b/ui/lib/kv/addon/components/page/secret/metadata/version-diff.js deleted file mode 100644 index b7784fb863..0000000000 --- a/ui/lib/kv/addon/components/page/secret/metadata/version-diff.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import { kvDataPath } from 'vault/utils/kv-path'; - -/** - * @module KvVersionDiff - * This component produces a JSON diff view between 2 secret versions. It uses the library jsondiffpatch. - * - * @param {string} backend - Backend from the kv/data model. - * @param {string} path - Backend from the kv/data model. - * @param {array} metadata - The kv/metadata model. It is version agnostic. - * @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component. - */ - -export default class KvVersionDiffComponent extends Component { - @service store; - @tracked leftSideVersion; - @tracked rightSideVersion; - @tracked visualDiff; - @tracked statesMatch = false; - - constructor() { - super(...arguments); - // tracked properties set here because they use args. - this.leftSideVersion = this.args.metadata.currentVersion; - this.rightSideVersion = this.defaultRightSideVersion; - this.createVisualDiff(); - } - - get defaultRightSideVersion() { - // unless the version is destroyed or deleted we return the version prior to the current version. - const versionData = this.args.metadata.sortedVersions.find( - (version) => - version.destroyed === false && version.deletion_time === '' && version.version != this.leftSideVersion - ); - return versionData ? versionData.version : this.leftSideVersion - 1; - } - - async fetchSecretData(version) { - const { backend, path } = this.args; - // check the store first, avoiding an extra network request if possible. - const storeData = await this.store.peekRecord('kv/data', kvDataPath(backend, path, version)); - const data = storeData ? storeData : await this.store.queryRecord('kv/data', { backend, path, version }); - - return data?.secretData; - } - - async createVisualDiff() { - /* eslint-disable no-undef */ - const leftSideData = await this.fetchSecretData(Number(this.leftSideVersion)); - const rightSideData = await this.fetchSecretData(Number(this.rightSideVersion)); - const diffpatcher = jsondiffpatch.create({}); - const delta = diffpatcher.diff(rightSideData, leftSideData); - - this.statesMatch = !delta; - this.visualDiff = delta - ? jsondiffpatch.formatters.html.format(delta, rightSideData) - : JSON.stringify(leftSideData, undefined, 2); - } - - @action selectVersion(version, actions, side) { - if (side === 'right') { - this.rightSideVersion = version; - } - if (side === 'left') { - this.leftSideVersion = version; - } - // close dropdown menu. - if (actions) actions.close(); - this.createVisualDiff(); - } -} diff --git a/ui/lib/kv/addon/routes.js b/ui/lib/kv/addon/routes.js index 529ecbe8d5..efd759b17f 100644 --- a/ui/lib/kv/addon/routes.js +++ b/ui/lib/kv/addon/routes.js @@ -19,7 +19,6 @@ export default buildRoutes(function () { this.route('metadata', function () { this.route('edit'); this.route('versions'); - this.route('diff'); }); }); this.route('configuration'); diff --git a/ui/lib/kv/addon/routes/secret/metadata.js b/ui/lib/kv/addon/routes/secret/metadata.js new file mode 100644 index 0000000000..36bd3f6c8c --- /dev/null +++ b/ui/lib/kv/addon/routes/secret/metadata.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Route from '@ember/routing/route'; +import { service } from '@ember/service'; + +export default class KvSecretMetadataRoute extends Route { + @service store; + @service secretMountPath; + + fetchMetadata(backend, path) { + return this.store.queryRecord('kv/metadata', { backend, path }).catch((error) => { + if (error.message === 'Control Group encountered') { + throw error; + } + return {}; + }); + } + + async model() { + const backend = this.secretMountPath.currentPath; + const { name: path } = this.paramsFor('secret'); + const parentModel = this.modelFor('secret'); + if (!parentModel.metadata) { + // metadata read on the secret root fails silently + // if there's no metadata, try again in case it's a control group + const metadata = await this.fetchMetadata(backend, path); + return { + ...parentModel, + metadata, + }; + } + return parentModel; + } +} diff --git a/ui/lib/kv/addon/routes/secret/metadata/diff.js b/ui/lib/kv/addon/routes/secret/metadata/diff.js deleted file mode 100644 index ad94103ad4..0000000000 --- a/ui/lib/kv/addon/routes/secret/metadata/diff.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Route from '@ember/routing/route'; -import { breadcrumbsForSecret } from 'kv/utils/kv-breadcrumbs'; - -export default class KvSecretMetadataDiffRoute extends Route { - setupController(controller, resolvedModel) { - super.setupController(controller, resolvedModel); - - const breadcrumbsArray = [ - { label: 'secrets', route: 'secrets', linkExternal: true }, - { label: resolvedModel.backend, route: 'list' }, - ...breadcrumbsForSecret(resolvedModel.path), - { label: 'version diff' }, - ]; - - controller.set('breadcrumbs', breadcrumbsArray); - } -} diff --git a/ui/lib/kv/addon/templates/secret/metadata/diff.hbs b/ui/lib/kv/addon/templates/secret/metadata/diff.hbs deleted file mode 100644 index 97b37f1261..0000000000 --- a/ui/lib/kv/addon/templates/secret/metadata/diff.hbs +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/ui/tests/acceptance/secrets/backend/kv/diff-test.js b/ui/tests/acceptance/secrets/backend/kv/diff-test.js deleted file mode 100644 index 8abb1c29a1..0000000000 --- a/ui/tests/acceptance/secrets/backend/kv/diff-test.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { click, settled, fillIn } from '@ember/test-helpers'; -import { create } from 'ember-cli-page-object'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import listPage from 'vault/tests/pages/secrets/backend/list'; -import apiStub from 'vault/tests/helpers/noop-all-api-requests'; -import authPage from 'vault/tests/pages/auth'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; - -const consoleComponent = create(consoleClass); - -// TODO: kv engine cleanup replace with workflow-diff-test -module('Acceptance | kv2 diff view', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.server = apiStub({ usePassthrough: true }); - return authPage.login(); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test.skip('it shows correct diff status based on versions', async function (assert) { - const secretPath = `my-secret`; - - await consoleComponent.runCommands([ - `write sys/mounts/secret type=kv options=version=2`, - // delete any kv previously written here so that tests can be re-run - `delete secret/metadata/${secretPath}`, - 'write -field=client_token auth/token/create policies=kv-v2-degrade', - ]); - - await listPage.visitRoot({ backend: 'secret' }); - await settled(); - await listPage.create(); - await settled(); - await editPage.createSecret(secretPath, 'version1', 'hello'); - await settled(); - await click('[data-test-popup-menu-trigger="version"]'); - - assert.dom('[data-test-view-diff]').doesNotExist('does not show diff view with only one version'); - // add another version - await click('[data-test-secret-edit="true"]'); - - const secondKey = document.querySelectorAll('[data-test-secret-key]')[1]; - const secondValue = document.querySelectorAll('.masked-value')[1]; - await fillIn(secondKey, 'version2'); - await fillIn(secondValue, 'world!'); - await click('[data-test-secret-save]'); - - await click('[data-test-popup-menu-trigger="version"]'); - - assert.dom('[data-test-view-diff]').exists('does show diff view with two versions'); - - await click('[data-test-view-diff]'); - - const diffBetweenVersion2and1 = document.querySelector('.jsondiffpatch-added').innerText; - assert.strictEqual(diffBetweenVersion2and1, 'version2"world!"', 'shows the correct added part'); - - await click('[data-test-popup-menu-trigger="right-version"]'); - - await click('[data-test-rightSide-version="2"]'); - - assert.dom('.diff-status').exists('shows States Match'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-create-new-version-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-create-new-version-test.js deleted file mode 100644 index 8c64718d6c..0000000000 --- a/ui/tests/acceptance/secrets/backend/kv/kv-create-new-version-test.js +++ /dev/null @@ -1,64 +0,0 @@ -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; -import { click, currentURL, fillIn, visit } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import enablePage from 'vault/tests/pages/settings/mount-secret-backend'; -import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; -import { deleteEngineCmd, runCmd } from 'vault/tests/helpers/commands'; - -module('Acceptance | kv | creates a secret and a new version', function (hooks) { - setupApplicationTest(hooks); - hooks.beforeEach(async function () { - await authPage.login(); - // Setup KV engine - this.mountPath = `kv-engine-${uuidv4()}`; - await enablePage.enable('kv', this.mountPath); - }); - - hooks.afterEach(async function () { - await authPage.login(); - // Cleanup engine - await runCmd(deleteEngineCmd(this.mountPath), false); - }); - - test('it creates a new secret then a new secret version and navigates to details route', async function (assert) { - assert.expect(9); - - const secretPath = 'my-secret'; - await visit(`/vault/secrets/${this.mountPath}/kv/list`); - assert.dom(PAGE.emptyStateTitle).hasText('No secrets yet'); - assert - .dom(`${PAGE.emptyStateActions} a`) - .hasAttribute('href', `/ui/vault/secrets/${this.mountPath}/kv/create`); - await click(PAGE.list.createSecret); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/create`, 'url is correct'); - - await fillIn(FORM.inputByAttr('path'), secretPath); - await fillIn(FORM.keyInput(), 'foo-1'); - await fillIn(FORM.maskedValueInput(), 'bar-1'); - await click(FORM.saveBtn); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/${secretPath}/details?version=1`); - - await click(PAGE.detail.createNewVersion); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/kv/${secretPath}/details/edit?version=1` - ); - assert.dom(FORM.inputByAttr('path')).isDisabled(); - - await fillIn(FORM.keyInput(1), 'foo-2'); - await fillIn(FORM.maskedValueInput(1), 'bar-2'); - await click(FORM.saveBtn); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/${secretPath}/details?version=2`); - - await visit(`/vault/secrets/${this.mountPath}/kv/list`); - await click(PAGE.list.item(secretPath)); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.mountPath}/kv/${secretPath}/details?version=2`, - 'list view navigates to latest version' - ); - assert.dom(PAGE.detail.versionTimestamp).hasTextContaining('Version 2'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-engine-permissions-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-engine-permissions-test.js deleted file mode 100644 index 7eeea27ac3..0000000000 --- a/ui/tests/acceptance/secrets/backend/kv/kv-engine-permissions-test.js +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import authPage from 'vault/tests/pages/auth'; -import { currentURL, visit } from '@ember/test-helpers'; -import { adminPolicy, dataPolicy, metadataPolicy } from 'vault/tests/helpers/policy-generator/kv'; -import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; -import { writeSecret } from 'vault/tests/helpers/kv/kv-run-commands'; -import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; - -/* -This test module tests KV permissions views, each module is is a separate tab (i.e. secret, metadata) -each sub-module is a different state, for example: -- it renders secret details -- it renders secret details after a version is deleted - -And each test authenticates using varying permissions testing that view state renders as expected. -*/ -// TODO: replace with workflow-* tests -module('Acceptance | kv permissions', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - await authPage.login(); - this.uid = uuidv4(); - // Setup KV engine - this.mountPath = `kv-engine-${this.uid}`; - await runCmd(mountEngineCmd('kv-v2', this.mountPath)); - return authPage.logout(); - }); - - hooks.afterEach(async function () { - await authPage.login(); - // Cleanup engine - await runCmd(deleteEngineCmd(this.mountPath)); - }); - - module('secret tab', function (hooks) { - hooks.beforeEach(async function () { - // Create secret - await authPage.login(); - this.secretPath = `my-secret-${this.uid}`; - await writeSecret(this.mountPath, this.secretPath, 'foo', 'bar'); - // Create different policy test cases - const kv_admin_policy = adminPolicy(this.mountPath); - this.kvAdminToken = await runCmd(tokenWithPolicyCmd('kv-admin', kv_admin_policy)); - - const no_metadata_read = - dataPolicy({ backend: this.mountPath, secretPath: this.secretPath }) + - metadataPolicy({ backend: this.mountPath, capabilities: ['list'] }); - this.cannotReadMetadata = await runCmd(tokenWithPolicyCmd('kv-no-metadata-read', no_metadata_read)); - - const no_data_read = dataPolicy({ - backend: this.mountPath, - secretPath: this.secretPath, - capabilities: ['list'], - }); - this.cannotReadData = await runCmd(tokenWithPolicyCmd('kv-no-metadata-read', no_data_read)); - await authPage.logout(); - }); - - module('it renders secret details page', function () { - test('it shows all tabs for admin policy', async function (assert) { - assert.expect(4); - await authPage.login(this.kvAdminToken); - await visit(`/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.dom(PAGE.secretTab('Secret')).exists(); - assert.dom(PAGE.secretTab('Metadata')).exists(); - assert.dom(PAGE.secretTab('Version History')).exists(); - }); - - test('it hides tabs when no metadata read', async function (assert) { - assert.expect(4); - await authPage.login(this.cannotReadMetadata); - await visit(`/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.dom(PAGE.secretTab('Secret')).exists(); - assert.dom(PAGE.secretTab('Metadata')).exists(); - assert.dom(PAGE.secretTab('Version History')).doesNotExist(); - }); - - test('it shows empty state when cannot read secret data', async function (assert) { - assert.expect(6); - await authPage.login(this.cannotReadData); - await visit(`/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/kv/${this.secretPath}/details`); - assert.dom(PAGE.secretTab('Secret')).exists(); - assert.dom(PAGE.secretTab('Metadata')).exists(); - assert.dom(PAGE.secretTab('Version History')).doesNotExist(); - assert.dom(PAGE.emptyStateTitle).hasText('You do not have permission to read this secret'); - assert - .dom(PAGE.emptyStateMessage) - .hasText( - 'Your policies may permit you to write a new version of this secret, but do not allow you to read its current contents.' - ); - }); - }); - - module('it renders secret details page after deleting a version', function () { - // TODO delete secret and test different policy views - }); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js index f2ad2e8f43..6b49b53941 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-create-test.js @@ -100,7 +100,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook 'Goes to details page after save' ); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created'); - assert.dom(PAGE.secretRow).exists({ count: 1 }, '1 row of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows'); assert.dom(PAGE.infoRowValue('api_key')).hasText('***********'); await click(PAGE.infoRowToggleMasked('api_key')); assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle'); @@ -111,7 +111,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`) .hasText('No custom metadata', 'No custom metadata empty state'); assert - .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.secretRow}`) + .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`) .exists({ count: 4 }, '4 metadata rows show'); assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0'); assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced'); @@ -137,7 +137,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook `/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details?version=2` ); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created'); - assert.dom(PAGE.secretRow).exists({ count: 2 }, '2 rows of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 2 }, '2 rows of data shows'); assert.dom(PAGE.infoRowValue('api_key')).hasText('***********'); assert.dom(PAGE.infoRowValue('api_url')).hasText('***********'); await click(PAGE.infoRowToggleMasked('api_key')); @@ -190,7 +190,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook `/vault/secrets/${backend}/kv/${encodeURIComponent('my/secret')}/details?version=1` ); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created'); - assert.dom(PAGE.secretRow).exists({ count: 1 }, '1 row of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows'); assert.dom(PAGE.infoRowValue('password')).hasText('***********'); await click(PAGE.infoRowToggleMasked('password')); assert.dom(PAGE.infoRowValue('password')).hasText('kittens1234', 'secret value shows after toggle'); @@ -198,12 +198,12 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // Metadata await click(PAGE.secretTab('Metadata')); assert - .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.secretRow}`) + .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.infoRow}`) .exists({ count: 1 }, 'One custom metadata row shows'); assert.dom(`${PAGE.metadata.customMetadataSection} ${PAGE.infoRowValue('team')}`).hasText('UI'); assert - .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.secretRow}`) + .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`) .exists({ count: 4 }, '4 metadata rows show'); assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('7', 'max versions shows 0'); assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('Yes', 'cas enforced'); @@ -328,7 +328,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // Since this persona can't create a new secret, test update with existing: await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created'); - assert.dom(PAGE.secretRow).exists({ count: 1 }, '1 row of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows'); assert.dom(PAGE.infoRowValue('foo')).hasText('***********'); await click(PAGE.infoRowToggleMasked('foo')); assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle'); @@ -473,7 +473,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // Since this persona can't create a new secret, test update with existing: await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created'); - assert.dom(PAGE.secretRow).exists({ count: 1 }, '1 row of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows'); assert.dom(PAGE.infoRowValue('foo')).hasText('***********'); await click(PAGE.infoRowToggleMasked('foo')); assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle'); @@ -620,7 +620,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook // Since this persona can't create a new secret, test update with existing: await visit(`/vault/secrets/${backend}/kv/app%2Ffirst/details`); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created tooltip does not show'); - assert.dom(PAGE.secretRow).doesNotExist('secret data not shown'); + assert.dom(PAGE.infoRow).doesNotExist('secret data not shown'); assert.dom(PAGE.emptyStateTitle).hasText('You do not have permission to read this secret'); // Metadata page @@ -629,7 +629,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`) .hasText('No custom metadata', 'No custom metadata empty state'); assert - .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.secretRow}`) + .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`) .exists({ count: 4 }, '4 metadata rows show'); assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0'); assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced'); @@ -835,7 +835,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook 'Goes to details page after save' ); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created not shown'); - assert.dom(PAGE.secretRow).doesNotExist('does not show data contents'); + assert.dom(PAGE.infoRow).doesNotExist('does not show data contents'); assert .dom(PAGE.emptyStateTitle) .hasText('You do not have permission to read this secret', 'shows permissions empty state'); @@ -871,7 +871,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook 'goes back to details page' ); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created does not show'); - assert.dom(PAGE.secretRow).doesNotExist('does not show data contents'); + assert.dom(PAGE.infoRow).doesNotExist('does not show data contents'); assert .dom(PAGE.emptyStateTitle) .hasText('You do not have permission to read this secret', 'shows permissions empty state'); @@ -922,7 +922,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook 'goes back to details page' ); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('version created not shown'); - assert.dom(PAGE.secretRow).doesNotExist('does not show data contents'); + assert.dom(PAGE.infoRow).doesNotExist('does not show data contents'); assert .dom(PAGE.emptyStateTitle) .hasText('You do not have permission to read this secret', 'shows permissions empty state'); @@ -991,7 +991,7 @@ module('Acceptance | kv-v2 workflow | secret and version create', function (hook `/vault/secrets/${backend}/kv/app%2Ffirst/details`, 'redirects to details page' ); - assert.dom(PAGE.secretRow).doesNotExist('does not show data contents'); + assert.dom(PAGE.infoRow).doesNotExist('does not show data contents'); assert .dom(PAGE.emptyStateTitle) .hasText('You do not have permission to read this secret', 'shows permissions empty state'); @@ -1084,7 +1084,7 @@ path "${this.backend}/metadata/*" { 'Goes to details page after save' ); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 1 created'); - assert.dom(PAGE.secretRow).exists({ count: 1 }, '1 row of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 1 }, '1 row of data shows'); assert.dom(PAGE.infoRowValue('api_key')).hasText('***********'); await click(PAGE.infoRowToggleMasked('api_key')); assert.dom(PAGE.infoRowValue('api_key')).hasText('partyparty', 'secret value shows after toggle'); @@ -1095,7 +1095,7 @@ path "${this.backend}/metadata/*" { .dom(`${PAGE.metadata.customMetadataSection} ${PAGE.emptyStateTitle}`) .hasText('No custom metadata', 'No custom metadata empty state'); assert - .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.secretRow}`) + .dom(`${PAGE.metadata.secretMetadataSection} ${PAGE.infoRow}`) .exists({ count: 4 }, '4 metadata rows show'); assert.dom(PAGE.infoRowValue('Maximum versions')).hasText('0', 'max versions shows 0'); assert.dom(PAGE.infoRowValue('Check-and-Set required')).hasText('No', 'cas not enforced'); @@ -1154,7 +1154,7 @@ path "${this.backend}/metadata/*" { `/vault/secrets/${backend}/kv/${encodeURIComponent(secretPath)}/details?version=2` ); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created'); - assert.dom(PAGE.secretRow).exists({ count: 2 }, '2 rows of data shows'); + assert.dom(PAGE.infoRow).exists({ count: 2 }, '2 rows of data shows'); assert.dom(PAGE.infoRowValue('api_key')).hasText('***********'); assert.dom(PAGE.infoRowValue('api_url')).hasText('***********'); await click(PAGE.infoRowToggleMasked('api_key')); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js index 5ee834a971..480d2110bd 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-delete-test.js @@ -6,6 +6,7 @@ import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vau import { personas } from 'vault/tests/helpers/policy-generator/kv'; import { clearRecords, + deleteLatestCmd, setupControlGroup, writeVersionedSecret, } from 'vault/tests/helpers/kv/kv-run-commands'; @@ -39,7 +40,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await writeVersionedSecret(this.backend, this.secretPath, 'foo', 'bar', 4); await writeVersionedSecret(this.backend, 'nuke', 'foo', 'bar', 2); // Delete latest version for testing undelete for users that can't delete - await runCmd(`delete ${this.backend}/data/nuke`); + await runCmd(deleteLatestCmd(this.backend, 'nuke')); return; }); @@ -61,7 +62,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details`); // correct toolbar options & details show assertDeleteActions(assert); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); // delete flow await click(PAGE.detail.delete); assert.dom(PAGE.detail.deleteModalTitle).includesText('Delete version?', 'shows correct modal title'); @@ -79,7 +80,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook // undelete flow await click(PAGE.detail.undelete); // details update accordingly - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 4 created'); // correct toolbar options assertDeleteActions(assert, ['delete', 'destroy']); @@ -90,7 +91,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details?version=2`); // correct toolbar options & details show assertDeleteActions(assert); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); // delete flow await click(PAGE.detail.delete); assert.dom(PAGE.detail.deleteModalTitle).includesText('Delete version?', 'shows correct modal title'); @@ -108,7 +109,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook // undelete flow await click(PAGE.detail.undelete); // details update accordingly - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); assert.dom(PAGE.detail.versionTimestamp).includesText('Version 2 created'); // correct toolbar options assertDeleteActions(assert, ['delete', 'destroy']); @@ -160,7 +161,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details`); // correct toolbar options & details show assertDeleteActions(assert, []); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); // data-reader can't delete, so check undelete with already-deleted version await visit(`/vault/secrets/${this.backend}/kv/nuke/details`); @@ -176,7 +177,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details?version=2`); // correct toolbar options & details show assertDeleteActions(assert, []); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); }); test('cannot destroy a secret version (dr)', async function (assert) { assert.expect(3); @@ -209,7 +210,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details`); // correct toolbar options & details show assertDeleteActions(assert, ['delete']); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); // delete flow await click(PAGE.detail.delete); assert.dom(PAGE.detail.deleteModalTitle).includesText('Delete version?', 'shows correct modal title'); @@ -232,7 +233,7 @@ module('Acceptance | kv-v2 workflow | delete, undelete, destroy', function (hook await visit(`/vault/secrets/${this.backend}/kv/${this.secretPath}/details?version=2`); // correct toolbar options & details show assertDeleteActions(assert, ['delete']); - assert.dom(PAGE.secretRow).exists('shows secret data'); + assert.dom(PAGE.infoRow).exists('shows secret data'); // delete flow await click(PAGE.detail.delete); assert.dom(PAGE.detail.deleteModalTitle).includesText('Delete version?', 'shows correct modal title'); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js index 8fc66ea461..4f6ed7f15a 100644 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-navigation-test.js @@ -35,6 +35,21 @@ const assertCorrectBreadcrumbs = (assert, expected) => { assert.dom(breadcrumbs[idx]).includesText(text, `position ${idx} breadcrumb includes text ${text}`); }); }; +const assertDetailTabs = (assert, current, hidden = []) => { + const allTabs = ['Secret', 'Metadata', 'Paths', 'Version History']; + allTabs.forEach((tab) => { + if (hidden.includes(tab)) { + assert.dom(PAGE.secretTab(tab)).doesNotExist(`${tab} tab does not render`); + return; + } + assert.dom(PAGE.secretTab(tab)).hasText(tab); + if (current === tab) { + assert.dom(PAGE.secretTab(tab)).hasClass('active'); + } else { + assert.dom(PAGE.secretTab(tab)).doesNotHaveClass('active'); + } + }); +}; const DETAIL_TOOLBARS = ['delete', 'destroy', 'copy', 'versionDropdown', 'createNewVersion']; const assertDetailsToolbar = (assert, expected = DETAIL_TOOLBARS) => { assert @@ -188,7 +203,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('versioned secret nav, tabs, breadcrumbs (a)', async function (assert) { - assert.expect(43); + assert.expect(45); const backend = this.backend; await navToBackend(backend); await click(PAGE.list.item(secretPath)); @@ -197,13 +212,8 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { `/vault/secrets/${backend}/kv/${secretPathUrlEncoded}/details?version=3`, 'Url includes version query param' ); - assert.dom(PAGE.title).hasText(secretPath, 'Goes to secret detail view'); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).hasClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); - assert.dom(PAGE.secretTab('Version History')).hasText('Version History'); - assert.dom(PAGE.secretTab('Version History')).doesNotHaveClass('active'); + assert.dom(PAGE.title).hasText(secretPath, 'title is correct on detail view'); + assertDetailTabs(assert, 'Secret'); assert.dom(PAGE.detail.versionDropdown).hasText('Version 3', 'Version dropdown shows current version'); assert.dom(PAGE.detail.createNewVersion).hasText('Create new version', 'Create version button shows'); assert.dom(PAGE.detail.versionTimestamp).containsText('Version 3 created'); @@ -279,7 +289,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { ); }); test('breadcrumbs & page titles are correct (a)', async function (assert) { - assert.expect(39); + assert.expect(45); const backend = this.backend; await navToBackend(backend); await click(PAGE.secretTab('Configuration')); @@ -308,6 +318,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.title).hasText('Edit Secret Metadata', 'correct page title for metadata edit'); await click(PAGE.breadcrumbAtIdx(3)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'paths']); + assert.dom(PAGE.title).hasText(secretPath, 'correct page title for paths'); + await click(PAGE.secretTab('Version History')); assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'version history']); assert.dom(PAGE.title).hasText(secretPath, 'correct page title for version history'); @@ -409,7 +423,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('versioned secret nav, tabs, breadcrumbs (dr)', async function (assert) { - assert.expect(25); + assert.expect(28); const backend = this.backend; await navToBackend(backend); @@ -423,10 +437,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { 'Url includes version query param' ); assert.dom(PAGE.title).hasText(secretPath, 'Goes to secret detail view'); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).hasClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); + assertDetailTabs(assert, 'Secret', ['Version History']); assert.dom(PAGE.detail.versionDropdown).doesNotExist('Version dropdown hidden'); assert.dom(PAGE.detail.createNewVersion).doesNotExist('unable to create a new version'); assert.dom(PAGE.detail.versionTimestamp).containsText('Version 3 created'); @@ -458,7 +469,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('edit button hidden'); }); test('breadcrumbs & page titles are correct (dr)', async function (assert) { - assert.expect(27); + assert.expect(35); const backend = this.backend; await navToBackend(backend); await click(PAGE.secretTab('Configuration')); @@ -482,7 +493,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('cannot edit metadata'); - await click(PAGE.breadcrumbAtIdx(2)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, 'app', 'nested', 'secret', 'paths']); + assert.dom(PAGE.title).hasText('app/nested/secret', 'correct page title for paths'); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown'); }); }); @@ -599,7 +613,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('versioned secret nav, tabs, breadcrumbs (dlr)', async function (assert) { - assert.expect(25); + assert.expect(28); const backend = this.backend; await navToBackend(backend); await click(PAGE.list.item(secretPath)); @@ -609,10 +623,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { 'Url includes version query param' ); assert.dom(PAGE.title).hasText(secretPath, 'Goes to secret detail view'); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).hasClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); + assertDetailTabs(assert, 'Secret', ['Version History']); assert.dom(PAGE.detail.versionDropdown).doesNotExist('does not show version dropdown'); assert.dom(PAGE.detail.createNewVersion).doesNotExist('unable to create a new version'); assert.dom(PAGE.detail.versionTimestamp).containsText('Version 3 created'); @@ -645,7 +656,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('edit button hidden'); }); test('breadcrumbs & page titles are correct (dlr)', async function (assert) { - assert.expect(23); + assert.expect(29); const backend = this.backend; await navToBackend(backend); @@ -669,7 +680,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('cannot edit metadata'); - await click(PAGE.breadcrumbAtIdx(2)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'paths']); + assert.dom(PAGE.title).hasText(secretPath, 'correct page title for paths'); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown'); }); }); @@ -790,7 +804,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('versioned secret nav, tabs, breadcrumbs (mm)', async function (assert) { - assert.expect(35); + assert.expect(37); const backend = this.backend; await navToBackend(backend); await click(PAGE.list.item(secretPath)); @@ -801,12 +815,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { 'Url includes version query param' ); assert.dom(PAGE.title).hasText(secretPath, 'Goes to secret detail view'); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).hasClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); - assert.dom(PAGE.secretTab('Version History')).hasText('Version History'); - assert.dom(PAGE.secretTab('Version History')).doesNotHaveClass('active'); + assertDetailTabs(assert, 'Secret'); assert.dom(PAGE.detail.versionDropdown).hasText('Version 3', 'Version dropdown shows current version'); assert.dom(PAGE.detail.createNewVersion).doesNotExist('Create new version button not shown'); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created text not shown'); @@ -863,7 +872,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { ); }); test('breadcrumbs & page titles are correct (mm)', async function (assert) { - assert.expect(33); + assert.expect(39); const backend = this.backend; await navToBackend(backend); await click(PAGE.secretTab('Configuration')); @@ -887,6 +896,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.title).hasText('Edit Secret Metadata', 'correct page title for metadata edit'); await click(PAGE.breadcrumbAtIdx(3)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'paths']); + assert.dom(PAGE.title).hasText(secretPath, 'correct page title for paths'); + await click(PAGE.secretTab('Version History')); assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'version history']); assert.dom(PAGE.title).hasText(secretPath, 'correct page title for version history'); @@ -983,7 +996,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('versioned secret nav, tabs, breadcrumbs (sc)', async function (assert) { - assert.expect(34); + assert.expect(36); const backend = this.backend; await navToBackend(backend); @@ -995,11 +1008,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { 'Goes to detail view' ); assert.dom(PAGE.title).hasText(secretPath, 'Goes to secret detail view'); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).hasClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); - assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab does not render'); + assertDetailTabs(assert, 'Secret', ['Version History']); assert.dom(PAGE.detail.versionDropdown).doesNotExist('Version dropdown does not render'); assert.dom(PAGE.detail.createNewVersion).hasText('Create new version', 'Create version button shows'); assert.dom(PAGE.detail.versionTimestamp).doesNotExist('Version created info is not rendered'); @@ -1061,7 +1070,7 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('edit metadata button does not render'); }); test('breadcrumbs & page titles are correct (sc)', async function (assert) { - assert.expect(28); + assert.expect(34); const backend = this.backend; await navToBackend(backend); await click(PAGE.secretTab('Configuration')); @@ -1089,6 +1098,10 @@ module('Acceptance | kv-v2 workflow | navigation', function (hooks) { assert.dom(PAGE.metadata.editBtn).doesNotExist('cannot edit metadata'); await click(PAGE.breadcrumbAtIdx(2)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'paths']); + assert.dom(PAGE.title).hasText(secretPath, 'correct page title for paths'); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown'); }); }); @@ -1189,7 +1202,7 @@ path "${this.backend}/*" { assert.ok(currentURL().startsWith(`/vault/secrets/${backend}/kv/list`), 'links back to list root'); }); test('breadcrumbs & page titles are correct (cg)', async function (assert) { - assert.expect(30); + assert.expect(36); const backend = this.backend; await navToBackend(backend); await click(PAGE.secretTab('Configuration')); @@ -1230,6 +1243,10 @@ path "${this.backend}/*" { assert.dom(PAGE.metadata.editBtn).doesNotExist('cannot edit metadata'); await click(PAGE.breadcrumbAtIdx(2)); + await click(PAGE.secretTab('Paths')); + assertCorrectBreadcrumbs(assert, ['secrets', backend, secretPath, 'paths']); + assert.dom(PAGE.title).hasText(secretPath, 'correct page title for paths'); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Version History tab not shown'); await click(PAGE.secretTab('Secret')); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-diff-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-diff-test.js deleted file mode 100644 index ac38bbb474..0000000000 --- a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-diff-test.js +++ /dev/null @@ -1,171 +0,0 @@ -import { module, test } from 'qunit'; -import { v4 as uuidv4 } from 'uuid'; -import { setupApplicationTest } from 'vault/tests/helpers'; -import authPage from 'vault/tests/pages/auth'; -import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; -import { personas } from 'vault/tests/helpers/policy-generator/kv'; -import { setupControlGroup, writeSecret, writeVersionedSecret } from 'vault/tests/helpers/kv/kv-run-commands'; - -import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; -import { click, currentURL, visit } from '@ember/test-helpers'; - -/** - * This test set is for testing version history & diff pages - * VAULT-18817 - */ -module('Acceptance | kv-v2 workflow | version history & diff', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(async function () { - this.backend = `kv-workflow-${uuidv4()}`; - this.secretPath = 'app/first-secret'; - const urlPath = `${this.backend}/kv/${encodeURIComponent(this.secretPath)}`; - this.navToSecret = async () => { - return visit(`/vault/secrets/${urlPath}/details?version=2`); - }; - await authPage.login(); - await runCmd(mountEngineCmd('kv-v2', this.backend), false); - await writeSecret(this.backend, this.secretPath, 'foo', 'bar'); - await writeVersionedSecret(this.backend, this.secretPath, 'hello', 'there'); - }); - - hooks.afterEach(async function () { - await authPage.login(); - return runCmd(deleteEngineCmd(this.backend)); - }); - - module('admin persona', function (hooks) { - hooks.beforeEach(async function () { - const token = await runCmd(tokenWithPolicyCmd('admin', personas.admin(this.backend))); - await authPage.login(token); - }); - test('can navigate to the version history page', async function (assert) { - assert.expect(10); - await this.navToSecret(); - await click(PAGE.secretTab('Version History')); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/kv/${encodeURIComponent(this.secretPath)}/metadata/versions`, - 'navigates to version history' - ); - assert.dom(PAGE.secretTab('Secret')).hasText('Secret'); - assert.dom(PAGE.secretTab('Secret')).doesNotHaveClass('active'); - assert.dom(PAGE.secretTab('Metadata')).hasText('Metadata'); - assert.dom(PAGE.secretTab('Metadata')).doesNotHaveClass('active'); - assert.dom(PAGE.secretTab('Version History')).hasText('Version History'); - assert.dom(PAGE.secretTab('Version History')).hasClass('active'); - assert.dom(PAGE.versions.linkedBlock(2)).hasTextContaining('Version 2'); - assert.dom(PAGE.versions.icon(2)).hasTextContaining('Current'); - assert.dom(PAGE.versions.linkedBlock(1)).hasTextContaining('Version 1'); - }); - test.skip('history works correctly when no secrets', async function (assert) { - assert.expect(0); - }); - test.skip('history works correctly when only one secret version', async function (assert) { - assert.expect(0); - }); - test.skip('history works correctly when many secret versions in various states', async function (assert) { - assert.expect(0); - }); - test('can navigate to the version diff view', async function (assert) { - assert.expect(4); - await this.navToSecret(); - await click(PAGE.detail.versionDropdown); - await click(`${PAGE.detail.version('diff')} a`); - assert.strictEqual( - currentURL(), - `/vault/secrets/${this.backend}/kv/${encodeURIComponent(this.secretPath)}/metadata/diff`, - 'navigates to version diff' - ); - - // No tabs render - assert.dom(PAGE.secretTab('Secret')).doesNotExist(); - assert.dom(PAGE.secretTab('Metadata')).doesNotExist(); - assert.dom(PAGE.secretTab('Version History')).doesNotExist(); - }); - test.skip('diff works correctly when no secrets', async function (assert) { - assert.expect(0); - }); - test.skip('diff works correctly when only one secret version', async function (assert) { - assert.expect(0); - }); - test.skip('diff works correctly between various secret versions', async function (assert) { - assert.expect(0); - }); - }); - - module('data-reader persona', function (hooks) { - hooks.beforeEach(async function () { - const token = await runCmd([tokenWithPolicyCmd('data-reader', personas.dataReader(this.backend))]); - await authPage.login(token); - }); - // Copy test outline from admin persona - }); - - module('data-list-reader persona', function (hooks) { - hooks.beforeEach(async function () { - const token = await runCmd([ - tokenWithPolicyCmd('data-list-reader', personas.dataListReader(this.backend)), - ]); - await authPage.login(token); - }); - // Copy test outline from admin persona - }); - - module('metadata-maintainer persona', function (hooks) { - hooks.beforeEach(async function () { - const token = await runCmd([ - tokenWithPolicyCmd('metadata-maintainer', personas.metadataMaintainer(this.backend)), - ]); - await authPage.login(token); - }); - // Copy test outline from admin persona - }); - - module('secret-creator persona', function (hooks) { - hooks.beforeEach(async function () { - const token = await runCmd([ - tokenWithPolicyCmd('secret-creator', personas.secretCreator(this.backend)), - ]); - await authPage.login(token); - }); - // Copy test outline from admin persona - }); - - module('enterprise controlled access persona', function (hooks) { - hooks.beforeEach(async function () { - const userPolicy = ` -path "${this.backend}/data/*" { - capabilities = ["create", "read", "update", "delete", "list"] - control_group = { - max_ttl = "24h" - factor "approver" { - controlled_capabilities = ["write"] - identity { - group_names = ["managers"] - approvals = 1 - } - } - } -} - -path "${this.backend}/*" { - capabilities = ["list"] -} - -// Can we allow this so user can self-authorize? -path "sys/control-group/authorize" { - capabilities = ["update"] -} - -path "sys/control-group/request" { - capabilities = ["update"] -} -`; - const { userToken } = await setupControlGroup({ userPolicy }); - this.userToken = userToken; - return authPage.login(userToken); - }); - // Copy test outline from admin persona - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-paths-test.js b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-paths-test.js new file mode 100644 index 0000000000..ddb2601fe0 --- /dev/null +++ b/ui/tests/acceptance/secrets/backend/kv/kv-v2-workflow-version-history-paths-test.js @@ -0,0 +1,314 @@ +import { module, test } from 'qunit'; +import { v4 as uuidv4 } from 'uuid'; +import { setupApplicationTest } from 'vault/tests/helpers'; +import authPage from 'vault/tests/pages/auth'; +import { deleteEngineCmd, mountEngineCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands'; +import { personas } from 'vault/tests/helpers/policy-generator/kv'; +import { + clearRecords, + deleteVersionCmd, + destroyVersionCmd, + writeVersionedSecret, +} from 'vault/tests/helpers/kv/kv-run-commands'; + +import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; +import { click, currentRouteName, currentURL, visit, waitUntil } from '@ember/test-helpers'; +import { grantAccess, setupControlGroup } from 'vault/tests/helpers/control-groups'; + +/** + * This test set is for testing version history & path pages for secret. + * Letter(s) in parenthesis at the end are shorthand for the persona, + * for ease of tracking down specific tests failures from CI + */ +module('Acceptance | kv-v2 workflow | version history, paths', function (hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(async function () { + this.store = this.owner.lookup('service:store'); + this.backend = `kv-workflow-${uuidv4()}`; + this.secretPath = 'app/first-secret'; + this.urlPath = `${this.backend}/kv/${encodeURIComponent(this.secretPath)}`; + this.navToSecret = async () => { + return visit(`/vault/secrets/${this.urlPath}/details?version=4`); + }; + await authPage.login(); + await runCmd(mountEngineCmd('kv-v2', this.backend), false); + await writeVersionedSecret(this.backend, this.secretPath, 'hello', 'there', 6); + await runCmd([ + destroyVersionCmd(this.backend, this.secretPath), + deleteVersionCmd(this.backend, this.secretPath, 2), + ]); + }); + + hooks.afterEach(async function () { + await authPage.login(); + return runCmd(deleteEngineCmd(this.backend)); + }); + + module('admin persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd(tokenWithPolicyCmd('admin', personas.admin(this.backend))); + await authPage.login(token); + clearRecords(this.store); + }); + test('can navigate to the version history page (a)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Version History')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/metadata/versions`, + 'navigates to version history' + ); + assert.dom(PAGE.versions.linkedBlock()).exists({ count: 6 }); + + assert.dom(PAGE.versions.linkedBlock(6)).hasTextContaining('Version 6'); + assert.dom(PAGE.versions.icon(6)).hasTextContaining('Current'); + + assert.dom(PAGE.versions.linkedBlock(2)).hasTextContaining('Version 2'); + assert.dom(PAGE.versions.icon(2)).hasTextContaining('Deleted'); + + assert.dom(PAGE.versions.linkedBlock(1)).hasTextContaining('Version 1'); + assert.dom(PAGE.versions.icon(1)).hasText('Destroyed'); + + await click(PAGE.versions.linkedBlock(5)); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/details?version=5`, + 'navigates to detail at specific version' + ); + }); + test('can navigate to the paths page (a)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); + + module('data-reader persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd(tokenWithPolicyCmd('data-reader', personas.dataReader(this.backend))); + await authPage.login(token); + clearRecords(this.store); + }); + test('cannot navigate to the version history page (dr)', async function (assert) { + await this.navToSecret(); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Does not render Version History tab'); + }); + test('can navigate to the paths page (dr)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); + + module('data-list-reader persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd( + tokenWithPolicyCmd('data-list-reader', personas.dataListReader(this.backend)) + ); + await authPage.login(token); + clearRecords(this.store); + }); + test('cannot navigate to the version history page (dlr)', async function (assert) { + await this.navToSecret(); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Does not render Version History tab'); + }); + test('can navigate to the paths page (dlr)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); + + module('metadata-maintainer persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd( + tokenWithPolicyCmd('metadata-maintainer', personas.metadataMaintainer(this.backend)) + ); + await authPage.login(token); + clearRecords(this.store); + }); + test('can navigate to the version history page (mm)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Version History')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/metadata/versions`, + 'navigates to version history' + ); + assert.dom(PAGE.versions.linkedBlock()).exists({ count: 6 }); + + assert.dom(PAGE.versions.linkedBlock(6)).hasTextContaining('Version 6'); + assert.dom(PAGE.versions.icon(6)).hasTextContaining('Current'); + + assert.dom(PAGE.versions.linkedBlock(2)).hasTextContaining('Version 2'); + assert.dom(PAGE.versions.icon(2)).hasTextContaining('Deleted'); + + assert.dom(PAGE.versions.linkedBlock(1)).hasTextContaining('Version 1'); + assert.dom(PAGE.versions.icon(1)).hasText('Destroyed'); + + await click(PAGE.versions.linkedBlock(5)); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/details?version=5`, + 'navigates to detail at specific version' + ); + }); + test('can navigate to the paths page (mm)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); + + module('secret-creator persona', function (hooks) { + hooks.beforeEach(async function () { + const token = await runCmd(tokenWithPolicyCmd('secret-creator', personas.secretCreator(this.backend))); + await authPage.login(token); + clearRecords(this.store); + }); + test('cannot navigate to the version history page (sc)', async function (assert) { + await this.navToSecret(); + assert.dom(PAGE.secretTab('Version History')).doesNotExist('Does not render Version History tab'); + }); + test('can navigate to the paths page (sc)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); + + module('enterprise controlled access persona', function (hooks) { + hooks.beforeEach(async function () { + const userPolicy = ` +path "${this.backend}/metadata/*" { + capabilities = ["create", "read", "update", "delete", "list"] + control_group = { + max_ttl = "24h" + factor "approver" { + controlled_capabilities = ["read"] + identity { + group_names = ["managers"] + approvals = 1 + } + } + } +} + +path "${this.backend}/*" { + capabilities = ["list"] +} +`; + const { userToken } = await setupControlGroup({ userPolicy }); + this.userToken = userToken; + await authPage.login(userToken); + clearRecords(this.store); + }); + test('can navigate to the version history page (cg)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Version History')); + assert.ok( + await waitUntil(() => currentRouteName() === 'vault.cluster.access.control-group-accessor'), + 'redirects to access control group route' + ); + await grantAccess({ + apiPath: `${this.backend}/metadata/${this.secretPath}`, + originUrl: `/vault/secrets/${this.urlPath}/details`, + userToken: this.userToken, + }); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/details`, + 'navigates back to secret overview after authorized' + ); + await click(PAGE.secretTab('Version History')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/metadata/versions`, + 'goes to version history page' + ); + assert.dom(PAGE.versions.linkedBlock()).exists({ count: 6 }); + + assert.dom(PAGE.versions.linkedBlock(6)).hasTextContaining('Version 6'); + assert.dom(PAGE.versions.icon(6)).hasTextContaining('Current'); + + assert.dom(PAGE.versions.linkedBlock(2)).hasTextContaining('Version 2'); + assert.dom(PAGE.versions.icon(2)).hasTextContaining('Deleted'); + + assert.dom(PAGE.versions.linkedBlock(1)).hasTextContaining('Version 1'); + assert.dom(PAGE.versions.icon(1)).hasText('Destroyed'); + + await click(PAGE.versions.linkedBlock(5)); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/details?version=5`, + 'navigates to detail at specific version' + ); + }); + test('can navigate to the paths page (cg)', async function (assert) { + await this.navToSecret(); + await click(PAGE.secretTab('Paths')); + assert.strictEqual( + currentURL(), + `/vault/secrets/${this.urlPath}/paths`, + 'navigates to secret paths route' + ); + assert.dom(PAGE.infoRow).exists({ count: 3 }, 'shows 3 rows of information'); + assert.dom(PAGE.infoRowValue('API path')).hasText(`/v1/${this.backend}/data/${this.secretPath}`); + assert.dom(PAGE.infoRowValue('CLI path')).hasText(`-mount="${this.backend}" "${this.secretPath}"`); + assert + .dom(PAGE.infoRowValue('API path for metadata')) + .hasText(`/v1/${this.backend}/metadata/${this.secretPath}`); + }); + }); +}); diff --git a/ui/tests/helpers/kv/kv-run-commands.js b/ui/tests/helpers/kv/kv-run-commands.js index 9f1b53a68a..602317a866 100644 --- a/ui/tests/helpers/kv/kv-run-commands.js +++ b/ui/tests/helpers/kv/kv-run-commands.js @@ -5,6 +5,7 @@ import { click, fillIn, visit } from '@ember/test-helpers'; import { FORM } from './kv-selectors'; +import { encodePath } from 'vault/utils/path-encoding-helpers'; // CUSTOM ACTIONS RELEVANT TO KV-V2 @@ -32,6 +33,16 @@ export const writeVersionedSecret = async function (backend, path, key, val, ver return; }; +export const deleteVersionCmd = function (backend, secretPath, version = 1) { + return `write ${backend}/delete/${encodePath(secretPath)} versions=${version}`; +}; +export const destroyVersionCmd = function (backend, secretPath, version = 1) { + return `write ${backend}/destroy/${encodePath(secretPath)} versions=${version}`; +}; +export const deleteLatestCmd = function (backend, secretPath) { + return `delete ${backend}/data/${encodePath(secretPath)}`; +}; + export const addSecretMetadataCmd = (backend, secret, options = { max_versions: 10 }) => { const stringOptions = Object.keys(options).reduce((prev, curr) => { return `${prev} ${curr}=${options[curr]}`; diff --git a/ui/tests/helpers/kv/kv-selectors.js b/ui/tests/helpers/kv/kv-selectors.js index c44ea9eb65..2d935aaca2 100644 --- a/ui/tests/helpers/kv/kv-selectors.js +++ b/ui/tests/helpers/kv/kv-selectors.js @@ -9,9 +9,10 @@ export const PAGE = { breadcrumbs: '[data-test-breadcrumbs]', breadcrumb: '[data-test-breadcrumbs] li', breadcrumbAtIdx: (idx) => `[data-test-crumb="${idx}"] a`, + infoRow: '[data-test-component="info-table-row"]', infoRowValue: (label) => `[data-test-value-div="${label}"]`, infoRowToggleMasked: (label) => `[data-test-value-div="${label}"] [data-test-button="toggle-masked"]`, - secretTab: (tab) => `[data-test-secrets-tab="${tab}"]`, + secretTab: (tab) => (tab ? `[data-test-secrets-tab="${tab}"]` : '[data-test-secrets-tab]'), emptyStateTitle: '[data-test-empty-state-title]', emptyStateMessage: '[data-test-empty-state-message]', emptyStateActions: '[data-test-empty-state-actions]', @@ -22,7 +23,7 @@ export const PAGE = { }, toolbar: 'nav.toolbar', toolbarAction: 'nav.toolbar-actions .toolbar-link', - secretRow: '[data-test-component="info-table-row"]', + secretRow: '[data-test-component="info-table-row"]', // replace with infoRow // specific page selectors backends: { link: (backend) => `[data-test-secrets-backend-link="${backend}"]`, @@ -49,6 +50,13 @@ export const PAGE = { deleteOptionLatest: 'input#delete-latest-version', deleteConfirm: '[data-test-delete-modal-confirm]', }, + edit: { + toggleDiff: '[data-test-toggle-input="Show diff"', + toggleDiffDescription: '[data-test-diff-description]', + visualDiff: '[data-test-visual-diff]', + added: `.jsondiffpatch-added`, + deleted: `.jsondiffpatch-deleted`, + }, list: { createSecret: '[data-test-toolbar-create-secret]', item: (secret) => (!secret ? '[data-test-list-item]' : `[data-test-list-item="${secret}"]`), diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js index f40a0a8e47..cfcaaefa9e 100644 --- a/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js +++ b/ui/tests/integration/components/kv/page/kv-page-secret-edit-test.js @@ -11,7 +11,7 @@ import { Response } from 'miragejs'; import { hbs } from 'ember-cli-htmlbars'; import { click, fillIn, render } from '@ember/test-helpers'; import codemirror from 'vault/tests/helpers/codemirror'; -import { FORM } from 'vault/tests/helpers/kv/kv-selectors'; +import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors'; import sinon from 'sinon'; module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) { @@ -95,6 +95,39 @@ module('Integration | Component | kv-v2 | Page::Secret::Edit', function (hooks) ); }); + test('diff works correctly', async function (assert) { + await render( + hbs``, + { owner: this.engine } + ); + + assert.dom(PAGE.edit.toggleDiff).isDisabled('Diff toggle is disabled'); + assert.dom(PAGE.edit.toggleDiffDescription).hasText('No changes to show. Update secret to view diff'); + assert.dom(PAGE.edit.visualDiff).doesNotExist('Does not show visual diff'); + + await fillIn(FORM.keyInput(1), 'foo2'); + await fillIn(FORM.maskedValueInput(1), 'bar2'); + + assert.dom(PAGE.edit.toggleDiff).isNotDisabled('Diff toggle is not disabled'); + assert.dom(PAGE.edit.toggleDiffDescription).hasText('Showing the diff will reveal secret values'); + assert.dom(PAGE.edit.visualDiff).doesNotExist('Does not show visual diff'); + await click(PAGE.edit.toggleDiff); + assert.dom(PAGE.edit.visualDiff).exists('Shows visual diff'); + assert.dom(PAGE.edit.added).hasText(`foo2"bar2"`); + + await click(FORM.toggleJson); + codemirror().setValue('{ "foo3": "bar3" }'); + + assert.dom(PAGE.edit.visualDiff).exists('Visual diff updates'); + assert.dom(PAGE.edit.deleted).hasText(`foo"bar"`); + assert.dom(PAGE.edit.added).hasText(`foo3"bar3"`); + }); + test('it saves nested secrets', async function (assert) { assert.expect(3); const nestedSecret = 'path/to/secret'; diff --git a/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js b/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js deleted file mode 100644 index 33c34a9d1e..0000000000 --- a/ui/tests/integration/components/kv/page/kv-page-version-diff-test.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupEngine } from 'ember-engines/test-support'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { click, render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { kvMetadataPath, kvDataPath } from 'vault/utils/kv-path'; -import { PAGE } from 'vault/tests/helpers/kv/kv-selectors'; -import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs'; -import { encodePath } from 'vault/utils/path-encoding-helpers'; - -const EXAMPLE_KV_DATA_GET_RESPONSE = { - request_id: 'foobar', - data: { - data: { hello: 'world' }, - metadata: { - created_time: '2023-06-20T21:26:47.592306Z', - custom_metadata: null, - deletion_time: '', - destroyed: false, - version: 1, - }, - }, -}; - -module('Integration | Component | kv | Page::Secret::Metadata::Version-Diff', function (hooks) { - setupRenderingTest(hooks); - setupEngine(hooks, 'kv'); - setupMirage(hooks); - - hooks.beforeEach(async function () { - const store = this.owner.lookup('service:store'); - this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); - const metadata = this.server.create('kv-metadatum'); - - metadata.id = kvMetadataPath('kv-engine', 'my-secret'); - store.pushPayload('kv/metadata', { - modelName: 'kv/metadata', - ...metadata, - }); - this.metadata = store.peekRecord('kv/metadata', metadata.id); - this.breadcrumbs = [ - { label: 'secrets', route: 'secrets', linkExternal: true }, - { label: this.metadata.backend, route: 'list' }, - { label: this.metadata.path, route: 'secret.details', model: this.metadata.path }, - { label: 'version diff' }, - ]; - - // compare version 4 - const dataId = kvDataPath('kv-engine', 'my-secret', 4); - store.pushPayload('kv/data', { - modelName: 'kv/data', - id: dataId, - secret_data: { foo: 'bar' }, - created_time: '2023-07-20T02:12:17.379762Z', - custom_metadata: null, - deletion_time: '', - destroyed: false, - version: 4, - }); - - this.endpoint = `${encodePath('kv-engine')}/data/${'my-secret'}`; - }); - - test('it renders compared data of the two versions and shows icons for deleted, destroyed and current', async function (assert) { - assert.expect(12); - this.server.get(this.endpoint, (schema, req) => { - assert.ok('request made to the correct endpoint.'); - assert.strictEqual( - req.queryParams.version, - '1', - 'request includes the version flag on queryRecord and correctly only queries for the record not in the store, which is retrieved by peekRecord.' - ); - return EXAMPLE_KV_DATA_GET_RESPONSE; - }); - - await render( - hbs` - - `, - { owner: this.engine } - ); - /* eslint-disable no-useless-escape */ - assert - .dom('[data-test-visual-diff]') - .hasText( - `foo"bar"\hello"world"`, - 'correctly pull in the data from version 4 and compared to version 1.' - ); - assert - .dom('[data-test-version-dropdown-left]') - .hasText('Version 4', 'shows the current version for the left side default version.'); - assert - .dom('[data-test-version-dropdown-right]') - .hasText( - 'Version 1', - 'shows the first version that is not deleted or destroyed for the right version on init.' - ); - - await click('[data-test-version-dropdown-right]'); - for (const version in this.metadata.versions) { - const data = this.metadata.versions[version]; - assert.dom(PAGE.versions.button(version)).exists('renders the button for each version.'); - - if (data.destroyed || data.deletion_time) { - assert - .dom(`${PAGE.versions.button(version)} [data-test-icon="x-square-fill"]`) - .hasClass(`${data.destroyed ? 'has-text-danger' : 'has-text-grey'}`); - } - } - assert - .dom(`${PAGE.versions.button('1')}`) - .hasClass('is-active', 'correctly shows the selected version 1 as active giving it text blue.'); - }); -});