From 1f64e6e9ca9cba5bbe98539925f3baa0cae03dec Mon Sep 17 00:00:00 2001
From: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Date: Mon, 3 Jun 2024 17:20:09 -0700
Subject: [PATCH] UI: Allow repeat data wrapping for wrap tool (#27289)
* update selectors
* add tests
* add tests
* add explanations to true only args
* allow token wrap to wrap again
* update test wording
* add wrap specific modules to tools acceptance test
* add changelog
* remove selectedAction
* trim args and update tests
---
changelog/27289.txt | 3 +
ui/app/components/tool-actions-form.js | 23 +--
ui/app/components/tool-wrap.js | 37 ++---
.../components/tool-actions-form.hbs | 7 +-
ui/app/templates/components/tool-hash.hbs | 4 +-
ui/app/templates/components/tool-lookup.hbs | 12 +-
ui/app/templates/components/tool-random.hbs | 2 +-
ui/app/templates/components/tool-rewrap.hbs | 2 +-
ui/app/templates/components/tool-unwrap.hbs | 6 +-
ui/app/templates/components/tool-wrap.hbs | 37 ++---
ui/tests/acceptance/tools-test.js | 155 +++++++++++-------
ui/tests/helpers/tools-selectors.ts | 11 ++
.../components/tools/tool-wrap-test.js | 81 +++++++++
13 files changed, 253 insertions(+), 127 deletions(-)
create mode 100644 changelog/27289.txt
create mode 100644 ui/tests/helpers/tools-selectors.ts
create mode 100644 ui/tests/integration/components/tools/tool-wrap-test.js
diff --git a/changelog/27289.txt b/changelog/27289.txt
new file mode 100644
index 000000000000..3e10cf0a02f6
--- /dev/null
+++ b/changelog/27289.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+ui: Allow users to wrap inputted data again instead of resetting form
+```
diff --git a/ui/app/components/tool-actions-form.js b/ui/app/components/tool-actions-form.js
index 306bfb7c4e5b..b6407255b6b4 100644
--- a/ui/app/components/tool-actions-form.js
+++ b/ui/app/components/tool-actions-form.js
@@ -32,12 +32,12 @@ export default Component.extend(DEFAULTS, {
flashMessages: service(),
store: service(),
// putting these attrs here so they don't get reset when you click back
- //random
+ // random
bytes: 32,
- //hash
+ // hash
format: 'base64',
algorithm: 'sha2-256',
-
+ data: '{\n}',
tagName: '',
didReceiveAttrs() {
@@ -139,15 +139,18 @@ export default Component.extend(DEFAULTS, {
this.reset();
},
- updateTtl(ttl) {
- set(this, 'wrapTTL', ttl);
+ onBack(properties) {
+ // only reset specific properties so user can reuse input data and repeat the action
+ if (this.isDestroyed || this.isDestroying) {
+ return;
+ }
+ properties.forEach((prop) => {
+ set(this, prop, DEFAULTS[prop]);
+ });
},
- codemirrorUpdated(val, hasErrors) {
- setProperties(this, {
- buttonDisabled: hasErrors,
- data: val,
- });
+ onChange(param, value) {
+ set(this, param, value);
},
},
});
diff --git a/ui/app/components/tool-wrap.js b/ui/app/components/tool-wrap.js
index 02eb0d6acfde..2435b14b247d 100644
--- a/ui/app/components/tool-wrap.js
+++ b/ui/app/components/tool-wrap.js
@@ -12,45 +12,36 @@ import { tracked } from '@glimmer/tracking';
* ToolWrap components are components that sys/wrapping/wrap functionality. Most of the functionality is passed through as actions from the tool-actions-form and then called back with properties.
*
* @example
- * ```js
*
- * ```
- * @param onClear {Function} - parent action that is passed through. Must be passed as {{action "onClear"}}
- * @param token=null {String} - property passed from parent to child and then passed back up to parent
- * @param selectedAction="wrap" - passed in from parent. This is the wrap action, others include hash, etc.
- * @param codemirrorUpdated {Function} - parent action that is passed through. Must be passed as {{action "codemirrorUpdated"}}.
- * @param updateTtl {Function} - parent action that is passed through. Must be passed as {{action "updateTtl"}}
- * @param buttonDisabled=false {Boolean} - false default and if there is an error on codemirror it turns to true.
- * @param error=null {Object} - errors passed from parent as default then from child back to parent.
+ * @token={{@token}}
+ * />
+ *
+ * @param {object} errors=null - errors returned if wrap fails
+ * @param {function} onBack - callback that only clears specific values so the action can be repeated. Must be passed as `{{action "onBack"}}`
+ * @param {function} onChange - callback that fires when inputs change and passes value and param name back to the parent
+ * @param {function} onClear - callback that resets all of values to defaults. Must be passed as `{{action "onClear"}}`
+ * @param {string} token=null - returned after user clicks "Wrap data", if there is a token value it displays instead of the JsonEditor
*/
export default class ToolWrap extends Component {
- @tracked data = '{\n}';
@tracked buttonDisabled = false;
- @action
- onClear() {
- this.args.onClear();
- }
@action
updateTtl(evt) {
if (!evt) return;
const ttl = evt.enabled ? `${evt.seconds}s` : '30m';
- this.args.updateTtl(ttl);
+ this.args.onChange('wrapTTL', ttl);
}
+
@action
codemirrorUpdated(val, codemirror) {
codemirror.performLint();
const hasErrors = codemirror?.state.lint.marked?.length > 0;
- this.data = val;
this.buttonDisabled = hasErrors;
- this.args.codemirrorUpdated(val, hasErrors);
+ this.args.onChange('data', val);
}
}
diff --git a/ui/app/templates/components/tool-actions-form.hbs b/ui/app/templates/components/tool-actions-form.hbs
index 53ae308392fc..fb88239f6ab8 100644
--- a/ui/app/templates/components/tool-actions-form.hbs
+++ b/ui/app/templates/components/tool-actions-form.hbs
@@ -54,12 +54,11 @@
{{else if (eq this.selectedAction "wrap")}}
{{else}}
diff --git a/ui/app/templates/components/tool-hash.hbs b/ui/app/templates/components/tool-hash.hbs
index b1eed813cfae..cc244f3c42dd 100644
--- a/ui/app/templates/components/tool-hash.hbs
+++ b/ui/app/templates/components/tool-hash.hbs
@@ -30,7 +30,7 @@
/>
-
+
{{else}}
@@ -80,7 +80,7 @@
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/tool-lookup.hbs b/ui/app/templates/components/tool-lookup.hbs
index d01e071d8b8e..682ab7bf7fda 100644
--- a/ui/app/templates/components/tool-lookup.hbs
+++ b/ui/app/templates/components/tool-lookup.hbs
@@ -13,12 +13,12 @@
{{#if (or @creation_time @creation_ttl)}}
-
-
-
+
+
+
{{#if @expirationDate}}
-
-
+
+
{{/if}}
@@ -42,7 +42,7 @@
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/tool-random.hbs b/ui/app/templates/components/tool-random.hbs
index ac0556a06ab1..36f71ea16e8a 100644
--- a/ui/app/templates/components/tool-random.hbs
+++ b/ui/app/templates/components/tool-random.hbs
@@ -63,7 +63,7 @@
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/tool-rewrap.hbs b/ui/app/templates/components/tool-rewrap.hbs
index 223a60a429e2..07fd050e40c8 100644
--- a/ui/app/templates/components/tool-rewrap.hbs
+++ b/ui/app/templates/components/tool-rewrap.hbs
@@ -56,7 +56,7 @@
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/tool-unwrap.hbs b/ui/app/templates/components/tool-unwrap.hbs
index 0aa14feae2d3..3716ced3e415 100644
--- a/ui/app/templates/components/tool-unwrap.hbs
+++ b/ui/app/templates/components/tool-unwrap.hbs
@@ -13,8 +13,8 @@
{{#if @unwrap_data}}
- Data
- Wrap Details
+ Data
+ Wrap Details
{{/if}}
\ No newline at end of file
diff --git a/ui/app/templates/components/tool-wrap.hbs b/ui/app/templates/components/tool-wrap.hbs
index ef28aa91bac1..04fd967782ec 100644
--- a/ui/app/templates/components/tool-wrap.hbs
+++ b/ui/app/templates/components/tool-wrap.hbs
@@ -16,48 +16,41 @@
{{else}}
-
+
{{/if}}
\ No newline at end of file
diff --git a/ui/tests/acceptance/tools-test.js b/ui/tests/acceptance/tools-test.js
index 95fbcc40fbbe..d5d7741c7b24 100644
--- a/ui/tests/acceptance/tools-test.js
+++ b/ui/tests/acceptance/tools-test.js
@@ -21,6 +21,8 @@ import authPage from 'vault/tests/pages/auth';
import { capitalize } from '@ember/string';
import codemirror from 'vault/tests/helpers/codemirror';
import { setupMirage } from 'ember-cli-mirage/test-support';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors';
module('Acceptance | tools', function (hooks) {
setupApplicationTest(hooks);
@@ -33,13 +35,6 @@ module('Acceptance | tools', function (hooks) {
const DATA_TO_WRAP = JSON.stringify({ tools: 'tests' });
const TOOLS_ACTIONS = toolsActions();
- /*
- data-test-tools-input="wrapping-token"
- data-test-tools-input="rewrapped-token"
- data-test-tools="token-lookup-row"
- data-test-sidebar-nav-link=supportedAction
- */
-
var createTokenStore = () => {
let token;
return {
@@ -51,92 +46,89 @@ module('Acceptance | tools', function (hooks) {
},
};
};
+
test('tools functionality', async function (assert) {
var tokenStore = createTokenStore();
await visit('/vault/tools');
assert.strictEqual(currentURL(), '/vault/tools/wrap', 'forwards to the first action');
TOOLS_ACTIONS.forEach((action) => {
- assert.dom(`[data-test-sidebar-nav-link="${capitalize(action)}"]`).exists(`${action} link renders`);
+ assert.dom(GENERAL.navLink(capitalize(action))).exists(`${action} link renders`);
});
await waitFor('.CodeMirror');
codemirror().setValue(DATA_TO_WRAP);
// wrap
- await click('[data-test-tools-submit]');
- const wrappedToken = await waitUntil(() => find('[data-test-tools-input="wrapping-token"]'));
- tokenStore.set(wrappedToken.value);
- assert
- .dom('[data-test-tools-input="wrapping-token"]')
- .hasValue(wrappedToken.value, 'has a wrapping token');
-
- //lookup
- await click('[data-test-sidebar-nav-link="Lookup"]');
-
- await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
- await click('[data-test-tools-submit]');
- await waitUntil(() => findAll('[data-test-tools="token-lookup-row"]').length >= 3);
- const rows = findAll('[data-test-tools="token-lookup-row"]');
- assert.dom(rows[0]).hasText(/Creation path/, 'show creation path row');
- assert.dom(rows[1]).hasText(/Creation time/, 'show creation time row');
- assert.dom(rows[2]).hasText(/Creation TTL/, 'show creation ttl row');
-
- //rewrap
- await click('[data-test-sidebar-nav-link="Rewrap"]');
-
- await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
- await click('[data-test-tools-submit]');
- const rewrappedToken = await waitUntil(() => find('[data-test-tools-input="rewrapped-token"]'));
+ await click(TS.submit);
+ const wrappedToken = await waitUntil(() => find(TS.toolsInput('wrapping-token')));
+ tokenStore.set(wrappedToken.innerText);
+
+ // lookup
+ await click(GENERAL.navLink('Lookup'));
+
+ await fillIn(TS.toolsInput('wrapping-token'), tokenStore.get());
+ await click(TS.submit);
+ await waitUntil(() => findAll('[data-test-component="info-table-row"]').length >= 3);
+ assert.dom(GENERAL.infoRowValue('Creation path')).hasText('sys/wrapping/wrap', 'show creation path row');
+ assert.dom(GENERAL.infoRowValue('Creation time')).exists();
+ assert.dom(GENERAL.infoRowValue('Creation TTL')).hasText('1800', 'show creation ttl row');
+
+ // rewrap
+ await click(GENERAL.navLink('Rewrap'));
+
+ await fillIn(TS.toolsInput('wrapping-token'), tokenStore.get());
+ await click(TS.submit);
+ const rewrappedToken = await waitUntil(() => find(TS.toolsInput('rewrapped-token')));
assert.ok(rewrappedToken.value, 'has a new re-wrapped token');
assert.notEqual(rewrappedToken.value, tokenStore.get(), 're-wrapped token is not the wrapped token');
tokenStore.set(rewrappedToken.value);
await settled();
- //unwrap
- await click('[data-test-sidebar-nav-link="Unwrap"]');
+ // unwrap
+ await click(GENERAL.navLink('Unwrap'));
- await fillIn('[data-test-tools-input="wrapping-token"]', tokenStore.get());
- await click('[data-test-tools-submit]');
+ await fillIn(TS.toolsInput('wrapping-token'), tokenStore.get());
+ await click(TS.submit);
await waitFor('.CodeMirror');
assert.deepEqual(
JSON.parse(codemirror().getValue()),
JSON.parse(DATA_TO_WRAP),
'unwrapped data equals input data'
);
- await waitUntil(() => find('[data-test-button-details]'));
- await click('[data-test-button-details]');
- await click('[data-test-button-data]');
+ await waitUntil(() => find(TS.tab('details')));
+ await click(TS.tab('details'));
+ await click(TS.tab('data'));
assert.deepEqual(
JSON.parse(codemirror().getValue()),
JSON.parse(DATA_TO_WRAP),
'data tab still has unwrapped data'
);
//random
- await click('[data-test-sidebar-nav-link="Random"]');
+ await click(GENERAL.navLink('Random'));
- assert.dom('[data-test-tools-input="bytes"]').hasValue('32', 'defaults to 32 bytes');
- await click('[data-test-tools-submit]');
- const randomBytes = await waitUntil(() => find('[data-test-tools-input="random-bytes"]'));
+ assert.dom(TS.toolsInput('bytes')).hasValue('32', 'defaults to 32 bytes');
+ await click(TS.submit);
+ const randomBytes = await waitUntil(() => find(TS.toolsInput('random-bytes')));
assert.ok(randomBytes.value, 'shows the returned value of random bytes');
- //hash
- await click('[data-test-sidebar-nav-link="Hash"]');
+ // hash
+ await click(GENERAL.navLink('Hash'));
- await fillIn('[data-test-tools-input="hash-input"]', 'foo');
+ await fillIn(TS.toolsInput('hash-input'), 'foo');
await click('[data-test-transit-b64-toggle="input"]');
- await click('[data-test-tools-submit]');
- let sumInput = await waitUntil(() => find('[data-test-tools-input="sum"]'));
+ await click(TS.submit);
+ let sumInput = await waitUntil(() => find(TS.toolsInput('sum')));
assert
.dom(sumInput)
.hasValue('LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', 'hashes the data, encodes input');
- await click('[data-test-tools-back]');
+ await click(TS.button('Back'));
- await fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9');
+ await fillIn(TS.toolsInput('hash-input'), 'e2RhdGE6ImZvbyJ9');
- await click('[data-test-tools-submit]');
- sumInput = await waitUntil(() => find('[data-test-tools-input="sum"]'));
+ await click(TS.submit);
+ sumInput = await waitUntil(() => find(TS.toolsInput('sum')));
assert
.dom(sumInput)
.hasValue('JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', 'hashes the data, passes b64 input through');
@@ -168,10 +160,10 @@ module('Acceptance | tools', function (hooks) {
await visit('/vault/tools');
//unwrap
- await click('[data-test-sidebar-nav-link="Unwrap"]');
+ await click(GENERAL.navLink('Unwrap'));
- await fillIn('[data-test-tools-input="wrapping-token"]', 'sometoken');
- await click('[data-test-tools-submit]');
+ await fillIn(TS.toolsInput('wrapping-token'), 'sometoken');
+ await click(TS.submit);
await waitFor('.CodeMirror');
assert.deepEqual(
@@ -180,4 +172,57 @@ module('Acceptance | tools', function (hooks) {
'unwrapped data equals input data'
);
});
+
+ module('wrap', function () {
+ test('it wraps data again after clicking "Back"', async function (assert) {
+ const tokenStore = createTokenStore();
+ await visit('/vault/tools/wrap');
+
+ await waitFor('.CodeMirror');
+ codemirror().setValue(DATA_TO_WRAP);
+
+ // initial wrap
+ await click(TS.submit);
+ await waitUntil(() => find(TS.toolsInput('wrapping-token')));
+ await click(TS.button('Back'));
+
+ // wrap again
+ await click(TS.submit);
+ const wrappedToken = await waitUntil(() => find(TS.toolsInput('wrapping-token')));
+ tokenStore.set(wrappedToken.innerText);
+
+ // there was a bug where clicking "back" cleared the parent's data, but not the child form component
+ // so when users attempted to wrap data again the payload was actually empty and unwrapping the token returned {token: ""}
+ // it is user desired behavior that the form does not clear on back, and that wrapping can be immediately repeated
+ // we use lookup to check our token from the second wrap returns the unwrapped data we expect
+ await click(GENERAL.navLink('Lookup'));
+ await fillIn(TS.toolsInput('wrapping-token'), tokenStore.get());
+ await click(TS.submit);
+ await waitUntil(() => findAll('[data-test-component="info-table-row"]').length >= 3);
+ assert.dom(GENERAL.infoRowValue('Creation TTL')).hasText('1800', 'show creation ttl row');
+ });
+
+ test('it sends wrap ttl', async function (assert) {
+ const tokenStore = createTokenStore();
+ await visit('/vault/tools/wrap');
+
+ await waitFor('.CodeMirror');
+ codemirror().setValue(DATA_TO_WRAP);
+
+ // update to non-default ttl
+ await click(GENERAL.toggleInput('Wrap TTL'));
+ await fillIn(GENERAL.ttl.input('Wrap TTL'), '20');
+
+ await click(TS.submit);
+ const wrappedToken = await waitUntil(() => find(TS.toolsInput('wrapping-token')));
+ tokenStore.set(wrappedToken.innerText);
+
+ // lookup to check unwrapped data is what we expect
+ await click(GENERAL.navLink('Lookup'));
+ await fillIn(TS.toolsInput('wrapping-token'), tokenStore.get());
+ await click(TS.submit);
+ await waitUntil(() => findAll('[data-test-component="info-table-row"]').length >= 3);
+ assert.dom(GENERAL.infoRowValue('Creation TTL')).hasText('1200', 'show creation ttl row');
+ });
+ });
});
diff --git a/ui/tests/helpers/tools-selectors.ts b/ui/tests/helpers/tools-selectors.ts
new file mode 100644
index 000000000000..8598728e424b
--- /dev/null
+++ b/ui/tests/helpers/tools-selectors.ts
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+export const TOOLS_SELECTORS = {
+ submit: '[data-test-tools-submit]',
+ toolsInput: (attr: string) => `[data-test-tools-input="${attr}"]`,
+ tab: (item: string) => `[data-test-tab="${item}"]`,
+ button: (action: string) => `[data-test-button="${action}"]`,
+};
diff --git a/ui/tests/integration/components/tools/tool-wrap-test.js b/ui/tests/integration/components/tools/tool-wrap-test.js
new file mode 100644
index 000000000000..358d174f5356
--- /dev/null
+++ b/ui/tests/integration/components/tools/tool-wrap-test.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'vault/tests/helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { click, fillIn, render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import sinon from 'sinon';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import codemirror from 'vault/tests/helpers/codemirror';
+import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors';
+
+module('Integration | Component | tools/tool-wrap', function (hooks) {
+ setupRenderingTest(hooks);
+ setupMirage(hooks);
+
+ hooks.beforeEach(function () {
+ this.onBack = sinon.spy();
+ this.onClear = sinon.spy();
+ this.onChange = sinon.spy();
+ this.data = '{\n}';
+ this.renderComponent = async () => {
+ await render(hbs`
+ `);
+ };
+ });
+
+ test('it renders defaults', async function (assert) {
+ await this.renderComponent();
+
+ assert.dom('h1').hasText('Wrap Data', 'Title renders');
+ assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object');
+ assert.dom(GENERAL.toggleInput('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
+ assert.dom(TS.submit).isEnabled();
+ assert.dom(TS.toolsInput('wrapping-token')).doesNotExist();
+ assert.dom(TS.button('Back')).doesNotExist();
+ assert.dom(TS.button('Done')).doesNotExist();
+ });
+
+ test('it renders token view', async function (assert) {
+ this.token = 'blah.jhfel7SmsVeZwihaGiIKHGh2cy5XZWtEeEt5WmRwS1VYSTNDb1BBVUNsVFAQ3JIK';
+ await this.renderComponent();
+
+ assert.dom('h1').hasText('Wrap Data');
+ assert.dom('label').hasText('Wrapped token');
+ assert.dom('.CodeMirror').doesNotExist();
+ assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token);
+ await click(TS.button('Back'));
+ assert.true(this.onBack.calledOnce, 'onBack is called');
+ await click(TS.button('Done'));
+ assert.true(this.onClear.calledOnce, 'onClear is called');
+ });
+
+ test('it calls onChange for json editor', async function (assert) {
+ const data = `{"foo": "bar"}`;
+ await this.renderComponent();
+ await codemirror().setValue(`{bad json}`);
+ assert.dom(TS.submit).isDisabled('submit disables if json editor has linting errors');
+
+ await codemirror().setValue(data);
+ assert.dom(TS.submit).isEnabled('submit reenables if json editor has no linting errors');
+ assert.propEqual(this.onChange.lastCall.args, ['data', data], 'onChange is called with json data');
+ });
+
+ test('it calls onChange for ttl picker', async function (assert) {
+ await this.renderComponent();
+ await click(GENERAL.toggleInput('Wrap TTL'));
+ await fillIn(GENERAL.ttl.input('Wrap TTL'), '20');
+ assert.propEqual(this.onChange.lastCall.args, ['wrapTTL', '1200s'], 'onChange is called with wrapTTL');
+ });
+});