mirror of
https://github.com/hashicorp/vault.git
synced 2025-09-03 21:11:10 +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
|
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">
|
<div class="flex column-gap-16 has-top-padding-s">
|
||||||
<Hds::Form::Label @controlId="newKey" class="one-fourth-width">
|
<Hds::Form::Label @controlId="newKey" class="one-fourth-width">
|
||||||
Key
|
Key
|
||||||
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
{{! Rows for existing keys (includes new rows after user clicks "Add") }}
|
{{! Rows for existing keys (includes new rows after user clicks "Add") }}
|
||||||
{{#each this.patchData as |kv idx|}}
|
{{#each this.patchData as |kv idx|}}
|
||||||
<KvPatchEditor::Row
|
<KvPatch::Editor::Row
|
||||||
@idx={{idx}}
|
@idx={{idx}}
|
||||||
@kvClass={{kv}}
|
@kvClass={{kv}}
|
||||||
@isOriginalSubkey={{this.isOriginalSubkey}}
|
@isOriginalSubkey={{this.isOriginalSubkey}}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<KvPatchEditor::Alerts
|
<KvPatch::Editor::Alerts
|
||||||
@idx="new"
|
@idx="new"
|
||||||
@keyError={{this.newKeyError}}
|
@keyError={{this.newKeyError}}
|
||||||
@keyWarning={{this.newKeyWarning}}
|
@keyWarning={{this.newKeyWarning}}
|
||||||
@ -60,16 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<hr class="has-background-gray-200" />
|
<hr class="has-background-gray-200" />
|
||||||
|
<KvPatch::SubkeysReveal @subkeys={{@subkeys}} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<hr class="has-background-gray-200" />
|
<hr class="has-background-gray-200" />
|
||||||
|
|
||||||
<Hds::ButtonSet>
|
<Hds::ButtonSet>
|
||||||
@ -77,7 +68,7 @@
|
|||||||
<Hds::Button @text="Cancel" {{on "click" @onCancel}} @color="secondary" disabled={{@isSaving}} data-test-kv-cancel />
|
<Hds::Button @text="Cancel" {{on "click" @onCancel}} @color="secondary" disabled={{@isSaving}} data-test-kv-cancel />
|
||||||
</Hds::ButtonSet>
|
</Hds::ButtonSet>
|
||||||
|
|
||||||
{{#if this.submitError}}
|
{{#if (or @submitError this.validationError)}}
|
||||||
<AlertInline @type="danger" @message={{this.submitError}} class="has-top-padding-s" />
|
<AlertInline @type="danger" @message={{or @submitError this.validationError}} class="has-top-padding-s" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</form>
|
</form>
|
@ -10,7 +10,7 @@ import { A } from '@ember/array';
|
|||||||
import { hasWhitespace, isNonString, WHITESPACE_WARNING, NON_STRING_WARNING } from 'vault/utils/validators';
|
import { hasWhitespace, isNonString, WHITESPACE_WARNING, NON_STRING_WARNING } from 'vault/utils/validators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module KvPatchEditor::Form
|
* @module KvPatch::Editor::Form
|
||||||
* @description
|
* @description
|
||||||
* This component renders one of two ways to patch a KV v2 secret (the other is using the JSON editor).
|
* 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.
|
* 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.
|
* Clicking the "Reveal subkeys in JSON" toggle displays the full, nested subkey structure returned by the API.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @example
|
* @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 {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 {
|
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 patchData; // key value pairs in form
|
||||||
@tracked showSubkeys = false;
|
@tracked showSubkeys = false;
|
||||||
@tracked submitError;
|
@tracked validationError;
|
||||||
|
|
||||||
// tracked variables for new (initially empty) row of inputs.
|
// tracked variables for new (initially empty) row of inputs.
|
||||||
// once a user clicks "Add" a KeyValueState class is instantiated for that row
|
// 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) {
|
submit(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.newKeyError || this.patchData.any((KV) => KV.keyError)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// patchData will not include the last row if a user has not clicked "Add"
|
// patchData will not include the last row if a user has not clicked "Add"
|
||||||
// manually check for data and add it to this.patchData
|
// manually check for data and add it to this.patchData
|
||||||
if (this.newKey && this.newValue) {
|
if (this.newKey) {
|
||||||
this.addRow();
|
this.addRow();
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +67,7 @@
|
|||||||
</Hds::Alert>
|
</Hds::Alert>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<KvPatchEditor::Alerts
|
<KvPatch::Editor::Alerts
|
||||||
@idx={{@idx}}
|
@idx={{@idx}}
|
||||||
@keyError={{@kvClass.keyError}}
|
@keyError={{@kvClass.keyError}}
|
||||||
@keyWarning={{@kvClass.keyWarning}}
|
@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';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module KvSubkeys
|
* @module KvSubkeysCard
|
||||||
* @description
|
* @description
|
||||||
sample secret data:
|
sample secret data:
|
||||||
```
|
```
|
||||||
@ -31,11 +31,11 @@ sample subkeys:
|
|||||||
```
|
```
|
||||||
*
|
*
|
||||||
* @example
|
* @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
|
* @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;
|
@tracked showJson = false;
|
||||||
}
|
}
|
@ -101,5 +101,5 @@
|
|||||||
</Hds::Card::Container>
|
</Hds::Card::Container>
|
||||||
|
|
||||||
{{#if @subkeys.subkeys}}
|
{{#if @subkeys.subkeys}}
|
||||||
<KvSubkeys @subkeys={{@subkeys.subkeys}} />
|
<KvSubkeysCard @subkeys={{@subkeys.subkeys}} />
|
||||||
{{/if}}
|
{{/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}"]`,
|
addRow: (idx = 0) => `[data-test-kv-add-row="${idx}"]`,
|
||||||
deleteRow: (idx = 0) => `[data-test-kv-delete-row="${idx}"]`,
|
deleteRow: (idx = 0) => `[data-test-kv-delete-row="${idx}"]`,
|
||||||
// <KvPatchEditor>
|
// <KvPatchEditor>
|
||||||
|
patchEditorForm: '[data-test-kv-patch-editor]',
|
||||||
patchEdit: (idx = 0) => `[data-test-edit-button="${idx}"]`,
|
patchEdit: (idx = 0) => `[data-test-edit-button="${idx}"]`,
|
||||||
patchDelete: (idx = 0) => `[data-test-delete-button="${idx}"]`,
|
patchDelete: (idx = 0) => `[data-test-delete-button="${idx}"]`,
|
||||||
patchUndo: (idx = 0) => `[data-test-undo-button="${idx}"]`,
|
patchUndo: (idx = 0) => `[data-test-undo-button="${idx}"]`,
|
||||||
@ -135,3 +136,5 @@ export const FORM = {
|
|||||||
export const parseJsonEditor = (find) => {
|
export const parseJsonEditor = (find) => {
|
||||||
return JSON.parse(find(FORM.jsonEditor).innerText);
|
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 { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
import { FORM } from 'vault/tests/helpers/kv/kv-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);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'kv');
|
setupEngine(hooks, 'kv');
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ module('Integration | Component | kv | kv-patch-editor/alerts', function (hooks)
|
|||||||
this.renderComponent = async () => {
|
this.renderComponent = async () => {
|
||||||
return render(
|
return render(
|
||||||
hbs`
|
hbs`
|
||||||
<KvPatchEditor::Alerts
|
<KvPatch::Editor::Alerts
|
||||||
@idx={{1}}
|
@idx={{1}}
|
||||||
@keyError={{this.keyError}}
|
@keyError={{this.keyError}}
|
||||||
@keyWarning={{this.keyWarning}}
|
@keyWarning={{this.keyWarning}}
|
@ -8,32 +8,39 @@ import { setupRenderingTest } from 'vault/tests/helpers';
|
|||||||
import { setupEngine } from 'ember-engines/test-support';
|
import { setupEngine } from 'ember-engines/test-support';
|
||||||
import { blur, click, fillIn, typeIn, render, focus } from '@ember/test-helpers';
|
import { blur, click, fillIn, typeIn, render, focus } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
import { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
||||||
import { NON_STRING_WARNING, WHITESPACE_WARNING } from 'vault/utils/validators';
|
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);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'kv');
|
setupEngine(hooks, 'kv');
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
this.subkeys = {
|
this.subkeys = {
|
||||||
foo: null,
|
foo: null,
|
||||||
baz: null,
|
baz: {
|
||||||
|
nested: null,
|
||||||
|
bar: {
|
||||||
|
hello: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this.onSubmit = sinon.spy();
|
this.onSubmit = sinon.spy();
|
||||||
this.onCancel = sinon.spy();
|
this.onCancel = sinon.spy();
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
|
this.submitError = '';
|
||||||
|
|
||||||
this.renderComponent = async () => {
|
this.renderComponent = async () => {
|
||||||
return render(
|
return render(
|
||||||
hbs`
|
hbs`
|
||||||
<KvPatchEditor::Form
|
<KvPatch::Editor::Form
|
||||||
@subkeys={{this.subkeys}}
|
@subkeys={{this.subkeys}}
|
||||||
@onSubmit={{this.onSubmit}}
|
@onSubmit={{this.onSubmit}}
|
||||||
@onCancel={{this.onCancel}}
|
@onCancel={{this.onCancel}}
|
||||||
@isSaving={{this.isSaving}}
|
@isSaving={{this.isSaving}}
|
||||||
|
@submitError={{this.submitError}}
|
||||||
/>`,
|
/>`,
|
||||||
{ owner: this.engine }
|
{ owner: this.engine }
|
||||||
);
|
);
|
||||||
@ -79,16 +86,13 @@ module('Integration | Component | kv | kv-patch-editor/form', function (hooks) {
|
|||||||
this.assertEmptyRow(assert);
|
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) {
|
test('it reveals subkeys', async function (assert) {
|
||||||
this.subkeys = {
|
|
||||||
foo: null,
|
|
||||||
bar: {
|
|
||||||
baz: null,
|
|
||||||
quux: {
|
|
||||||
hello: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked('toggle is initially unchecked');
|
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) => {
|
NON_STRING_VALUES.forEach((value) => {
|
||||||
test(`for new non-string values: ${value}`, async function (assert) {
|
test(`for new non-string values: ${value}`, async function (assert) {
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
await fillIn(FORM.keyInput('new'), 'aKey');
|
await fillIn(FORM.keyInput('new'), 'aKey');
|
||||||
await fillIn(FORM.valueInput('new'), value);
|
await fillIn(FORM.valueInput('new'), value);
|
||||||
await blur(FORM.valueInput('new')); // unfocus input
|
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 { FORM } from 'vault/tests/helpers/kv/kv-selectors';
|
||||||
import sinon from 'sinon';
|
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);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'kv');
|
setupEngine(hooks, 'kv');
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ module('Integration | Component | kv | kv-patch-editor/row', function (hooks) {
|
|||||||
this.renderComponent = async () => {
|
this.renderComponent = async () => {
|
||||||
return render(
|
return render(
|
||||||
hbs`
|
hbs`
|
||||||
<KvPatchEditor::Row
|
<KvPatch::Editor::Row
|
||||||
@idx={{0}}
|
@idx={{0}}
|
||||||
@kvClass={{this.kvClass}}
|
@kvClass={{this.kvClass}}
|
||||||
@isOriginalSubkey={{this.isOriginalSubkey}}
|
@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';
|
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||||
|
|
||||||
const { overviewCard } = GENERAL;
|
const { overviewCard } = GENERAL;
|
||||||
module('Integration | Component | kv | kv-subkeys', function (hooks) {
|
module('Integration | Component | kv | kv-subkeys-card', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
setupEngine(hooks, 'kv');
|
setupEngine(hooks, 'kv');
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
@ -22,7 +22,7 @@ module('Integration | Component | kv | kv-subkeys', function (hooks) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.renderComponent = async () => {
|
this.renderComponent = async () => {
|
||||||
return render(hbs`<KvSubkeys @subkeys={{this.subkeys}} />`, {
|
return render(hbs`<KvSubkeysCard @subkeys={{this.subkeys}} />`, {
|
||||||
owner: this.engine,
|
owner: this.engine,
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -9,9 +9,9 @@ import { setupEngine } from 'ember-engines/test-support';
|
|||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { kvDataPath, kvMetadataPath } from 'vault/utils/kv-path';
|
import { kvDataPath } from 'vault/utils/kv-path';
|
||||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
|
||||||
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
|
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) {
|
module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@ -19,27 +19,8 @@ module('Integration | Component | kv-v2 | Page::Secret::Metadata::Details', func
|
|||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
this.store = this.owner.lookup('service:store');
|
baseSetup(this);
|
||||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
|
||||||
this.backend = 'kv-engine';
|
|
||||||
this.path = 'my-secret';
|
|
||||||
this.dataId = kvDataPath(this.backend, this.path);
|
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
|
// empty secret model always exists for permissions
|
||||||
this.store.pushPayload('kv/data', {
|
this.store.pushPayload('kv/data', {
|
||||||
modelName: '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) {
|
test('it renders metadata details', async function (assert) {
|
||||||
assert.expect(8);
|
assert.expect(8);
|
||||||
this.metadata = this.metadataModel();
|
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
<Page::Secret::Metadata::Details
|
<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) {
|
test('it renders custom metadata from secret model', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
this.metadata = this.metadataModel();
|
|
||||||
this.secret.customMetadata = { hi: 'there' };
|
this.secret.customMetadata = { hi: 'there' };
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
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) {
|
test('it renders custom metadata from metadata model', async function (assert) {
|
||||||
assert.expect(4);
|
assert.expect(4);
|
||||||
this.metadata = this.metadataModel({ withCustom: true });
|
this.model.metadata = metadataModel(this, { withCustom: true });
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
<Page::Secret::Metadata::Details
|
<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) {
|
test('it renders tabs', async function (assert) {
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
const tabs = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History'];
|
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 () {
|
hooks.beforeEach(async function () {
|
||||||
this.secretState = 'deleted';
|
this.secretState = 'deleted';
|
||||||
// subkeys is null but metadata still has data
|
// 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 () {
|
hooks.beforeEach(async function () {
|
||||||
this.secretState = 'destroyed';
|
this.secretState = 'destroyed';
|
||||||
// subkeys is null but metadata still has data
|
// 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 { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { click, find, render } from '@ember/test-helpers';
|
import { click, find, render } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { kvDataPath, kvMetadataPath } from 'vault/utils/kv-path';
|
import { kvDataPath } from 'vault/utils/kv-path';
|
||||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
|
||||||
import { FORM, PAGE, parseJsonEditor } from 'vault/tests/helpers/kv/kv-selectors';
|
import { FORM, PAGE, parseJsonEditor } from 'vault/tests/helpers/kv/kv-selectors';
|
||||||
import { syncStatusResponse } from 'vault/mirage/handlers/sync';
|
import { syncStatusResponse } from 'vault/mirage/handlers/sync';
|
||||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
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) {
|
module('Integration | Component | kv-v2 | Page::Secret::Details', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@ -21,15 +21,11 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
|||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
this.store = this.owner.lookup('service:store');
|
baseSetup(this);
|
||||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
|
||||||
this.backend = 'kv-engine';
|
|
||||||
this.path = 'my-secret';
|
|
||||||
this.pathComplex = 'my-secret-object';
|
this.pathComplex = 'my-secret-object';
|
||||||
this.version = 2;
|
this.version = 2;
|
||||||
this.dataId = kvDataPath(this.backend, this.path);
|
this.dataId = kvDataPath(this.backend, this.path);
|
||||||
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
|
this.dataIdComplex = kvDataPath(this.backend, this.pathComplex);
|
||||||
this.metadataId = kvMetadataPath(this.backend, this.path);
|
|
||||||
|
|
||||||
this.secretData = { foo: 'bar' };
|
this.secretData = { foo: 'bar' };
|
||||||
this.store.pushPayload('kv/data', {
|
this.store.pushPayload('kv/data', {
|
||||||
@ -60,15 +56,6 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
|
|||||||
destroyed: false,
|
destroyed: false,
|
||||||
version: this.version,
|
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.secret = this.store.peekRecord('kv/data', this.dataId);
|
||||||
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);
|
this.secretComplex = this.store.peekRecord('kv/data', this.dataIdComplex);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user