Prep for SSH and AWS configuration changes (#27869)

* move non user facing changes to another pr

* remove non-relevant test coverage

* address pr fixes

* Update mountable-secret-engines.js

* Update secrets-engine-mount-config.ts

* clean up

* put back console because of tests and use debug instead

* missed one

* blah fix
This commit is contained in:
Angel Garbarino 2024-07-26 10:47:33 -06:00 committed by GitHub
parent 5d172d5861
commit 5787fa20f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 266 additions and 108 deletions

View File

@ -61,7 +61,7 @@
</div>
</div>
<ToggleButton @isOpen={{this.showOptions}} @onClick={{fn (mut this.showOptions)}} />
<ToggleButton @isOpen={{this.showOptions}} @onClick={{fn (mut this.showOptions)}} data-test-popup-menu-trigger />
{{#if this.showOptions}}
<div class="box is-marginless">
<div class="field">
@ -74,7 +74,7 @@
name="region"
id="region"
onchange={{action (mut @region) value="target.value"}}
data-test-input="region"
data-test-select="region"
>
<option value=""></option>
{{#each (aws-regions) as |val|}}
@ -104,7 +104,7 @@
{{/if}}
<div class="box is-bottomless is-fullwidth">
<Hds::Button @text="Save" data-test-save type="submit" />
<Hds::Button @text="Save" data-test-save="root" type="submit" />
</div>
</form>
</T.Panel>
@ -134,7 +134,7 @@
@onChange={{fn this.handleTtlChange "leaseMax"}}
/>
<div class="box is-bottomless is-fullwidth">
<Hds::Button @text="Save" data-test-save type="submit" />
<Hds::Button @text="Save" data-test-save="lease" type="submit" />
</div>
</form>
</T.Panel>

View File

@ -5,10 +5,10 @@
import { computed } from '@ember/object';
import Controller from '@ember/controller';
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
export default Controller.extend({
isConfigurable: computed('model.type', function () {
const configurableEngines = ['aws', 'ssh'];
return configurableEngines.includes(this.model.type);
return CONFIGURABLE_SECRET_ENGINES.includes(this.model.type);
}),
});

View File

@ -134,6 +134,15 @@ const MOUNTABLE_SECRET_ENGINES = [
},
];
// Secret Engines that have their own configuration page and actions
// These engines do not exist in their own Ember engine.
// Ex: AWS vs. LDAP which is configurable but is handled inside the routing of its own Ember engine.
export const CONFIGURABLE_SECRET_ENGINES = ['aws', 'ssh'];
export function configurableSecretEngines() {
return MOUNTABLE_SECRET_ENGINES.slice();
}
export function mountableEngines() {
return MOUNTABLE_SECRET_ENGINES.slice();
}

View File

@ -6,8 +6,9 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
export default Route.extend({
store: service(),
export default class SecretsBackendConfigurationRoute extends Route {
@service store;
async model() {
const backend = this.modelFor('vault.cluster.secrets.backend');
if (backend.isV2KV) {
@ -17,11 +18,8 @@ export default Route.extend({
// only set these config params if they can read the config endpoint.
if (canRead) {
// design wants specific default to show that can't be set in the model
backend.set('casRequired', backend.casRequired ? backend.casRequired : 'False');
backend.set(
'deleteVersionAfter',
backend.deleteVersionAfter !== '0s' ? backend.deleteVersionAfter : 'Never delete'
);
backend.casRequired = backend.casRequired ? backend.casRequired : 'False';
backend.deleteVersionAfter = backend.deleteVersionAfter ? backend.deleteVersionAfter : 'Never delete';
} else {
// remove the default values from the model if they don't have read access otherwise it will display the defaults even if they've been set (because they error on returning config data)
backend.set('casRequired', null);
@ -30,5 +28,5 @@ export default Route.extend({
}
}
return backend;
},
});
}
}

View File

@ -7,7 +7,7 @@ import AdapterError from '@ember-data/adapter/error';
import { set } from '@ember/object';
import Route from '@ember/routing/route';
import { service } from '@ember/service';
const CONFIGURABLE_BACKEND_TYPES = ['aws', 'ssh'];
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
export default Route.extend({
store: service(),
@ -16,7 +16,7 @@ export default Route.extend({
const { backend } = this.paramsFor(this.routeName);
return this.store.query('secret-engine', { path: backend }).then((modelList) => {
const model = modelList && modelList[0];
if (!model || !CONFIGURABLE_BACKEND_TYPES.includes(model.type)) {
if (!model || !CONFIGURABLE_SECRET_ENGINES.includes(model.type)) {
const error = new AdapterError();
set(error, 'httpStatus', 404);
throw error;

View File

@ -13,7 +13,7 @@
/>
</p.top>
<p.levelLeft>
<h1 class="title is-3">
<h1 class="title is-3" data-test-backend-configure-title={{this.model.type}}>
Configure
{{get (options-for-backend this.model.type) "displayName"}}
</h1>

View File

@ -5,9 +5,20 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { duration } from 'core/helpers/format-duration';
import type SecretEngineModel from 'vault/models/secret-engine';
/**
* @module SecretsEngineMountConfig
* SecretsEngineMountConfig component is used to display a "Show mount configuration" toggle section. It is generally used alongside the fetch-secret-engine-config decorator which displays the engine configuration above this component. Mount configuration is always available for display but is hidden by default behind a toggle.
*
* @example
* <SecretsEngineMountConfig @model={{model}} />
*
* @param {Model} model- The secret engines model, generated via the secret-engine model and a belongsTo relationship connecting to the mount-config model.
*/
interface Args {
model: SecretEngineModel;
}
@ -16,7 +27,7 @@ interface Field {
value: string | boolean;
}
export default class SecretsEngineMountConfigComponent extends Component<Args> {
export default class SecretsEngineMountConfig extends Component<Args> {
@tracked showConfig = false;
get fields(): Array<Field> {
@ -27,8 +38,8 @@ export default class SecretsEngineMountConfigComponent extends Component<Args> {
{ label: 'Accessor', value: model.accessor },
{ label: 'Local', value: model.local },
{ label: 'Seal Wrap', value: model.sealWrap },
{ label: 'Default Lease TTL', value: model.config.defaultLeaseTtl },
{ label: 'Max Lease TTL', value: model.config.maxLeaseTtl },
{ label: 'Default Lease TTL', value: duration([model.config.defaultLeaseTtl]) },
{ label: 'Max Lease TTL', value: duration([model.config.maxLeaseTtl]) },
];
}
}

View File

@ -27,7 +27,7 @@ export function withConfig(modelName: string) {
return function <RouteClass extends new (...args: any[]) => BaseRoute>(SuperClass: RouteClass) {
if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) {
// eslint-disable-next-line
console.error(
console.debug(
'withConfig decorator must be used on an instance of Ember Route class. Decorator not applied to returned class'
);
return SuperClass;
@ -40,7 +40,10 @@ export function withConfig(modelName: string) {
async beforeModel(transition: Transition) {
super.beforeModel(transition);
if (!this.secretMountPath) {
// eslint-disable-next-line
console.debug('secretMountPath service is required for withConfig decorator. Add it to the route');
}
const backend = this.secretMountPath.currentPath;
// check the store for record first
this.configModel = this.store.peekRecord(modelName, backend);

View File

@ -12,6 +12,7 @@
<div class="select {{if this.isFullwidth 'is-fullwidth'}}">
<select
class="select"
{{! TODO: when 2 TtlPickers are on the same view this becomes two of the same ids. Shows up as dom warning. JIRA 29298}}
id="select-{{this.name}}"
onchange={{action this.onChange value="target.value"}}
data-test-select={{this.name}}

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, fillIn, currentURL } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import { spy } from 'sinon';
import authPage from 'vault/tests/pages/auth';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { runCmd } from 'vault/tests/helpers/commands';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
module('Acceptance | aws | configuration', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
const flash = this.owner.lookup('service:flash-messages');
this.store = this.owner.lookup('service:store');
this.flashSuccessSpy = spy(flash, 'success');
this.flashDangerSpy = spy(flash, 'danger');
this.uid = uuidv4();
return authPage.login();
});
test('it should transition to configure page on Configure click from toolbar', async function (assert) {
const path = `aws-${this.uid}`;
await enablePage.enable('aws', path);
await click(SES.configTab);
await click(SES.configure);
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`);
assert.dom(SES.configureTitle('aws')).hasText('Configure AWS');
assert.dom(SES.aws.rootForm).exists('it lands on the root configuration form.');
assert.dom(GENERAL.tab('access-to-aws')).exists('renders the root creds tab');
assert.dom(GENERAL.tab('lease')).exists('renders the leases config tab');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should save root AWS configuration', async function (assert) {
assert.expect(3);
const path = `aws-${this.uid}`;
await enablePage.enable('aws', path);
await click(SES.configTab);
await click(SES.configure);
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
await fillIn(GENERAL.inputByAttr('secretKey'), 'bar');
this.server.post(`${path}/config/root`, (schema, req) => {
const payload = JSON.parse(req.requestBody);
assert.deepEqual(payload.access_key, 'foo', 'access_key is foo');
assert.deepEqual(payload.secret_key, 'bar', 'secret_key is foo');
return { data: { id: path, type: 'aws', attributes: payload } };
});
await click(GENERAL.saveButtonId('root'));
assert.true(
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
'Success flash message is rendered'
);
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should save lease AWS configuration', async function (assert) {
assert.expect(3);
const path = `aws-${this.uid}`;
this.server.post(`${path}/config/lease`, (schema, req) => {
const payload = JSON.parse(req.requestBody);
assert.deepEqual(payload.lease, '55s', 'lease is set to 55s');
assert.deepEqual(payload.lease_max, '65s', 'maximum_lease is set to 65s');
return { data: { id: path, type: 'aws', attributes: payload } };
});
await enablePage.enable('aws', path);
await click(SES.configTab);
await click(SES.configure);
await click(GENERAL.hdsTab('lease'));
await click(GENERAL.toggleInput('Lease'));
await fillIn(GENERAL.ttl.input('Lease'), '55');
await click(GENERAL.toggleInput('Maximum Lease'));
await fillIn(GENERAL.ttl.input('Maximum Lease'), '65');
await click(GENERAL.saveButtonId('lease'));
assert.true(
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
'Success flash message is rendered'
);
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
});

View File

@ -3,34 +3,20 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, fillIn, currentURL, find, settled, waitUntil, visit } from '@ember/test-helpers';
import { click, fillIn, currentURL, find, waitUntil, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import { spy } from 'sinon';
import { GENERAL } from '../helpers/general-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import authPage from 'vault/tests/pages/auth';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { deleteEngineCmd, mountEngineCmd, runCmd } from 'vault/tests/helpers/commands';
import { overrideResponse } from 'vault/tests/helpers/stubs';
const AWS_CREDS = {
configTab: '[data-test-configuration-tab]',
configure: '[data-test-secret-backend-configure]',
awsForm: '[data-test-aws-root-creds-form]',
viewBackend: '[data-test-backend-view-link]',
createSecret: '[data-test-secret-create]',
secretHeader: '[data-test-secret-header]',
secretLink: (name) => (name ? `[data-test-secret-link="${name}"]` : '[data-test-secret-link]'),
crumb: (path) => `[data-test-secret-breadcrumb="${path}"] a`,
ttlToggle: '[data-test-ttl-toggle="TTL"]',
warning: '[data-test-warning]',
delete: (role) => `[data-test-aws-role-delete="${role}"]`,
backButton: '[data-test-back-button]',
generateLink: '[data-test-backend-credentials]',
};
const ROLE_TYPES = [
{
credentialType: 'iam_user',
@ -83,52 +69,20 @@ module('Acceptance | aws secret backend', function (hooks) {
return authPage.login();
});
test('aws backend', async function (assert) {
test('it creates role and deletes role', async function (assert) {
const path = `aws-${this.uid}`;
const roleName = 'awsrole';
await enablePage.enable('aws', path);
await settled();
await click(AWS_CREDS.configTab);
await click(AWS_CREDS.configure);
assert.strictEqual(currentURL(), `/vault/settings/secrets/configure/${path}`);
assert.dom(AWS_CREDS.awsForm).exists();
assert.dom(GENERAL.tab('access-to-aws')).exists('renders the root creds tab');
assert.dom(GENERAL.tab('lease')).exists('renders the leases config tab');
await fillIn(GENERAL.inputByAttr('accessKey'), 'foo');
await fillIn(GENERAL.inputByAttr('secretKey'), 'bar');
await click(GENERAL.saveButton);
assert.true(
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
'success flash message is rendered'
assert.strictEqual(
currentURL(),
`/vault/secrets/${path}/list`,
'After enabling aws secrets engine it navigates to roles list'
);
await click(GENERAL.tab('lease'));
await click(GENERAL.saveButton);
assert.true(
this.flashSuccessSpy.calledTwice,
'a new success flash message is rendered upon saving lease'
);
await click(AWS_CREDS.viewBackend);
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`, 'navigates to the roles list');
await click(AWS_CREDS.createSecret);
assert.dom(AWS_CREDS.secretHeader).hasText('Create an AWS Role', 'aws: renders the create page');
await click(SES.createSecret);
assert.dom(SES.secretHeader).hasText('Create an AWS Role', 'It renders the create role page');
await fillIn(GENERAL.inputByAttr('name'), roleName);
// save the role
await click(GENERAL.saveButton);
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this
assert.strictEqual(
@ -136,17 +90,17 @@ module('Acceptance | aws secret backend', function (hooks) {
`/vault/secrets/${path}/show/${roleName}`,
'aws: navigates to the show page on creation'
);
await click(AWS_CREDS.crumb(path));
await click(SES.crumb(path));
assert.strictEqual(currentURL(), `/vault/secrets/${path}/list`);
assert.dom(AWS_CREDS.secretLink(roleName)).exists();
assert.dom(SES.secretLink(roleName)).exists();
//and delete
await click(`${AWS_CREDS.secretLink(roleName)} [data-test-popup-menu-trigger]`);
await waitUntil(() => find(AWS_CREDS.delete(roleName))); // flaky without
await click(AWS_CREDS.delete(roleName));
// delete role
await click(`${SES.secretLink(roleName)} [data-test-popup-menu-trigger]`);
await waitUntil(() => find(SES.aws.delete(roleName))); // flaky without
await click(SES.aws.delete(roleName));
await click(GENERAL.confirmButton);
assert.dom(AWS_CREDS.secretLink(roleName)).doesNotExist('aws: role is no longer in the list');
assert.dom(SES.secretLink(roleName)).doesNotExist('aws: role is no longer in the list');
});
ROLE_TYPES.forEach((scenario) => {
@ -184,7 +138,7 @@ module('Acceptance | aws secret backend', function (hooks) {
await click(GENERAL.saveButton);
await waitUntil(() => currentURL() === `/vault/secrets/${path}/show/${roleName}`); // flaky without this
assert.strictEqual(currentURL(), `/vault/secrets/${path}/show/${roleName}`);
await click(AWS_CREDS.generateLink);
await click(SES.generateLink);
assert
.dom(GENERAL.inputByAttr('credentialType'))
.hasValue(scenario.credentialType, 'credentialType matches backing role');
@ -193,7 +147,7 @@ module('Acceptance | aws secret backend', function (hooks) {
await scenario.fillOutForm(assert);
await click(GENERAL.saveButton);
assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation');
assert.dom(SES.warning).exists('Shows access warning after generation');
assert.dom(GENERAL.infoRowValue('Access key')).exists();
assert.dom(GENERAL.infoRowValue('Secret key')).exists();
assert.dom(GENERAL.infoRowValue('Security token')).exists();
@ -221,8 +175,8 @@ module('Acceptance | aws secret backend', function (hooks) {
await runCmd(`write ${path}/roles/${roleName} credential_type=assumed_role`);
await visit(`/vault/secrets/${path}/list`);
assert.dom(AWS_CREDS.secretLink(roleName)).exists();
await click(AWS_CREDS.secretLink(roleName));
assert.dom(SES.secretLink(roleName)).exists();
await click(SES.secretLink(roleName));
assert.strictEqual(currentURL(), `/vault/secrets/${path}/credentials/${roleName}`);
assert
@ -232,7 +186,7 @@ module('Acceptance | aws secret backend', function (hooks) {
await fillIn(GENERAL.inputByAttr('credentialType'), 'assumed_role');
await click(GENERAL.saveButton);
assert.dom(AWS_CREDS.warning).exists('Shows access warning after generation');
assert.dom(SES.warning).exists('Shows access warning after generation');
assert.dom(GENERAL.infoRowValue('Access key')).exists();
assert.dom(GENERAL.infoRowValue('Secret key')).exists();
assert.dom(GENERAL.infoRowValue('Security token')).exists();

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { click, currentURL, waitFor } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
import { runCmd } from 'vault/tests/helpers/commands';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
module('Acceptance | ssh | configuration', function (hooks) {
setupApplicationTest(hooks);
hooks.beforeEach(function () {
this.uid = uuidv4();
return authPage.login();
});
test('it should show a public key after saving default configuration', async function (assert) {
const sshPath = `ssh-${this.uid}`;
await enablePage.enable('ssh', sshPath);
await click(SES.configTab);
await click(SES.configure);
assert.strictEqual(
currentURL(),
`/vault/settings/secrets/configure/${sshPath}`,
'transitions to the configuration page'
);
assert.dom(SES.ssh.configureForm).exists('renders ssh configuration form');
// default has generate CA checked so we just submit the form
await click(SES.ssh.sshInput('configure-submit'));
assert.strictEqual(
currentURL(),
`/vault/settings/secrets/configure/${sshPath}`,
'stays on configuration form page.'
);
await waitFor(SES.ssh.sshInput('public-key'));
assert.dom(SES.ssh.sshInput('public-key')).exists('renders the public key input on form page');
assert.dom(SES.ssh.sshInput('public-key')).hasClass('masked-input', 'public key is masked');
// cleanup
await runCmd(`delete sys/mounts/${sshPath}`);
});
});

View File

@ -12,6 +12,7 @@ import syncHandlers from 'vault/mirage/handlers/sync';
import authPage from 'vault/tests/pages/auth';
import { settled, click, visit, currentURL, fillIn, currentRouteName } from '@ember/test-helpers';
import { PAGE as ts } from 'vault/tests/helpers/sync/sync-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
// sync is an enterprise feature but since mirage is used the enterprise label has been intentionally omitted from the module name
module('Acceptance | sync | destination (singular)', function (hooks) {
@ -36,11 +37,11 @@ module('Acceptance | sync | destination (singular)', function (hooks) {
test('it should transition to correct routes when performing actions', async function (assert) {
await click(ts.navLink('Secrets Sync'));
await click(ts.tab('Destinations'));
await click(GENERAL.tab('Destinations'));
await click(ts.listItem);
assert.dom(ts.tab('Secrets')).hasClass('active', 'Secrets tab is active');
assert.dom(GENERAL.tab('Secrets')).hasClass('active', 'Secrets hdsTab is active');
await click(ts.tab('Details'));
await click(GENERAL.tab('Details'));
assert.dom(ts.infoRowLabel('Name')).exists('Destination details display');
await click(ts.toolbar('Sync secrets'));
@ -49,7 +50,7 @@ module('Acceptance | sync | destination (singular)', function (hooks) {
await click(ts.toolbar('Edit destination'));
assert.dom(ts.inputByAttr('name')).isDisabled('Edit view renders with disabled name field');
await click(ts.cancelButton);
assert.dom(ts.tab('Details')).hasClass('active', 'Details view is active');
assert.dom(GENERAL.tab('Details')).hasClass('active', 'Details view is active');
});
test('it should delete destination', async function (assert) {

View File

@ -12,6 +12,7 @@ import sinon from 'sinon';
import authPage from 'vault/tests/pages/auth';
import { click, waitFor, visit, currentURL } from '@ember/test-helpers';
import { PAGE as ts } from 'vault/tests/helpers/sync/sync-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { runCmd } from 'vault/tests/helpers/commands';
// sync is an enterprise feature but since mirage is used the enterprise label has been intentionally omitted from the module name
@ -73,7 +74,9 @@ module('Acceptance | sync | overview', function (hooks) {
await waitFor(ts.overview.table.actionToggle(0));
await click(ts.overview.table.actionToggle(0));
await click(ts.overview.table.action('details'));
assert.dom(ts.tab('Secrets')).hasClass('active', 'Navigates to secrets view for destination');
assert
.dom(GENERAL.tab('Secrets'))
.hasClass('active', 'Navigates to secrets view for destination');
});
});

View File

@ -88,9 +88,9 @@ module('Acceptance | tools', function (hooks) {
JSON.parse(DATA_TO_WRAP),
'unwrapped data equals input data'
);
await waitUntil(() => find(TS.tab('details')));
await click(TS.tab('details'));
await click(TS.tab('data'));
await waitUntil(() => find(GENERAL.hdsTab('details')));
await click(GENERAL.hdsTab('details'));
await click(GENERAL.hdsTab('data'));
assert.deepEqual(
JSON.parse(codemirror().getValue()),
JSON.parse(DATA_TO_WRAP),

View File

@ -14,6 +14,7 @@ export const GENERAL = {
headerContainer: 'header.page-header',
icon: (name: string) => `[data-test-icon="${name}"]`,
tab: (name: string) => `[data-test-tab="${name}"]`,
hdsTab: (name: string) => `[data-test-tab="${name}"] button`, // hds tabs are li elements and QUnit needs a clickable element so add button to the selector
secretTab: (name: string) => `[data-test-secret-list-tab="${name}"]`,
flashMessage: '[data-test-flash-message]',
latestFlashContent: '[data-test-flash-message]:last-of-type [data-test-flash-message-body]',
@ -80,6 +81,7 @@ export const GENERAL = {
navLink: (label: string) => `[data-test-sidebar-nav-link="${label}"]`,
cancelButton: '[data-test-cancel]',
saveButton: '[data-test-save]',
saveButtonId: (id: string) => `[data-test-save="${id}"]`, // there are many uses of save button, but very few with an id. Instead of making all instances of saveButton a function with an empty string, we can just use this selector. TODO: should be removed after refactor of AWS.
maskedInput: (name: string) => `[data-test-textarea="${name}"]`,
codemirror: `[data-test-component="code-mirror-modifier"]`,
codemirrorTextarea: `[data-test-component="code-mirror-modifier"] textarea`,

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export const SECRET_ENGINE_SELECTORS = {
backButton: '[data-test-back-button]',
configTab: '[data-test-configuration-tab]',
configure: '[data-test-secret-backend-configure]',
configureTitle: (type: string) => `[data-test-backend-configure-title="${type}"]`,
configurationToggle: '[data-test-mount-config-toggle]',
createSecret: '[data-test-secret-create]',
crumb: (path: string) => `[data-test-secret-breadcrumb="${path}"] a`,
generateLink: '[data-test-backend-credentials]',
mountType: (name: string) => `[data-test-mount-type="${name}"]`,
mountSubmit: '[data-test-mount-submit]',
secretHeader: '[data-test-secret-header]',
secretLink: (name: string) => (name ? `[data-test-secret-link="${name}"]` : '[data-test-secret-link]'),
viewBackend: '[data-test-backend-view-link]',
warning: '[data-test-warning]',
aws: {
rootForm: '[data-test-aws-root-creds-form]',
delete: (role: string) => `[data-test-aws-role-delete="${role}"]`,
},
ssh: {
configureForm: '[data-test-ssh-configure-form]',
sshInput: (name: string) => `[data-test-ssh-input="${name}"]`,
},
};

View File

@ -6,6 +6,5 @@
export const TOOLS_SELECTORS = {
submit: '[data-test-tools-submit]',
toolsInput: (attr: string) => `[data-test-tools-input="${attr}"]`,
tab: (item: string) => `[data-test-tab="${item}"] button`,
button: (action: string) => `[data-test-button="${action}"]`,
};

View File

@ -63,7 +63,9 @@ module('Integration | Component | secrets-engine-mount-config', function (hooks)
assert.dom(selectors.rowValue('Local')).includesText('No', 'Local renders');
assert.dom(selectors.rowValue('Seal Wrap')).includesText('Yes', 'Seal wrap renders');
assert.dom(selectors.rowValue('Default Lease TTL')).includesText('0', 'Default Lease TTL renders');
assert.dom(selectors.rowValue('Max Lease TTL')).includesText('10000', 'Max Lease TTL renders');
assert
.dom(selectors.rowValue('Max Lease TTL'))
.includesText('2 hours 46 minutes 40 seconds', 'Max Lease TTL renders');
});
test('it should yield block for additional fields', async function (assert) {

View File

@ -31,8 +31,8 @@ module('Integration | Component | tools/unwrap', function (hooks) {
assert.dom('h1').hasText('Unwrap Data', 'Title renders');
assert.dom(TS.submit).hasText('Unwrap data');
assert.dom(TS.toolsInput('unwrap-token')).hasValue('');
assert.dom(TS.tab('data')).doesNotExist();
assert.dom(TS.tab('details')).doesNotExist();
assert.dom(GENERAL.hdsTab('data')).doesNotExist();
assert.dom(GENERAL.hdsTab('details')).doesNotExist();
assert.dom('.CodeMirror').doesNotExist();
assert.dom(TS.button('Done')).doesNotExist();
});
@ -77,10 +77,10 @@ module('Integration | Component | tools/unwrap', function (hooks) {
assert.true(flashSpy.calledWith('Unwrap was successful.'), 'it renders success flash');
assert.dom('label').hasText('Unwrapped Data');
assert.strictEqual(codemirror().getValue(' '), '{ "foo": "bar" }', 'it renders unwrapped data');
assert.dom(TS.tab('data')).hasAttribute('aria-selected', 'true');
assert.dom(GENERAL.hdsTab('data')).hasAttribute('aria-selected', 'true');
await click(TS.tab('details'));
assert.dom(TS.tab('details')).hasAttribute('aria-selected', 'true');
await click(GENERAL.hdsTab('details'));
assert.dom(GENERAL.hdsTab('details')).hasAttribute('aria-selected', 'true');
assert
.dom(`${GENERAL.infoRowValue('Renewable')} ${GENERAL.icon('x-square')}`)
.exists('renders falsy icon for renewable');
@ -121,7 +121,7 @@ module('Integration | Component | tools/unwrap', function (hooks) {
await click(TS.submit);
await waitUntil(() => find('.CodeMirror'));
await click(TS.tab('details'));
await click(GENERAL.hdsTab('details'));
assert
.dom(`${GENERAL.infoRowValue('Renewable')} ${GENERAL.icon('check-circle')}`)
.exists('renders truthy icon for renewable');

View File

@ -17,7 +17,7 @@ module('Unit | Decorators | fetch-secrets-engine-config', function (hooks) {
setupMirage(hooks);
hooks.beforeEach(function () {
this.spy = sinon.spy(console, 'error');
this.spy = sinon.spy(console, 'debug');
this.store = this.owner.lookup('service:store');
this.backend = 'test-path';
this.owner.lookup('service:secretMountPath').update(this.backend);