/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
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';
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',
},
};
});
});
test('it renders passed in models', async function (assert) {
await render(hbs`
`);
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`
`);
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`
`);
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('[data-test-modal-div]').hasAttribute('class', 'modal is-info is-active', '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`
`);
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]');
assert.dom('[data-test-tab-example-policy]').hasClass('is-active');
await click('[data-test-tab-your-policy]');
assert.dom('[data-test-tab-your-policy]').hasClass('is-active');
await fillIn(
'[data-test-component="code-mirror-modifier"] textarea',
'path "secret/super-secret" { capabilities = ["deny"] }'
);
await click('[data-test-policy-save]');
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`
`);
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');
});
test('it renders fallback component if both models return 403', async function (assert) {
assert.expect(7);
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'] })
);
});
await render(hbs`
`);
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'
);
});
});