mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-01 03:51:08 +02:00
UI: Build kv v2 patch page (#28114)
* build patch component * pass submit error to child component * add copyright header * aphabetize * rename kv-patch components * build json editor patch form * finish patch component and tests * use baseSetup in other kv tests * small styling changes * remove linting conditional, set json editor value on change * rename subkeys card * add reveal component to both patch forms * implement subkeys card * add copyright header, add transition assertions * assert flash spy * add assertion for empty values * separate tests into it does not submit module * update flash copy
This commit is contained in:
parent
89aecf4f93
commit
581c999f56
@ -3,7 +3,7 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" this.submit}} ...attributes>
|
||||
<form {{on "submit" this.submit}} data-test-kv-patch-editor>
|
||||
<div class="flex column-gap-16 has-top-padding-s">
|
||||
<Hds::Form::Label @controlId="newKey" class="one-fourth-width">
|
||||
Key
|
||||
@ -15,7 +15,7 @@
|
||||
|
||||
{{! Rows for existing keys (includes new rows after user clicks "Add") }}
|
||||
{{#each this.patchData as |kv idx|}}
|
||||
<KvPatchEditor::Row
|
||||
<KvPatch::Editor::Row
|
||||
@idx={{idx}}
|
||||
@kvClass={{kv}}
|
||||
@isOriginalSubkey={{this.isOriginalSubkey}}
|
||||
@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<KvPatchEditor::Alerts
|
||||
<KvPatch::Editor::Alerts
|
||||
@idx="new"
|
||||
@keyError={{this.newKeyError}}
|
||||
@keyWarning={{this.newKeyWarning}}
|
||||
@ -60,16 +60,7 @@
|
||||
/>
|
||||
|
||||
<hr class="has-background-gray-200" />
|
||||
|
||||
<div class="has-top-margin-m">
|
||||
<Toggle @onChange={{fn (mut this.showSubkeys)}} @checked={{this.showSubkeys}} @name="Reveal subkeys">
|
||||
<Hds::Text::Body @tag="p" @weight="semibold">Reveal subkeys in JSON</Hds::Text::Body>
|
||||
</Toggle>
|
||||
{{#if this.showSubkeys}}
|
||||
<Hds::CodeBlock @value={{stringify @subkeys}} @hasLineNumbers={{false}} data-test-subkeys />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<KvPatch::SubkeysReveal @subkeys={{@subkeys}} />
|
||||
<hr class="has-background-gray-200" />
|
||||
|
||||
<Hds::ButtonSet>
|
||||
@ -77,7 +68,7 @@
|
||||
<Hds::Button @text="Cancel" {{on "click" @onCancel}} @color="secondary" disabled={{@isSaving}} data-test-kv-cancel />
|
||||
</Hds::ButtonSet>
|
||||
|
||||
{{#if this.submitError}}
|
||||
<AlertInline @type="danger" @message={{this.submitError}} class="has-top-padding-s" />
|
||||
{{#if (or @submitError this.validationError)}}
|
||||
<AlertInline @type="danger" @message={{or @submitError this.validationError}} class="has-top-padding-s" />
|
||||
{{/if}}
|
||||
</form>
|
@ -10,7 +10,7 @@ import { A } from '@ember/array';
|
||||
import { hasWhitespace, isNonString, WHITESPACE_WARNING, NON_STRING_WARNING } from 'vault/utils/validators';
|
||||
|
||||
/**
|
||||
* @module KvPatchEditor::Form
|
||||
* @module KvPatch::Editor::Form
|
||||
* @description
|
||||
* This component renders one of two ways to patch a KV v2 secret (the other is using the JSON editor).
|
||||
* Each top-level subkey returned by the API endpoint renders in a disabled column with an empty (also disabled) value input beside it.
|
||||
@ -25,14 +25,14 @@ import { hasWhitespace, isNonString, WHITESPACE_WARNING, NON_STRING_WARNING } fr
|
||||
*
|
||||
* Clicking the "Reveal subkeys in JSON" toggle displays the full, nested subkey structure returned by the API.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* <KvPatchEditor::Form @subkeys={{@subkeys}} @onSubmit={{perform this.save}} @onCancel={{this.onCancel}} @isSaving={{this.save.isRunning}} />
|
||||
* <KvPatch::Editor::Form @subkeys={{@subkeys}} @onSubmit={{perform this.save}} @onCancel={{this.onCancel}} @isSaving={{this.save.isRunning}} />
|
||||
*
|
||||
* @param {object} subkeys - leaf keys of a kv v2 secret, all values (unless a nested object with more keys) return null. https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-subkeys
|
||||
* @param {function} onSubmit - called when form is saved, called with with the key value object containing patch data
|
||||
* @param {function} onCancel - called when form is canceled
|
||||
* @param {boolean} isSaving - if true, disables the save and cancel buttons. useful if the onSubmit callback is a concurrency task
|
||||
* @param {function} onCancel - called when form is canceled
|
||||
* @param {function} onSubmit - called when form is saved, called with with the key value object containing patch data
|
||||
* @param {object} subkeys - leaf keys of a kv v2 secret, all values (unless a nested object with more keys) return null. https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-subkeys
|
||||
* @param {string} submitError - error message string from parent if submit failed
|
||||
*/
|
||||
|
||||
export class KeyValueState {
|
||||
@ -72,10 +72,10 @@ export class KeyValueState {
|
||||
}
|
||||
}
|
||||
|
||||
export default class KvPatchEditor extends Component {
|
||||
export default class KvPatchEditorForm extends Component {
|
||||
@tracked patchData; // key value pairs in form
|
||||
@tracked showSubkeys = false;
|
||||
@tracked submitError;
|
||||
@tracked validationError;
|
||||
|
||||
// tracked variables for new (initially empty) row of inputs.
|
||||
// once a user clicks "Add" a KeyValueState class is instantiated for that row
|
||||
@ -169,13 +169,13 @@ export default class KvPatchEditor extends Component {
|
||||
submit(event) {
|
||||
event.preventDefault();
|
||||
if (this.newKeyError || this.patchData.any((KV) => KV.keyError)) {
|
||||
this.submitError = 'This form contains validations errors, please resolve those before submitting.';
|
||||
this.validationError = 'This form contains validations errors, please resolve those before submitting.';
|
||||
return;
|
||||
}
|
||||
|
||||
// patchData will not include the last row if a user has not clicked "Add"
|
||||
// manually check for data and add it to this.patchData
|
||||
if (this.newKey && this.newValue) {
|
||||
if (this.newKey) {
|
||||
this.addRow();
|
||||
}
|
||||
|
@ -67,7 +67,7 @@
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
|
||||
<KvPatchEditor::Alerts
|
||||
<KvPatch::Editor::Alerts
|
||||
@idx={{@idx}}
|
||||
@keyError={{@kvClass.keyError}}
|
||||
@keyWarning={{@kvClass.keyWarning}}
|
25
ui/lib/kv/addon/components/kv-patch/json-form.hbs
Normal file
25
ui/lib/kv/addon/components/kv-patch/json-form.hbs
Normal file
@ -0,0 +1,25 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<form {{on "submit" this.submit}}>
|
||||
<JsonEditor class="has-top-margin-l" @title="Patch data" @valueUpdated={{this.handleJson}} @value={{this.jsonObject}} />
|
||||
{{#if this.lintingErrors}}
|
||||
<AlertInline
|
||||
@color="warning"
|
||||
class="has-top-padding-s"
|
||||
@message="JSON is unparsable. Fix linting errors to avoid data discrepancies."
|
||||
/>
|
||||
{{/if}}
|
||||
<hr class="has-background-gray-200" />
|
||||
<KvPatch::SubkeysReveal @subkeys={{@subkeys}} />
|
||||
<hr class="has-background-gray-200" />
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Save" type="submit" @icon={{if @isSaving "loading"}} disabled={{@isSaving}} data-test-kv-save />
|
||||
<Hds::Button @text="Cancel" {{on "click" @onCancel}} @color="secondary" disabled={{@isSaving}} data-test-kv-cancel />
|
||||
</Hds::ButtonSet>
|
||||
{{#if @submitError}}
|
||||
<AlertInline @type="danger" @message={{@submitError}} class="has-top-padding-s" />
|
||||
{{/if}}
|
||||
</form>
|
50
ui/lib/kv/addon/components/kv-patch/json-form.js
Normal file
50
ui/lib/kv/addon/components/kv-patch/json-form.js
Normal file
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module KvPatchJsonForm
|
||||
* @description
|
||||
* This component renders one of two ways to patch a KV v2 secret (the other is using the KvPatch::Editor::Form).
|
||||
*
|
||||
* @example
|
||||
* <KvPatch::JsonForm @onSubmit={{perform this.save}} @onCancel={{this.onCancel}} @isSaving={{this.save.isRunning}} />
|
||||
*
|
||||
* @param {boolean} isSaving - if true, disables the save and cancel buttons. useful if the onSubmit callback is a concurrency task
|
||||
* @param {function} onCancel - called when form is canceled
|
||||
* @param {function} onSubmit - called when form is saved, called with with the key value object containing patch data
|
||||
* @param {object} subkeys - leaf keys of a kv v2 secret, all values (unless a nested object with more keys) return null. used for toggle that reveals codeblock of subkey structure
|
||||
* @param {string} submitError - error message string from parent if submit failed
|
||||
*/
|
||||
|
||||
export default class KvPatchJsonForm extends Component {
|
||||
@tracked jsonObject;
|
||||
@tracked lintingErrors;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// prefill JSON editor with an empty object
|
||||
this.jsonObject = JSON.stringify({ '': '' }, null, 2);
|
||||
}
|
||||
|
||||
@action
|
||||
handleJson(value, codemirror) {
|
||||
codemirror.performLint();
|
||||
this.lintingErrors = codemirror.state.lint.marked.length > 0;
|
||||
if (!this.lintingErrors) {
|
||||
this.jsonObject = value;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
submit(event) {
|
||||
event.preventDefault();
|
||||
const patchData = JSON.parse(this.jsonObject);
|
||||
this.args.onSubmit(patchData);
|
||||
}
|
||||
}
|
13
ui/lib/kv/addon/components/kv-patch/subkeys-reveal.hbs
Normal file
13
ui/lib/kv/addon/components/kv-patch/subkeys-reveal.hbs
Normal file
@ -0,0 +1,13 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="has-top-margin-m">
|
||||
<Toggle @onChange={{fn (mut this.showSubkeys)}} @checked={{this.showSubkeys}} @name="Reveal subkeys">
|
||||
<Hds::Text::Body @tag="p" @weight="semibold">Reveal subkeys in JSON</Hds::Text::Body>
|
||||
</Toggle>
|
||||
{{#if this.showSubkeys}}
|
||||
<Hds::CodeBlock @value={{stringify @subkeys}} @hasLineNumbers={{false}} data-test-subkeys />
|
||||
{{/if}}
|
||||
</div>
|
20
ui/lib/kv/addon/components/kv-patch/subkeys-reveal.js
Normal file
20
ui/lib/kv/addon/components/kv-patch/subkeys-reveal.js
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module SubkeysReveal
|
||||
*
|
||||
* @example
|
||||
* <SubkeysReveal @subkeys={{this.subkeys}} />
|
||||
*
|
||||
* @param {object} subkeys - leaf keys of a kv v2 secret, all values (unless a nested object with more keys) return null. https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2#read-secret-subkeys
|
||||
*/
|
||||
|
||||
export default class SubkeysReveal extends Component {
|
||||
@tracked showSubkeys = false;
|
||||
}
|
@ -7,7 +7,7 @@ import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
* @module KvSubkeys
|
||||
* @module KvSubkeysCard
|
||||
* @description
|
||||
sample secret data:
|
||||
```
|
||||
@ -31,11 +31,11 @@ sample subkeys:
|
||||
```
|
||||
*
|
||||
* @example
|
||||
* <KvSubkeys @subkeys={{this.subkeys}} />
|
||||
* <KvSubkeysCard @subkeys={{this.subkeys}} />
|
||||
*
|
||||
* @param {object} subkeys - leaf keys of a kv v2 secret, all values (unless a nested object with more keys) return null
|
||||
*/
|
||||
|
||||
export default class KvSubkeys extends Component {
|
||||
export default class KvSubkeysCard extends Component {
|
||||
@tracked showJson = false;
|
||||
}
|
@ -101,5 +101,5 @@
|
||||
</Hds::Card::Container>
|
||||
|
||||
{{#if @subkeys.subkeys}}
|
||||
<KvSubkeys @subkeys={{@subkeys.subkeys}} />
|
||||
<KvSubkeysCard @subkeys={{@subkeys.subkeys}} />
|
||||
{{/if}}
|
65
ui/lib/kv/addon/components/page/secret/patch.hbs
Normal file
65
ui/lib/kv/addon/components/page/secret/patch.hbs
Normal file
@ -0,0 +1,65 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle="Patch Secret to New Version" />
|
||||
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-bottomless">
|
||||
<NamespaceReminder @mode="patch" @noun="secret" />
|
||||
<Hds::Form::TextInput::Field name="secret path" @value={{@path}} disabled data-test-field="Path" as |F|>
|
||||
<F.Label>Path for this secret</F.Label>
|
||||
</Hds::Form::TextInput::Field>
|
||||
|
||||
<hr class="has-background-gray-200" />
|
||||
|
||||
<Hds::Text::Display @tag="h2" @size="300" class="has-bottom-padding-s">Patch secret data</Hds::Text::Display>
|
||||
<Hds::Alert @type="compact" @icon="alert-triangle-fill" @color="warning" class="has-bottom-padding-m" as |A|>
|
||||
<A.Description>
|
||||
The
|
||||
<code>PATCH</code>
|
||||
action allows you to partially update or add a key-value pair to the current version of the secret. The values will
|
||||
remain the same in the new version if no changes are made to them.
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
|
||||
<Hds::Form::Radio::Group @name="patch-method" class="has-bottom-margin-m" as |G|>
|
||||
<G.Legend>Edit via</G.Legend>
|
||||
<G.HelperText>
|
||||
Choose how to patch the secret data.
|
||||
<strong>Switching to another method will reset the form data.</strong>
|
||||
</G.HelperText>
|
||||
|
||||
{{#each (array "JSON" "UI") as |method|}}
|
||||
<G.RadioField
|
||||
@value={{method}}
|
||||
{{on "change" this.selectPatchMethod}}
|
||||
checked={{eq this.patchMethod method}}
|
||||
data-test-input={{method}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>{{method}}</F.Label>
|
||||
</G.RadioField>
|
||||
{{/each}}
|
||||
</Hds::Form::Radio::Group>
|
||||
|
||||
{{#if (eq this.patchMethod "UI")}}
|
||||
<KvPatch::Editor::Form
|
||||
@isSaving={{this.save.isRunning}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSubmit={{perform this.save}}
|
||||
@subkeys={{@subkeys}}
|
||||
@submitError={{this.invalidFormAlert}}
|
||||
/>
|
||||
{{else}}
|
||||
<KvPatch::JsonForm
|
||||
@isSaving={{this.save.isRunning}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@onSubmit={{perform this.save}}
|
||||
@subkeys={{@subkeys}}
|
||||
@submitError={{this.invalidFormAlert}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
92
ui/lib/kv/addon/components/page/secret/patch.js
Normal file
92
ui/lib/kv/addon/components/page/secret/patch.js
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { waitFor } from '@ember/test-waiters';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
/**
|
||||
* @module KvSecretPatch
|
||||
* @description
|
||||
* This page template provides two methods for submitting patch data to update a KV v2 secret.
|
||||
* Either using a key/value form KvPatch::Editor::Form or the json editor via KvPatch::JsonForm
|
||||
*
|
||||
* <Page::Secret::Patch
|
||||
* @backend="my-kv-engine"
|
||||
* @breadcrumbs={{this.breadcrumbs}
|
||||
* @metadata={{this.model.metadata}}
|
||||
* @path="my-secret"
|
||||
* @subkeys={{this.subkeys}
|
||||
* @subkeysMeta={{this.subkeysMeta}
|
||||
* />
|
||||
*
|
||||
* @param {model} path - Secret path
|
||||
* @param {string} backend - Mount backend path
|
||||
* @param {model} metadata - Ember data model: 'kv/metadata'
|
||||
* @param {object} subkeys - subkeys (leaf keys with null values) of kv v2 secret
|
||||
* @param {object} subkeysMeta - metadata object returned from the /subkeys endpoint, contains: version, created_time, custom_metadata, deletion status and time
|
||||
* @param {array} breadcrumbs - breadcrumb objects to render in page header
|
||||
*/
|
||||
|
||||
export default class KvSecretPatch extends Component {
|
||||
@service controlGroup;
|
||||
@service flashMessages;
|
||||
@service router;
|
||||
@service store;
|
||||
|
||||
@tracked errorMessage;
|
||||
@tracked invalidFormAlert;
|
||||
@tracked patchMethod = 'UI';
|
||||
|
||||
@action
|
||||
selectPatchMethod(event) {
|
||||
this.patchMethod = event.target.value;
|
||||
}
|
||||
|
||||
@task
|
||||
@waitFor
|
||||
*save(patchData) {
|
||||
const isEmpty = this.isEmpty(patchData);
|
||||
if (isEmpty) {
|
||||
this.flashMessages.info(`No changes to submit. No updates made to "${this.args.path}".`);
|
||||
return this.onCancel();
|
||||
}
|
||||
|
||||
const { backend, path, metadata, subkeysMeta } = this.args;
|
||||
// if no metadata permission, use subkey metadata as backup
|
||||
const version = metadata.currentVersion || subkeysMeta.version;
|
||||
const adapter = this.store.adapterFor('kv/data');
|
||||
try {
|
||||
yield adapter.patchSecret(backend, path, patchData, version);
|
||||
this.flashMessages.success(`Successfully patched new version of ${path}.`);
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kv.secret');
|
||||
} catch (error) {
|
||||
// TODO test...this is copy pasta'd from the edit page
|
||||
let message = errorMessage(error);
|
||||
if (error.message === 'Control Group encountered') {
|
||||
this.controlGroup.saveTokenFromError(error);
|
||||
const err = this.controlGroup.logFromError(error);
|
||||
message = err.content;
|
||||
}
|
||||
this.errorMessage = message;
|
||||
this.invalidFormAlert = 'There was an error submitting this form.';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onCancel() {
|
||||
this.router.transitionTo('vault.cluster.secrets.backend.kv.secret');
|
||||
}
|
||||
|
||||
isEmpty(object) {
|
||||
const emptyKeys = Object.keys(object).every((k) => k === '');
|
||||
const emptyValues = Object.values(object).every((v) => v === '');
|
||||
return emptyKeys && emptyValues;
|
||||
}
|
||||
}
|
@ -114,6 +114,7 @@ export const FORM = {
|
||||
addRow: (idx = 0) => `[data-test-kv-add-row="${idx}"]`,
|
||||
deleteRow: (idx = 0) => `[data-test-kv-delete-row="${idx}"]`,
|
||||
// <KvPatchEditor>
|
||||
patchEditorForm: '[data-test-kv-patch-editor]',
|
||||
patchEdit: (idx = 0) => `[data-test-edit-button="${idx}"]`,
|
||||
patchDelete: (idx = 0) => `[data-test-delete-button="${idx}"]`,
|
||||
patchUndo: (idx = 0) => `[data-test-undo-button="${idx}"]`,
|
||||
@ -135,3 +136,5 @@ export const FORM = {
|
||||
export const parseJsonEditor = (find) => {
|
||||
return JSON.parse(find(FORM.jsonEditor).innerText);
|
||||
};
|
||||
|
||||
export const parseObject = (cm) => JSON.parse(cm().getValue());
|
||||
|
@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
|
||||
module('Integration | Component | kv | kv-patch-editor/alerts', function (hooks) {
|
||||
module('Integration | Component | kv | kv-patch/editor/alerts', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
|
||||
@ -22,7 +22,7 @@ module('Integration | Component | kv | kv-patch-editor/alerts', function (hooks)
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<KvPatchEditor::Alerts
|
||||
<KvPatch::Editor::Alerts
|
||||
@idx={{1}}
|
||||
@keyError={{this.keyError}}
|
||||
@keyWarning={{this.keyWarning}}
|
@ -8,32 +8,39 @@ import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { blur, click, fillIn, typeIn, render, focus } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { NON_STRING_WARNING, WHITESPACE_WARNING } from 'vault/utils/validators';
|
||||
|
||||
module('Integration | Component | kv | kv-patch-editor/form', function (hooks) {
|
||||
module('Integration | Component | kv | kv-patch/editor/form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.subkeys = {
|
||||
foo: null,
|
||||
baz: null,
|
||||
baz: {
|
||||
nested: null,
|
||||
bar: {
|
||||
hello: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.onSubmit = sinon.spy();
|
||||
this.onCancel = sinon.spy();
|
||||
this.isSaving = false;
|
||||
this.submitError = '';
|
||||
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<KvPatchEditor::Form
|
||||
<KvPatch::Editor::Form
|
||||
@subkeys={{this.subkeys}}
|
||||
@onSubmit={{this.onSubmit}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@isSaving={{this.isSaving}}
|
||||
@submitError={{this.submitError}}
|
||||
/>`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
@ -79,16 +86,13 @@ module('Integration | Component | kv | kv-patch-editor/form', function (hooks) {
|
||||
this.assertEmptyRow(assert);
|
||||
});
|
||||
|
||||
test('it renders submit error from parent', async function (assert) {
|
||||
this.submitError = 'There was a problem submitting this form.';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inlineError).hasText(this.submitError);
|
||||
});
|
||||
|
||||
test('it reveals subkeys', async function (assert) {
|
||||
this.subkeys = {
|
||||
foo: null,
|
||||
bar: {
|
||||
baz: null,
|
||||
quux: {
|
||||
hello: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked('toggle is initially unchecked');
|
||||
@ -495,7 +499,6 @@ module('Integration | Component | kv | kv-patch-editor/form', function (hooks) {
|
||||
NON_STRING_VALUES.forEach((value) => {
|
||||
test(`for new non-string values: ${value}`, async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||
await fillIn(FORM.valueInput('new'), value);
|
||||
await blur(FORM.valueInput('new')); // unfocus input
|
@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module('Integration | Component | kv | kv-patch-editor/row', function (hooks) {
|
||||
module('Integration | Component | kv | kv-patch/editor/row', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
|
||||
@ -32,7 +32,7 @@ module('Integration | Component | kv | kv-patch-editor/row', function (hooks) {
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<KvPatchEditor::Row
|
||||
<KvPatch::Editor::Row
|
||||
@idx={{0}}
|
||||
@kvClass={{this.kvClass}}
|
||||
@isOriginalSubkey={{this.isOriginalSubkey}}
|
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import sinon from 'sinon';
|
||||
import { FORM, parseObject } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import codemirror from 'vault/tests/helpers/codemirror';
|
||||
|
||||
module('Integration | Component | kv | kv-patch/editor/json-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.onSubmit = sinon.spy();
|
||||
this.onCancel = sinon.spy();
|
||||
this.isSaving = false;
|
||||
this.submitError = '';
|
||||
this.subkeys = {
|
||||
foo: null,
|
||||
bar: {
|
||||
baz: null,
|
||||
quux: {
|
||||
hello: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<KvPatch::JsonForm
|
||||
@onSubmit={{this.onSubmit}}
|
||||
@onCancel={{this.onCancel}}
|
||||
@isSaving={{this.isSaving}}
|
||||
@subkeys={{this.subkeys}}
|
||||
@submitError={{this.submitError}}
|
||||
/>`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.propEqual(parseObject(codemirror), { '': '' }, 'json editor initializes with empty object');
|
||||
await click(FORM.saveBtn);
|
||||
assert.true(this.onSubmit.calledOnce, 'clicking "Save" calls @onSubmit');
|
||||
await click(FORM.cancelBtn);
|
||||
assert.true(this.onCancel.calledOnce, 'clicking "Cancel" calls @onCancel');
|
||||
});
|
||||
|
||||
test('it reveals subkeys', async function (assert) {
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked('toggle is initially unchecked');
|
||||
assert.dom('[data-test-subkeys]').doesNotExist();
|
||||
await click(GENERAL.toggleInput('Reveal subkeys'));
|
||||
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isChecked();
|
||||
assert.dom('[data-test-subkeys]').hasText(JSON.stringify(this.subkeys, null, 2));
|
||||
|
||||
await click(GENERAL.toggleInput('Reveal subkeys'));
|
||||
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked();
|
||||
assert.dom('[data-test-subkeys]').doesNotExist('unchecking re-hides subkeys');
|
||||
});
|
||||
|
||||
test('it renders linting errors', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue('{ "foo3": }');
|
||||
assert
|
||||
.dom(GENERAL.inlineError)
|
||||
.hasText('JSON is unparsable. Fix linting errors to avoid data discrepancies.');
|
||||
await codemirror().setValue('{ "foo": "bar" }');
|
||||
assert.dom(GENERAL.inlineError).doesNotExist('error disappears when linting is fixed');
|
||||
});
|
||||
|
||||
test('it renders submit error from parent', async function (assert) {
|
||||
this.submitError = 'There was a problem';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inlineError).hasText(this.submitError);
|
||||
});
|
||||
|
||||
test('it submits data', async function (assert) {
|
||||
this.submitError = 'There was a problem';
|
||||
await this.renderComponent();
|
||||
await codemirror().setValue('{ "foo": "bar" }');
|
||||
await click(FORM.saveBtn);
|
||||
const [data] = this.onSubmit.lastCall.args;
|
||||
assert.propEqual(data, { foo: 'bar' }, `onSubmit called with ${JSON.stringify(data)}`);
|
||||
});
|
||||
});
|
@ -11,7 +11,7 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const { overviewCard } = GENERAL;
|
||||
module('Integration | Component | kv | kv-subkeys', function (hooks) {
|
||||
module('Integration | Component | kv | kv-subkeys-card', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
hooks.beforeEach(function () {
|
||||
@ -22,7 +22,7 @@ module('Integration | Component | kv | kv-subkeys', function (hooks) {
|
||||
},
|
||||
};
|
||||
this.renderComponent = async () => {
|
||||
return render(hbs`<KvSubkeys @subkeys={{this.subkeys}} />`, {
|
||||
return render(hbs`<KvSubkeysCard @subkeys={{this.subkeys}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
};
|
@ -9,9 +9,9 @@ import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { kvDataPath, kvMetadataPath } from 'vault/utils/kv-path';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { kvDataPath } from 'vault/utils/kv-path';
|
||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { baseSetup, metadataModel } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
|
||||
module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@ -19,27 +19,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
this.backend = 'kv-engine';
|
||||
this.path = 'my-secret';
|
||||
baseSetup(this);
|
||||
this.dataId = kvDataPath(this.backend, this.path);
|
||||
this.metadataId = kvMetadataPath(this.backend, this.path);
|
||||
|
||||
this.metadataModel = (withCustom = false) => {
|
||||
const metadata = withCustom
|
||||
? this.server.create('kv-metadatum', 'withCustomMetadata')
|
||||
: this.server.create('kv-metadatum');
|
||||
metadata.id = this.metadataId;
|
||||
this.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...metadata,
|
||||
});
|
||||
return this.store.peekRecord('kv/metadata', this.metadataId);
|
||||
};
|
||||
|
||||
this.metadata = this.metadataModel();
|
||||
|
||||
// empty secret model always exists for permissions
|
||||
this.store.pushPayload('kv/data', {
|
||||
modelName: 'kv/data',
|
||||
@ -64,7 +45,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
|
||||
test('it renders metadata details', async function (assert) {
|
||||
assert.expect(8);
|
||||
this.metadata = this.metadataModel();
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Metadata::Details
|
||||
@ -95,7 +75,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
|
||||
test('it renders custom metadata from secret model', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.metadata = this.metadataModel();
|
||||
this.secret.customMetadata = { hi: 'there' };
|
||||
await render(
|
||||
hbs`
|
||||
@ -115,7 +94,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
||||
|
||||
test('it renders custom metadata from metadata model', async function (assert) {
|
||||
assert.expect(4);
|
||||
this.metadata = this.metadataModel({ withCustom: true });
|
||||
this.model.metadata = metadataModel(this, { withCustom: true });
|
||||
await render(
|
||||
hbs`
|
||||
<Page::Secret::Metadata::Details
|
||||
|
@ -67,7 +67,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
};
|
||||
});
|
||||
|
||||
module('it renders when version is not deleted nor destroyed', function () {
|
||||
module('active secret (version not deleted or destroyed)', function () {
|
||||
test('it renders tabs', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const tabs = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History'];
|
||||
@ -190,7 +190,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
});
|
||||
});
|
||||
|
||||
module('it renders when version is deleted', function (hooks) {
|
||||
module('deleted version', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.secretState = 'deleted';
|
||||
// subkeys is null but metadata still has data
|
||||
@ -266,7 +266,7 @@ module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hoo
|
||||
});
|
||||
});
|
||||
|
||||
module('it renders when version is destroyed', function (hooks) {
|
||||
module('destroyed version', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.secretState = 'destroyed';
|
||||
// subkeys is null but metadata still has data
|
||||
|
342
ui/tests/integration/components/kv/page/kv-page-patch-test.js
Normal file
342
ui/tests/integration/components/kv/page/kv-page-patch-test.js
Normal file
@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
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 { blur, click, fillIn, find, render, waitUntil } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import sinon from 'sinon';
|
||||
import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
import codemirror from 'vault/tests/helpers/codemirror';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
|
||||
module('Integration | Component | kv-v2 | Page::Secret::Patch', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'kv');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
baseSetup(this);
|
||||
this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo');
|
||||
this.breadcrumbs = [
|
||||
{ label: 'Secrets', route: 'secrets', linkExternal: true },
|
||||
{ label: this.backend, route: 'list' },
|
||||
{ label: this.path, route: 'index' },
|
||||
{ label: 'Patch' },
|
||||
];
|
||||
this.subkeys = {
|
||||
foo: null,
|
||||
bar: {
|
||||
baz: null,
|
||||
},
|
||||
quux: null,
|
||||
};
|
||||
this.subkeyMeta = {
|
||||
created_time: '2021-12-14T20:28:00.773477Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
this.renderComponent = async () => {
|
||||
return render(
|
||||
hbs`
|
||||
<Page::Secret::Patch
|
||||
@backend={{this.backend}}
|
||||
@breadcrumbs={{this.breadcrumbs}}
|
||||
@metadata={{this.metadata}}
|
||||
@path={{this.path}}
|
||||
@subkeys={{this.subkeys}}
|
||||
@subkeysMeta={{this.subkeysMeta}}
|
||||
/>`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path} Patch`);
|
||||
assert.dom(PAGE.title).hasText('Patch Secret to New Version');
|
||||
assert.dom(GENERAL.fieldByAttr('Path')).isDisabled();
|
||||
assert.dom(GENERAL.fieldByAttr('Path')).hasValue(this.path);
|
||||
assert.dom(GENERAL.inputByAttr('JSON')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('UI')).isChecked();
|
||||
assert.dom(FORM.patchEditorForm).exists('it renders editor form by default');
|
||||
assert.dom(GENERAL.codemirror).doesNotExist();
|
||||
Object.keys(this.subkeys).forEach((key, idx) => {
|
||||
assert.dom(FORM.keyInput(idx)).hasValue(key);
|
||||
assert.dom(FORM.keyInput(idx)).isDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test('it selects JSON as an edit option', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(FORM.patchEditorForm).exists();
|
||||
await click(GENERAL.inputByAttr('JSON'));
|
||||
assert.dom(GENERAL.inputByAttr('JSON')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('UI')).isNotChecked();
|
||||
assert.dom(FORM.patchEditorForm).doesNotExist();
|
||||
assert.dom(GENERAL.codemirror).exists();
|
||||
});
|
||||
|
||||
test('it transitions on cancel', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await click(FORM.cancelBtn);
|
||||
const [route] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret',
|
||||
`it transitions on cancel to: ${route}`
|
||||
);
|
||||
});
|
||||
|
||||
module('it submits', function (hooks) {
|
||||
const EXAMPLE_KV_DATA_CREATE_RESPONSE = {
|
||||
request_id: 'foobar',
|
||||
data: {
|
||||
created_time: '2023-06-21T16:18:31.479993Z',
|
||||
custom_metadata: null,
|
||||
deletion_time: '',
|
||||
destroyed: false,
|
||||
version: 1,
|
||||
},
|
||||
};
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.endpoint = `${encodePath(this.backend)}/data/${encodePath(this.path)}`;
|
||||
});
|
||||
|
||||
test('patch data from kv editor form', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.patch(this.endpoint, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
data: { bar: null, foo: 'foovalue', aKey: '1', bKey: 'null' },
|
||||
options: {
|
||||
cas: this.metadata.currentVersion,
|
||||
},
|
||||
};
|
||||
assert.true(true, `PATCH request made to ${this.endpoint}`);
|
||||
assert.propEqual(
|
||||
payload,
|
||||
expected,
|
||||
`payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
|
||||
);
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
// patch existing, delete and create a new key key
|
||||
await click(FORM.patchEdit());
|
||||
await fillIn(FORM.valueInput(), 'foovalue');
|
||||
await blur(FORM.valueInput());
|
||||
await click(FORM.patchDelete(1));
|
||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||
await fillIn(FORM.valueInput('new'), '1');
|
||||
await click(FORM.patchAdd);
|
||||
// add new key and do NOT click add
|
||||
await fillIn(FORM.keyInput('new'), 'bKey');
|
||||
await fillIn(FORM.valueInput('new'), 'null');
|
||||
await click(FORM.saveBtn);
|
||||
const [route] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret',
|
||||
`it transitions on save to: ${route}`
|
||||
);
|
||||
});
|
||||
|
||||
test('patch data from json form', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.patch(this.endpoint, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
data: { foo: 'foovalue', bar: null, number: 1 },
|
||||
options: {
|
||||
cas: 4,
|
||||
},
|
||||
};
|
||||
assert.true(true, `PATCH request made to ${this.endpoint}`);
|
||||
assert.propEqual(
|
||||
payload,
|
||||
expected,
|
||||
`payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
|
||||
);
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.inputByAttr('JSON'));
|
||||
await waitUntil(() => find('.CodeMirror'));
|
||||
await codemirror().setValue('{ "foo": "foovalue", "bar":null, "number":1 }');
|
||||
await click(FORM.saveBtn);
|
||||
const [route] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret',
|
||||
`it transitions on save to: ${route}`
|
||||
);
|
||||
});
|
||||
|
||||
// this assertion confirms submit allows empty values
|
||||
test('empty string values from kv editor form', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.patch(this.endpoint, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
data: { foo: '', aKey: '', bKey: '' },
|
||||
options: {
|
||||
cas: this.metadata.currentVersion,
|
||||
},
|
||||
};
|
||||
assert.propEqual(
|
||||
payload,
|
||||
expected,
|
||||
`payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
|
||||
);
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await click(FORM.patchEdit());
|
||||
// edit existing key's value
|
||||
await fillIn(FORM.valueInput(), '');
|
||||
// add a new key with empty value, click add
|
||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||
await fillIn(FORM.valueInput('new'), '');
|
||||
await click(FORM.patchAdd);
|
||||
// add new key and do NOT click add
|
||||
await fillIn(FORM.keyInput('new'), 'bKey');
|
||||
await fillIn(FORM.valueInput('new'), '');
|
||||
await click(FORM.saveBtn);
|
||||
});
|
||||
|
||||
// this assertion confirms submit allows empty values
|
||||
test('empty string value from json form', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.patch(this.endpoint, (schema, req) => {
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
data: { foo: '' },
|
||||
options: {
|
||||
cas: this.metadata.currentVersion,
|
||||
},
|
||||
};
|
||||
assert.propEqual(
|
||||
payload,
|
||||
expected,
|
||||
`payload: ${JSON.stringify(payload)} matches expected: ${JSON.stringify(payload)}`
|
||||
);
|
||||
return EXAMPLE_KV_DATA_CREATE_RESPONSE;
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.inputByAttr('JSON'));
|
||||
await waitUntil(() => find('.CodeMirror'));
|
||||
await codemirror().setValue('{ "foo": "" }');
|
||||
await click(FORM.saveBtn);
|
||||
});
|
||||
});
|
||||
|
||||
module('it does not submit', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.endpoint = `${encodePath(this.backend)}/data/${encodePath(this.path)}`;
|
||||
this.flashSpy = sinon.spy(this.owner.lookup('service:flash-messages'), 'info');
|
||||
});
|
||||
|
||||
test('if no changes from kv editor form', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.patch(this.endpoint, () =>
|
||||
overrideResponse(500, `Request made to: ${this.endpoint}. This should not have happened!`)
|
||||
);
|
||||
await this.renderComponent();
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(GENERAL.messageError).doesNotExist('PATCH request is not made');
|
||||
const route = this.transitionStub.lastCall?.args[0] || '';
|
||||
const flash = this.flashSpy.lastCall?.args[0] || '';
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret',
|
||||
`it transitions to overview route: ${route}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
flash,
|
||||
`No changes to submit. No updates made to "${this.path}".`,
|
||||
`flash message has message: "${flash}"`
|
||||
);
|
||||
});
|
||||
|
||||
test('if no changes from json form', async function (assert) {
|
||||
assert.expect(3);
|
||||
this.server.patch(this.endpoint, () =>
|
||||
overrideResponse(500, `Request made to: ${this.endpoint}. This should not have happened!`)
|
||||
);
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.inputByAttr('JSON'));
|
||||
await waitUntil(() => find('.CodeMirror'));
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(GENERAL.messageError).doesNotExist('PATCH request is not made');
|
||||
const route = this.transitionStub.lastCall?.args[0] || '';
|
||||
const flash = this.flashSpy.lastCall?.args[0] || '';
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.kv.secret',
|
||||
`it transitions to overview route: ${route}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
flash,
|
||||
`No changes to submit. No updates made to "${this.path}".`,
|
||||
`flash message has message: "${flash}"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module('it passes error', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.endpoint = `${encodePath(this.backend)}/data/${encodePath(this.path)}`;
|
||||
this.server.patch(this.endpoint, () => {
|
||||
return overrideResponse(403);
|
||||
});
|
||||
});
|
||||
|
||||
test('to kv editor form', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await this.renderComponent();
|
||||
// patch existing, delete and create a new key key
|
||||
await click(FORM.patchEdit());
|
||||
await fillIn(FORM.valueInput(), 'foovalue');
|
||||
await blur(FORM.valueInput());
|
||||
await click(FORM.patchDelete(1));
|
||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||
await fillIn(FORM.valueInput('new'), 'aValue');
|
||||
await click(FORM.patchAdd);
|
||||
// add new key and do NOT click add
|
||||
await fillIn(FORM.keyInput('new'), 'bKey');
|
||||
await fillIn(FORM.valueInput('new'), 'bValue');
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(GENERAL.messageError).hasText('Error permission denied');
|
||||
assert.dom(GENERAL.inlineError).hasText('There was an error submitting this form.');
|
||||
});
|
||||
|
||||
test('to json form', async function (assert) {
|
||||
assert.expect(2);
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.inputByAttr('JSON'));
|
||||
await waitUntil(() => find('.CodeMirror'));
|
||||
await codemirror().setValue('{ "foo": "foovalue", "bar":null, "number":1 }');
|
||||
await click(FORM.saveBtn);
|
||||
await click(FORM.saveBtn);
|
||||
assert.dom(GENERAL.messageError).hasText('Error permission denied');
|
||||
assert.dom(GENERAL.inlineError).hasText('There was an error submitting this form.');
|
||||
});
|
||||
});
|
||||
});
|
@ -9,11 +9,11 @@ import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { click, find, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { kvDataPath, kvMetadataPath } from 'vault/utils/kv-path';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import { kvDataPath } from 'vault/utils/kv-path';
|
||||
import { FORM, PAGE, parseJsonEditor } from 'vault/tests/helpers/kv/kv-selectors';
|
||||
import { syncStatusResponse } from 'vault/mirage/handlers/sync';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands';
|
||||
|
||||
module('Integration | Component | kv-v2 | Page::Secret::Details', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
@ -21,15 +21,11 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
this.backend = 'kv-engine';
|
||||
this.path = 'my-secret';
|
||||
baseSetup(this);
|
||||
this.pathComplex = 'my-secret-object';
|
||||
this.version = 2;
|
||||
this.dataId = kvDataPath(this.backend, this.path);
|
||||
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
|
||||
this.metadataId = kvMetadataPath(this.backend, this.path);
|
||||
|
||||
this.secretData = { foo: 'bar' };
|
||||
this.store.pushPayload('kv/data', {
|
||||
@ -60,15 +56,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
||||
destroyed: false,
|
||||
version: this.version,
|
||||
});
|
||||
|
||||
const metadata = this.server.create('kv-metadatum');
|
||||
metadata.id = this.metadataId;
|
||||
this.store.pushPayload('kv/metadata', {
|
||||
modelName: 'kv/metadata',
|
||||
...metadata,
|
||||
});
|
||||
|
||||
this.metadata = this.store.peekRecord('kv/metadata', this.metadataId);
|
||||
this.secret = this.store.peekRecord('kv/data', this.dataId);
|
||||
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user