/** * 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, setupOnerror } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { ACL_CAPABILITIES, formatStanzas, PolicyStanza } from 'core/utils/code-generators/policy'; module('Integration | Component | code-generator/policy/builder', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { this.onPolicyChange = ({ stanzas }) => { // Mimics consumers of this component which use formatStanzas as needed this.set('policyCallback', formatStanzas(stanzas)); this.set('stanzas', stanzas); }; this.policyCallback = ''; this.policyName = undefined; this.renderValidations = false; this.stanzas = [new PolicyStanza()]; this.renderComponent = () => { return render(hbs` `); }; this.assertPolicyUpdate = (assert, expected, message) => { // this.policyCallback is set by onPolicyChange assert.strictEqual(this.policyCallback, expected, `onPolicyChange is called ${message}`); }; this.assertEmptyTemplate = async (assert, { index } = {}) => { const container = index ? GENERAL.cardContainer(index) : ''; assert.dom(`${container} ${GENERAL.inputByAttr('path')}`).hasValue(''); assert.dom(`${container} ${GENERAL.toggleInput('preview')}`).isNotChecked(); ACL_CAPABILITIES.forEach((capability) => { assert.dom(`${container} ${GENERAL.checkboxByAttr(capability)}`).isNotChecked(); }); // check empty preview state await click(`${container} ${GENERAL.toggleInput('preview')}`); const expectedPreview = `path "" { capabilities = [] }`; assert .dom(GENERAL.fieldByAttr('preview')) .exists('it renders preview') .hasText(expectedPreview, 'preview is empty'); }; }); test('it renders', async function (assert) { await this.renderComponent(); await this.assertEmptyTemplate(assert); assert.dom(GENERAL.button('Add rule')).exists({ count: 1 }); }); test('it throws an error when stanzas are not provided', async function (assert) { this.stanzas = undefined; // catches error so qunit test doesn't fail setupOnerror(({ message }) => { assert.strictEqual( message, 'Assertion Failed: @stanzas are required and must be an array of PolicyStanza instances' ); }); await this.renderComponent(); }); test('it adds a rule', async function (assert) { await this.renderComponent(); await click(GENERAL.button('Add rule')); assert .dom(GENERAL.cardContainer()) .exists({ count: 2 }, 'two templates render after clicking "Add rule"'); await this.assertEmptyTemplate(assert, { index: '1' }); }); test('it deletes a rule', async function (assert) { await this.renderComponent(); assert.dom(GENERAL.cardContainer()).exists({ count: 1 }); // Fill in template await fillIn(GENERAL.inputByAttr('path'), 'some/api/path'); await click(GENERAL.checkboxByAttr('patch')); // Delete the only rendered template await click(GENERAL.button('Delete')); // One template renders but content should reset assert .dom(GENERAL.cardContainer()) .exists({ count: 1 }, 'it still renders one rule after deleting the only rule'); await this.assertEmptyTemplate(assert); }); test('it maintains state across multiple rules', async function (assert) { await this.renderComponent(); // Set up first rule await fillIn(GENERAL.inputByAttr('path'), 'first/path'); await click(GENERAL.checkboxByAttr('read')); // Add second rule await click(GENERAL.button('Add rule')); await fillIn(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`, 'second/path'); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('update')}`); assert.dom(`${GENERAL.cardContainer('0')} ${GENERAL.inputByAttr('path')}`).hasValue('first/path'); assert.dom(`${GENERAL.cardContainer('0')} ${GENERAL.checkboxByAttr('read')}`).isChecked(); assert.dom(`${GENERAL.cardContainer('0')} ${GENERAL.checkboxByAttr('update')}`).isNotChecked(); assert.dom(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`).hasValue('second/path'); assert.dom(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('update')}`).isChecked(); assert.dom(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('read')}`).isNotChecked(); }); test('it deletes the correct rule when multiple exist', async function (assert) { await this.renderComponent(); await fillIn(GENERAL.inputByAttr('path'), 'first/path'); await click(GENERAL.checkboxByAttr('read')); // Second rule await click(GENERAL.button('Add rule')); await fillIn(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`, 'second/path'); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('update')}`); // Third rule await click(GENERAL.button('Add rule')); await fillIn(`${GENERAL.cardContainer('2')} ${GENERAL.inputByAttr('path')}`, 'third/path'); await click(`${GENERAL.cardContainer('2')} ${GENERAL.checkboxByAttr('list')}`); assert.dom(GENERAL.cardContainer()).exists({ count: 3 }); // Delete middle rule await click(`${GENERAL.cardContainer('1')} ${GENERAL.button('Delete')}`); assert.dom(GENERAL.cardContainer()).exists({ count: 2 }); assert.dom(`${GENERAL.cardContainer('0')} ${GENERAL.inputByAttr('path')}`).hasValue('first/path'); assert.dom(`${GENERAL.cardContainer('0')} ${GENERAL.checkboxByAttr('read')}`).isChecked(); assert.dom(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`).hasValue('third/path'); assert.dom(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('list')}`).isChecked(); }); test('it passes policy updates as changes are made', async function (assert) { await this.renderComponent(); // Inputting path triggers callback await fillIn(GENERAL.inputByAttr('path'), 'my/super/secret/*'); let expectedPolicy = `path "my/super/secret/*" { capabilities = [] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when path changes'); // Clicking checkbox triggers callback await click(GENERAL.checkboxByAttr('update')); expectedPolicy = `path "my/super/secret/*" { capabilities = ["update"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when a capability is selected'); // Adding a rule triggers callback await click(GENERAL.button('Add rule')); expectedPolicy = `path "my/super/secret/*" { capabilities = ["update"] } path "" { capabilities = [] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when a rule is added'); // Updating added rule triggers callback await fillIn(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`, 'prod/'); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('list')}`); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('read')}`); expectedPolicy = `path "my/super/secret/*" { capabilities = ["update"] } path "prod/" { capabilities = ["read", "list"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when an additional rule updates'); // Unchecking box triggers callback await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('read')}`); expectedPolicy = `path "my/super/secret/*" { capabilities = ["update"] } path "prod/" { capabilities = ["list"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when checkbox is unselected'); // Deleting a rule triggers callback await click(GENERAL.button('Delete')); expectedPolicy = `path "prod/" { capabilities = ["list"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when a rule is deleted'); }); // These tests ensure paths are never used as input identifiers. // The policy generator may not render in a form and needs to be flexible so it intentionally supports // multiple templates with the same or no path. test('it supports multiple rules with the same path', async function (assert) { await this.renderComponent(); await fillIn(GENERAL.inputByAttr('path'), 'test/path'); await click(GENERAL.checkboxByAttr('patch')); await click(GENERAL.button('Add rule')); await fillIn(`${GENERAL.cardContainer('1')} ${GENERAL.inputByAttr('path')}`, 'test/path'); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('update')}`); const expectedPolicy = `path "test/path" { capabilities = ["patch"] } path "test/path" { capabilities = ["update"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when rules have the same path'); }); test('it supports multiple rules with an empty path', async function (assert) { await this.renderComponent(); await click(GENERAL.checkboxByAttr('list')); await click(GENERAL.button('Add rule')); await click(`${GENERAL.cardContainer('1')} ${GENERAL.checkboxByAttr('delete')}`); const expectedPolicy = `path "" { capabilities = ["list"] } path "" { capabilities = ["delete"] }`; this.assertPolicyUpdate(assert, expectedPolicy, 'when rules do have an empty path'); }); test('it passes @renderValidations through to CodeGenerator::Policy::Stanza', async function (assert) { this.renderValidations = true; await this.renderComponent(); assert.dom(GENERAL.validationErrorByAttr('path-0')).hasText('Path cannot be empty.'); assert .dom(GENERAL.validationErrorByAttr('capabilities-0')) .hasText('Rule must have at least one capability.'); }); });