mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	UI: kv-v2 version history & path tests (#22593)
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/22593.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/22593.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:improvement | ||||
| ui: JSON diff view available in "Create New Version" form for KV v2 | ||||
| ``` | ||||
| @@ -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 | ||||
|   | ||||
| @@ -22,15 +22,6 @@ | ||||
|             </LinkTo> | ||||
|           </li> | ||||
|         {{/each}} | ||||
|         {{! version diff }} | ||||
|         {{#if (gt @metadata.sortedVersions.length 1)}} | ||||
|           <hr /> | ||||
|           <li data-test-version="diff"> | ||||
|             <LinkTo @route="secret.metadata.diff" {{on "click" (fn @onClose D)}}> | ||||
|               Version Diff | ||||
|             </LinkTo> | ||||
|           </li> | ||||
|         {{/if}} | ||||
|       </ul> | ||||
|     </nav> | ||||
|   </D.Content> | ||||
|   | ||||
| @@ -38,6 +38,29 @@ | ||||
|       @modelValidations={{this.modelValidations}} | ||||
|       @isEdit={{true}} | ||||
|     /> | ||||
|  | ||||
|     <div class="has-top-margin-m"> | ||||
|       <Toggle | ||||
|         @name="Show diff" | ||||
|         @status="success" | ||||
|         @size="small" | ||||
|         @onChange={{fn (mut this.showDiff)}} | ||||
|         @checked={{this.showDiff}} | ||||
|         @disabled={{not this.diffDelta}} | ||||
|       > | ||||
|         <span class="ttl-picker-label is-large">Show diff</span><br /> | ||||
|         <div class="description has-text-grey" data-test-diff-description>{{if | ||||
|             this.diffDelta | ||||
|             "Showing the diff will reveal secret values" | ||||
|             "No changes to show. Update secret to view diff" | ||||
|           }}</div> | ||||
|         {{#if this.showDiff}} | ||||
|           <div class="form-section visual-diff text-grey-lightest background-color-black has-top-margin-s"> | ||||
|             <pre data-test-visual-diff>{{sanitized-html this.visualDiff}}</pre> | ||||
|           </div> | ||||
|         {{/if}} | ||||
|       </Toggle> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="box is-fullwidth is-bottomless"> | ||||
|     <div class="control"> | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -1,91 +0,0 @@ | ||||
| <KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}> | ||||
|   <:toolbarFilters> | ||||
|     {{! left side version selector }} | ||||
|     <BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|> | ||||
|       <D.Trigger data-test-version-dropdown-left class="toolbar-link {{if D.isOpen ' is-active'}}"> | ||||
|         Version | ||||
|         {{this.leftSideVersion}} | ||||
|         <Chevron @direction="down" @isButton={{true}} /> | ||||
|       </D.Trigger> | ||||
|       <D.Content @defaultClass="popup-menu-content"> | ||||
|         <nav class="box menu" aria-label="version-left"> | ||||
|           <ul class="menu-list"> | ||||
|             {{#each @metadata.sortedVersions as |versionData|}} | ||||
|               <li> | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   class="link {{if (loose-equal versionData.version this.leftSideVersion) 'is-active'}}" | ||||
|                   {{on "click" (fn this.selectVersion versionData.version D.actions "left")}} | ||||
|                   disabled={{(or versionData.destroyed versionData.deletion_time)}} | ||||
|                 > | ||||
|                   Version | ||||
|                   {{versionData.version}} | ||||
|                   {{#if versionData.destroyed}} | ||||
|                     <Icon @name="x-square-fill" class="has-text-danger is-pulled-right" /> | ||||
|                   {{else if versionData.deletion_time}} | ||||
|                     <Icon @name="x-square-fill" class="has-text-grey is-pulled-right" /> | ||||
|                   {{else if (loose-equal @metadata.currentVersion versionData.version)}} | ||||
|                     <Icon @name="check-circle-fill" class="has-text-success is-pulled-right" /> | ||||
|                   {{/if}} | ||||
|                 </button> | ||||
|               </li> | ||||
|             {{/each}} | ||||
|           </ul> | ||||
|         </nav> | ||||
|       </D.Content> | ||||
|     </BasicDropdown> | ||||
|     {{! right side version selector }} | ||||
|     <BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|> | ||||
|       <D.Trigger data-test-version-dropdown-right class="toolbar-link {{if D.isOpen ' is-active'}}"> | ||||
|         Version | ||||
|         {{this.rightSideVersion}} | ||||
|         <Chevron @direction="down" @isButton={{true}} /> | ||||
|       </D.Trigger> | ||||
|       <D.Content @defaultClass="popup-menu-content"> | ||||
|         <nav class="box menu" aria-label="version-right"> | ||||
|           <ul class="menu-list"> | ||||
|             {{#each @metadata.sortedVersions as |versionData|}} | ||||
|               <li> | ||||
|                 <button | ||||
|                   type="button" | ||||
|                   class="link {{if (loose-equal versionData.version this.rightSideVersion) 'is-active'}}" | ||||
|                   {{on "click" (fn this.selectVersion versionData.version D.actions "right")}} | ||||
|                   disabled={{(or versionData.destroyed versionData.deletion_time)}} | ||||
|                   data-test-version-button={{versionData.version}} | ||||
|                 > | ||||
|                   Version | ||||
|                   {{versionData.version}} | ||||
|                   {{#if versionData.destroyed}} | ||||
|                     <Icon @name="x-square-fill" class="has-text-danger is-pulled-right" /> | ||||
|                   {{else if versionData.deletion_time}} | ||||
|                     <Icon @name="x-square-fill" class="has-text-grey is-pulled-right" /> | ||||
|                   {{else if (loose-equal @metadata.currentVersion versionData.version)}} | ||||
|                     <Icon @name="check-circle-fill" class="has-text-success is-pulled-right" /> | ||||
|                   {{/if}} | ||||
|                 </button> | ||||
|               </li> | ||||
|             {{/each}} | ||||
|           </ul> | ||||
|         </nav> | ||||
|       </D.Content> | ||||
|     </BasicDropdown> | ||||
|     {{! status icon }} | ||||
|     {{#if this.statesMatch}} | ||||
|       <div class="has-left-padding-s"> | ||||
|         <Icon @name="check-circle-fill" class="has-text-success" /> | ||||
|         <span>States match</span> | ||||
|       </div> | ||||
|     {{/if}} | ||||
|   </:toolbarFilters> | ||||
| </KvPageHeader> | ||||
| {{! 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)}} | ||||
|   <EmptyState | ||||
|     @title="Version {{@metadata.currentVersion}} of {{@path}} has been {{@metadata.currentSecret.state}}" | ||||
|     @message="Please select another version of the secret to compare." | ||||
|   /> | ||||
| {{else}} | ||||
|   <div class="form-section visual-diff text-grey-lightest background-color-black"> | ||||
|     <pre data-test-visual-diff>{{sanitized-html this.visualDiff}}</pre> | ||||
|   </div> | ||||
| {{/if}} | ||||
| @@ -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(); | ||||
|   } | ||||
| } | ||||
| @@ -19,7 +19,6 @@ export default buildRoutes(function () { | ||||
|     this.route('metadata', function () { | ||||
|       this.route('edit'); | ||||
|       this.route('versions'); | ||||
|       this.route('diff'); | ||||
|     }); | ||||
|   }); | ||||
|   this.route('configuration'); | ||||
|   | ||||
							
								
								
									
										37
									
								
								ui/lib/kv/addon/routes/secret/metadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ui/lib/kv/addon/routes/secret/metadata.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
|   } | ||||
| } | ||||
| @@ -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); | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| <Page::Secret::Metadata::VersionDiff | ||||
|   @metadata={{this.model.metadata}} | ||||
|   @path={{this.model.path}} | ||||
|   @backend={{this.model.backend}} | ||||
|   @breadcrumbs={{this.breadcrumbs}} | ||||
| /> | ||||
| @@ -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'); | ||||
|   }); | ||||
| }); | ||||
| @@ -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'); | ||||
|   }); | ||||
| }); | ||||
| @@ -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 | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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')); | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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')); | ||||
|   | ||||
| @@ -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 | ||||
|   }); | ||||
| }); | ||||
| @@ -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}`); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -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]}`; | ||||
|   | ||||
| @@ -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}"]`), | ||||
|   | ||||
| @@ -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`<Page::Secret::Edit | ||||
|   @secret={{this.secret}} | ||||
|   @previousVersion={{4}} | ||||
|   @currentVersion={{4}} | ||||
|   @breadcrumbs={{this.breadcrumbs}} | ||||
| />`, | ||||
|       { 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'; | ||||
|   | ||||
| @@ -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` | ||||
|        <Page::Secret::Metadata::VersionDiff | ||||
|         @metadata={{this.metadata}}  | ||||
|         @path={{this.metadata.path}} | ||||
|         @backend={{this.metadata.backend}} | ||||
|         @breadcrumbs={{this.breadcrumbs}} | ||||
|       /> | ||||
|       `, | ||||
|       { 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.'); | ||||
|   }); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw