vault/ui/tests/integration/components/search-select-with-modal-test.js
Angel Garbarino a51c5ed362
Clean up button test selectors (#30694)
* clean up selectors file and then update testButton to buttonByAttr

* a lot but not really in the scheme of things

* fix component test failures

* fix acceptance test failures

* fix namespace selector

* clean up remaining tests

* another test

* last test

* small changes, but I have test failures

* a mess in custom messages, really hard to test because of test pollution.

* make data-test-submit vs data-test-save

* change other-methods to sign in with other methods

* clean up of failing test

* buttonByAttr to button

* clean up test pollution on config messages

* sweep of clean ups

* another round of small cleanups

* fix some message things, remaining oidc issue?

* use a runCmd to better delete things

* fix to amend for recent auth test changes

* remove skip
2025-06-10 16:25:34 -04:00

357 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { Response } from 'miragejs';
import { clickTrigger, typeInSearch } from 'ember-power-select/test-support/helpers';
import { render, fillIn, click, findAll } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import ss from 'vault/tests/pages/components/search-select';
import sinon from 'sinon';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const component = create(ss);
module('Integration | Component | search select with modal', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.set('onChange', sinon.spy());
this.server.get('sys/policies/acl', () => {
return {
request_id: 'acl-policy-list',
data: {
keys: ['default', 'root', 'acl-test'],
},
};
});
this.server.get('sys/policies/rgp', () => {
return {
request_id: 'rgp-policy-list',
data: {
keys: ['rgp-test'],
},
};
});
this.server.get('/sys/policies/acl/acl-test', () => {
return {
request_id: 'policy-acl',
data: {
name: 'acl-test',
policy:
'\n# Grant \'create\', \'read\' , \'update\', and list permission\n# to paths prefixed by \'secret/*\'\npath "secret/*" {\n capabilities = [ "create", "read", "update", "list" ]\n}\n\n# Even though we allowed secret/*, this line explicitly denies\n# secret/super-secret. This takes precedence.\npath "secret/super-secret" {\n capabilities = ["deny"]\n}\n',
},
};
});
this.server.get('/sys/policies/rgp/rgp-test', () => {
return {
request_id: 'policy-rgp',
data: {
name: 'rgp-test',
enforcement_level: 'hard-mandatory',
policy:
'\n# Import strings library that exposes common string operations\nimport "strings"\n\n# Conditional rule (precond) checks the incoming request endpoint\n# targeted to sys/policies/acl/admin\nprecond = rule {\n strings.has_prefix(request.path, "sys/policies/admin")\n}\n\n# Vault checks to see if the request was made by an entity\n# named James Thomas or Team Lead role defined as its metadata\nmain = rule when precond {\n identity.entity.metadata.role is "Team Lead" or\n identity.entity.name is "James Thomas"\n}\n',
},
};
});
setRunOptions({
rules: {
// TODO: Fix this component
'color-contrast': { enabled: false },
label: { enabled: false },
'aria-input-field-name': { enabled: false },
'aria-required-attr': { enabled: false },
'aria-valid-attr-value': { enabled: false },
},
});
});
test('it renders passed in models', async function (assert) {
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
@excludeOptions={{array "root"}}
@subText="Some modal subtext"
/>
`);
assert.dom('[data-test-search-select-with-modal]').exists('the component renders');
assert.dom('[data-test-modal-subtext]').hasText('Some modal subtext', 'renders modal text');
assert.strictEqual(component.labelText, 'Policies', 'label text is correct');
assert.ok(component.hasTrigger, 'it renders the power select trigger');
assert.strictEqual(component.selectedOptions.length, 0, 'there are no selected options');
await clickTrigger();
const dropdownOptions = findAll('[data-option-index]').map((o) => o.innerText);
assert.notOk(dropdownOptions.includes('root'), 'root policy is not listed as option');
assert.strictEqual(component.options.length, 3, 'dropdown renders passed in models as options');
assert.ok(this.onChange.notCalled, 'onChange is not called');
});
test('it renders input value', async function (assert) {
this.policies = ['acl-test'];
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
@subText="Some modal subtext"
/>
`);
assert.strictEqual(component.selectedOptions.length, 1, 'there is one selected option');
assert.strictEqual(component.selectedOptions.objectAt(0).text, 'acl-test', 'renders inputted policies');
await clickTrigger();
assert.strictEqual(component.options.length, 3, 'does not render all options returned from query');
const dropdownOptions = findAll('[data-option-index]').map((o) => o.innerText);
assert.notOk(dropdownOptions.includes('acl-test'), 'selected option is not included in the dropdown');
assert.ok(this.onChange.notCalled, 'onChange is not called');
});
test('it filters options, shows option to create new item and opens modal on select', async function (assert) {
assert.expect(7);
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
/>
`);
await clickTrigger();
assert.strictEqual(component.options.length, 4, 'dropdown renders all options');
await typeInSearch('a');
assert.strictEqual(component.options.length, 3, 'dropdown renders all matching options plus add option');
await typeInSearch('acl-test');
assert.strictEqual(component.options[0].text, 'acl-test', 'dropdown renders only matching option');
await typeInSearch('acl-test-new');
assert.strictEqual(
component.options[0].text,
'No results found for "acl-test-new". Click here to create it.',
'dropdown gives option to create new option'
);
await component.selectOption();
assert.dom('#search-select-modal').exists('modal is active');
assert.dom('[data-test-empty-state-title]').hasText('No policy type selected');
assert.ok(this.onChange.notCalled, 'onChange is not called');
});
test('it renders policy template and selects policy type', async function (assert) {
assert.expect(9);
this.server.put('/sys/policies/acl/acl-test-new', async (schema, req) => {
const requestBody = JSON.parse(req.requestBody);
assert.propEqual(
requestBody,
{
name: 'acl-test-new',
policy: 'path "secret/super-secret" { capabilities = ["deny"] }',
},
'onSave sends request to endpoint with correct policy attributes'
);
});
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
/>
`);
await clickTrigger();
await typeInSearch('acl-test-new');
assert.strictEqual(
component.options[0].text,
'No results found for "acl-test-new". Click here to create it.',
'dropdown gives option to create new option'
);
await component.selectOption();
assert.dom('[data-test-empty-state-title]').hasText('No policy type selected');
await fillIn('[data-test-select="policyType"]', 'acl');
assert.dom('[data-test-policy-form]').exists('policy form renders after type is selected');
await click('[data-test-tab-example-policy] button');
assert.dom('[data-test-tab-example-policy] button').hasAttribute('aria-selected', 'true');
await click('[data-test-tab-your-policy] button');
assert.dom('[data-test-tab-your-policy] button').hasAttribute('aria-selected', 'true');
await fillIn(
'[data-test-component="code-mirror-modifier"] textarea',
'path "secret/super-secret" { capabilities = ["deny"] }'
);
await click(GENERAL.submitButton);
assert.dom('[data-test-modal-div]').doesNotExist('modal closes after save');
assert
.dom('[data-test-selected-option="0"]')
.hasText('acl-test-new', 'adds newly created policy to selected options');
assert.ok(
this.onChange.calledWithExactly(['acl-test-new']),
'onChange is called only after item is created'
);
});
test('it still renders search select if only second model returns 403', async function (assert) {
assert.expect(4);
this.server.get('sys/policies/rgp', () => {
return new Response(
403,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: ['permission denied'] })
);
});
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
/>
`);
assert.dom('[data-test-search-select-with-modal]').exists('the component renders');
assert.dom('[data-test-component="string-list"]').doesNotExist('does not render fallback component');
await clickTrigger();
assert.strictEqual(component.options.length, 3, 'only options from successful query render');
assert.ok(this.onChange.notCalled, 'onChange is not called');
});
module('fallback component', function (hooks) {
hooks.beforeEach(function () {
this.server.get('sys/policies/acl', () => {
return new Response(
403,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: ['permission denied'] })
);
});
this.server.get('sys/policies/rgp', () => {
return new Response(
403,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: ['permission denied'] })
);
});
this.server.get('identity/oidc/key?list=true', () => {
return new Response(
403,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: ['permission denied'] })
);
});
});
test('it renders fallback component if both models return 403', async function (assert) {
assert.expect(7);
await render(hbs`
<SearchSelectWithModal
@id="policies"
@label="Policies"
@labelClass="title is-4"
@models={{array "policy/acl" "policy/rgp"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="string-list"
@modalFormTemplate="modal-form/policy-template"
/>
`);
assert.dom('[data-test-component="string-list"]').exists('renders fallback component');
assert.false(component.hasTrigger, 'does not render power select trigger');
await fillIn('[data-test-string-list-input="0"]', 'string-list-policy');
await click('[data-test-string-list-button="add"]');
assert
.dom('[data-test-string-list-input="0"]')
.hasValue('string-list-policy', 'first row renders inputted string');
assert
.dom('[data-test-string-list-row="0"] [data-test-string-list-button="delete"]')
.exists('first row renders delete icon');
assert.dom('[data-test-string-list-row="1"]').exists('renders second input row');
assert
.dom('[data-test-string-list-row="1"] [data-test-string-list-button="add"]')
.exists('second row renders add icon');
assert.ok(
this.onChange.calledWithExactly(['string-list-policy']),
'onChange is called only after item is created'
);
});
test('it renders fallback placeholder text for fallback component', async function (assert) {
assert.expect(1);
await render(hbs`
<SearchSelectWithModal
@id="key"
@label="Keys"
@models={{array "oidc/key"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="input-search"
@modalFormTemplate="modal-form/oidc-key-template"
@selectLimit="1"
@placeholder="Search or type to create a key item"
@fallbackComponentPlaceholder="Input key name"
/>
`);
assert
.dom(GENERAL.inputSearch('key'))
.hasAttribute('placeholder', 'Input key name', 'Fallback placeholder was passed to input search');
});
test('it renders placeholder text for fallback component', async function (assert) {
assert.expect(1);
await render(hbs`
<SearchSelectWithModal
@id="key"
@label="Keys"
@models={{array "oidc/key"}}
@inputValue={{this.policies}}
@onChange={{this.onChange}}
@fallbackComponent="input-search"
@modalFormTemplate="modal-form/oidc-key-template"
@selectLimit="1"
@placeholder="Search or type to create a key item"
/>
`);
assert
.dom(GENERAL.inputSearch('key'))
.hasAttribute(
'placeholder',
'Search or type to create a key item',
'Placeholder was passed to input search'
);
});
});
});