mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
* add parallel command * declare vault-keys module for test helpers * use mirage to make dropdown check more reliably * wait for inputs * attempt to stabilize dashboard tests in parallel * revert wait for inputs * move problem acceptance tests to integration tests * move more tests to integration * remove assert.expect() because there are no callback assertions * delete redundant acceptance tests * cleanup login state in afterEach hook * use mirage for login settings test * update other test based on mirage handler changes * throw some waitFor in there * revert waitFor * use mirage in shared-identity-test * remove storage cleanup from this pr * remove parallel..again * remove unnecessary auth login changes * add version to dashboard/overview test "it shows the learn more card on enterprise" * convert "version 2 with no update to config endpoint still allows mount of secret engine" to integration test * restart tests * Apply suggestion from @hellobontempo Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
b2c8b1b78d
commit
6886447328
30 changed files with 762 additions and 1132 deletions
|
|
@ -26,13 +26,16 @@
|
|||
data-test-type="dr-perf"
|
||||
>
|
||||
<Dashboard::ReplicationStateText
|
||||
@title="DR primary"
|
||||
@title="DR{{if (not-eq @replication.dr.mode 'disabled') (concat ' ' @replication.dr.mode)}}"
|
||||
@name="dr"
|
||||
@state={{@replication.dr.state}}
|
||||
@clusterStates={{cluster-states @replication.dr.state}}
|
||||
/>
|
||||
<Dashboard::ReplicationStateText
|
||||
@title="Performance {{if @replication.performance.isPrimary 'primary' 'secondary'}}"
|
||||
@title="Performance{{if
|
||||
(not-eq @replication.performance.mode 'disabled')
|
||||
(concat ' ' @replication.performance.mode)
|
||||
}}"
|
||||
@name="performance"
|
||||
@state={{if @replication.performance.clusterId @replication.performance.state "not set up"}}
|
||||
@clusterStates={{if @replication.performance.clusterId (cluster-states @replication.performance.state)}}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
class="list-item-row"
|
||||
@params={{array "login-settings.rule.details" rule.name}}
|
||||
@linkPrefix="vault.cluster.config-ui"
|
||||
data-test-rule={{rule.name}}
|
||||
data-test-list-item={{rule.name}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 } };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
112
ui/tests/acceptance/custom-messages-test.js
Normal file
112
ui/tests/acceptance/custom-messages-test.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'vault/tests/helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { CUSTOM_MESSAGES } from 'vault/tests/helpers/config-ui/message-selectors';
|
||||
|
||||
const authenticatedMessageResponse = {
|
||||
request_id: '664fbad0-fcd8-9023-4c5b-81a7962e9f4b',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
data: {
|
||||
key_info: {
|
||||
'some-awesome-id-2': {
|
||||
authenticated: true,
|
||||
end_time: null,
|
||||
link: {
|
||||
'some link title': 'www.link.com',
|
||||
},
|
||||
message: 'aGVsbG8gd29ybGQgaGVsbG8gd29scmQ=',
|
||||
options: null,
|
||||
start_time: '2024-01-04T08:00:00Z',
|
||||
title: 'Banner title',
|
||||
type: 'banner',
|
||||
},
|
||||
'some-awesome-id-1': {
|
||||
authenticated: true,
|
||||
end_time: null,
|
||||
message: 'aGVyZSBpcyBhIGNvb2wgbWVzc2FnZQ==',
|
||||
options: null,
|
||||
start_time: '2024-01-01T08:00:00Z',
|
||||
title: 'Modal title',
|
||||
type: 'modal',
|
||||
},
|
||||
},
|
||||
keys: ['some-awesome-id-2', 'some-awesome-id-1'],
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
mount_type: '',
|
||||
};
|
||||
|
||||
module('Acceptance | custom messages', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('it shows the alert banner and modal message', async function (assert) {
|
||||
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
|
||||
return authenticatedMessageResponse;
|
||||
});
|
||||
await visit('/vault/dashboard');
|
||||
|
||||
const modalId = 'some-awesome-id-1';
|
||||
const alertId = 'some-awesome-id-2';
|
||||
assert.dom(CUSTOM_MESSAGES.modal(modalId)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalTitle(modalId)).hasText('Modal title');
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalId)).hasText('here is a cool message');
|
||||
assert.dom(CUSTOM_MESSAGES.alertTitle(alertId)).hasText('Banner title');
|
||||
assert.dom(CUSTOM_MESSAGES.alertDescription(alertId)).hasText('hello world hello wolrd');
|
||||
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
|
||||
});
|
||||
|
||||
test('it shows the multiple modal messages', async function (assert) {
|
||||
const modalIdOne = 'some-awesome-id-2';
|
||||
const modalIdTwo = 'some-awesome-id-1';
|
||||
|
||||
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
|
||||
authenticatedMessageResponse.data.key_info[modalIdOne].type = 'modal';
|
||||
authenticatedMessageResponse.data.key_info[modalIdOne].title = 'Modal title 1';
|
||||
authenticatedMessageResponse.data.key_info[modalIdTwo].type = 'modal';
|
||||
authenticatedMessageResponse.data.key_info[modalIdTwo].title = 'Modal title 2';
|
||||
return authenticatedMessageResponse;
|
||||
});
|
||||
await visit('/vault/dashboard');
|
||||
|
||||
assert.dom(CUSTOM_MESSAGES.modal(modalIdOne)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdOne)).hasText('Modal title 1');
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdOne)).hasText('hello world hello wolrd some link title');
|
||||
assert.dom(CUSTOM_MESSAGES.modal(modalIdTwo)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalTitle(modalIdTwo)).hasText('Modal title 2');
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).exists();
|
||||
assert.dom(CUSTOM_MESSAGES.modalBody(modalIdTwo)).hasText('here is a cool message');
|
||||
});
|
||||
|
||||
test('it shows the multiple banner messages', async function (assert) {
|
||||
const bannerIdOne = 'some-awesome-id-2';
|
||||
const bannerIdTwo = 'some-awesome-id-1';
|
||||
|
||||
this.server.get('/sys/internal/ui/unauthenticated-messages', function () {
|
||||
authenticatedMessageResponse.data.key_info[bannerIdOne].type = 'banner';
|
||||
authenticatedMessageResponse.data.key_info[bannerIdOne].title = 'Banner title 1';
|
||||
authenticatedMessageResponse.data.key_info[bannerIdTwo].type = 'banner';
|
||||
authenticatedMessageResponse.data.key_info[bannerIdTwo].title = 'Banner title 2';
|
||||
return authenticatedMessageResponse;
|
||||
});
|
||||
await visit('/vault/dashboard');
|
||||
|
||||
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdOne)).hasText('Banner title 1');
|
||||
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdOne)).hasText('hello world hello wolrd');
|
||||
assert.dom(CUSTOM_MESSAGES.alertAction('link')).hasText('some link title');
|
||||
assert.dom(CUSTOM_MESSAGES.alertTitle(bannerIdTwo)).hasText('Banner title 2');
|
||||
assert.dom(CUSTOM_MESSAGES.alertDescription(bannerIdTwo)).hasText('here is a cool message');
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}"]`,
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { render } from '@ember/test-helpers';
|
|||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
|
||||
import { SECRET_ENGINE_SELECTORS as SES } from 'vault/tests/helpers/secret-engine/secret-engine-selectors';
|
||||
|
||||
module('Integration | Component | dashboard/overview', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
|
@ -20,19 +21,10 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||
this.permissions = this.owner.lookup('service:permissions');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.isRootNamespace = true;
|
||||
this.replication = {
|
||||
dr: {
|
||||
clusterId: '123',
|
||||
state: 'running',
|
||||
},
|
||||
performance: {
|
||||
clusterId: 'abc-1',
|
||||
state: 'running',
|
||||
isPrimary: true,
|
||||
},
|
||||
dr: { clusterId: '123', state: 'running' },
|
||||
performance: { clusterId: 'abc-1', state: 'running', isPrimary: true },
|
||||
};
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
|
|
@ -67,7 +59,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||
};
|
||||
this.refreshModel = () => {};
|
||||
this.renderComponent = async () => {
|
||||
return await render(
|
||||
return render(
|
||||
hbs`
|
||||
<Dashboard::Overview
|
||||
@secretsEngines={{this.secretsEngines}}
|
||||
|
|
@ -100,7 +92,20 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||
assert.dom(DASHBOARD.cardName('client-count')).doesNotExist();
|
||||
});
|
||||
|
||||
module('client count and replication card', function () {
|
||||
test('it renders the secrets engine card', async function (assert) {
|
||||
assert.expect(3);
|
||||
await this.renderComponent();
|
||||
assert.dom(DASHBOARD.cardHeader('Secrets engines')).hasText('Secrets engines');
|
||||
assert.dom(SES.secretPath('kv-1/')).exists();
|
||||
assert.dom(SES.secretPath('kv-test/')).exists();
|
||||
});
|
||||
|
||||
module('client count and replication card', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it should hide cards on community in root namespace', async function (assert) {
|
||||
this.version.version = '1.13.1';
|
||||
this.version.type = 'community';
|
||||
|
|
@ -272,35 +277,35 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||
});
|
||||
});
|
||||
|
||||
module('learn more card', function () {
|
||||
test('shows the learn more card on community', async function (assert) {
|
||||
this.version.version = '1.13.1';
|
||||
this.version.type = 'community';
|
||||
await this.renderComponent();
|
||||
test('it shows the learn more card on community', async function (assert) {
|
||||
this.version.version = '1.13.1';
|
||||
this.version.type = 'community';
|
||||
await this.renderComponent();
|
||||
|
||||
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
|
||||
assert
|
||||
.dom('[data-test-learn-more-subtext]')
|
||||
.hasText(
|
||||
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
|
||||
);
|
||||
assert.dom('[data-test-learn-more-links] a').exists({ count: 3 });
|
||||
});
|
||||
test('shows the learn more card on enterprise', async function (assert) {
|
||||
this.version.features = [
|
||||
'Performance Replication',
|
||||
'DR Replication',
|
||||
'Namespaces',
|
||||
'Transform Secrets Engine',
|
||||
];
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
|
||||
assert
|
||||
.dom('[data-test-learn-more-subtext]')
|
||||
.hasText(
|
||||
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
|
||||
);
|
||||
assert.dom('[data-test-learn-more-links] a').exists({ count: 4 });
|
||||
});
|
||||
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
|
||||
assert
|
||||
.dom('[data-test-learn-more-subtext]')
|
||||
.hasText(
|
||||
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
|
||||
);
|
||||
assert.dom('[data-test-learn-more-links] a').exists({ count: 3 });
|
||||
});
|
||||
|
||||
test('it shows the learn more card on enterprise', async function (assert) {
|
||||
this.version.type = 'enterprise';
|
||||
this.version.features = [
|
||||
'Performance Replication',
|
||||
'DR Replication',
|
||||
'Namespaces',
|
||||
'Transform Secrets Engine',
|
||||
];
|
||||
await this.renderComponent();
|
||||
assert.dom('[data-test-learn-more-title]').hasText('Learn more');
|
||||
assert
|
||||
.dom('[data-test-learn-more-subtext]')
|
||||
.hasText(
|
||||
'Explore the features of Vault and learn advance practices with the following tutorials and documentation.'
|
||||
);
|
||||
assert.dom('[data-test-learn-more-links] a').exists({ count: 4 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,91 +5,40 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render, findAll } from '@ember/test-helpers';
|
||||
import { render, findAll, click } from '@ember/test-helpers';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { fillIn } from '@ember/test-helpers';
|
||||
import { selectChoose } from 'ember-power-select/test-support';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
|
||||
module('Integration | Component | dashboard/quick-actions-card', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'kubernetes_f3400dee',
|
||||
path: 'kubernetes-test/',
|
||||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'database_f3400dee',
|
||||
path: 'database-test/',
|
||||
type: 'database',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'pki_i1234dd',
|
||||
path: 'apki-test/',
|
||||
type: 'pki',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'secrets_j2350ii',
|
||||
path: 'secrets-test/',
|
||||
type: 'kv',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'nomad_123hh',
|
||||
path: 'nomad/',
|
||||
type: 'nomad',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'pki_f3400dee',
|
||||
path: 'pki-0-test/',
|
||||
type: 'pki',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'pki_i1234dd',
|
||||
path: 'pki-1-test/',
|
||||
description: 'pki-1-path-description',
|
||||
type: 'pki',
|
||||
},
|
||||
});
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'secrets_j2350ii',
|
||||
path: 'kv-v2-test/',
|
||||
options: {
|
||||
version: 2,
|
||||
},
|
||||
type: 'kv',
|
||||
},
|
||||
});
|
||||
|
||||
this.secretsEngines = this.store.peekAll('secret-engine', {});
|
||||
const store = this.owner.lookup('service:store');
|
||||
const router = this.owner.lookup('service:router');
|
||||
this.transitionStub = sinon.stub(router, 'transitionTo');
|
||||
|
||||
const models = [
|
||||
{ accessor: 'kubernetes_f3400dee', path: 'kubernetes-test/', type: 'kubernetes' },
|
||||
{ accessor: 'database_f3400dee', path: 'database-test/', type: 'database' },
|
||||
{ accessor: 'pki_i1234dd', path: 'apki-test/', type: 'pki' },
|
||||
{ accessor: 'secrets_j2350ii', path: 'secrets-test/', type: 'kv' },
|
||||
{ accessor: 'nomad_123hh', path: 'nomad/', type: 'nomad' },
|
||||
{ accessor: 'pki_f3400dee', path: 'pki-0-test/', type: 'pki' },
|
||||
{ accessor: 'pki_i1234dd', path: 'pki-1-test/', description: 'pki-1-path-description', type: 'pki' },
|
||||
{ accessor: 'secrets_j2350ii', path: 'kv-v2-test/', options: { version: 2 }, type: 'kv' },
|
||||
{ accessor: 'secrets_j2350ii', path: 'kv-v1-test/', options: { version: 1 }, type: 'kv' },
|
||||
];
|
||||
models.forEach((modelData) => {
|
||||
store.pushPayload('secret-engine', { modelName: 'secret-engine', data: modelData });
|
||||
});
|
||||
this.secretsEngines = store.peekAll('secret-engine', {});
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`<Dashboard::QuickActionsCard @secretsEngines={{this.secretsEngines}} />`);
|
||||
};
|
||||
|
|
@ -103,6 +52,19 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks
|
|||
});
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.transitionStub.restore();
|
||||
});
|
||||
|
||||
test('it does not include kvv1 mounts', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await clickTrigger('#type-to-select-a-mount');
|
||||
|
||||
findAll('.ember-power-select-option').forEach((o) => {
|
||||
assert.dom(o).doesNotHaveTextContaining('kv-v1-test');
|
||||
});
|
||||
});
|
||||
|
||||
test('it should show quick action empty state if no engine is selected', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom('.title').hasText('Quick actions');
|
||||
|
|
@ -110,29 +72,96 @@ module('Integration | Component | dashboard/quick-actions-card', function (hooks
|
|||
assert.dom(DASHBOARD.emptyState('no-mount-selected')).exists({ count: 1 });
|
||||
});
|
||||
|
||||
test('it should show correct actions for pki', async function (assert) {
|
||||
test('it selects a pki role and issues a leaf certificate', async function (assert) {
|
||||
const backend = 'pki-0-test';
|
||||
this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['some-role'] } }));
|
||||
await this.renderComponent();
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'pki-0-test');
|
||||
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
|
||||
await fillIn(DASHBOARD.selectEl, 'Issue certificate');
|
||||
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
|
||||
await fillIn(DASHBOARD.selectEl, 'Issue certificate');
|
||||
assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 });
|
||||
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
|
||||
await selectChoose(DASHBOARD.searchSelect('params'), 'some-role');
|
||||
assert.dom(DASHBOARD.actionButton('Issue leaf certificate')).exists({ count: 1 });
|
||||
await click(DASHBOARD.actionButton('Issue leaf certificate'));
|
||||
const [route, backendParam, roleParam] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.pki.roles.role.generate',
|
||||
'transition is called with expected route'
|
||||
);
|
||||
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
|
||||
assert.strictEqual(roleParam, 'some-role', 'transition has expected role param');
|
||||
});
|
||||
|
||||
test('it views a pki certificate', async function (assert) {
|
||||
const backend = 'pki-0-test';
|
||||
this.server.get(`/${backend}/certs`, () => ({ data: { keys: ['51:1c:39:42:ba'] } }));
|
||||
await this.renderComponent();
|
||||
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
|
||||
await fillIn(DASHBOARD.selectEl, 'View certificate');
|
||||
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
|
||||
assert.dom(DASHBOARD.subtitle('param')).hasText('Certificate serial number');
|
||||
assert.dom(DASHBOARD.actionButton('View certificate')).exists({ count: 1 });
|
||||
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
|
||||
await click(DASHBOARD.actionButton('View certificate'));
|
||||
const [route, backendParam, certParam] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.pki.certificates.certificate.details',
|
||||
'transition is called with expected route'
|
||||
);
|
||||
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
|
||||
assert.strictEqual(certParam, '51:1c:39:42:ba', 'transition has expected cert param');
|
||||
});
|
||||
|
||||
test('it views a pki issuer', async function (assert) {
|
||||
const backend = 'pki-0-test';
|
||||
this.server.get(`/${backend}/issuers`, () => {
|
||||
return { data: { key_info: { '101709a1': { issuer_name: 'test' } }, keys: ['101709a1'] } };
|
||||
});
|
||||
await this.renderComponent();
|
||||
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
|
||||
await fillIn(DASHBOARD.selectEl, 'View issuer');
|
||||
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
|
||||
assert.dom(DASHBOARD.subtitle('param')).hasText('Issuer');
|
||||
assert.dom(DASHBOARD.actionButton('View issuer')).exists({ count: 1 });
|
||||
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
|
||||
await click(DASHBOARD.actionButton('View issuer'));
|
||||
const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.pki.issuers.issuer.details',
|
||||
'transition is called with expected route'
|
||||
);
|
||||
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
|
||||
assert.strictEqual(issuerParam, '101709a1', 'transition has expected issuer param');
|
||||
});
|
||||
test('it should show correct actions for database', async function (assert) {
|
||||
|
||||
test('it selects a role and generates credentials for a database', async function (assert) {
|
||||
const backend = 'database-test';
|
||||
this.server.get(`/${backend}/roles`, () => ({ data: { keys: ['my-role'] } }));
|
||||
await this.renderComponent();
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), 'database-test');
|
||||
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
|
||||
|
||||
await selectChoose(DASHBOARD.searchSelect('secrets-engines'), backend);
|
||||
await fillIn(DASHBOARD.selectEl, 'Generate credentials for database');
|
||||
assert.dom(DASHBOARD.emptyState('quick-actions')).doesNotExist();
|
||||
assert.dom(DASHBOARD.subtitle('param')).hasText('Role to use');
|
||||
assert.dom(DASHBOARD.actionButton('Generate credentials')).exists({ count: 1 });
|
||||
await selectChoose(DASHBOARD.searchSelect('params'), '.ember-power-select-option', 0);
|
||||
await click(DASHBOARD.actionButton('Generate credentials'));
|
||||
const [route, backendParam, issuerParam] = this.transitionStub.lastCall.args;
|
||||
assert.strictEqual(
|
||||
route,
|
||||
'vault.cluster.secrets.backend.credentials',
|
||||
'transition is called with expected route'
|
||||
);
|
||||
assert.strictEqual(backendParam, backend, 'transition has expected backend param');
|
||||
assert.strictEqual(issuerParam, 'my-role', 'transition has expected role param');
|
||||
});
|
||||
|
||||
test('it should show correct actions for kv', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await clickTrigger('#type-to-select-a-mount');
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
dr: {
|
||||
clusterId: '123',
|
||||
state: 'running',
|
||||
mode: 'primary',
|
||||
},
|
||||
performance: {
|
||||
clusterId: 'abc-1',
|
||||
state: 'running',
|
||||
isPrimary: true,
|
||||
mode: 'primary',
|
||||
},
|
||||
};
|
||||
this.version = {
|
||||
|
|
@ -52,17 +53,9 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('running');
|
||||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'check-circle')).exists();
|
||||
});
|
||||
|
||||
test('it should display replication information if both dr and performance replication are enabled as features and only dr is setup', async function (assert) {
|
||||
this.replication = {
|
||||
dr: {
|
||||
clusterId: '123',
|
||||
state: 'running',
|
||||
},
|
||||
performance: {
|
||||
clusterId: '',
|
||||
isPrimary: true,
|
||||
},
|
||||
};
|
||||
this.replication.performance = { mode: 'disabled' };
|
||||
await render(
|
||||
hbs`
|
||||
<Dashboard::ReplicationCard
|
||||
|
|
@ -77,13 +70,10 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'check-circle')).exists();
|
||||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'DR primary', 'check-circle')).hasClass('has-text-success');
|
||||
|
||||
assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary');
|
||||
|
||||
assert.dom(DASHBOARD.tooltipTitle('Performance primary')).hasText('not set up');
|
||||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'x-circle')).exists();
|
||||
assert
|
||||
.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance primary', 'x-circle'))
|
||||
.hasClass('has-text-danger');
|
||||
assert.dom(DASHBOARD.title('Performance')).hasText('Performance');
|
||||
assert.dom(DASHBOARD.tooltipTitle('Performance')).hasText('not set up');
|
||||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance', 'x-circle')).exists();
|
||||
assert.dom(DASHBOARD.tooltipIcon('dr-perf', 'Performance', 'x-circle')).hasClass('has-text-danger');
|
||||
});
|
||||
|
||||
test('it should display only dr replication information if vault version only has hasDRReplication', async function (assert) {
|
||||
|
|
@ -124,11 +114,12 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
dr: {
|
||||
clusterId: 'abc',
|
||||
state: 'idle',
|
||||
mode: 'primary',
|
||||
},
|
||||
performance: {
|
||||
clusterId: 'def',
|
||||
state: 'shutdown',
|
||||
isPrimary: true,
|
||||
mode: 'primary',
|
||||
},
|
||||
};
|
||||
await render(
|
||||
|
|
@ -158,10 +149,11 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
dr: {
|
||||
clusterId: 'abc',
|
||||
state: 'running',
|
||||
mode: 'primary',
|
||||
},
|
||||
performance: {
|
||||
clusterId: 'def',
|
||||
isPrimary: true,
|
||||
mode: 'primary',
|
||||
},
|
||||
};
|
||||
await render(
|
||||
|
|
@ -176,16 +168,7 @@ module('Integration | Component | dashboard/replication-card', function (hooks)
|
|||
assert.dom(DASHBOARD.title('DR primary')).hasText('DR primary');
|
||||
assert.dom(DASHBOARD.title('Performance primary')).hasText('Performance primary');
|
||||
|
||||
this.replication = {
|
||||
dr: {
|
||||
clusterId: 'abc',
|
||||
state: 'running',
|
||||
},
|
||||
performance: {
|
||||
clusterId: 'def',
|
||||
isPrimary: false,
|
||||
},
|
||||
};
|
||||
this.replication.performance.mode = 'secondary';
|
||||
await render(
|
||||
hbs`
|
||||
<Dashboard::ReplicationCard
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
|
|||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
});
|
||||
|
||||
test('it should hide show all button', async function (assert) {
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
|
|
@ -26,7 +23,9 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
|
|||
type: 'kubernetes',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it should hide show all button', async function (assert) {
|
||||
this.secretsEngines = this.store.peekAll('secret-engine', {});
|
||||
|
||||
await render(hbs`<Dashboard::SecretsEnginesCard @secretsEngines={{this.secretsEngines}} />`);
|
||||
|
|
@ -39,6 +38,22 @@ module('Integration | Component | dashboard/secrets-engines-card', function (hoo
|
|||
assert.dom('[data-test-secrets-engines-card-show-all]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it disables unsupported secret engines', async function (assert) {
|
||||
this.store.pushPayload('secret-engine', {
|
||||
modelName: 'secret-engine',
|
||||
data: {
|
||||
accessor: 'nomad_f3400dee',
|
||||
path: 'nomad-test/',
|
||||
type: 'nomad',
|
||||
},
|
||||
});
|
||||
this.secretsEngines = this.store.peekAll('secret-engine', {});
|
||||
|
||||
await render(hbs`<Dashboard::SecretsEnginesCard @secretsEngines={{this.secretsEngines}} />`);
|
||||
assert.dom('[data-test-secrets-engines-row="nomad"] [data-test-view]').doesNotExist();
|
||||
assert.dom(SES.secretPath('nomad-test/')).hasClass('has-text-grey');
|
||||
});
|
||||
|
||||
module('secrets engines with 5 or more enabled', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.store.pushPayload('secret-engine', {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
|
||||
|
||||
module('Integration | Component | dashboard/vault-configuration-details-card', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.data = {
|
||||
api_addr: 'http://127.0.0.1:8200',
|
||||
cache_size: 0,
|
||||
cluster_addr: 'https://127.0.0.1:8201',
|
||||
cluster_cipher_suites: '',
|
||||
cluster_name: '',
|
||||
default_lease_ttl: 0,
|
||||
default_max_request_duration: 0,
|
||||
detect_deadlocks: '',
|
||||
disable_cache: false,
|
||||
disable_clustering: false,
|
||||
disable_indexing: false,
|
||||
disable_mlock: true,
|
||||
disable_performance_standby: false,
|
||||
disable_printable_check: false,
|
||||
disable_sealwrap: false,
|
||||
disable_sentinel_trace: false,
|
||||
enable_response_header_hostname: false,
|
||||
enable_response_header_raft_node_id: false,
|
||||
enable_ui: true,
|
||||
experiments: null,
|
||||
introspection_endpoint: false,
|
||||
listeners: [
|
||||
{
|
||||
config: {
|
||||
address: '0.0.0.0:8200',
|
||||
cluster_address: '0.0.0.0:8201',
|
||||
tls_disable: true,
|
||||
},
|
||||
type: 'tcp',
|
||||
},
|
||||
],
|
||||
log_format: '',
|
||||
log_level: 'debug',
|
||||
log_requests_level: '',
|
||||
max_lease_ttl: '48h',
|
||||
pid_file: '',
|
||||
plugin_directory: '',
|
||||
plugin_file_permissions: 0,
|
||||
plugin_file_uid: 0,
|
||||
raw_storage_endpoint: true,
|
||||
seals: [
|
||||
{
|
||||
disabled: false,
|
||||
type: 'shamir',
|
||||
},
|
||||
],
|
||||
storage: {
|
||||
cluster_addr: 'https://127.0.0.1:8201',
|
||||
disable_clustering: false,
|
||||
raft: {
|
||||
max_entry_size: '',
|
||||
},
|
||||
redirect_addr: 'http://127.0.0.1:8200',
|
||||
type: 'raft',
|
||||
},
|
||||
telemetry: {
|
||||
add_lease_metrics_namespace_labels: false,
|
||||
circonus_api_app: '',
|
||||
circonus_api_token: '',
|
||||
circonus_api_url: '',
|
||||
circonus_broker_id: '',
|
||||
circonus_broker_select_tag: '',
|
||||
circonus_check_display_name: '',
|
||||
circonus_check_force_metric_activation: '',
|
||||
circonus_check_id: '',
|
||||
circonus_check_instance_id: '',
|
||||
circonus_check_search_tag: '',
|
||||
circonus_check_tags: '',
|
||||
circonus_submission_interval: '',
|
||||
circonus_submission_url: '',
|
||||
disable_hostname: true,
|
||||
dogstatsd_addr: '',
|
||||
dogstatsd_tags: null,
|
||||
lease_metrics_epsilon: 3600000000000,
|
||||
maximum_gauge_cardinality: 500,
|
||||
metrics_prefix: '',
|
||||
num_lease_metrics_buckets: 168,
|
||||
prometheus_retention_time: 86400000000000,
|
||||
stackdriver_debug_logs: false,
|
||||
stackdriver_location: '',
|
||||
stackdriver_namespace: '',
|
||||
stackdriver_project_id: '',
|
||||
statsd_address: '',
|
||||
statsite_address: '',
|
||||
usage_gauge_period: 5000000000,
|
||||
},
|
||||
};
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`<Dashboard::VaultConfigurationDetailsCard @vaultConfiguration={{this.data}} />`);
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders configuration details', async function (assert) {
|
||||
await this.renderComponent();
|
||||
assert.dom(DASHBOARD.cardHeader('configuration')).hasText('Configuration details');
|
||||
assert
|
||||
.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('api_addr'))
|
||||
.hasText('http://127.0.0.1:8200');
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('default_lease_ttl')).hasText('0');
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('max_lease_ttl')).hasText('2 days');
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled'); // tls_disable=true
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_format')).hasText('None');
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('log_level')).hasText('debug');
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('type')).hasText('raft');
|
||||
});
|
||||
|
||||
test('it should show tls as enabled if tls_disable, tls_cert_file and tls_key_file are in the config', async function (assert) {
|
||||
this.data.listeners[0].config.tls_disable = false;
|
||||
this.data.listeners[0].config.tls_cert_file = './cert.pem';
|
||||
this.data.listeners[0].config.tls_key_file = './key.pem';
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
|
||||
});
|
||||
|
||||
test('it should show tls as enabled if only cert and key exist in config', async function (assert) {
|
||||
delete this.data.listeners[0].config.tls_disable;
|
||||
this.data.listeners[0].config.tls_cert_file = './cert.pem';
|
||||
this.data.listeners[0].config.tls_key_file = './key.pem';
|
||||
|
||||
await this.renderComponent();
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Enabled');
|
||||
});
|
||||
|
||||
test('it should show tls as disabled if there is no tls information in the config', async function (assert) {
|
||||
this.data.listeners = [];
|
||||
await this.renderComponent();
|
||||
assert.dom(DASHBOARD.vaultConfigurationCard.configDetailsField('tls')).hasText('Disabled');
|
||||
});
|
||||
});
|
||||
|
|
@ -5,9 +5,14 @@
|
|||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render, click, typeIn } from '@ember/test-helpers';
|
||||
import { render, click, typeIn, fillIn } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { allowAllCapabilitiesStub, noopStub } from 'vault/tests/helpers/stubs';
|
||||
import {
|
||||
allowAllCapabilitiesStub,
|
||||
capabilitiesStub,
|
||||
noopStub,
|
||||
overrideResponse,
|
||||
} from 'vault/tests/helpers/stubs';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { ALL_ENGINES } from 'vault/utils/all-engines-metadata';
|
||||
|
||||
|
|
@ -23,9 +28,8 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
|
||||
hooks.beforeEach(function () {
|
||||
this.flashMessages = this.owner.lookup('service:flash-messages');
|
||||
this.flashMessages.registerTypes(['success', 'danger']);
|
||||
this.flashSuccessSpy = sinon.spy(this.flashMessages, 'success');
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.flashWarningSpy = sinon.spy(this.flashMessages, 'warning');
|
||||
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
||||
this.server.post('/sys/mounts/foo', noopStub());
|
||||
this.onMountSuccess = sinon.spy();
|
||||
|
|
@ -95,9 +99,12 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
module('KV engine', function () {
|
||||
test('it shows KV specific fields when type is kv', async function (assert) {
|
||||
module('KV engine', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.model.type = 'kv';
|
||||
});
|
||||
|
||||
test('it shows KV specific fields when type is kv', async function (assert) {
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
|
|
@ -105,6 +112,39 @@ module('Integration | Component | mount/secrets-engine-form', function (hooks) {
|
|||
assert.dom(GENERAL.inputByAttr('kv_config.cas_required')).exists('shows CAS required field');
|
||||
assert.dom(GENERAL.inputByAttr('kv_config.delete_version_after')).exists('shows delete after field');
|
||||
});
|
||||
|
||||
test('version 2 with no update to config endpoint still allows mount of secret engine', async function (assert) {
|
||||
assert.expect(6);
|
||||
this.server.post('/sys/capabilities-self', () => capabilitiesStub('my-kv-engine/config', ['deny']));
|
||||
this.server.post('/sys/mounts/my-kv-engine', (schema, req) => {
|
||||
assert.true(true, 'it makes request to mount engine');
|
||||
const payload = JSON.parse(req.requestBody);
|
||||
const expected = {
|
||||
config: { listing_visibility: 'hidden', force_no_cache: false },
|
||||
options: { version: 2 },
|
||||
type: 'kv',
|
||||
};
|
||||
assert.propEqual(payload, expected, 'mount request has expected payload');
|
||||
return overrideResponse(204);
|
||||
});
|
||||
|
||||
await render(
|
||||
hbs`<Mount::SecretsEngineForm @model={{this.model}} @onMountSuccess={{this.onMountSuccess}} />`
|
||||
);
|
||||
await fillIn(GENERAL.inputByAttr('path'), 'my-kv-engine');
|
||||
await fillIn(GENERAL.inputByAttr('kv_config.max_versions'), '101');
|
||||
await click(GENERAL.submitButton);
|
||||
const [message] = this.flashWarningSpy.lastCall.args;
|
||||
assert.strictEqual(
|
||||
message,
|
||||
`You do not have access to the config endpoint. The secret engine was mounted, but the configuration settings were not saved.`,
|
||||
'it calls warning flash with expected message'
|
||||
);
|
||||
const [type, enginePath, useEngineRoute] = this.onMountSuccess.lastCall.args;
|
||||
assert.strictEqual(type, 'kv', 'onMountSuccess called with expected type');
|
||||
assert.strictEqual(enginePath, 'my-kv-engine', 'onMountSuccess called with expected engine path');
|
||||
assert.true(useEngineRoute, 'onMountSuccess called useEngineRoute: true');
|
||||
});
|
||||
});
|
||||
|
||||
module('WIF secret engines', function () {
|
||||
|
|
|
|||
2
ui/types/global.d.ts
vendored
2
ui/types/global.d.ts
vendored
|
|
@ -15,3 +15,5 @@ declare module '@icholy/duration' {
|
|||
import Duration from '@icholy/duration';
|
||||
export default Duration;
|
||||
}
|
||||
|
||||
declare module 'vault/tests/helpers/vault-keys';
|
||||
|
|
|
|||
Loading…
Reference in a new issue