Prep for configuration.edit refactor (#27948)

* move files around

* move fetches to config to the configuration.index route

* working... for aws, lots of clean up left

* move error handling to parent route

* standarize configModel param

* add test coverage

* welp a miss for non configurable engines

* pr comments

* remove mirage interrupts and test actual api

* update configuration details test to test for template only things

* api error coverage
This commit is contained in:
Angel Garbarino 2024-08-05 13:39:10 -06:00 committed by GitHub
parent 40698e962b
commit a81b482158
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 275 additions and 222 deletions

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default class AwsRootConfig extends ApplicationAdapter {
namespace = 'v1';
// For now this is only being used on the vault.cluster.secrets.backend.configuration route. This is a read-only route.
// Eventually, this will be used to create the lease config for the AWS secret backend, replacing the requests located on the secret-engine adapter.
queryRecord(store, type, query) {
const { backend } = query;
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/config/lease`, 'GET').then((resp) => {
resp.id = backend;
return resp;
});
}
}

View File

@ -3,42 +3,46 @@
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if this.configError}}
{{! Surface API errors not associated with empty configuration details }}
<Page::Error @error={{this.configError}} />
{{else if this.configModel}}
{{#each this.configModel.attrs as |attr|}}
{{#if attr.options.sensitive}}
<InfoTableRow
alwaysRender={{not (is-empty-value (get @model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
>
{{#if attr.options.sensitive}}
<MaskedInput @value={{get @model attr.name}} @name={{attr.name}} @displayOnly={{true}} @allowCopy={{true}} />
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get @model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.configModel (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{#if @configModels.length}}
{{#each @configModels as |configModel|}}
{{#each configModel.attrs as |attr|}}
{{#if attr.options.sensitive}}
<InfoTableRow
alwaysRender={{not (is-empty-value (get configModel attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get configModel (or attr.options.fieldValue attr.name)}}
>
{{#if attr.options.sensitive}}
<MaskedInput
@value={{get configModel attr.name}}
@name={{attr.name}}
@displayOnly={{true}}
@allowCopy={{true}}
/>
{{/if}}
</InfoTableRow>
{{else}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get @model attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get configModel (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{/each}}
{{/each}}
{{else}}
{{! Prompt for a user to configure the secret engine }}
{{! Prompt user to configure the secret engine }}
<EmptyState
data-test-config-cta
@title="{{this.typeDisplay}} not configured"
@message="Get started by configuring your {{this.typeDisplay}} engine."
@title="{{@typeDisplay}} not configured"
@message="Get started by configuring your {{@typeDisplay}} engine."
>
<Hds::Link::Standalone
@icon="chevron-right"
@iconPosition="trailing"
@text="Configure {{this.typeDisplay}}"
@text="Configure {{@typeDisplay}}"
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{@model.id}}
@model={{@id}}
/>
</EmptyState>
{{/if}}

View File

@ -1,94 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { allEngines } from 'vault/helpers/mountable-secret-engines';
import type Store from '@ember-data/store';
import type SecretEngineModel from 'vault/models/secret-engine';
import type AdapterError from '@ember-data/adapter';
import type Model from '@ember-data/model';
/**
* @module ConfigurationDetails
* `ConfigurationDetails` is used by configurable secret engines (AWS, SSH) to show either an API error, configuration details, or a prompt to configure the engine. Which of these is shown is determined by the engine type and whether the user has configured the engine yet.
*
* @example
* ```js
* <SecretEngine::ConfigurationDetails @model={{this.model}} />
* ```
*
* @param {object} model - The secret-engine model to be configured.
*/
interface Args {
model: SecretEngineModel | null;
}
interface ConfigError {
httpStatus: number | null;
message: string | null;
errors: object | null;
}
export default class ConfigurationDetails extends Component<Args> {
@service declare readonly store: Store;
@tracked configError: ConfigError | null = null;
@tracked configModel: Model | null = null;
constructor(owner: unknown, args: Args) {
super(owner, args);
const { model } = this.args;
// Should not be able to get here without a model, but in case an upstream change allows it, handle the error higher up.
if (!model) {
return;
}
const { id, type } = model;
// Fetch the config for the engine type.
switch (type) {
case 'aws':
this.fetchAwsRootConfig(id);
break;
case 'ssh':
this.fetchSshCaConfig(id);
break;
}
}
async fetchAwsRootConfig(backend: string) {
try {
this.configModel = await this.store.queryRecord('aws/root-config', { backend });
} catch (e: AdapterError) {
// If the error is something other than 404 "not found" then an API error has come back and this will be displayed to the user as an error.
// If it's 404 then configError is not set nor is the configModel and a prompt to configure will be shown.
if (e.httpStatus !== 404) {
this.configError = e;
}
return;
}
}
async fetchSshCaConfig(backend: string) {
try {
this.configModel = await this.store.queryRecord('ssh/ca-config', { backend });
} catch (e: AdapterError) {
// The SSH api does not return a 404 not found but a 400 error after first mounting the engine with the
// message that keys have not been configured yet.
// We need to check the message of the 400 error and if it's the keys message, return a prompt instead of a configError.
if (e.httpStatus !== 404 && e.errors[0] !== `keys haven't been configured yet`) {
this.configError = e;
}
return;
}
}
get typeDisplay() {
if (!this.args.model) return;
const { type } = this.args.model;
return allEngines().find((engine) => engine.type === type)?.displayName;
}
}

View File

@ -10,11 +10,11 @@ import type SecretEngineModel from 'vault/models/secret-engine';
import type { TtlEvent } from 'vault/app-types';
/**
* @module ConfigureAwsSecretComponent
* @module ConfigureAwsComponent
*
* @example
* ```js
* <ConfigureAwsSecret
* <SecretEngine::ConfigureAws
@model={{model}}
@tab={{tab}}
@accessKey={{accessKey}}
@ -60,7 +60,7 @@ interface Args {
saveAWSLease: (data: LeaseFields) => void;
}
export default class ConfigureAwsSecretComponent extends Component<Args> {
export default class ConfigureAwsComponent extends Component<Args> {
@action
saveRootCreds(data: AWSRootCredsFields, event: Event) {
event.preventDefault();

View File

@ -7,11 +7,11 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
/**
* @module ConfigureSshSecretComponent
* @module ConfigureSshSComponent
*
* @example
* ```js
* <ConfigureSshSecret
* <SecretEngine::ConfigureSsh
* @model={{this.model}}
* @configured={{this.configured}}
* @saveConfig={{action "saveConfig"}}
@ -24,7 +24,7 @@ import { action } from '@ember/object';
* @param {boolean} loading - property in parent that updates depending on status of parent's action
*
*/
export default class ConfigureSshSecretComponent extends Component {
export default class ConfigureSshComponent extends Component {
@action
delete() {
this.args.saveConfig({ delete: true });

View File

@ -1,14 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
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 () {
return CONFIGURABLE_SECRET_ENGINES.includes(this.model.type);
}),
});

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Model, { attr } from '@ember-data/model';
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
export default class AwsLeaseConfig extends Model {
@attr('string') backend; // dynamic path of secret -- set on response from value passed to queryRecord
@attr({
label: 'Max Lease TTL',
editType: 'ttl',
})
leaseMax;
@attr({
label: 'Default Lease TTL',
editType: 'ttl',
})
lease;
get attrs() {
const keys = ['lease', 'leaseMax'];
return expandAttributeMeta(this, keys);
}
}

View File

@ -5,28 +5,114 @@
import { service } from '@ember/service';
import Route from '@ember/routing/route';
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
import { allEngines } from 'vault/helpers/mountable-secret-engines';
import { reject } from 'rsvp';
/**
* This route is responsible for fetching all configuration model(s).
* This includes the mount-configuration model attached to the secret-engine model via a belongsTo relationship.
* As well as any additional configuration models if the engine is a configurable engine.
*/
export default class SecretsBackendConfigurationRoute extends Route {
@service store;
async model() {
const backend = this.modelFor('vault.cluster.secrets.backend');
if (backend.isV2KV) {
const secretEngineModel = this.modelFor('vault.cluster.secrets.backend');
if (secretEngineModel.isV2KV) {
const canRead = await this.store
.findRecord('capabilities', `${backend.id}/config`)
.findRecord('capabilities', `${secretEngineModel.id}/config`)
.then((response) => response.canRead);
// 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.casRequired = backend.casRequired ? backend.casRequired : 'False';
backend.deleteVersionAfter = backend.deleteVersionAfter ? backend.deleteVersionAfter : 'Never delete';
secretEngineModel.casRequired = secretEngineModel.casRequired
? secretEngineModel.casRequired
: 'False';
secretEngineModel.deleteVersionAfter = secretEngineModel.deleteVersionAfter
? secretEngineModel.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);
backend.set('deleteVersionAfter', null);
backend.set('maxVersions', null);
secretEngineModel.set('casRequired', null);
secretEngineModel.set('deleteVersionAfter', null);
secretEngineModel.set('maxVersions', null);
}
}
return backend;
// If the engine is configurable fetch the config model(s) for the engine and return it alongside the model
if (CONFIGURABLE_SECRET_ENGINES.includes(secretEngineModel.type)) {
let configModels = await this.fetchConfig(secretEngineModel.type, secretEngineModel.id);
configModels = this.standardizeConfigModels(configModels);
return {
secretEngineModel,
configModels,
};
}
return { secretEngineModel };
}
standardizeConfigModels(configModels) {
// standardize the configModels to an array so that the component can handle it correctly
Array.isArray(configModels) ? configModels : (configModels = [configModels]);
// make sure no items in the array are null or undefined
return configModels.filter((configModel) => {
return !!configModel;
});
}
fetchConfig(type, id) {
switch (type) {
case 'aws':
return this.fetchAwsConfigs(id);
case 'ssh':
return this.fetchSshCaConfig(id);
default:
return reject({ httpStatus: 404, message: 'not found', path: id });
}
}
async fetchAwsConfigs(id) {
// AWS has two configuration endpoints root and lease, return an array of these responses.
const configArray = [];
const configRoot = await this.fetchAwsConfig(id, 'aws/root-config');
const configLease = await this.fetchAwsConfig(id, 'aws/lease-config');
configArray.push(configRoot, configLease);
return configArray;
}
async fetchAwsConfig(id, modelPath) {
try {
return await this.store.queryRecord(modelPath, { backend: id });
} catch (e) {
if (e.httpStatus === 404) {
// a 404 error is thrown when the lease config hasn't been set yet.
return;
}
throw e;
}
}
async fetchSshCaConfig(id) {
try {
return await this.store.queryRecord('ssh/ca-config', { backend: id });
} catch (e) {
if (e.httpStatus === 400 && e.errors[0] === `keys haven't been configured yet`) {
// When first mounting a SSH engine it throws a 400 error with this specific message.
// We want to catch this situation and return nothing so that the component can handle it correctly.
return;
}
throw e;
}
}
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.typeDisplay = allEngines().find(
(engine) => engine.type === resolvedModel.secretEngineModel.type
)?.displayName;
controller.isConfigurable = CONFIGURABLE_SECRET_ENGINES.includes(resolvedModel.secretEngineModel.type);
controller.modelId = resolvedModel.secretEngineModel.id;
}
}

View File

@ -29,7 +29,7 @@
</Toolbar>
{{#if (eq this.model.type "aws")}}
<ConfigureAwsSecret
<SecretEngine::ConfigureAws
@model={{this.model}}
@tab={{this.tab}}
@accessKey={{this.accessKey}}
@ -41,7 +41,7 @@
@saveAWSLease={{action "save" "saveAWSLease"}}
/>
{{else if (eq this.model.type "ssh")}}
<ConfigureSshSecret
<SecretEngine::ConfigureSsh
@model={{this.model}}
@configured={{this.configured}}
@saveConfig={{action "saveConfig"}}

View File

@ -4,12 +4,12 @@
~}}
<SecretListHeader
@model={{this.model}}
@model={{this.model.secretEngineModel}}
@backendCrumb={{hash
label=this.model.id
text=this.model.id
label=this.model.secretEngineModel.id
text=this.model.secretEngineModel.id
path="vault.cluster.secrets.backend.list-root"
model=this.model.id
model=this.model.secretEngineModel.id
}}
@isConfigure={{true}}
/>
@ -19,7 +19,7 @@
<ToolbarActions>
<ToolbarLink
@route="vault.cluster.secrets.backend.configuration.edit"
@model={{this.model.id}}
@model={{this.model.secretEngineModel.id}}
data-test-secret-backend-configure
>
Configure
@ -27,24 +27,36 @@
</ToolbarActions>
</Toolbar>
<SecretEngine::ConfigurationDetails @model={{this.model}} />
<SecretsEngineMountConfig @model={{this.model}} class="has-top-margin-xl has-bottom-margin-xl" data-test-mount-config />
<SecretEngine::ConfigurationDetails
@configModels={{this.model.configModels}}
@typeDisplay={{this.typeDisplay}}
@id={{this.modelId}}
/>
<SecretsEngineMountConfig
@model={{this.model.secretEngineModel}}
class="has-top-margin-xl has-bottom-margin-xl"
data-test-mount-config
/>
{{else}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.model.attrs as |attr|}}
{{#each this.model.secretEngineModel.attrs as |attr|}}
{{#if (eq attr.type "object")}}
<InfoTableRow
@alwaysRender={{not (is-empty-value (get this.model attr.name))}}
@alwaysRender={{not (is-empty-value (get this.model.secretEngineModel attr.name))}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{stringify (get this.model (or attr.options.fieldValue attr.name))}}
@value={{stringify (get this.model.secretEngineModel (or attr.options.fieldValue attr.name))}}
/>
{{else}}
<InfoTableRow
@alwaysRender={{and (not (is-empty-value (get this.model attr.name))) (not-eq attr.name "version")}}
@alwaysRender={{and
(not (is-empty-value (get this.model.secretEngineModel attr.name)))
(not-eq attr.name "version")
}}
@formatTtl={{eq attr.options.editType "ttl"}}
@label={{or attr.options.label (to-label attr.name)}}
@value={{get this.model (or attr.options.fieldValue attr.name)}}
@value={{get this.model.secretEngineModel (or attr.options.fieldValue attr.name)}}
/>
{{/if}}
{{/each}}

View File

@ -14,11 +14,13 @@ 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 { overrideResponse } from 'vault/tests/helpers/stubs';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
import {
createConfig,
expectedConfigKeys,
expectedValueOfConfigKeys,
configUrl,
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
module('Acceptance | aws | configuration', function (hooks) {
@ -82,18 +84,18 @@ module('Acceptance | aws | configuration', function (hooks) {
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'
);
await visit(`/vault/secrets/${path}/configuration`);
assert.dom(GENERAL.infoRowValue('Access key')).hasText('foo', `Access Key has been set.`);
assert
.dom(GENERAL.infoRowValue('Secret key'))
.doesNotExist(`Secret key is not shown because it does not get returned by the api.`);
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
@ -101,12 +103,6 @@ module('Acceptance | aws | configuration', function (hooks) {
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);
@ -120,11 +116,16 @@ module('Acceptance | aws | configuration', function (hooks) {
this.flashSuccessSpy.calledWith('The backend configuration saved successfully!'),
'Success flash message is rendered'
);
await visit(`/vault/secrets/${path}/configuration`);
assert.dom(GENERAL.infoRowValue('Default Lease TTL')).hasText('55s', `Default TTL has been set.`);
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('1m5s', `Default TTL has been set.`);
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it show AWS configuration details', async function (assert) {
test('it shows AWS mount configuration details', async function (assert) {
assert.expect(12);
const path = `aws-${this.uid}`;
const type = 'aws';
@ -153,7 +154,7 @@ module('Acceptance | aws | configuration', function (hooks) {
});
test('it should update AWS configuration details after editing', async function (assert) {
assert.expect(4);
assert.expect(6);
const path = `aws-${this.uid}`;
const type = 'aws';
await enablePage.enable(type, path);
@ -168,17 +169,40 @@ module('Acceptance | aws | configuration', function (hooks) {
assert
.dom(GENERAL.infoRowValue('Region'))
.doesNotExist('Region has not been added therefor it does not show up on the details view.');
// edit accessKey and another field and confirm the details page is updated.
// edit root config details and lease config details and confirm the configuration.index page is updated.
await click(SES.configure);
await fillIn(GENERAL.inputByAttr('accessKey'), 'hello');
await click(GENERAL.menuTrigger);
await fillIn(GENERAL.selectByAttr('region'), 'ca-central-1');
await click(GENERAL.saveButtonId('root'));
// add lease config details
await click(GENERAL.hdsTab('lease'));
await click(GENERAL.toggleInput('Lease'));
await fillIn(GENERAL.ttl.input('Lease'), '33');
await click(GENERAL.toggleInput('Maximum Lease'));
await fillIn(GENERAL.ttl.input('Maximum Lease'), '43');
await click(GENERAL.saveButtonId('lease'));
await click(SES.viewBackend);
await click(SES.configTab);
assert.dom(GENERAL.infoRowValue('Access key')).hasText('hello', 'Access key has been updated to hello');
assert.dom(GENERAL.infoRowValue('Region')).hasText('ca-central-1', 'Region has been added');
assert.dom(GENERAL.infoRowValue('Default Lease TTL')).hasText('33s', 'Default Lease TTL has been added');
assert.dom(GENERAL.infoRowValue('Max Lease TTL')).hasText('43s', 'Max Lease TTL has been added');
// cleanup
await runCmd(`delete sys/mounts/${path}`);
});
test('it should show API error when AWS configuration read fails', async function (assert) {
assert.expect(1);
const path = `aws-${this.uid}`;
const type = 'aws';
await enablePage.enable(type, path);
// interrupt get and return API error
this.server.get(configUrl(type, path), () => {
return overrideResponse(400, { errors: ['bad request'] });
});
await click(SES.configTab);
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
});
});

View File

@ -10,12 +10,16 @@ import { v4 as uuidv4 } from 'uuid';
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';
import { configUrl } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
import { overrideResponse } from 'vault/tests/helpers/stubs';
module('Acceptance | ssh | configuration', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.uid = uuidv4();
@ -82,4 +86,17 @@ module('Acceptance | ssh | configuration', function (hooks) {
// cleanup
await runCmd(`delete sys/mounts/${sshPath}`);
});
test('it should show API error when SSH configuration read fails', async function (assert) {
assert.expect(1);
const path = `ssh-${this.uid}`;
const type = 'ssh';
await enablePage.enable(type, path);
// interrupt get and return API error
this.server.get(configUrl(type, path), () => {
return overrideResponse(400, { errors: ['bad request'] });
});
await click(SES.configTab);
assert.dom(SES.error.title).hasText('Error', 'shows the secrets backend error route');
});
});

View File

@ -11,6 +11,9 @@ export const SECRET_ENGINE_SELECTORS = {
configurationToggle: '[data-test-mount-config-toggle]',
createSecret: '[data-test-secret-create]',
crumb: (path: string) => `[data-test-secret-breadcrumb="${path}"] a`,
error: {
title: '[data-test-backend-error-title]',
},
generateLink: '[data-test-backend-credentials]',
mountType: (name: string) => `[data-test-mount-type="${name}"]`,
mountSubmit: '[data-test-mount-submit]',

View File

@ -6,70 +6,39 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { overrideResponse, allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
import { CONFIGURABLE_SECRET_ENGINES } from 'vault/helpers/mountable-secret-engines';
import {
createSecretsEngine,
createConfig,
configUrl,
expectedConfigKeys,
expectedValueOfConfigKeys,
} from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
module('Integration | Component | SecretEngine::configuration-details', function (hooks) {
module('Integration | Component | SecretEngine/configuration-details', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.store = this.owner.lookup('service:store');
this.configModels = [];
});
test('it shows prompt message if no config is returned', async function (assert) {
assert.expect(CONFIGURABLE_SECRET_ENGINES.length * 2);
for (const type of CONFIGURABLE_SECRET_ENGINES) {
const title = type.toUpperCase();
const backend = `test-404-${type}`;
this.model = createSecretsEngine(this.store, type, backend);
this.server.get(configUrl(type, backend), () => {
return overrideResponse(404);
});
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
assert.dom(GENERAL.emptyStateTitle).hasText(`${title} not configured`);
assert.dom(GENERAL.emptyStateMessage).hasText(`Get started by configuring your ${title} engine.`);
}
test('it shows prompt message if no config models are passed in', async function (assert) {
assert.expect(2);
await render(hbs`
<SecretEngine::ConfigurationDetails @typeDisplay="Display Name" />
`);
assert.dom(GENERAL.emptyStateTitle).hasText(`Display Name not configured`);
assert.dom(GENERAL.emptyStateMessage).hasText(`Get started by configuring your Display Name engine.`);
});
test('it shows API error', async function (assert) {
assert.expect(CONFIGURABLE_SECRET_ENGINES.length * 2);
for (const type of CONFIGURABLE_SECRET_ENGINES) {
const backend = `test-400-${type}`;
this.model = createSecretsEngine(this.store, type, backend);
this.server.get(configUrl(type, backend), () => {
return overrideResponse(400, { errors: ['bad request'] });
});
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
assert.dom(GENERAL.pageError.errorTitle(400)).hasText('Error');
assert.dom(GENERAL.pageError.errorDetails).hasText('bad request');
}
});
test('it shows config details if config data is returned', async function (assert) {
test('it shows config details if configModel(s) are passed in', async function (assert) {
assert.expect(14);
for (const type of CONFIGURABLE_SECRET_ENGINES) {
const backend = `test-${type}`;
this.model = createSecretsEngine(this.store, type, backend);
createConfig(this.store, backend, type);
this.server.get(configUrl(type, backend), () => {
return overrideResponse(200);
});
this.configModels = createConfig(this.store, backend, type);
await render(hbs`<SecretEngine::ConfigurationDetails @model={{this.model}}/>`);
await render(hbs`<SecretEngine::ConfigurationDetails @configModels={{array this.configModels}}/>`);
for (const key of expectedConfigKeys(type)) {
assert.dom(GENERAL.infoRowLabel(key)).exists(`${key} on the ${type} config details exists.`);
const responseKeyAndValue = expectedValueOfConfigKeys(type, key);