From b12e3768476f07be2e36c889b28712d86c9fa694 Mon Sep 17 00:00:00 2001 From: "clairebontempo@gmail.com" Date: Fri, 7 Jun 2024 13:55:04 -0700 Subject: [PATCH] wrap refactor --- ui/app/components/tool-wrap.js | 55 ++++--- .../components/tool-actions-form.hbs | 9 +- ui/app/templates/components/tool-wrap.hbs | 78 +++++----- .../integration/components/tools/wrap-test.js | 136 +++++++++++++----- 4 files changed, 179 insertions(+), 99 deletions(-) diff --git a/ui/app/components/tool-wrap.js b/ui/app/components/tool-wrap.js index 2435b14b24..03f7810d93 100644 --- a/ui/app/components/tool-wrap.js +++ b/ui/app/components/tool-wrap.js @@ -4,37 +4,41 @@ */ import Component from '@glimmer/component'; +import { service } from '@ember/service'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; +import errorMessage from 'vault/utils/error-message'; /** - * @module ToolWrap - * 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. + * @module - * - * @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 { + @service store; + @service flashMessages; + @tracked buttonDisabled = false; + @tracked token = ''; + @tracked wrapTTL = null; + @tracked wrapData = '{\n}'; + @tracked errorMessage = ''; + + @action + reset(clearData = true) { + this.token = ''; + this.errorMessage = ''; + this.wrapTTL = null; + if (clearData) this.wrapData = '{\n}'; + } @action updateTtl(evt) { if (!evt) return; - const ttl = evt.enabled ? `${evt.seconds}s` : '30m'; - this.args.onChange('wrapTTL', ttl); + this.wrapTTL = evt.enabled ? `${evt.seconds}s` : '30m'; } @action @@ -42,6 +46,21 @@ export default class ToolWrap extends Component { codemirror.performLint(); const hasErrors = codemirror?.state.lint.marked?.length > 0; this.buttonDisabled = hasErrors; - this.args.onChange('data', val); + if (!hasErrors) this.wrapData = val; + } + + @action + async handleSubmit(evt) { + evt.preventDefault(); + const data = JSON.parse(this.wrapData); + const wrapTTL = this.wrapTTL || null; + + try { + const response = await this.store.adapterFor('tools').toolAction('wrap', data, { wrapTTL }); + this.token = response.wrap_info.token; + this.flashMessages.success('Wrap was successful.'); + } catch (error) { + this.errorMessage = errorMessage(error); + } } } diff --git a/ui/app/templates/components/tool-actions-form.hbs b/ui/app/templates/components/tool-actions-form.hbs index 29188a6faf..cb73f511c9 100644 --- a/ui/app/templates/components/tool-actions-form.hbs +++ b/ui/app/templates/components/tool-actions-form.hbs @@ -15,14 +15,7 @@ {{else if (eq this.selectedAction "lookup")}} {{else if (eq this.selectedAction "wrap")}} - + {{else}} {{/if}} diff --git a/ui/app/templates/components/tool-wrap.hbs b/ui/app/templates/components/tool-wrap.hbs index 04fd967782..e294aba9ac 100644 --- a/ui/app/templates/components/tool-wrap.hbs +++ b/ui/app/templates/components/tool-wrap.hbs @@ -11,54 +11,58 @@ -{{#if @token}} +{{#if this.token}}
-
- -
+
- - + +
{{else}} -
- - -
+
+
+ + +
+
+ +
+
+ +
+
- +
- -
-
-
- -
-
+ {{/if}} \ No newline at end of file diff --git a/ui/tests/integration/components/tools/wrap-test.js b/ui/tests/integration/components/tools/wrap-test.js index 358d174f53..daf296c081 100644 --- a/ui/tests/integration/components/tools/wrap-test.js +++ b/ui/tests/integration/components/tools/wrap-test.js @@ -6,76 +6,140 @@ 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 { Response } from 'miragejs'; +import { click, fillIn, find, render, waitUntil } 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 { TTL_PICKER as TTL } from 'vault/tests/helpers/components/ttl-picker-selectors'; import { TOOLS_SELECTORS as TS } from 'vault/tests/helpers/tools-selectors'; +import codemirror from 'vault/tests/helpers/codemirror'; -module('Integration | Component | tools/tool-wrap', function (hooks) { +module('Integration | Component | tools/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` - `); + `); }; + this.wrapData = `{"foo": "bar"}`; + this.token = 'blah.jhfel7SmsVeZwihaGiIKHGh2cy5XZWtEeEt5WmRwS1VYSTNDb1BBVUNsVFAQ3JIK'; + // default mirage response here is overridden in some tests + this.server.post('sys/wrapping/wrap', () => { + // removed superfluous response data for this test + return { wrap_info: { token: this.token } }; + }); }); test('it renders defaults', async function (assert) { await this.renderComponent(); assert.dom('h1').hasText('Wrap Data', 'Title renders'); + assert.dom('label').hasText('Data to wrap (json-formatted)'); assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object'); - assert.dom(GENERAL.toggleInput('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); + assert.dom(TTL.toggleByLabel('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(); + + await click(TTL.toggleByLabel('Wrap TTL')); + assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl defaults to 30 when toggled'); + assert.dom(TTL.ttlUnit).hasValue('m', 'ttl defaults to minutes when toggled'); }); - test('it renders token view', async function (assert) { - this.token = 'blah.jhfel7SmsVeZwihaGiIKHGh2cy5XZWtEeEt5WmRwS1VYSTNDb1BBVUNsVFAQ3JIK'; + test('it renders errors', async function (assert) { + this.server.post('sys/wrapping/wrap', () => new Response(500, {}, { errors: ['Something is wrong'] })); await this.renderComponent(); + await click(TS.submit); + await waitUntil(() => find(GENERAL.messageError)); + assert.dom(GENERAL.messageError).hasText('Error Something is wrong', 'Error renders'); + }); - assert.dom('h1').hasText('Wrap Data'); + test('it submits with defaults', async function (assert) { + assert.expect(5); + this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => { + const payload = JSON.parse(requestBody); + const expectedHeaders = { + 'X-Vault-Wrap-TTL': '30m', + 'content-type': 'application/json; charset=utf-8', + }; + assert.propEqual(payload, JSON.parse(this.wrapData), `payload contains data: ${requestBody}`); + assert.propEqual(requestHeaders, expectedHeaders, 'header has default wrap ttl'); + return { + wrap_info: { + token: this.token, + accessor: '5yjKx6Om9NmBx1mjiN1aIrnm', + ttl: 1800, + creation_time: '2024-06-07T12:02:22.096254-07:00', + creation_path: 'sys/wrapping/wrap', + }, + }; + }); + + await this.renderComponent(); + await codemirror().setValue(this.wrapData); + await click(TS.submit); + await waitUntil(() => find(TS.toolsInput('wrapping-token'))); + assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token); 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"}`; + test('it submits with updated ttl', async function (assert) { + assert.expect(2); + this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => { + const payload = JSON.parse(requestBody); + const expectedHeaders = { + 'X-Vault-Wrap-TTL': '1200s', + 'content-type': 'application/json; charset=utf-8', + }; + assert.propEqual(payload, JSON.parse(this.wrapData), `payload contains data: ${requestBody}`); + assert.propEqual(requestHeaders, expectedHeaders, 'header has updated wrap ttl'); + // only testing payload/header assertions, no need for return here + return {}; + }); + + await this.renderComponent(); + await codemirror().setValue(this.wrapData); + await click(TTL.toggleByLabel('Wrap TTL')); + await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20'); + await click(TS.submit); + }); + + test('it resets on done', async function (assert) { + await this.renderComponent(); + await codemirror().setValue(this.wrapData); + await click(TTL.toggleByLabel('Wrap TTL')); + await fillIn(TTL.valueInputByLabel('Wrap TTL'), '20'); + await click(TS.submit); + + await waitUntil(() => find(TS.button('Done'))); + await click(TS.button('Done')); + assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor resets to empty object'); + assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked'); + await click(TTL.toggleByLabel('Wrap TTL')); + assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl resets to default when toggled'); + }); + + test('it preserves input data on back', async function (assert) { + await this.renderComponent(); + await codemirror().setValue(this.wrapData); + await click(TS.submit); + + await waitUntil(() => find(TS.button('Back'))); + await click(TS.button('Back')); + assert.strictEqual(codemirror().getValue(' '), `{"foo": "bar"}`, 'json editor has original data'); + assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); + }); + + test('it disables/enables submit based on json linting', async function (assert) { 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); + await codemirror().setValue(this.wrapData); 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'); }); });