diff --git a/ui/lib/config-ui/addon/components/login-settings/page/list.js b/ui/lib/config-ui/addon/components/login-settings/page/list.js
index bef39a0fca..7e1ff592e2 100644
--- a/ui/lib/config-ui/addon/components/login-settings/page/list.js
+++ b/ui/lib/config-ui/addon/components/login-settings/page/list.js
@@ -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);
diff --git a/ui/mirage/factories/login-rule.js b/ui/mirage/factories/login-rule.js
index dda7875dbe..2290af46ac 100644
--- a/ui/mirage/factories/login-rule.js
+++ b/ui/mirage/factories/login-rule.js
@@ -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,
diff --git a/ui/mirage/handlers/custom-login.js b/ui/mirage/handlers/custom-login.js
index c8090f6bb1..9784694c72 100644
--- a/ui/mirage/handlers/custom-login.js
+++ b/ui/mirage/handlers/custom-login.js
@@ -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 } };
diff --git a/ui/mirage/scenarios/custom-login.js b/ui/mirage/scenarios/custom-login.js
index bafb297e19..1d29085ddf 100644
--- a/ui/mirage/scenarios/custom-login.js
+++ b/ui/mirage/scenarios/custom-login.js
@@ -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,
diff --git a/ui/package.json b/ui/package.json
index 40a7007491..c2d847385f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -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",
diff --git a/ui/tests/acceptance/access/identity/entities/index-test.js b/ui/tests/acceptance/access/identity/entities/index-test.js
index 4742c20215..9d3d38797f 100644
--- a/ui/tests/acceptance/access/identity/entities/index-test.js
+++ b/ui/tests/acceptance/access/identity/entities/index-test.js
@@ -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);
});
});
diff --git a/ui/tests/acceptance/access/identity/shared-identity-test.js b/ui/tests/acceptance/access/identity/shared-identity-test.js
index eb6eb33fc9..451c78a9eb 100644
--- a/ui/tests/acceptance/access/identity/shared-identity-test.js
+++ b/ui/tests/acceptance/access/identity/shared-identity-test.js
@@ -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');
});
}
});
diff --git a/ui/tests/acceptance/auth/auth-login-test.js b/ui/tests/acceptance/auth/auth-login-test.js
index e77ee25023..d1829eaa97 100644
--- a/ui/tests/acceptance/auth/auth-login-test.js
+++ b/ui/tests/acceptance/auth/auth-login-test.js
@@ -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();
});
diff --git a/ui/tests/acceptance/auth/login-settings-test.js b/ui/tests/acceptance/auth/login-settings-test.js
index 215054c7bc..e454f04a43 100644
--- a/ui/tests/acceptance/auth/login-settings-test.js
+++ b/ui/tests/acceptance/auth/login-settings-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/config-ui/login-settings-test.js b/ui/tests/acceptance/config-ui/login-settings-test.js
index 87a55cb80e..38bdac3ee5 100644
--- a/ui/tests/acceptance/config-ui/login-settings-test.js
+++ b/ui/tests/acceptance/config-ui/login-settings-test.js
@@ -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');
});
});
diff --git a/ui/tests/acceptance/custom-messages-test.js b/ui/tests/acceptance/custom-messages-test.js
new file mode 100644
index 0000000000..7be96669db
--- /dev/null
+++ b/ui/tests/acceptance/custom-messages-test.js
@@ -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');
+ });
+});
diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js
index 953f24818b..e2239832ab 100644
--- a/ui/tests/acceptance/dashboard-test.js
+++ b/ui/tests/acceptance/dashboard-test.js
@@ -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();
});
});
diff --git a/ui/tests/acceptance/jwt-auth-method-test.js b/ui/tests/acceptance/jwt-auth-method-test.js
deleted file mode 100644
index 9217f26563..0000000000
--- a/ui/tests/acceptance/jwt-auth-method-test.js
+++ /dev/null
@@ -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');
- });
-});
diff --git a/ui/tests/acceptance/mfa-login-test.js b/ui/tests/acceptance/mfa-login-test.js
index 124a7fb06d..4b213f3a01 100644
--- a/ui/tests/acceptance/mfa-login-test.js
+++ b/ui/tests/acceptance/mfa-login-test.js
@@ -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');
diff --git a/ui/tests/acceptance/oidc-auth-method-test.js b/ui/tests/acceptance/oidc-auth-method-test.js
index 3a57c66425..51e27cae3f 100644
--- a/ui/tests/acceptance/oidc-auth-method-test.js
+++ b/ui/tests/acceptance/oidc-auth-method-test.js
@@ -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
diff --git a/ui/tests/acceptance/secrets/mounts-test.js b/ui/tests/acceptance/secrets/mounts-test.js
index 54b819f8ba..817968192c 100644
--- a/ui/tests/acceptance/secrets/mounts-test.js
+++ b/ui/tests/acceptance/secrets/mounts-test.js
@@ -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(
diff --git a/ui/tests/helpers/auth/auth-helpers.ts b/ui/tests/helpers/auth/auth-helpers.ts
index 8e4825e1e1..ee42ff1c55 100644
--- a/ui/tests/helpers/auth/auth-helpers.ts
+++ b/ui/tests/helpers/auth/auth-helpers.ts
@@ -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,
};
diff --git a/ui/tests/helpers/auth/response-stubs.ts b/ui/tests/helpers/auth/response-stubs.ts
index 85c31b9545..4b2b5a0613 100644
--- a/ui/tests/helpers/auth/response-stubs.ts
+++ b/ui/tests/helpers/auth/response-stubs.ts
@@ -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',
},
diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts
index 59ca51c80d..ff4d218d28 100644
--- a/ui/tests/helpers/general-selectors.ts
+++ b/ui/tests/helpers/general-selectors.ts
@@ -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}"]`,
diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js
index 4a74b0d46e..8d6b53ae89 100644
--- a/ui/tests/integration/components/auth/form-template-test.js
+++ b/ui/tests/integration/components/auth/form-template-test.js
@@ -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) {
diff --git a/ui/tests/integration/components/auth/page/method-authentication-test.js b/ui/tests/integration/components/auth/page/method-authentication-test.js
index 2ddf48f7fc..520ddec90c 100644
--- a/ui/tests/integration/components/auth/page/method-authentication-test.js
+++ b/ui/tests/integration/components/auth/page/method-authentication-test.js
@@ -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();
});
diff --git a/ui/tests/integration/components/dashboard/overview-test.js b/ui/tests/integration/components/dashboard/overview-test.js
index d689b90298..1f71b6fd69 100644
--- a/ui/tests/integration/components/dashboard/overview-test.js
+++ b/ui/tests/integration/components/dashboard/overview-test.js
@@ -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`
{
+ store.pushPayload('secret-engine', { modelName: 'secret-engine', data: modelData });
+ });
+ this.secretsEngines = store.peekAll('secret-engine', {});
this.renderComponent = () => {
return render(hbs``);
};
@@ -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');
diff --git a/ui/tests/integration/components/dashboard/replication-card-test.js b/ui/tests/integration/components/dashboard/replication-card-test.js
index e8d9a154ba..acbdaa450f 100644
--- a/ui/tests/integration/components/dashboard/replication-card-test.js
+++ b/ui/tests/integration/components/dashboard/replication-card-test.js
@@ -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`
`);
@@ -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``);
+ 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', {
diff --git a/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js b/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js
new file mode 100644
index 0000000000..5f588fe5bf
--- /dev/null
+++ b/ui/tests/integration/components/dashboard/vault-configuration-details-card-test.js
@@ -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``);
+ };
+ });
+
+ 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');
+ });
+});
diff --git a/ui/tests/integration/components/mount/secrets-engine-form-test.js b/ui/tests/integration/components/mount/secrets-engine-form-test.js
index 99102dcc89..0923e5e586 100644
--- a/ui/tests/integration/components/mount/secrets-engine-form-test.js
+++ b/ui/tests/integration/components/mount/secrets-engine-form-test.js
@@ -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``
);
@@ -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``
+ );
+ 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 () {
diff --git a/ui/types/global.d.ts b/ui/types/global.d.ts
index f2c2d26101..59e57c6764 100644
--- a/ui/types/global.d.ts
+++ b/ui/types/global.d.ts
@@ -15,3 +15,5 @@ declare module '@icholy/duration' {
import Duration from '@icholy/duration';
export default Duration;
}
+
+declare module 'vault/tests/helpers/vault-keys';