UI: kv-v2 version history & path tests (#22593)

This commit is contained in:
Chelsea Shaw
2023-08-30 10:23:15 -05:00
committed by GitHub
parent c4a8b23d93
commit b18313b4eb
23 changed files with 540 additions and 820 deletions

3
changelog/22593.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: JSON diff view available in "Create New Version" form for KV v2
```

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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;

View File

@@ -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}}

View File

@@ -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();
}
}

View File

@@ -19,7 +19,6 @@ export default buildRoutes(function () {
this.route('metadata', function () {
this.route('edit');
this.route('versions');
this.route('diff');
});
});
this.route('configuration');

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -1,6 +0,0 @@
<Page::Secret::Metadata::VersionDiff
@metadata={{this.model.metadata}}
@path={{this.model.path}}
@backend={{this.model.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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
});
});
});

View File

@@ -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'));

View File

@@ -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');

View File

@@ -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'));

View File

@@ -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
});
});

View File

@@ -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}`);
});
});
});

View File

@@ -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]}`;

View File

@@ -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}"]`),

View File

@@ -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';

View File

@@ -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.');
});
});