vault/ui/tests/integration/components/code-generator/policy/stanza-test.js
Vault Automation db7a0ee071
UI: Add validations to visual policy editor (#14688) (#14696)
* add validations to policy flyout

* add validations to policy form

* remove passing formatted policy back from policy/builder

* add changelog

* change label to "path"

Co-authored-by: claire b <68122737+hellobontempo@users.noreply.github.com>
2026-05-08 23:03:46 +00:00

229 lines
9.5 KiB
JavaScript

/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render, click, fillIn, typeIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import Sinon from 'sinon';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { ACL_CAPABILITIES, PolicyStanza } from 'core/utils/code-generators/policy';
module('Integration | Component | code-generator/policy/stanza', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.stanza = new PolicyStanza();
this.onDelete = Sinon.spy();
this.onChange = Sinon.spy();
this.renderComponent = () => {
return render(hbs`
<CodeGenerator::Policy::Stanza
@index="0"
@onChange={{this.onChange}}
@onDelete={{this.onDelete}}
@stanza={{this.stanza}}
@renderValidations={{this.renderValidations}}
/>`);
};
});
test('it renders', async function (assert) {
await this.renderComponent();
assert
.dom(GENERAL.inputByAttr('path'))
.hasValue('')
.hasAttribute('placeholder', 'Enter a resource path')
.hasAttribute('aria-label', 'Resource path')
.hasAttribute('autocomplete', 'off');
assert.dom(GENERAL.inputByAttr('path')).hasValue('');
assert.dom(GENERAL.button('Delete')).exists({ count: 1 });
// Assert checkboxes
assert.dom('fieldset input[type="checkbox"]').exists({ count: 7 }, 'it renders 7 checkboxes');
ACL_CAPABILITIES.forEach((capability) => {
assert.dom(GENERAL.fieldLabel(capability)).hasText(capability);
assert.dom(GENERAL.checkboxByAttr(capability)).isNotChecked();
});
// Assert preview toggle
assert.dom(GENERAL.toggleInput('preview')).exists().isNotChecked();
assert.dom(GENERAL.fieldLabel('preview')).hasText('Show preview');
// Check empty preview state
await click(GENERAL.toggleInput('preview'));
assert.dom(GENERAL.toggleInput('preview')).isChecked();
assert.dom(GENERAL.fieldLabel('preview')).hasText('Hide preview');
const expectedPreview = `path "" {
capabilities = []
}`;
assert.dom(GENERAL.fieldByAttr('preview')).hasText(expectedPreview);
});
test('it renders policy preview', async function (assert) {
await this.renderComponent();
await fillIn(GENERAL.inputByAttr('path'), 'some/api/path');
await click(GENERAL.checkboxByAttr('update'));
await click(GENERAL.checkboxByAttr('patch'));
await click(GENERAL.toggleInput('preview'));
let expectedPreview = `path "some/api/path" {
capabilities = ["update", "patch"]
}`;
assert.dom(GENERAL.fieldByAttr('preview')).hasText(expectedPreview, 'it renders initial preview');
// Toggle back to add more capabilities then check preview again
await click(GENERAL.toggleInput('preview'));
await typeIn(GENERAL.inputByAttr('path'), '/*');
await click(GENERAL.checkboxByAttr('patch')); // uncheck
await click(GENERAL.checkboxByAttr('list')); // check new
// Confirm policy preview updated
await click(GENERAL.toggleInput('preview'));
expectedPreview = `path "some/api/path/*" {
capabilities = ["update", "list"]
}`;
assert.dom(GENERAL.fieldByAttr('preview')).hasText(expectedPreview, 'it updates preview');
});
test('it maintains checkbox state when toggling to show and hide preview', async function (assert) {
await this.renderComponent();
await fillIn(GENERAL.inputByAttr('path'), 'some/api/path');
await click(GENERAL.checkboxByAttr('update'));
assert.dom(GENERAL.checkboxByAttr('update')).isChecked();
// Toggle to show preview
await click(GENERAL.toggleInput('preview'));
assert.dom(GENERAL.toggleInput('preview')).isChecked();
// Toggle back to checkboxes
await click(GENERAL.toggleInput('preview'));
assert.dom(GENERAL.toggleInput('preview')).isNotChecked();
assert.dom(GENERAL.checkboxByAttr('update')).isChecked('update is still checked after viewing preview');
});
test('it selects and unselects capabilities', async function (assert) {
await this.renderComponent();
await click(GENERAL.checkboxByAttr('update')); // first onChange call
assert.dom(GENERAL.checkboxByAttr('update')).isChecked();
let expectedSet = new Set(['update']);
assert.deepEqual(
this.stanza.capabilities,
expectedSet,
`has expected capabilities: ${[...expectedSet].join(', ')}`
);
// Check "delete"
await click(GENERAL.checkboxByAttr('delete')); // second onChange call
assert.dom(GENERAL.checkboxByAttr('delete')).isChecked();
expectedSet = new Set(['update', 'delete']);
assert.deepEqual(
this.stanza.capabilities,
expectedSet,
`has expected capabilities: ${[...expectedSet].join(', ')}`
);
// Uncheck "delete"
await click(GENERAL.checkboxByAttr('delete')); // third onChange call
assert.dom(GENERAL.checkboxByAttr('delete')).isNotChecked();
expectedSet = new Set(['update']);
assert.deepEqual(
this.stanza.capabilities,
expectedSet,
`has expected capabilities: ${[...expectedSet].join(', ')}`
);
assert.strictEqual(this.onChange.callCount, 3, 'onChange is called every time a capability is selected');
});
test('it selects all capabilities and updates @stanza', async function (assert) {
await this.renderComponent();
// check in random order to assert generator orders them
for (const capability of ['list', 'read', 'sudo', 'create', 'delete', 'patch', 'update']) {
await click(GENERAL.checkboxByAttr(capability));
}
await click(GENERAL.toggleInput('preview'));
const expectedPreview = `path "" {
capabilities = ["create", "read", "update", "delete", "list", "patch", "sudo"]
}`;
assert.dom(GENERAL.fieldByAttr('preview')).hasText(expectedPreview);
assert.deepEqual(
this.stanza.capabilities,
new Set(['create', 'read', 'update', 'delete', 'list', 'patch', 'sudo']),
'stanza includes every capability, in order'
);
});
test('it updates @stanza when path changes', async function (assert) {
await this.renderComponent();
await typeIn(GENERAL.inputByAttr('path'), 'my/super/secret/*');
assert.strictEqual(this.stanza.path, 'my/super/secret/*', '"path" is updated');
});
test('it calls onChange when path changes', async function (assert) {
await this.renderComponent();
await fillIn(GENERAL.inputByAttr('path'), 'my/super/secret/*');
assert.true(this.onChange.calledOnce, 'onChange is called');
});
test('it calls onChange when a checkbox is selected', async function (assert) {
await this.renderComponent();
await click(GENERAL.checkboxByAttr('update'));
assert.true(this.onChange.calledOnce, 'onChange is called');
});
test('it calls onDelete', async function (assert) {
await this.renderComponent();
await click(GENERAL.button('Delete'));
assert.true(this.onDelete.calledOnce, 'onDelete is called');
});
test('it does not render validations when @renderValidations is undefined', async function (assert) {
await this.renderComponent();
// Make sure path and capabilities are empty (which would normally render validations)
assert.dom(GENERAL.inputByAttr('path')).hasNoValue();
ACL_CAPABILITIES.forEach((capability) => {
assert.dom(GENERAL.fieldLabel(capability)).hasText(capability);
assert.dom(GENERAL.checkboxByAttr(capability)).isNotChecked();
});
assert.dom('[data-test-validation-error]').doesNotExist();
});
module('validations', function (hooks) {
hooks.beforeEach(function () {
this.renderValidations = true;
});
test('it renders validations when path is empty', async function (assert) {
await this.renderComponent();
// Click capability to just assert path
await click(GENERAL.checkboxByAttr('update'));
assert.dom(GENERAL.validationErrorByAttr('path-0')).exists().hasText('Path cannot be empty.');
assert.dom('[data-test-validation-error]').exists({ count: 1 });
});
test('it renders validations when no capabilities are selected', async function (assert) {
await this.renderComponent();
// Fill in path to only assert capabilities
await fillIn(GENERAL.inputByAttr('path'), 'my/super/secret/*');
assert
.dom(GENERAL.validationErrorByAttr('capabilities-0'))
.exists()
.hasText('Rule must have at least one capability.');
assert.dom('[data-test-validation-error]').exists({ count: 1 });
});
test('it renders validations for neither path or capabilities', async function (assert) {
await this.renderComponent();
assert.dom(GENERAL.validationErrorByAttr('path-0')).exists();
assert.dom(GENERAL.validationErrorByAttr('capabilities-0')).exists();
assert.dom('[data-test-validation-error]').exists({ count: 2 });
});
test('it removes validation when path is valid', async function (assert) {
await this.renderComponent();
assert.dom(GENERAL.validationErrorByAttr('path-0')).exists();
await fillIn(GENERAL.inputByAttr('path'), 'secret/data/*');
assert.dom(GENERAL.validationErrorByAttr('path-0')).doesNotExist();
});
test('it removes validation when at least one capability is selected', async function (assert) {
await this.renderComponent();
assert.dom(GENERAL.validationErrorByAttr('capabilities-0')).exists();
await click(GENERAL.checkboxByAttr('read'));
assert.dom(GENERAL.validationErrorByAttr('capabilities-0')).doesNotExist();
});
});
});