General test improvements (#10099) (#10147)

* add parallel command

* declare vault-keys module for test helpers

* use mirage to make dropdown check more reliably

* wait for inputs

* attempt to stabilize dashboard tests in parallel

* revert wait for inputs

* move problem acceptance tests to integration tests

* move more tests to integration

* remove assert.expect()  because there are no callback assertions

* delete redundant acceptance tests

* cleanup login state in afterEach hook

* use mirage for login settings test

* update other test based on mirage handler changes

* throw some waitFor in there

* revert waitFor

* use mirage in shared-identity-test

* remove storage cleanup from this pr

* remove parallel..again

* remove unnecessary auth login changes

* add version to dashboard/overview test "it shows the learn more card on enterprise"

* convert "version 2 with no update to config endpoint still allows mount of secret engine" to integration test

* restart tests

* Apply suggestion from @hellobontempo

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
Vault Automation 2025-10-15 15:50:39 -04:00 committed by GitHub
parent b2c8b1b78d
commit 6886447328
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 762 additions and 1132 deletions

View file

@ -26,13 +26,16 @@
data-test-type="dr-perf"
>
<Dashboard::ReplicationStateText
@title="DR primary"
@title="DR{{if (not-eq @replication.dr.mode 'disabled') (concat ' ' @replication.dr.mode)}}"
@name="dr"
@state={{@replication.dr.state}}
@clusterStates={{cluster-states @replication.dr.state}}
/>
<Dashboard::ReplicationStateText
@title="Performance {{if @replication.performance.isPrimary 'primary' 'secondary'}}"
@title="Performance{{if
(not-eq @replication.performance.mode 'disabled')
(concat ' ' @replication.performance.mode)
}}"
@name="performance"
@state={{if @replication.performance.clusterId @replication.performance.state "not set up"}}
@clusterStates={{if @replication.performance.clusterId (cluster-states @replication.performance.state)}}

View file

@ -19,7 +19,7 @@
class="list-item-row"
@params={{array "login-settings.rule.details" rule.name}}
@linkPrefix="vault.cluster.config-ui"
data-test-rule={{rule.name}}
data-test-list-item={{rule.name}}
>
<div class="level is-mobile">
<div class="level-left">

View file

@ -31,8 +31,7 @@ export default class LoginSettingsList extends Component {
try {
await this.api.sys.uiLoginDefaultAuthDeleteConfiguration(this.ruleToDelete.id);
this.flashMessages.success(`Successfully deleted rule ${this.ruleToDelete.id}.`);
this.router.transitionTo('vault.cluster.config-ui.login-settings');
this.router.refresh('vault.cluster.config-ui.login-settings');
} catch (error) {
const message = errorMessage(error, 'Error deleting rule. Please try again.');
this.flashMessages.danger(message);

View file

@ -6,8 +6,8 @@
import { Factory } from 'miragejs';
export default Factory.extend({
name: (i) => `Login rule ${i}`,
namespace_path: (i) => `namespace-${i}`,
name: (i) => `Login-rule-${i}`,
namespace_path: (i) => `namespace-${i}/`,
default_auth_type: 'okta',
backup_auth_types: () => ['oidc', 'token'],
disable_inheritance: false,

View file

@ -3,6 +3,8 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { sanitizePath } from 'core/utils/sanitize-path';
export default function (server) {
// LIST, READ and DELETE requests for default-auth (login customizations)
server.get('sys/config/ui/login/default-auth', (schema, req) => {
@ -12,9 +14,8 @@ export default function (server) {
if (records) {
const keys = records.map(({ name }) => name);
const key_info = records.reduce((obj, record) => {
const { name, namespace, disable_inheritance } = record;
// TBD, but likely only limited information will be returned about the record from the LIST request
obj[name] = { namespace, disable_inheritance };
const { name, namespace_path, disable_inheritance } = record;
obj[name] = { namespace_path, disable_inheritance, name };
return obj;
}, {});
return {
@ -47,20 +48,21 @@ export default function (server) {
// UNAUTHENTICATED READ ONLY for login form display logic
server.get('sys/internal/ui/default-auth-methods', (schema, req) => {
const nsHeader = req.requestHeaders['X-Vault-Namespace'];
// if no namespace is passed, assume root
const findRule = (ns = '') => schema.db['loginRules'].findBy({ namespace_path: ns });
let rule = findRule(nsHeader || '');
const findRule = (ns) => schema.db['loginRules'].findBy({ namespace_path: ns });
// the namespace header shouldn't have a trailing slash, but sanitize just in case
// if no namespace is passed then it's the root namespace (which does not have a trailing slash)
const nsPath = sanitizePath(nsHeader) ? `${nsHeader}/` : 'root';
let rule = findRule(nsPath);
if (!rule && nsHeader?.includes('/')) {
// for simplicity, tests only nest namespaces one level, e.g. "test-ns/child"
// for simplicity, tests only support namespaces nested one level, e.g. "test-ns/child"
const [parent] = nsHeader.split('/');
const parentRule = findRule(parent);
const parentRule = findRule(`${parent}/`);
rule = parentRule?.disable_inheritance ? null : parentRule;
}
// Fallback to root namespace settings to simulate inheritance if no rule exists or parent has disabled inheritance
rule = rule || findRule();
rule = rule || findRule('root');
const { default_auth_type, backup_auth_types, disable_inheritance } = rule || {};
return { data: { default_auth_type, backup_auth_types, disable_inheritance } };

View file

@ -6,19 +6,19 @@
export default function (server) {
server.create('login-rule', {
name: 'root-rule',
namespace_path: '',
default_auth_type: 'okta',
backup_auth_types: ['token'],
namespace_path: 'root',
default_auth_type: 'token',
backup_auth_types: ['userpass', 'ldap'],
disable_inheritance: false,
});
server.create('login-rule', {
namespace_path: 'admin',
namespace_path: 'admin/',
default_auth_type: 'oidc',
backup_auth_types: ['token'],
});
server.create('login-rule', {
name: 'ns-rule',
namespace_path: 'test-ns',
namespace_path: 'test-ns/',
default_auth_type: 'ldap',
backup_auth_types: [],
disable_inheritance: true,

View file

@ -30,12 +30,12 @@
"start": "VAULT_ADDR=http://127.0.0.1:8200; ember server --proxy=$VAULT_ADDR",
"start2": "ember server --proxy=http://127.0.0.1:8202 --port=4202",
"start:chroot": "ember server --proxy=http://127.0.0.1:8300 --port=4300",
"test": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types,vault \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\" \"node scripts/start-vault.js {@}\" --",
"lint": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\"",
"test": "yarn lint && node scripts/start-vault.js",
"test:enos": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,lint:types,enos \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"yarn:lint:types\" \"node scripts/enos-test-ember.js {@}\" --",
"test:oss": "yarn run test -f='!enterprise'",
"test:oss": "yarn test -f='!enterprise'",
"test:ent": "node scripts/start-vault.js -f='enterprise'",
"test:quick": "node scripts/start-vault.js --split=8 --preserve-test-name --parallel=1",
"test:quick-oss": "node scripts/start-vault.js -f='!enterprise' --split=8 --preserve-test-name --parallel=1",
"test:filter": "node scripts/start-vault.js --server -f='!enterprise'",
"test:server": "node scripts/start-vault.js --server",
"test:dev": "node scripts/start-vault.js",

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import { fillIn, click, currentRouteName, currentURL, visit } from '@ember/test-helpers';
import { click, currentRouteName, currentURL, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import page from 'vault/tests/pages/access/identity/index';
@ -11,14 +11,15 @@ import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { runCmd } from 'vault/tests/helpers/commands';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { v4 as uuidv4 } from 'uuid';
import { setupMirage } from 'ember-cli-mirage/test-support';
const SELECTORS = {
listItem: (name) => `[data-test-identity-row="${name}"]`,
menu: `[data-test-popup-menu-trigger]`,
menuItem: (element) => `[data-test-popup-menu="${element}"]`,
};
module('Acceptance | /access/identity/entities', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
return login();
@ -42,35 +43,17 @@ module('Acceptance | /access/identity/entities', function (hooks) {
);
});
test('it navigates away from the entities page', async function (assert) {
const name = `entity-${uuidv4()}`;
await runCmd(`vault write identity/entity name="${name}" policies="default"`);
await page.visit({ item_type: 'entities' });
await click(GENERAL.navLink('Back to main navigation'));
assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'navigates back to dashboard');
await runCmd(`vault delete identity/entity/name/${name}`);
});
test('it navigates away from the groups page', async function (assert) {
const name = `entity-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`);
await page.visit({ item_type: 'groups' });
await click(GENERAL.navLink('Back to main navigation'));
assert.strictEqual(currentRouteName(), 'vault.cluster.dashboard', 'navigates back to dashboard');
await runCmd(`vault delete identity/group/name/${name}`);
});
test('it renders popup menu for entities', async function (assert) {
const name = `entity-${uuidv4()}`;
await runCmd(`vault write identity/entity name="${name}" policies="default"`);
await visit('/vault/access/identity/entities');
assert.strictEqual(currentURL(), '/vault/access/identity/entities', 'navigates to entities tab');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Create alias Edit Disable Delete', 'all actions render for entities');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`);
await click(GENERAL.confirmButton);
});
@ -80,41 +63,52 @@ module('Acceptance | /access/identity/entities', function (hooks) {
await visit('/vault/access/identity/groups');
assert.strictEqual(currentURL(), '/vault/access/identity/groups', 'navigates to the groups tab');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Create alias Edit Delete', 'all actions render for external groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`);
await click(GENERAL.confirmButton);
});
test('it renders popup menu for external groups with alias', async function (assert) {
const name = `external-hasalias-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="external"`);
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(SELECTORS.menuItem('create alias'));
await fillIn(GENERAL.inputByAttr('name'), 'alias-test');
await click(GENERAL.submitButton);
const groupId = '44b2f1d1-699a-4a79-3a7b-37e53e17e7b2';
const groupName = 'external-hasalias';
// only relevant response keys are stubbed to simplify testing (more data is actually returned by both endpoints)
this.server.get('/identity/group/id', () => {
return {
data: {
key_info: { [groupId]: { name: groupName } },
keys: [groupId],
},
};
});
this.server.get(`/identity/group/id/${groupId}`, () => {
return {
data: {
alias: { id: '15bac764-d690-b72a-9cbc-b1fdeac1af9e', name: 'alias-test' },
type: 'external',
},
};
});
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(`${SELECTORS.listItem(groupName)} ${GENERAL.menuTrigger}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Edit Delete', 'no "Create alias" option for external groups with an alias');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(GENERAL.confirmButton);
});
test('it renders popup menu for internal groups', async function (assert) {
const name = `internal-${uuidv4()}`;
await runCmd(`vault write identity/group name="${name}" policies="default" type="internal"`);
await visit('/vault/access/identity/groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menu}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuTrigger}`);
assert
.dom('.hds-dropdown ul')
.hasText('Details Edit Delete', 'no "Create alias" option for internal groups');
await click(`${SELECTORS.listItem(name)} ${SELECTORS.menuItem('delete')}`);
await click(`${SELECTORS.listItem(name)} ${GENERAL.menuItem('delete')}`);
await click(GENERAL.confirmButton);
});
});

View file

@ -12,6 +12,7 @@ import { click, fillIn, visit, currentURL } from '@ember/test-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { capitalize } from '@ember/string';
import { singularize } from 'ember-inflector';
import { setupMirage } from 'ember-cli-mirage/test-support';
// Helper to create an entity or group
async function createEntityOrGroup(itemType, name) {
@ -36,6 +37,7 @@ async function createAlias(itemType, itemGeneratedId, name) {
// Creation of an Entity or Group is inherently tested as part of the alias flow, so no separate test is needed.
module('Acceptance | Create groups and entities alias test', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
this.flashMessages = this.owner.lookup('service:flash-messages');
@ -86,30 +88,48 @@ module('Acceptance | Create groups and entities alias test', function (hooks) {
});
test(`${itemType}: it allows delete from the edit form`, async function (assert) {
const name = `${itemType}-${uuidv4()}`;
const itemGeneratedId = await createEntityOrGroup(itemType, name);
const aliasGeneratedId = await createAlias(itemType, itemGeneratedId, name);
await click('[data-test-alias-edit-link]');
assert.strictEqual(
currentURL(),
`/vault/access/identity/${itemType}/aliases/edit/${aliasGeneratedId}`,
`${itemType}: correctly navigates to edit`
);
assert.expect(3);
const itemId = uuidv4();
const name = `${itemType}-${itemId}`;
this.server.get(`/identity/${singularize(itemType)}/id/${itemId}`, () => {
return { data: { id: itemId, name } };
});
this.server.delete(`/identity/${singularize(itemType)}/id/${itemId}`, () => {
assert.true(true, `request made to delete ${name}`);
});
await visit(`/vault/access/identity/${itemType}/edit/${itemId}`);
await click(GENERAL.confirmTrigger); // click the Delete entity-alias trigger button
await click(GENERAL.confirmButton);
assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted');
assert.strictEqual(
currentURL(),
`/vault/access/identity/${itemType}/aliases`,
`/vault/access/identity/${itemType}`,
`${itemType}: navigates to the list page after deletion`
);
});
test(`${itemType}: it allows you to delete the ${itemType} from the list view`, async function (assert) {
const name = `${itemType}-${uuidv4()}`;
await createEntityOrGroup(itemType, name);
assert.expect(3);
const itemId = uuidv4();
const name = `${itemType}-${itemId}`;
this.server.get(`/identity/${singularize(itemType)}/id`, () => {
return {
data: {
key_info: { [itemId]: { name } },
keys: [itemId],
},
};
});
this.server.get(`/identity/${singularize(itemType)}/id/${itemId}`, () => {
return { data: { id: itemId, name } };
});
this.server.delete(`/identity/${singularize(itemType)}/id/${itemId}`, () => {
assert.true(true, `request made to delete ${name}`);
});
await visit(`/vault/access/identity/${itemType}`);
const rowSelector = `[data-test-identity-row="${name}"]`;
@ -120,8 +140,7 @@ module('Acceptance | Create groups and entities alias test', function (hooks) {
await click(menuTriggerSelector);
await click(GENERAL.menuItem('delete'));
await click(GENERAL.confirmButton);
assert.dom(rowSelector).doesNotExist(`${itemType}: is NOT in the list view`);
assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted');
});
}
});

View file

@ -21,7 +21,8 @@ import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
import { v4 as uuidv4 } from 'uuid';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import sinon from 'sinon';
import { supportedTypes } from 'vault/utils/auth-form-helpers';
import { ERROR_JWT_LOGIN, supportedTypes } from 'vault/utils/auth-form-helpers';
import { overrideResponse } from 'vault/tests/helpers/stubs';
const { rootToken } = VAULT_KEYS;
@ -197,7 +198,7 @@ module('Acceptance | auth login', function (hooks) {
// Assertion count is one for the URL and one for each payload key
module('it sends the right payload when authenticating', function (hooks) {
hooks.beforeEach(function () {
this.assertAuthRequest = (assert, req, expectedPayload) => {
this.assertAuthPayload = (assert, req, expectedPayload) => {
const body = JSON.parse(req.requestBody);
assert.true(true, `it calls the correct URL: ${req.url}`);
@ -242,7 +243,7 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'github';
this.expectedPayload = { token: 'mysupersecuretoken' };
this.server.post('/auth/custom-github/login', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
@ -254,24 +255,30 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'ldap';
this.expectedPayload = { password: 'some-password' };
this.server.post('/auth/custom-ldap/login/matilda', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
await this.fillAndLogIn();
});
// JWT and OIDC use the same request URLs and how the login happens depends on the configuration.
// For simplicity, mocking each to be configured as their namesake.
test('jwt', async function (assert) {
// auth_url is hit twice (once when inputs are filled and again on submit)
// so the assertion count is doubled
assert.expect(6);
// assertion for each payload key and request made
assert.expect(3);
this.authType = 'jwt';
this.expectedPayload = {
redirect_uri: 'http://localhost:7357/ui/vault/auth/custom-jwt/oidc/callback',
jwt: 'some-jwt-token',
role: 'some-dev',
};
this.server.post('/auth/custom-jwt/oidc/auth_url', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
// Additional setup so that JWT method is configured as jwt (not OIDC)
this.server.post(`/auth/:path/oidc/auth_url`, () =>
overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })
);
this.server.post('/auth/custom-jwt/login', (schema, req) => {
this.assertAuthPayload(assert, req, this.expectedPayload); // 1 assertion + 1 for each payload key
req.passthrough();
});
@ -288,7 +295,7 @@ module('Acceptance | auth login', function (hooks) {
role: 'some-dev',
};
this.server.post('/auth/custom-oidc/oidc/auth_url', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
@ -300,7 +307,7 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'okta';
this.expectedPayload = { password: 'some-password' };
this.server.post('/auth/custom-okta/login/matilda', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
@ -312,7 +319,7 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'radius';
this.expectedPayload = { password: 'some-password' };
this.server.post('/auth/custom-radius/login/matilda', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
@ -324,7 +331,7 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'userpass';
this.expectedPayload = { password: 'some-password' };
this.server.post('/auth/custom-userpass/login/matilda', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});
@ -336,7 +343,7 @@ module('Acceptance | auth login', function (hooks) {
this.authType = 'saml';
this.expectedPayload = { role: 'some-dev' };
this.server.post('/auth/custom-saml/sso_service_url', (schema, req) => {
this.assertAuthRequest(assert, req, this.expectedPayload);
this.assertAuthPayload(assert, req, this.expectedPayload);
req.passthrough();
});

View file

@ -23,19 +23,29 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho
customLoginHandler(this.server);
customLoginScenario(this.server);
// mirage scenario sets:
// root namespace with 'okta' as default and 'token' as backup
// root namespace with 'token' as default backups are 'userpass' and 'ldap'
// 'test-ns' with 'ldap' as default and no backups
});
test('it renders login settings for root namespace', async function (assert) {
await visit('/vault/auth');
await waitFor(AUTH_FORM.tabBtn('okta'));
assert.dom(AUTH_FORM.tabBtn('okta')).hasAttribute('aria-selected', 'true');
assert.dom(AUTH_FORM.authForm('okta')).exists('it renders default method');
assert.dom(AUTH_FORM.advancedSettings).exists();
await waitFor(AUTH_FORM.tabBtn('token'));
assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'true');
assert.dom(AUTH_FORM.authForm('token')).exists('it renders default method');
assert
.dom(AUTH_FORM.advancedSettings)
.doesNotExist('it does not render advanced settings for token auth method');
await click(GENERAL.button('Sign in with other methods'));
assert.dom(AUTH_FORM.authForm('token')).exists('it renders backup method');
assert
.dom(AUTH_FORM.tabBtn('userpass'))
.exists('it renders backup "Userpass" method')
.hasAttribute('aria-selected', 'true');
assert.dom(AUTH_FORM.authForm('userpass')).exists('it renders "Userpass" form when method is selected');
assert.dom(AUTH_FORM.advancedSettings).exists('it renders advanced settings for "Userpass"');
assert.dom(AUTH_FORM.tabBtn('ldap')).exists('it renders backup "LDAP" method');
await click(AUTH_FORM.tabBtn('ldap'));
assert.dom(AUTH_FORM.authForm('ldap')).exists('it renders "LDAP" form when method is selected');
assert.dom(AUTH_FORM.advancedSettings).exists('it renders advanced settings for "LDAP"');
});
test('it renders login settings for namespaces', async function (assert) {
@ -48,10 +58,13 @@ module('Acceptance | Enterprise | auth form custom login settings', function (ho
.dom(GENERAL.button('Sign in with other methods'))
.doesNotExist('it does not render alternate view');
// type in so that the namespace is "test-ns/child"
// All we're testing here is that the form settings update for nested namespaces.
// (We're not concerned with what the settings are since the mirage handler is stubbing the API logic)
// typeIn so that the text appends to the existing namespace input: "test-ns/child"
await typeIn(GENERAL.inputByAttr('namespace'), '/child');
await waitFor(AUTH_FORM.authForm('okta'));
assert.dom(AUTH_FORM.authForm('okta')).exists('it updates to render child namespace settings');
await waitFor(AUTH_FORM.authForm('token'));
assert.dom(AUTH_FORM.authForm('token')).exists('it updates to render child namespace settings');
assert.dom(AUTH_FORM.authForm('ldap')).doesNotExist('it does not render default view for parent');
});
module('listing visibility', function (hooks) {

View file

@ -5,37 +5,27 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, visit, currentRouteName } from '@ember/test-helpers';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { click, visit, currentRouteName, fillIn, waitUntil } from '@ember/test-helpers';
import { login, logout, rootToken } from 'vault/tests/helpers/auth/auth-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { runCmd } from 'vault/tests/helpers/commands';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { overrideResponse } from 'vault/tests/helpers/stubs';
import { v4 as uuidv4 } from 'uuid';
const SELECTORS = {
rule: (name) => (name ? `[data-test-rule="${name}"]` : '[data-test-rule]'),
popupMenu: (name) => `[data-test-rule="${name}"] ${GENERAL.menuTrigger}`,
};
import customLoginScenario from 'vault/mirage/scenarios/custom-login';
import customLoginHandler from 'vault/mirage/handlers/custom-login';
import Sinon from 'sinon';
// read view for custom login settings
module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
// a login rule cannot be created for a namespace that already has a rule applied.
// rules are deleted in the afterEach hook, but when debugging tests locally they may
// re-run before cleanup happens.
// using a uuid ensures the runCmd that creates new rules is always successful
// by using a different namespace_path each run.
this.ns1 = `ns1-${uuidv4()}`;
this.ns2 = `ns2-${uuidv4()}`;
return await login();
await login();
});
test('it renders empty state if no login settings exist', async function (assert) {
await visit('vault/config-ui/login-settings');
assert.dom(GENERAL.emptyStateTitle).hasText('No UI login settings yet');
assert
.dom(GENERAL.emptyStateMessage)
@ -44,7 +34,7 @@ module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) {
);
});
test('it falls back error template if no permission', async function (assert) {
test('it renders error template when permission is denied', async function (assert) {
this.server.get('/sys/config/ui/login/default-auth', () => overrideResponse(403));
await visit('vault/config-ui/login-settings');
assert.dom(GENERAL.pageError.error).hasText('Error permission denied');
@ -52,90 +42,75 @@ module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) {
module('list, read and delete', function (hooks) {
hooks.beforeEach(async function () {
await login();
customLoginScenario(this.server);
customLoginHandler(this.server);
this.loginRules = this.server.db.loginRules;
// create login rules
await runCmd([
`write sys/config/ui/login/default-auth/testRule1 backup_auth_types=userpass default_auth_type=okta disable_inheritance=false namespace_path=${this.ns1}`,
`write sys/config/ui/login/default-auth/testRule2 backup_auth_types=oidc default_auth_type=ldap disable_inheritance=true namespace_path=${this.ns2}`,
]);
// Cannot use the login() helper because customLoginHandler returns "token" as the default auth method
await logout();
await fillIn(GENERAL.inputByAttr('token'), rootToken);
await click(GENERAL.submitButton);
await waitUntil(() => currentRouteName() === 'vault.cluster.dashboard');
await click(GENERAL.navLink('UI Login Settings'));
});
hooks.afterEach(async function () {
// since the test login rules are created for namespaces that do not exist
// they do not affect the login view for the "root" namespace
await login();
// cleanup login rules
await runCmd(
[
'delete sys/config/ui/login/default-auth/testRule1',
'delete sys/config/ui/login/default-auth/testRule2',
],
true
);
test('it renders login rules', async function (assert) {
assert
.dom(GENERAL.listItem())
.exists({ count: this.loginRules.length }, `${this.loginRules.length} rules render`);
this.loginRules.forEach(({ name, disable_inheritance, namespace_path }) => {
const inheritance = disable_inheritance ? 'Inheritance disabled' : 'Inheritance enabled';
assert.dom(GENERAL.listItem(name)).hasText(`${name} ${namespace_path} ${inheritance}`);
});
});
test('fetched login rule list renders', async function (assert) {
// Visit the login settings list index page
await visit('vault/config-ui/login-settings');
// verify fetched rules are rendered in list
assert.dom(SELECTORS.rule()).exists({ count: 2 });
assert.dom(SELECTORS.rule('testRule1')).hasText(`testRule1 ${this.ns1}/ Inheritance enabled`);
assert.dom(SELECTORS.rule('testRule2')).hasText(`testRule2 ${this.ns2}/ Inheritance disabled`);
});
test('delete rule from list view', async function (assert) {
// Visit the login settings list index page
await visit('vault/config-ui/login-settings');
assert.dom(SELECTORS.rule()).exists({ count: 2 });
await click(SELECTORS.popupMenu('testRule1'));
test('it deletes rule from list view', async function (assert) {
const successFlashSpy = Sinon.spy(this.owner.lookup('service:flash-messages'), 'success');
const ruleToDelete = this.loginRules[0].name;
const initialCount = this.loginRules.length; // cache record length so we can confirm delete
await click(`${GENERAL.listItem(ruleToDelete)} ${GENERAL.menuTrigger}`);
await click(GENERAL.menuItem('delete-rule'));
assert.dom(GENERAL.confirmationModal).exists();
await click(GENERAL.confirmButton);
// verify success message from deletion
assert.dom(GENERAL.latestFlashContent).includesText('Successfully deleted rule testRule1.');
assert.dom(SELECTORS.rule('testRule1')).doesNotExist();
assert.dom(SELECTORS.rule()).exists({ count: 1 });
const [success] = successFlashSpy.lastCall.args;
assert.strictEqual(
success,
`Successfully deleted rule ${ruleToDelete}.`,
'it calls flash success with expected message'
);
assert.dom(GENERAL.listItem(ruleToDelete)).doesNotExist('the deleted rule does not exist');
assert.dom(GENERAL.listItem()).exists({ count: initialCount - 1 }, `${initialCount - 1} rules render`);
});
test('navigate to rule details page and renders rule data', async function (assert) {
test('it navigates to rule details page and renders rule data', async function (assert) {
const rule = this.server.db.loginRules[0];
// visit individual rule page
await visit('vault/config-ui/login-settings');
await click(SELECTORS.popupMenu('testRule1'));
await click(`${GENERAL.listItem(rule.name)} ${GENERAL.menuTrigger}`);
await click(GENERAL.menuItem('view-rule'));
// verify that user is redirected to the rule details page
assert.strictEqual(
currentRouteName(),
'vault.cluster.config-ui.login-settings.rule.details',
'goes to rule details page'
);
// verify fetched rule data is rendered
assert.dom(GENERAL.infoRowValue('Default method')).hasText('okta');
assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(`${this.ns1}/`);
assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('userpass');
assert.dom(GENERAL.infoRowValue('Default method')).hasText(rule.default_auth_type);
assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(rule.namespace_path);
assert.dom(GENERAL.infoRowValue('Backup methods')).hasText(rule.backup_auth_types.join(','));
assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('Yes');
});
test('it navigates to rule details from linked block', async function (assert) {
const rule = this.server.db.loginRules[2];
await visit('vault/config-ui/login-settings');
await click(SELECTORS.rule('testRule2'));
await click(GENERAL.listItem(rule.name));
assert.strictEqual(
currentRouteName(),
'vault.cluster.config-ui.login-settings.rule.details',
'goes to rule details page'
);
assert.dom(GENERAL.infoRowValue('Default method')).hasText('ldap');
assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(`${this.ns2}/`);
assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('oidc');
assert.dom(GENERAL.infoRowValue('Default method')).hasText(rule.default_auth_type);
assert.dom(GENERAL.infoRowValue('Namespace the rule applies to')).hasText(rule.namespace_path);
assert.dom(GENERAL.infoRowValue('Backup methods')).hasText(rule.backup_auth_types.join(', '));
assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('No');
});
});

View file

@ -0,0 +1,112 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { visit } from '@ember/test-helpers';
import { setupApplicationTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors';
const authenticatedMessageResponse = {
request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
key_info: {
'some-awesome-id-2': {
authenticated: true,
end_time: null,
link: {
'some link title': 'www.link.com',
},
message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
options: null,
start_time: '2024-01-04T08:00:00Z',
title: 'Banner title',
type: 'banner',
},
'some-awesome-id-1': {
authenticated: true,
end_time: null,
message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
options: null,
start_time: '2024-01-01T08:00:00Z',
title: 'Modal title',
type: 'modal',
},
},
keys: ['some-awesome-id-2', 'some-awesome-id-1'],
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: '',
};
module('Acceptance | custom messages', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
test('it shows the alert banner and modal message', async function (assert) {
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
const modalId = 'some-awesome-id-1';
const alertId = 'some-awesome-id-2';
assert.dom(CUSTOM_MESSAGES.modal(modalId)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalId)).hasText('Modal title');
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).hasText('here is a cool message');
assert.dom(CUSTOM_MESSAGES.alertTitle(alertId)).hasText('Banner title');
assert.dom(CUSTOM_MESSAGES.alertDescription(alertId)).hasText('hello world hello wolrd');
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
});
test('it shows the multiple modal messages', async function (assert) {
const modalIdOne = 'some-awesome-id-2';
const modalIdTwo = 'some-awesome-id-1';
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
authenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal';
authenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1';
authenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal';
authenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2';
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
assert.dom(CUSTOM_MESSAGES.modal(modalIdOne)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdOne)).hasText('Modal title 1');
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).hasText('hello world hello wolrd some link title');
assert.dom(CUSTOM_MESSAGES.modal(modalIdTwo)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdTwo)).hasText('Modal title 2');
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).hasText('here is a cool message');
});
test('it shows the multiple banner messages', async function (assert) {
const bannerIdOne = 'some-awesome-id-2';
const bannerIdTwo = 'some-awesome-id-1';
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
authenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner';
authenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1';
authenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner';
authenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2';
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdOne)).hasText('Banner title 1');
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdOne)).hasText('hello world hello wolrd');
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdTwo)).hasText('Banner title 2');
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdTwo)).hasText('here is a cool message');
});
});

View file

@ -4,538 +4,41 @@
*/
import { module, test } from 'qunit';
import { visit, currentURL, settled, fillIn, click, waitFor, currentRouteName } from '@ember/test-helpers';
import { visit, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'vault/tests/helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { selectChoose } from 'ember-power-select/test-support';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend';
import clientsHandlers from 'vault/mirage/handlers/clients';
import { formatNumber } from 'core/helpers/format-number';
import { pollCluster } from 'vault/tests/helpers/poll-cluster';
import { disableReplication } from 'vault/tests/helpers/replication';
import connectionPage from 'vault/tests/pages/secrets/backend/database/connection';
import { v4 as uuidv4 } from 'uuid';
import { runCmd, deleteEngineCmd, createNS, deleteNS } from 'vault/tests/helpers/commands';
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const authenticatedMessageResponse = {
request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
lease_id: '',
renewable: false,
lease_duration: 0,
data: {
key_info: {
'some-awesome-id-2': {
authenticated: true,
end_time: null,
link: {
'some link title': 'www.link.com',
},
message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
options: null,
start_time: '2024-01-04T08:00:00Z',
title: 'Banner title',
type: 'banner',
},
'some-awesome-id-1': {
authenticated: true,
end_time: null,
message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
options: null,
start_time: '2024-01-01T08:00:00Z',
title: 'Modal title',
type: 'modal',
},
},
keys: ['some-awesome-id-2', 'some-awesome-id-1'],
},
wrap_info: null,
warnings: null,
auth: null,
mount_type: '',
};
import Sinon from 'sinon';
module('Acceptance | landing page dashboard', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(async function () {
this.version = this.owner.lookup('service:version');
this.namespace = this.owner.lookup('service:namespace');
});
test('navigate to dashboard on login', async function (assert) {
assert.expect(1);
await login();
assert.strictEqual(currentURL(), '/vault/dashboard');
});
test('display the version number for the title', async function (assert) {
assert.expect(1);
await login();
await visit('/vault/dashboard');
const version = this.owner.lookup('service:version');
// Since we're using mirage, version is mocked static value
const versionText = version.isEnterprise
? `Vault ${version.versionDisplay} root`
: `Vault ${version.versionDisplay}`;
const versionText = this.version.isEnterprise
? `Vault ${this.version.versionDisplay} root`
: `Vault ${this.version.versionDisplay}`;
assert.dom(DASHBOARD.cardHeader('Vault version')).hasText(versionText);
});
module('secrets engines card', function (hooks) {
hooks.beforeEach(async function () {
await login();
});
test('shows a secrets engine card', async function (assert) {
assert.expect(1);
await mountSecrets.enable('pki', 'pki');
await settled();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.cardHeader('Secrets engines')).hasText('Secrets engines');
// cleanup engine mount
await runCmd(deleteEngineCmd('pki'));
});
test('it adds disabled css styling to unsupported secret engines', async function (assert) {
assert.expect(1);
await mountSecrets.enable('nomad', 'nomad');
await settled();
await visit('/vault/dashboard');
assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist();
// cleanup engine mount
await runCmd(deleteEngineCmd('nomad'));
});
});
module('configuration details card', function (hooks) {
hooks.beforeEach(async function () {
this.data = {
api_addr: 'http://127.0.0.1:8200',
cache_size: 0,
cluster_addr: 'https://127.0.0.1:8201',
cluster_cipher_suites: '',
cluster_name: '',
default_lease_ttl: 0,
default_max_request_duration: 0,
detect_deadlocks: '',
disable_cache: false,
disable_clustering: false,
disable_indexing: false,
disable_mlock: true,
disable_performance_standby: false,
disable_printable_check: false,
disable_sealwrap: false,
disable_sentinel_trace: false,
enable_response_header_hostname: false,
enable_response_header_raft_node_id: false,
enable_ui: true,
experiments: null,
introspection_endpoint: false,
listeners: [
{
config: {
address: '0.0.0.0:8200',
cluster_address: '0.0.0.0:8201',
tls_disable: true,
},
type: 'tcp',
},
],
log_format: '',
log_level: 'debug',
log_requests_level: '',
max_lease_ttl: '48h',
pid_file: '',
plugin_directory: '',
plugin_file_permissions: 0,
plugin_file_uid: 0,
raw_storage_endpoint: true,
seals: [
{
disabled: false,
type: 'shamir',
},
],
storage: {
cluster_addr: 'https://127.0.0.1:8201',
disable_clustering: false,
raft: {
max_entry_size: '',
},
redirect_addr: 'http://127.0.0.1:8200',
type: 'raft',
},
telemetry: {
add_lease_metrics_namespace_labels: false,
circonus_api_app: '',
circonus_api_token: '',
circonus_api_url: '',
circonus_broker_id: '',
circonus_broker_select_tag: '',
circonus_check_display_name: '',
circonus_check_force_metric_activation: '',
circonus_check_id: '',
circonus_check_instance_id: '',
circonus_check_search_tag: '',
circonus_check_tags: '',
circonus_submission_interval: '',
circonus_submission_url: '',
disable_hostname: true,
dogstatsd_addr: '',
dogstatsd_tags: null,
lease_metrics_epsilon: 3600000000000,
maximum_gauge_cardinality: 500,
metrics_prefix: '',
num_lease_metrics_buckets: 168,
prometheus_retention_time: 86400000000000,
stackdriver_debug_logs: false,
stackdriver_location: '',
stackdriver_namespace: '',
stackdriver_project_id: '',
statsd_address: '',
statsite_address: '',
usage_gauge_period: 5000000000,
},
};
this.server.get('sys/config/state/sanitized', () => ({
data: this.data,
wrap_info: null,
warnings: null,
auth: null,
}));
});
test('hides the configuration details card on a non-root namespace enterprise version', async function (assert) {
assert.expect(3);
await login();
await visit('/vault/dashboard');
const version = this.owner.lookup('service:version');
assert.true(version.isEnterprise, 'vault is enterprise');
assert.dom(DASHBOARD.cardName('configuration-details')).exists();
await runCmd(createNS('world'), false);
await visit('/vault/dashboard?namespace=world');
assert.dom(DASHBOARD.cardName('configuration-details')).doesNotExist();
// navigate to "root" before deleting
await visit('vault/dashboard');
// clean up namespace pollution
await runCmd(deleteNS('world'));
});
test('shows the configuration details card', async function (assert) {
assert.expect(8);
await login();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.cardHeader('configuration')).hasText('Configuration details');
assert
.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('api_addr'))
.hasText('http://127.0.0.1:8200');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('default_lease_ttl')).hasText('0');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('max_lease_ttl')).hasText('2 days');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); // tls_disable=true
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_format')).hasText('None');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_level')).hasText('debug');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('type')).hasText('raft');
});
test('it should show tls as enabled if tls_disable, tls_cert_file and tls_key_file are in the config', async function (assert) {
assert.expect(1);
this.data.listeners[0].config.tls_disable = false;
this.data.listeners[0].config.tls_cert_file = './cert.pem';
this.data.listeners[0].config.tls_key_file = './key.pem';
await login();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
});
test('it should show tls as enabled if only cert and key exist in config', async function (assert) {
assert.expect(1);
delete this.data.listeners[0].config.tls_disable;
this.data.listeners[0].config.tls_cert_file = './cert.pem';
this.data.listeners[0].config.tls_key_file = './key.pem';
await login();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
});
test('it should show tls as disabled if there is no tls information in the config', async function (assert) {
assert.expect(1);
this.data.listeners = [];
await login();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled');
});
});
module('quick actions card', function (hooks) {
hooks.beforeEach(async function () {
await login();
});
test('shows the default state of the quick actions card', async function (assert) {
assert.expect(1);
assert.dom(DASHBOARD.emptyState('no-mount-selected')).exists();
});
test('shows the correct actions and links associated with pki', async function (assert) {
assert.expect(12);
const backend = 'pki-dashboard';
await mountSecrets.enable('pki', backend);
await runCmd([
`write ${backend}/roles/some-role \
issuer_ref="default" \
allowed_domains="example.com" \
allow_subdomains=true \
max_ttl="720h"`,
]);
await runCmd([`write ${backend}/root/generate/internal issuer_name="Hashicorp" common_name="Hello"`]);
await settled();
await visit('/vault/dashboard');
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'Issue certificate');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
await selectChoose(DASHBOARD.searchSelect('params'), 'some-role');
assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 });
await click(DASHBOARD.actionButton('Issue leaf certificate'));
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.roles.role.generate');
await visit('/vault/dashboard');
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'View certificate');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Certificate serial number');
assert.dom(DASHBOARD.actionButton('View certificate')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('View certificate'));
assert.strictEqual(
currentRouteName(),
'vault.cluster.secrets.backend.pki.certificates.certificate.details'
);
await visit('/vault/dashboard');
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'View issuer');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Issuer');
assert.dom(DASHBOARD.actionButton('View issuer')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('View issuer'));
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.pki.issuers.issuer.details');
// cleanup engine mount
await runCmd(deleteEngineCmd(backend));
});
const newConnection = async (backend, plugin = 'mongodb-database-plugin') => {
const name = `connection-${Date.now()}`;
await connectionPage.visitCreate({ backend });
await connectionPage.dbPlugin(plugin);
await connectionPage.name(name);
await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`);
await connectionPage.toggleVerify();
await click(GENERAL.submitButton);
await connectionPage.enable();
return name;
};
test('shows the correct actions and links associated with database', async function (assert) {
assert.expect(4);
const databaseBackend = `database-${uuidv4()}`;
await mountSecrets.enable('database', databaseBackend);
await newConnection(databaseBackend);
await runCmd([
`write ${databaseBackend}/roles/my-role \
db_name=mongodb-database-plugin \
creation_statements='{ "db": "admin", "roles": [{ "role": "readWrite" }, {"role": "read", "db": "foo"}] }' \
default_ttl="1h" \
max_ttl="24h`,
]);
await settled();
await visit('/vault/dashboard');
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), databaseBackend);
await fillIn(DASHBOARD.selectEl, 'Generate credentials for database');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
assert.dom(DASHBOARD.actionButton('Generate credentials')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('Generate credentials'));
assert.strictEqual(currentRouteName(), 'vault.cluster.secrets.backend.credentials');
await runCmd(deleteEngineCmd(databaseBackend));
});
test('does not show kv1 mounts', async function (assert) {
assert.expect(1);
// delete before in case you are rerunning the test and it fails without deleting
await runCmd(deleteEngineCmd('kv1'));
await runCmd([`write sys/mounts/kv1 type=kv`]);
await settled();
await visit('/vault/dashboard');
await clickTrigger('#type-to-select-a-mount');
assert
.dom('.ember-power-select-option')
.doesNotHaveTextContaining('kv1', 'dropdown does not show kv1 mount');
await runCmd(deleteEngineCmd('kv1'));
});
});
module('client counts card enterprise', function (hooks) {
hooks.beforeEach(async function () {
clientsHandlers(this.server);
this.store = this.owner.lookup('service:store');
await login();
this.response = await this.store.findRecord('clients/activity', 'clients/activity');
});
test('shows the client count card for enterprise', async function (assert) {
const version = this.owner.lookup('service:version');
assert.true(version.isEnterprise, 'version is enterprise');
assert.strictEqual(currentURL(), '/vault/dashboard');
assert.dom(DASHBOARD.cardName('client-count')).exists();
assert.dom('[data-test-client-count-title]').hasText('Client count');
await waitFor('[data-test-stat-text="Total"] .stat-label');
assert.dom('[data-test-stat-text="Total"] .stat-label').hasText('Total');
assert
.dom('[data-test-stat-text="Total"] .stat-value')
.hasText(formatNumber([this.response.total.clients]));
assert.dom('[data-test-stat-text="New"] .stat-label').hasText('New');
assert
.dom('[data-test-stat-text="New"] .stat-text')
.hasText('The number of clients new to Vault in the current month.');
assert
.dom('[data-test-stat-text="New"] .stat-value')
.hasText(formatNumber([this.response.byMonth.lastObject.new_clients.clients]));
assert
.dom(`${GENERAL.flashMessage}.is-info`)
.doesNotExist('Does not show warning about client count estimations');
});
});
module('replication card enterprise', function (hooks) {
hooks.beforeEach(async function () {
await login();
await settled();
await disableReplication('dr');
await settled();
await disableReplication('performance');
await settled();
});
test('shows the replication card empty state in enterprise version', async function (assert) {
await visit('/vault/dashboard');
const version = this.owner.lookup('service:version');
assert.true(version.isEnterprise, 'vault is enterprise');
assert.dom(DASHBOARD.emptyState('replication')).exists();
assert.dom(DASHBOARD.emptyStateTitle('replication')).hasText('Replication not set up');
assert
.dom(DASHBOARD.emptyStateMessage('replication'))
.hasText('Data will be listed here. Enable a primary replication cluster to get started.');
assert.dom(DASHBOARD.emptyStateActions('replication')).hasText('Enable replication');
});
test('hides the replication card on a non-root namespace enterprise version', async function (assert) {
await visit('/vault/dashboard');
const version = this.owner.lookup('service:version');
assert.true(version.isEnterprise, 'vault is enterprise');
assert.dom(DASHBOARD.cardName('replication')).exists();
await runCmd(createNS('blah'), false);
await visit('/vault/dashboard?namespace=blah');
assert.dom(DASHBOARD.cardName('replication')).doesNotExist();
// navigate to "root" before deleting
await visit('vault/dashboard');
// clean up namespace pollution
await runCmd(deleteNS('blah'));
});
test('it should show replication status if both dr and performance replication are enabled as features in enterprise', async function (assert) {
const version = this.owner.lookup('service:version');
assert.true(version.isEnterprise, 'vault is enterprise');
await visit('/vault/replication');
assert.strictEqual(currentURL(), '/vault/replication');
await click('[data-test-replication-type-select="performance"]');
await fillIn('[data-test-replication-cluster-mode-select]', 'primary');
await click(GENERAL.submitButton);
await pollCluster(this.owner);
await waitFor('[data-test-replication-dashboard]');
await visit('/vault/dashboard');
assert.dom(DASHBOARD.title('DR primary')).hasText('DR primary');
assert.dom(DASHBOARD.tooltipTitle('DR primary')).hasText('not set up');
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'x-circle')).exists();
assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary');
assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('running');
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'check-circle')).exists();
});
});
module('custom messages auth tests', function (hooks) {
hooks.beforeEach(function () {
return this.server.get('/sys/internal/ui/mounts', () => ({}));
});
test('it shows the alert banner and modal message', async function (assert) {
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
const modalId = 'some-awesome-id-1';
const alertId = 'some-awesome-id-2';
assert.dom(CUSTOM_MESSAGES.modal(modalId)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalId)).hasText('Modal title');
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).hasText('here is a cool message');
assert.dom(CUSTOM_MESSAGES.alertTitle(alertId)).hasText('Banner title');
assert.dom(CUSTOM_MESSAGES.alertDescription(alertId)).hasText('hello world hello wolrd');
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
});
test('it shows the multiple modal messages', async function (assert) {
const modalIdOne = 'some-awesome-id-2';
const modalIdTwo = 'some-awesome-id-1';
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
authenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal';
authenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1';
authenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal';
authenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2';
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
assert.dom(CUSTOM_MESSAGES.modal(modalIdOne)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdOne)).hasText('Modal title 1');
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).hasText('hello world hello wolrd some link title');
assert.dom(CUSTOM_MESSAGES.modal(modalIdTwo)).exists();
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdTwo)).hasText('Modal title 2');
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).exists();
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).hasText('here is a cool message');
});
test('it shows the multiple banner messages', async function (assert) {
const bannerIdOne = 'some-awesome-id-2';
const bannerIdTwo = 'some-awesome-id-1';
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
authenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner';
authenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1';
authenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner';
authenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2';
return authenticatedMessageResponse;
});
await visit('/vault/dashboard');
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdOne)).hasText('Banner title 1');
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdOne)).hasText('hello world hello wolrd');
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdTwo)).hasText('Banner title 2');
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdTwo)).hasText('here is a cool message');
});
test('hides the configuration details card on a non-root namespace enterprise version', async function (assert) {
// The route checks `inRootNamespace` so stub that return
const nsStub = Sinon.stub(this.namespace, 'inRootNamespace').get(() => false);
await login();
await visit('/vault/dashboard');
assert.dom(DASHBOARD.cardName('configuration-details')).doesNotExist();
nsStub.restore();
});
});

View file

@ -1,102 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, visit, fillIn, waitFor } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { Response } from 'miragejs';
import { ERROR_JWT_LOGIN } from 'vault/utils/auth-form-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
import { overrideResponse } from 'vault/tests/helpers/stubs';
module('Acceptance | jwt auth method', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
localStorage.clear(); // ensure that a token isn't stored otherwise visit('/vault/auth') will redirect to secrets
this.server.post(
'/auth/:path/oidc/auth_url',
() =>
new Response(
400,
{ 'Content-Type': 'application/json' },
JSON.stringify({ errors: [ERROR_JWT_LOGIN] })
)
);
this.server.get('/auth/foo/oidc/callback', () => ({
auth: { client_token: 'root' },
}));
});
test('it works correctly with default name and no role', async function (assert) {
assert.expect(6);
this.server.post('/auth/jwt/login', (schema, req) => {
const { jwt, role } = JSON.parse(req.requestBody);
assert.true(true, 'request made to auth/jwt/login after submit');
assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body');
assert.strictEqual(role, undefined, 'role is not sent in body when not filled in');
return overrideResponse(403);
});
await visit('/vault/auth');
await fillIn(AUTH_FORM.selectMethod, 'jwt');
assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists');
assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists');
await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token');
await click(GENERAL.submitButton);
await waitFor(GENERAL.messageError);
assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied');
});
test('it works correctly with default name and a role', async function (assert) {
assert.expect(7);
this.server.post('/auth/jwt/login', (schema, req) => {
const { jwt, role } = JSON.parse(req.requestBody);
assert.ok(true, 'request made to auth/jwt/login after login');
assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body');
assert.strictEqual(role, 'some-role', 'role is sent in the body when filled in');
return overrideResponse(403);
});
await visit('/vault/auth');
await fillIn(AUTH_FORM.selectMethod, 'jwt');
assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists');
assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists');
await fillIn(GENERAL.inputByAttr('role'), 'some-role');
await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token');
assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists');
await click(GENERAL.submitButton);
await waitFor(GENERAL.messageError);
assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied');
});
test('it works correctly with custom endpoint and a role', async function (assert) {
assert.expect(6);
this.server.get('/sys/internal/ui/mounts', () => ({
data: {
auth: {
'test-jwt/': { description: '', options: {}, type: 'jwt' },
},
},
}));
this.server.post('/auth/test-jwt/login', (schema, req) => {
const { jwt, role } = JSON.parse(req.requestBody);
assert.ok(true, 'request made to auth/custom-jwt-login after login');
assert.strictEqual(jwt, 'my-test-jwt-token', 'JWT token is sent in body');
assert.strictEqual(role, 'some-role', 'role is sent in body when filled in');
return overrideResponse(403);
});
await visit('/vault/auth');
await click(AUTH_FORM.tabBtn('jwt'));
assert.dom(GENERAL.inputByAttr('role')).exists({ count: 1 }, 'Role input exists');
assert.dom(GENERAL.inputByAttr('jwt')).exists({ count: 1 }, 'JWT input exists');
await fillIn(GENERAL.inputByAttr('role'), 'some-role');
await fillIn(GENERAL.inputByAttr('jwt'), 'my-test-jwt-token');
await click(GENERAL.submitButton);
await waitFor(GENERAL.messageError);
assert.dom(GENERAL.messageError).hasText('Error Authentication failed: permission denied');
});
});

View file

@ -75,6 +75,7 @@ module('Acceptance | mfa-login', function (hooks) {
test('it should handle single constraint with passcode method', async function (assert) {
assert.expect(4);
await login('mfa-a');
await waitFor(GENERAL.title);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(MFA_SELECTORS.select()).doesNotExist('Select is hidden for single method');
assert.dom(MFA_SELECTORS.passcode()).exists({ count: 1 }, 'Single passcode input renders');
@ -117,6 +118,7 @@ module('Acceptance | mfa-login', function (hooks) {
test('it should handle single constraint with 2 push methods', async function (assert) {
assert.expect(4);
await login('mfa-d');
await waitFor(MFA_SELECTORS.mfaForm);
assert.dom(GENERAL.title).hasText('Verify your identity');
assert.dom(GENERAL.button('Verify with Okta')).exists('It renders button for Okta');
assert.dom(GENERAL.button('Verify with Duo')).exists('It renders button for Duo');
@ -127,6 +129,7 @@ module('Acceptance | mfa-login', function (hooks) {
test('it should handle single constraint with 1 passcode and 1 push method', async function (assert) {
assert.expect(3);
await login('mfa-e');
await waitFor(MFA_SELECTORS.mfaForm);
assert.dom(GENERAL.button('Verify with Okta')).exists('It renders button for Okta');
await click(GENERAL.button('Verify with TOTP'));
assert.dom(MFA_SELECTORS.passcode()).exists('Passcode input renders');

View file

@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, fillIn, visit, waitFor } from '@ember/test-helpers';
import { click, fillIn, waitFor } from '@ember/test-helpers';
import { logout } from 'vault/tests/helpers/auth/auth-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import {
@ -13,12 +13,12 @@ import {
triggerMessageEvent,
windowStub,
} from 'vault/tests/helpers/oidc-window-stub';
import { Response } from 'miragejs';
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { ERROR_MISSING_PARAMS, ERROR_POPUP_FAILED, ERROR_WINDOW_CLOSED } from 'vault/utils/auth-form-helpers';
import { getErrorResponse } from 'vault/tests/helpers/api/error-response';
import sinon from 'sinon';
import { RESPONSE_STUBS } from '../helpers/auth/response-stubs';
module('Acceptance | oidc auth method', function (hooks) {
setupApplicationTest(hooks);
@ -27,64 +27,20 @@ module('Acceptance | oidc auth method', function (hooks) {
hooks.beforeEach(function () {
this.openStub = windowStub();
this.setupMocks = (assert) => {
this.setupMocks = () => {
this.server.post('/auth/oidc/oidc/auth_url', () => ({
data: { auth_url: 'http://example.com' },
}));
// there was a bug that would result in the /auth/:path/login endpoint hit with an empty payload rather than lookup-self
// ensure that the correct endpoint is hit after the oidc callback
if (assert) {
this.server.get('/auth/token/lookup-self', (schema, req) => {
assert.ok(true, 'request made to auth/token/lookup-self after oidc callback');
return req.passthrough();
});
}
this.server.get(`/auth/oidc/oidc/callback`, () => RESPONSE_STUBS.oidc['oidc/callback']);
this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.oidc['lookup-self']);
};
this.server.get('/auth/oidc/oidc/callback', () => ({
auth: { client_token: 'root' },
}));
// ensure clean state
// Cannot use logout() here because it will hit the internal mount request before the mocks can interrupt it
window.localStorage.clear();
});
hooks.afterEach(async function () {
this.openStub.restore();
});
test('it should login with oidc when selected from auth methods dropdown', async function (assert) {
assert.expect(1);
this.setupMocks(assert);
await visit('/vault/auth');
await fillIn(AUTH_FORM.selectMethod, 'oidc');
triggerMessageEvent('oidc');
await click(GENERAL.submitButton);
});
test('it should login with oidc from listed auth mount tab', async function (assert) {
assert.expect(3);
this.setupMocks(assert); // assert count (1)
this.server.get('/sys/internal/ui/mounts', () => ({
data: {
auth: {
'test-path/': { description: '', options: {}, type: 'oidc' },
},
},
}));
// this assertion is hit twice, once on the initial visit to the login form, then again on "Sign in"
this.server.post('/auth/test-path/oidc/auth_url', () => {
assert.true(true, 'auth_url request made to correct non-standard mount path');
return { data: { auth_url: 'http://example.com' } };
});
await visit('/vault/auth');
triggerMessageEvent('oidc');
await click(GENERAL.submitButton);
// ensure clean state
// Cannot use logout() here because it will hit the internal mount request before the mocks can interrupt it
localStorage.clear();
});
// coverage for bug where token was selected as auth method for oidc and jwt
@ -105,44 +61,6 @@ module('Acceptance | oidc auth method', function (hooks) {
assert.dom(AUTH_FORM.selectMethod).hasValue('oidc', 'Previous auth method selected on logout');
});
test('it should fetch role when switching between oidc/jwt auth methods and changing the mount path', async function (assert) {
await logout();
let reqCount = 0;
this.server.post('/auth/:method/oidc/auth_url', (schema, req) => {
reqCount++;
const errors =
req.params.method === 'jwt' ? ['OIDC login is not configured for this mount'] : ['missing role'];
return new Response(400, {}, { errors });
});
await fillIn(AUTH_FORM.selectMethod, 'oidc');
assert.dom(GENERAL.inputByAttr('jwt')).doesNotExist('JWT Token input hidden for OIDC');
await fillIn(AUTH_FORM.selectMethod, 'jwt');
assert.dom(GENERAL.inputByAttr('jwt')).exists('JWT Token input renders for JWT configured method');
await click(AUTH_FORM.advancedSettings);
await fillIn(GENERAL.inputByAttr('path'), 'foo');
assert.strictEqual(reqCount, 3, 'Role is fetched when dependant values are changed');
});
test('it should display role fetch errors when signing in with OIDC', async function (assert) {
this.server.post('/auth/:method/oidc/auth_url', (schema, req) => {
const { role } = JSON.parse(req.requestBody);
const status = role ? 403 : 400;
const errors = role ? ['permission denied'] : ['missing role'];
return new Response(status, {}, { errors });
});
await logout();
await fillIn(AUTH_FORM.selectMethod, 'oidc');
await click(GENERAL.submitButton);
assert.dom(GENERAL.messageError).hasText('Error Authentication failed: Invalid role. Please try again.');
await fillIn(GENERAL.inputByAttr('role'), 'test');
await click(GENERAL.submitButton);
assert
.dom(GENERAL.messageError)
.hasText('Error Authentication failed: Error fetching role: permission denied');
});
// test case for https://github.com/hashicorp/vault/issues/12436
test('it should ignore messages sent from outside the app while waiting for oidc callback', async function (assert) {
assert.expect(3); // one for both message events (2) and one for callback request

View file

@ -143,62 +143,6 @@ module('Acceptance | secrets/mounts', function (hooks) {
.exists({ count: 1 }, 'renders only one instance of the engine');
});
test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) {
const enginePath = `kv-noUpdate-${this.uid}`;
const V2_POLICY = `
path "${enginePath}/*" {
capabilities = ["list","create","read","sudo","delete"]
}
path "sys/mounts/*"
{
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# List existing secrets engines.
path "sys/mounts"
{
capabilities = ["read"]
}
# Allow page to load after mount
path "sys/internal/ui/mounts/${enginePath}" {
capabilities = ["read"]
}
`;
await consoleComponent.toggle();
await consoleComponent.runCommands(
[
// delete any previous mount with same name
`delete sys/mounts/${enginePath}`,
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
'write -field=client_token auth/token/create policies=kv-v2-degrade',
],
false
);
await settled();
const userToken = consoleComponent.lastLogOutput;
await login(userToken);
// create the engine
await mountSecrets.visit();
await click(GENERAL.cardContainer('kv'));
await fillIn(GENERAL.inputByAttr('path'), enginePath);
await mountSecrets.setMaxVersion(101);
await click(GENERAL.submitButton);
assert
.dom('[data-test-flash-message]')
.containsText(
`You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.`
);
assert.strictEqual(
currentURL(),
`/vault/secrets/${enginePath}/kv/list`,
'After mounting, redirects to secrets list page'
);
await configPage.visit({ backend: enginePath });
await settled();
});
test('it should transition to mountable addon engine after mount success', async function (assert) {
// test supported backends that ARE ember engines (enterprise only engines are tested individually)
const addons = filterEnginesByMountCategory({ mountCategory: 'secret', isEnterprise: false }).filter(

View file

@ -78,6 +78,7 @@ const LOGIN_DATA = {
token: { token: 'mysupersecuretoken' },
username: { username: 'matilda', password: 'some-password' },
role: { role: 'some-dev' },
jwt: { role: 'some-dev', jwt: 'some-jwt-token' },
};
// maps auth type to login input data
export const AUTH_METHOD_LOGIN_DATA = {
@ -91,7 +92,7 @@ export const AUTH_METHOD_LOGIN_DATA = {
radius: LOGIN_DATA.username,
// role
oidc: LOGIN_DATA.role,
jwt: LOGIN_DATA.role,
jwt: LOGIN_DATA.jwt,
saml: LOGIN_DATA.role,
};

View file

@ -3,6 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import timestamp from 'core/utils/timestamp';
import { addDays } from 'date-fns';
/*
Authentication requests return authentication information in either the "auth" or "data" key,
depending on the authentication method.
@ -52,14 +55,14 @@ export const RESPONSE_STUBS = {
...BASE_REQUEST_DATA,
data: {
accessor: 'MkjSR78ducuarJ6ypCDbHhBp',
creation_time: 1749659696,
creation_time: timestamp.now().getTime(),
creation_ttl: 2764800,
display_name: 'jwt-ugaKjSEAKwQkiGh1rbnGkp39oCSe3LQ2@clients',
entity_id: 'b6061dc8-a19e-195e-43a8-43d37f4625dd',
expire_time: '2025-07-13T12:34:56.345108-04:00',
expire_time: addDays(timestamp.now(), 1),
explicit_max_ttl: 0,
id: 'hvs.myvaultgeneratedjwttoken',
issue_time: '2025-06-11T12:34:56.345113-04:00',
issue_time: timestamp.now().toISOString(),
meta: {
role: 'reader',
},
@ -145,14 +148,14 @@ export const RESPONSE_STUBS = {
...BASE_REQUEST_DATA,
data: {
accessor: 'ew50HTqF2xgsmaKIsdKpJtTc',
creation_time: 1749584514,
creation_time: timestamp.now().getTime(),
creation_ttl: 2764800,
display_name: 'my-oidc-google-oauth2|105299854624506884705',
entity_id: '18b57edf-acff-3e65-2ff2-6c772ce44924',
expire_time: '2025-07-12T15:41:54.961915-04:00',
expire_time: addDays(timestamp.now(), 1),
explicit_max_ttl: 0,
id: 'hvs.myvaultgeneratedoidctoken',
issue_time: '2025-06-10T15:41:54.961919-04:00',
issue_time: timestamp.now().toISOString(),
meta: {
role: 'reader',
},
@ -219,14 +222,14 @@ export const RESPONSE_STUBS = {
lease_duration: 0,
data: {
accessor: '3tl0hAUwdDJVduSEnIca7Tr6',
creation_time: 1744649084,
creation_time: timestamp.now().getTime(),
creation_ttl: 2764800,
display_name: 'token',
entity_id: '',
expire_time: '2025-05-16T09:44:44.837733-07:00',
expire_time: addDays(timestamp.now(), 1),
explicit_max_ttl: 0,
id: 'hvs.myvaultgeneratedtoken',
issue_time: '2025-04-14T09:44:44.837735-07:00',
issue_time: timestamp.now().toISOString(),
meta: null,
num_uses: 0,
orphan: false,
@ -289,14 +292,14 @@ export const RESPONSE_STUBS = {
...BASE_REQUEST_DATA,
data: {
accessor: 'H4fWtQaYX3aaEg1JIPSWiK9v',
creation_time: 1749585309,
creation_time: timestamp.now().getTime(),
creation_ttl: 1800,
display_name: 'saml-vaultuser@hashicorp.com',
entity_id: '81fc10e5-49a3-d0a2-9835-ac6b551ee266',
expire_time: '2025-06-10T16:25:09.246659-04:00',
expire_time: addDays(timestamp.now(), 1),
explicit_max_ttl: 0,
id: 'hvs.myvaultgeneratedsamltoken',
issue_time: '2025-06-10T15:55:09.246666-04:00',
issue_time: timestamp.now().toISOString(),
meta: {
role: 'dev',
},

View file

@ -38,7 +38,7 @@ export const GENERAL = {
/* ────── Menus & Lists ────── */
menuTrigger: '[data-test-popup-menu-trigger]',
menuItem: (name: string) => `[data-test-popup-menu="${name}"]`,
listItem: (label: string) => `[data-test-list-item="${label}"]`,
listItem: (label: string) => (label ? `[data-test-list-item="${label}"]` : '[data-test-list-item]'),
listItemLink: '[data-test-list-item-link]',
linkedBlock: (item: string) => `[data-test-linked-block="${item}"]`,

View file

@ -232,9 +232,12 @@ module('Integration | Component | auth | form template', function (hooks) {
});
// in the ent module to test ALL supported login methods
// iterating in tests should generally be avoided, but purposefully wanted to test the component
// renders as expected as auth types change
// iterating in tests should generally be avoided, but the for loop below is intentional
// to test the component renders as expected when auth types change
test('it selects each supported auth type and renders its form and relevant fields', async function (assert) {
const routerStub = sinon.stub(this.router, 'urlFor').returns('123-example.com');
// Setup so jwt auth mount is configured for jwt login and not oidc (jwt/oidc are interchangeable)
this.server.post(`/auth/jwt/oidc/auth_url`, () => overrideResponse(400, { errors: [ERROR_JWT_LOGIN] }));
const authMethodTypes = supportedTypes(true);
const totalFields = Object.values(AUTH_METHOD_LOGIN_DATA).reduce(
(sum, obj) => sum + Object.keys(obj).length,
@ -245,15 +248,8 @@ module('Integration | Component | auth | form template', function (hooks) {
await this.renderComponent();
for (const authType of authMethodTypes) {
let stub;
if (['oidc', 'jwt'].includes(authType)) {
stub = sinon.stub(this.router, 'urlFor').returns('123-example.com');
}
const loginData = AUTH_METHOD_LOGIN_DATA[authType];
const fields = Object.keys(loginData);
await fillIn(GENERAL.selectByAttr('auth type'), authType);
assert.dom(GENERAL.selectByAttr('auth type')).hasValue(authType), `${authType}: it selects type`;
assert.dom(AUTH_FORM.authForm(authType)).exists(`${authType}: it renders form component`);
@ -269,14 +265,11 @@ module('Integration | Component | auth | form template', function (hooks) {
const assertion = authType === 'token' ? 'doesNotExist' : 'exists';
assert.dom(GENERAL.inputByAttr('path'))[assertion](`${authType}: mount path input ${assertion}`);
fields.forEach((field) => {
Object.keys(loginData).forEach((field) => {
assert.dom(GENERAL.inputByAttr(field)).exists(`${authType}: ${field} input renders`);
});
if (stub) {
stub.restore();
}
}
routerStub.restore();
});
test('dropdown includes enterprise methods', async function (assert) {

View file

@ -19,9 +19,14 @@ import sinon from 'sinon';
const methodAuthenticationTests = (test) => {
test('it sets token data on login for default path', async function (assert) {
assert.expect(5);
const count = this.assertTokenLookup ? 6 : 5;
assert.expect(count);
// Setup
this.stubRequests();
if (this.assertTokenLookup) {
this.assertTokenLookup(assert);
}
// Render and log in
await this.renderComponent();
await fillIn(AUTH_FORM.selectMethod, this.authType);
@ -117,7 +122,12 @@ module('Integration | Component | auth | page | method authentication', function
overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })
);
this.server.post(`/auth/${this.path}/login`, () => this.response);
this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.jwt['lookup-self']);
};
this.assertTokenLookup = (assert) => {
this.server.get(`/auth/token/lookup-self`, () => {
assert.true(true, 'request made to auth/token/lookup-self after jwt login');
return RESPONSE_STUBS.jwt['lookup-self'];
});
};
});
@ -157,7 +167,14 @@ module('Integration | Component | auth | page | method authentication', function
return { data: { auth_url: 'http://dev-foo-bar.com' } };
});
this.server.get(`/auth/${this.path}/oidc/callback`, () => this.response);
this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.oidc['lookup-self']);
};
this.assertTokenLookup = (assert) => {
this.server.get(`/auth/token/lookup-self`, () => {
// there was a bug that would result in the /auth/:path/login endpoint hit with an empty payload rather than lookup-self
// ensure that the correct endpoint is hit after the oidc callback
assert.true(true, 'request made to auth/token/lookup-self after oidc callback');
return RESPONSE_STUBS.oidc['lookup-self'];
});
};
// additional OIDC setup
@ -275,7 +292,12 @@ module('Integration | Component | auth | page | method authentication', function
},
}));
this.server.post(`/auth/${this.path}/token`, () => this.response);
this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.saml['lookup-self']);
};
this.assertTokenLookup = (assert) => {
this.server.get(`/auth/token/lookup-self`, () => {
assert.true(true, 'request made to auth/token/lookup-self after saml token exchange and login');
return RESPONSE_STUBS.saml['lookup-self'];
});
};
this.windowStub = windowStub();
});

View file

@ -9,6 +9,7 @@ import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
module('Integration | Component | dashboard/overview', function (hooks) {
setupRenderingTest(hooks);
@ -20,19 +21,10 @@ module('Integration | Component | dashboard/overview', function (hooks) {
this.permissions = this.owner.lookup('service:permissions');
this.store = this.owner.lookup('service:store');
this.version = this.owner.lookup('service:version');
this.version.version = '1.13.1+ent';
this.version.type = 'enterprise';
this.isRootNamespace = true;
this.replication = {
dr: {
clusterId: '123',
state: 'running',
},
performance: {
clusterId: 'abc-1',
state: 'running',
isPrimary: true,
},
dr: { clusterId: '123', state: 'running' },
performance: { clusterId: 'abc-1', state: 'running', isPrimary: true },
};
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
@ -67,7 +59,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
};
this.refreshModel = () => {};
this.renderComponent = async () => {
return await render(
return render(
hbs`
<Dashboard::Overview
@secretsEngines={{this.secretsEngines}}
@ -100,7 +92,20 @@ module('Integration | Component | dashboard/overview', function (hooks) {
assert.dom(DASHBOARD.cardName('client-count')).doesNotExist();
});
module('client count and replication card', function () {
test('it renders the secrets engine card', async function (assert) {
assert.expect(3);
await this.renderComponent();
assert.dom(DASHBOARD.cardHeader('Secrets engines')).hasText('Secrets engines');
assert.dom(SES.secretPath('kv-1/')).exists();
assert.dom(SES.secretPath('kv-test/')).exists();
});
module('client count and replication card', function (hooks) {
hooks.beforeEach(function () {
this.version.version = '1.13.1+ent';
this.version.type = 'enterprise';
});
test('it should hide cards on community in root namespace', async function (assert) {
this.version.version = '1.13.1';
this.version.type = 'community';
@ -272,35 +277,35 @@ module('Integration | Component | dashboard/overview', function (hooks) {
});
});
module('learn more card', function () {
test('shows the learn more card on community', async function (assert) {
this.version.version = '1.13.1';
this.version.type = 'community';
await this.renderComponent();
test('it shows the learn more card on community', async function (assert) {
this.version.version = '1.13.1';
this.version.type = 'community';
await this.renderComponent();
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
assert
.dom('[data-test-learn-more-subtext]')
.hasText(
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
);
assert.dom('[data-test-learn-more-links] a').exists({ count: 3 });
});
test('shows the learn more card on enterprise', async function (assert) {
this.version.features = [
'Performance Replication',
'DR Replication',
'Namespaces',
'Transform Secrets Engine',
];
await this.renderComponent();
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
assert
.dom('[data-test-learn-more-subtext]')
.hasText(
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
);
assert.dom('[data-test-learn-more-links] a').exists({ count: 4 });
});
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
assert
.dom('[data-test-learn-more-subtext]')
.hasText(
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
);
assert.dom('[data-test-learn-more-links] a').exists({ count: 3 });
});
test('it shows the learn more card on enterprise', async function (assert) {
this.version.type = 'enterprise';
this.version.features = [
'Performance Replication',
'DR Replication',
'Namespaces',
'Transform Secrets Engine',
];
await this.renderComponent();
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
assert
.dom('[data-test-learn-more-subtext]')
.hasText(
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
);
assert.dom('[data-test-learn-more-links] a').exists({ count: 4 });
});
});

View file

@ -5,91 +5,40 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render, findAll } from '@ember/test-helpers';
import { render, findAll, click } from '@ember/test-helpers';
import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { hbs } from 'ember-cli-htmlbars';
import { fillIn } from '@ember/test-helpers';
import { selectChoose } from 'ember-power-select/test-support';
import sinon from 'sinon';
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
module('Integration | Component | dashboard/quick-actions-card', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'kubernetes_f3400dee',
path: 'kubernetes-test/',
type: 'kubernetes',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'database_f3400dee',
path: 'database-test/',
type: 'database',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'pki_i1234dd',
path: 'apki-test/',
type: 'pki',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'secrets_j2350ii',
path: 'secrets-test/',
type: 'kv',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'nomad_123hh',
path: 'nomad/',
type: 'nomad',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'pki_f3400dee',
path: 'pki-0-test/',
type: 'pki',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'pki_i1234dd',
path: 'pki-1-test/',
description: 'pki-1-path-description',
type: 'pki',
},
});
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'secrets_j2350ii',
path: 'kv-v2-test/',
options: {
version: 2,
},
type: 'kv',
},
});
this.secretsEngines = this.store.peekAll('secret-engine', {});
const store = this.owner.lookup('service:store');
const router = this.owner.lookup('service:router');
this.transitionStub = sinon.stub(router, 'transitionTo');
const models = [
{ accessor: 'kubernetes_f3400dee', path: 'kubernetes-test/', type: 'kubernetes' },
{ accessor: 'database_f3400dee', path: 'database-test/', type: 'database' },
{ accessor: 'pki_i1234dd', path: 'apki-test/', type: 'pki' },
{ accessor: 'secrets_j2350ii', path: 'secrets-test/', type: 'kv' },
{ accessor: 'nomad_123hh', path: 'nomad/', type: 'nomad' },
{ accessor: 'pki_f3400dee', path: 'pki-0-test/', type: 'pki' },
{ accessor: 'pki_i1234dd', path: 'pki-1-test/', description: 'pki-1-path-description', type: 'pki' },
{ accessor: 'secrets_j2350ii', path: 'kv-v2-test/', options: { version: 2 }, type: 'kv' },
{ accessor: 'secrets_j2350ii', path: 'kv-v1-test/', options: { version: 1 }, type: 'kv' },
];
models.forEach((modelData) => {
store.pushPayload('secret-engine', { modelName: 'secret-engine', data: modelData });
});
this.secretsEngines = store.peekAll('secret-engine', {});
this.renderComponent = () => {
return render(hbs`<Dashboard::QuickActionsCard @secretsEngines={{this.secretsEngines}} />`);
};
@ -103,6 +52,19 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks
});
});
hooks.afterEach(function () {
this.transitionStub.restore();
});
test('it does not include kvv1 mounts', async function (assert) {
await this.renderComponent();
await clickTrigger('#type-to-select-a-mount');
findAll('.ember-power-select-option').forEach((o) => {
assert.dom(o).doesNotHaveTextContaining('kv-v1-test');
});
});
test('it should show quick action empty state if no engine is selected', async function (assert) {
await this.renderComponent();
assert.dom('.title').hasText('Quick actions');
@ -110,29 +72,96 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks
assert.dom(DASHBOARD.emptyState('no-mount-selected')).exists({ count: 1 });
});
test('it should show correct actions for pki', async function (assert) {
test('it selects a pki role and issues a leaf certificate', async function (assert) {
const backend = 'pki-0-test';
this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['some-role'] } }));
await this.renderComponent();
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'pki-0-test');
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'Issue certificate');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
await fillIn(DASHBOARD.selectEl, 'Issue certificate');
assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 });
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
await selectChoose(DASHBOARD.searchSelect('params'), 'some-role');
assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 });
await click(DASHBOARD.actionButton('Issue leaf certificate'));
const [route, backendParam, roleParam] = this.transitionStub.lastCall.args;
assert.strictEqual(
route,
'vault.cluster.secrets.backend.pki.roles.role.generate',
'transition is called with expected route'
);
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
assert.strictEqual(roleParam, 'some-role', 'transition has expected role param');
});
test('it views a pki certificate', async function (assert) {
const backend = 'pki-0-test';
this.server.get(`/${backend}/certs`, () => ({ data: { keys: ['51:1c:39:42:ba'] } }));
await this.renderComponent();
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'View certificate');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Certificate serial number');
assert.dom(DASHBOARD.actionButton('View certificate')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('View certificate'));
const [route, backendParam, certParam] = this.transitionStub.lastCall.args;
assert.strictEqual(
route,
'vault.cluster.secrets.backend.pki.certificates.certificate.details',
'transition is called with expected route'
);
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
assert.strictEqual(certParam, '51:1c:39:42:ba', 'transition has expected cert param');
});
test('it views a pki issuer', async function (assert) {
const backend = 'pki-0-test';
this.server.get(`/${backend}/issuers`, () => {
return { data: { key_info: { '101709a1': { issuer_name: 'test' } }, keys: ['101709a1'] } };
});
await this.renderComponent();
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'View issuer');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Issuer');
assert.dom(DASHBOARD.actionButton('View issuer')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('View issuer'));
const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args;
assert.strictEqual(
route,
'vault.cluster.secrets.backend.pki.issuers.issuer.details',
'transition is called with expected route'
);
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
assert.strictEqual(issuerParam, '101709a1', 'transition has expected issuer param');
});
test('it should show correct actions for database', async function (assert) {
test('it selects a role and generates credentials for a database', async function (assert) {
const backend = 'database-test';
this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['my-role'] } }));
await this.renderComponent();
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'database-test');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
await fillIn(DASHBOARD.selectEl, 'Generate credentials for database');
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
assert.dom(DASHBOARD.actionButton('Generate credentials')).exists({ count: 1 });
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
await click(DASHBOARD.actionButton('Generate credentials'));
const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args;
assert.strictEqual(
route,
'vault.cluster.secrets.backend.credentials',
'transition is called with expected route'
);
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
assert.strictEqual(issuerParam, 'my-role', 'transition has expected role param');
});
test('it should show correct actions for kv', async function (assert) {
await this.renderComponent();
await clickTrigger('#type-to-select-a-mount');

View file

@ -20,11 +20,12 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
dr: {
clusterId: '123',
state: 'running',
mode: 'primary',
},
performance: {
clusterId: 'abc-1',
state: 'running',
isPrimary: true,
mode: 'primary',
},
};
this.version = {
@ -52,17 +53,9 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('running');
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'check-circle')).exists();
});
test('it should display replication information if both dr and performance replication are enabled as features and only dr is setup', async function (assert) {
this.replication = {
dr: {
clusterId: '123',
state: 'running',
},
performance: {
clusterId: '',
isPrimary: true,
},
};
this.replication.performance = { mode: 'disabled' };
await render(
hbs`
<Dashboard::ReplicationCard
@ -77,13 +70,10 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'check-circle')).exists();
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'check-circle')).hasClass('has-text-success');
assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary');
assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('not set up');
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'x-circle')).exists();
assert
.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'x-circle'))
.hasClass('has-text-danger');
assert.dom(DASHBOARD.title('Performance')).hasText('Performance');
assert.dom(DASHBOARD.tooltipTitle('Performance')).hasText('not set up');
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance', 'x-circle')).exists();
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance', 'x-circle')).hasClass('has-text-danger');
});
test('it should display only dr replication information if vault version only has hasDRReplication', async function (assert) {
@ -124,11 +114,12 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
dr: {
clusterId: 'abc',
state: 'idle',
mode: 'primary',
},
performance: {
clusterId: 'def',
state: 'shutdown',
isPrimary: true,
mode: 'primary',
},
};
await render(
@ -158,10 +149,11 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
dr: {
clusterId: 'abc',
state: 'running',
mode: 'primary',
},
performance: {
clusterId: 'def',
isPrimary: true,
mode: 'primary',
},
};
await render(
@ -176,16 +168,7 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
assert.dom(DASHBOARD.title('DR primary')).hasText('DR primary');
assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary');
this.replication = {
dr: {
clusterId: 'abc',
state: 'running',
},
performance: {
clusterId: 'def',
isPrimary: false,
},
};
this.replication.performance.mode = 'secondary';
await render(
hbs`
<Dashboard::ReplicationCard

View file

@ -15,9 +15,6 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
});
test('it should hide show all button', async function (assert) {
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
@ -26,7 +23,9 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
type: 'kubernetes',
},
});
});
test('it should hide show all button', async function (assert) {
this.secretsEngines = this.store.peekAll('secret-engine', {});
await render(hbs`<Dashboard::SecretsEnginesCard @secretsEngines={{this.secretsEngines}} />`);
@ -39,6 +38,22 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
assert.dom('[data-test-secrets-engines-card-show-all]').doesNotExist();
});
test('it disables unsupported secret engines', async function (assert) {
this.store.pushPayload('secret-engine', {
modelName: 'secret-engine',
data: {
accessor: 'nomad_f3400dee',
path: 'nomad-test/',
type: 'nomad',
},
});
this.secretsEngines = this.store.peekAll('secret-engine', {});
await render(hbs`<Dashboard::SecretsEnginesCard @secretsEngines={{this.secretsEngines}} />`);
assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist();
assert.dom(SES.secretPath('nomad-test/')).hasClass('has-text-grey');
});
module('secrets engines with 5 or more enabled', function (hooks) {
hooks.beforeEach(function () {
this.store.pushPayload('secret-engine', {

View file

@ -0,0 +1,147 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
module('Integration | Component | dashboard/vault-configuration-details-card', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.data = {
api_addr: 'http://127.0.0.1:8200',
cache_size: 0,
cluster_addr: 'https://127.0.0.1:8201',
cluster_cipher_suites: '',
cluster_name: '',
default_lease_ttl: 0,
default_max_request_duration: 0,
detect_deadlocks: '',
disable_cache: false,
disable_clustering: false,
disable_indexing: false,
disable_mlock: true,
disable_performance_standby: false,
disable_printable_check: false,
disable_sealwrap: false,
disable_sentinel_trace: false,
enable_response_header_hostname: false,
enable_response_header_raft_node_id: false,
enable_ui: true,
experiments: null,
introspection_endpoint: false,
listeners: [
{
config: {
address: '0.0.0.0:8200',
cluster_address: '0.0.0.0:8201',
tls_disable: true,
},
type: 'tcp',
},
],
log_format: '',
log_level: 'debug',
log_requests_level: '',
max_lease_ttl: '48h',
pid_file: '',
plugin_directory: '',
plugin_file_permissions: 0,
plugin_file_uid: 0,
raw_storage_endpoint: true,
seals: [
{
disabled: false,
type: 'shamir',
},
],
storage: {
cluster_addr: 'https://127.0.0.1:8201',
disable_clustering: false,
raft: {
max_entry_size: '',
},
redirect_addr: 'http://127.0.0.1:8200',
type: 'raft',
},
telemetry: {
add_lease_metrics_namespace_labels: false,
circonus_api_app: '',
circonus_api_token: '',
circonus_api_url: '',
circonus_broker_id: '',
circonus_broker_select_tag: '',
circonus_check_display_name: '',
circonus_check_force_metric_activation: '',
circonus_check_id: '',
circonus_check_instance_id: '',
circonus_check_search_tag: '',
circonus_check_tags: '',
circonus_submission_interval: '',
circonus_submission_url: '',
disable_hostname: true,
dogstatsd_addr: '',
dogstatsd_tags: null,
lease_metrics_epsilon: 3600000000000,
maximum_gauge_cardinality: 500,
metrics_prefix: '',
num_lease_metrics_buckets: 168,
prometheus_retention_time: 86400000000000,
stackdriver_debug_logs: false,
stackdriver_location: '',
stackdriver_namespace: '',
stackdriver_project_id: '',
statsd_address: '',
statsite_address: '',
usage_gauge_period: 5000000000,
},
};
this.renderComponent = () => {
return render(hbs`<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{this.data}} />`);
};
});
test('it renders configuration details', async function (assert) {
await this.renderComponent();
assert.dom(DASHBOARD.cardHeader('configuration')).hasText('Configuration details');
assert
.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('api_addr'))
.hasText('http://127.0.0.1:8200');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('default_lease_ttl')).hasText('0');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('max_lease_ttl')).hasText('2 days');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); // tls_disable=true
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_format')).hasText('None');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_level')).hasText('debug');
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('type')).hasText('raft');
});
test('it should show tls as enabled if tls_disable, tls_cert_file and tls_key_file are in the config', async function (assert) {
this.data.listeners[0].config.tls_disable = false;
this.data.listeners[0].config.tls_cert_file = './cert.pem';
this.data.listeners[0].config.tls_key_file = './key.pem';
await this.renderComponent();
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
});
test('it should show tls as enabled if only cert and key exist in config', async function (assert) {
delete this.data.listeners[0].config.tls_disable;
this.data.listeners[0].config.tls_cert_file = './cert.pem';
this.data.listeners[0].config.tls_key_file = './key.pem';
await this.renderComponent();
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
});
test('it should show tls as disabled if there is no tls information in the config', async function (assert) {
this.data.listeners = [];
await this.renderComponent();
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled');
});
});

View file

@ -5,9 +5,14 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, typeIn } from '@ember/test-helpers';
import { render, click, typeIn, fillIn } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
import {
allowAllCapabilitiesStub,
capabilitiesStub,
noopStub,
overrideResponse,
} from 'vault/tests/helpers/stubs';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
@ -23,9 +28,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
hooks.beforeEach(function () {
this.flashMessages = this.owner.lookup('service:flash-messages');
this.flashMessages.registerTypes(['success', 'danger']);
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
this.store = this.owner.lookup('service:store');
this.flashWarningSpy = sinon.spy(this.flashMessages, 'warning');
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
this.server.post('/sys/mounts/foo', noopStub());
this.onMountSuccess = sinon.spy();
@ -95,9 +99,12 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
);
});
module('KV engine', function () {
test('it shows KV specific fields when type is kv', async function (assert) {
module('KV engine', function (hooks) {
hooks.beforeEach(function () {
this.model.type = 'kv';
});
test('it shows KV specific fields when type is kv', async function (assert) {
await render(
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
@ -105,6 +112,39 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
assert.dom(GENERAL.inputByAttr('kv_config.cas_required')).exists('shows CAS required field');
assert.dom(GENERAL.inputByAttr('kv_config.delete_version_after')).exists('shows delete after field');
});
test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) {
assert.expect(6);
this.server.post('/sys/capabilities-self', () => capabilitiesStub('my-kv-engine/config', ['deny']));
this.server.post('/sys/mounts/my-kv-engine', (schema, req) => {
assert.true(true, 'it makes request to mount engine');
const payload = JSON.parse(req.requestBody);
const expected = {
config: { listing_visibility: 'hidden', force_no_cache: false },
options: { version: 2 },
type: 'kv',
};
assert.propEqual(payload, expected, 'mount request has expected payload');
return overrideResponse(204);
});
await render(
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
);
await fillIn(GENERAL.inputByAttr('path'), 'my-kv-engine');
await fillIn(GENERAL.inputByAttr('kv_config.max_versions'), '101');
await click(GENERAL.submitButton);
const [message] = this.flashWarningSpy.lastCall.args;
assert.strictEqual(
message,
`You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.`,
'it calls warning flash with expected message'
);
const [type, enginePath, useEngineRoute] = this.onMountSuccess.lastCall.args;
assert.strictEqual(type, 'kv', 'onMountSuccess called with expected type');
assert.strictEqual(enginePath, 'my-kv-engine', 'onMountSuccess called with expected engine path');
assert.true(useEngineRoute, 'onMountSuccess called useEngineRoute: true');
});
});
module('WIF secret engines', function () {

View file

@ -15,3 +15,5 @@ declare module '@icholy/duration' {
import Duration from '@icholy/duration';
export default Duration;
}
declare module 'vault/tests/helpers/vault-keys';