-
-
-
{{rule.Name}}
+
+
+
+
+
+
+ {{rule.name}}
+
+
+ {{rule.namespace}}
+
+
+
+
+
+
+
+
+
+ View
+
+ {{#if (has-capability this.capabilities "delete" pathKey="customLogin" params=rule)}}
+ Delete
+ {{/if}}
+
+
-
{{rule.Namespace}}
-
-
+
{{/each}}
{{else}}
- * ```
+ *
* @param {array} loginRules - array of rule objects
*/
diff --git a/ui/lib/config-ui/addon/routes/login-settings/index.js b/ui/lib/config-ui/addon/routes/login-settings/index.js
index 73556e90c1..1c7d41e926 100644
--- a/ui/lib/config-ui/addon/routes/login-settings/index.js
+++ b/ui/lib/config-ui/addon/routes/login-settings/index.js
@@ -10,9 +10,18 @@ export default class LoginSettingsRoute extends Route {
@service api;
async model() {
- const res = await this.api.sys.uiLoginDefaultAuthList(true);
- const loginRules = this.api.keyInfoToArray({ keyInfo: res.keyInfo, keys: res.keys });
-
- return { loginRules };
+ try {
+ const res = await this.api.sys.uiLoginDefaultAuthList(true);
+ const loginRules = this.api.keyInfoToArray({ keyInfo: res.keyInfo, keys: res.keys });
+ return { loginRules };
+ } catch (e) {
+ const error = await this.api.parseError(e);
+ if (error.status === 404) {
+ // If no login settings exist, return an empty array to render the empty state
+ return { loginRules: [] };
+ }
+ // Otherwise fallback to the standard error template
+ throw error;
+ }
}
}
diff --git a/ui/mirage/handlers/custom-login.js b/ui/mirage/handlers/custom-login.js
index cb82141076..6f7b7602f2 100644
--- a/ui/mirage/handlers/custom-login.js
+++ b/ui/mirage/handlers/custom-login.js
@@ -45,7 +45,7 @@ export default function (server) {
});
// UNAUTHENTICATED READ ONLY for login form display logic
- server.get('sys/internal/ui/default-login-methods', (schema, req) => {
+ 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 namespace = !nsHeader ? '' : nsHeader;
diff --git a/ui/mirage/scenarios/custom-login.js b/ui/mirage/scenarios/custom-login.js
index 5fe4bf312e..cfeb372f99 100644
--- a/ui/mirage/scenarios/custom-login.js
+++ b/ui/mirage/scenarios/custom-login.js
@@ -12,11 +12,11 @@ export default function (server) {
disable_inheritance: true,
});
server.create('login-rule', {
- namespace: 'admin/',
+ namespace: 'admin',
default_auth_type: 'oidc',
backup_auth_types: ['token'],
});
- server.create('login-rule', { default_auth_type: 'jwt', backup_auth_types: null }); // namespace-2
+ server.create('login-rule', { default_auth_type: 'jwt', backup_auth_types: [] }); // namespace-2
server.create('login-rule', { default_auth_type: '', backup_auth_types: ['oidc', 'jwt'] }); // namespace-3
server.create('login-rule', { default_auth_type: '', backup_auth_types: ['token'] }); // namespace-4
}
diff --git a/ui/tests/acceptance/auth/auth-test.js b/ui/tests/acceptance/auth/auth-test.js
index 4414888577..ef4ae7fff8 100644
--- a/ui/tests/acceptance/auth/auth-test.js
+++ b/ui/tests/acceptance/auth/auth-test.js
@@ -16,7 +16,13 @@ import {
mountEngineCmd,
runCmd,
} from 'vault/tests/helpers/commands';
-import { login, loginMethod, loginNs, logout, VISIBLE_MOUNTS } from 'vault/tests/helpers/auth/auth-helpers';
+import {
+ login,
+ loginMethod,
+ loginNs,
+ logout,
+ SYS_INTERNAL_UI_MOUNTS,
+} from 'vault/tests/helpers/auth/auth-helpers';
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';
@@ -29,6 +35,17 @@ module('Acceptance | auth login form', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
+ test('it does not request login settings for community versions', async function (assert) {
+ assert.expect(1); // should only be one assertion because the stubbed mirage request should NOT be hit
+ this.owner.lookup('service:version').type = 'community';
+ this.server.get('/sys/internal/ui/default-auth-methods', () => {
+ // cannot throw error here because request errors are swallowed
+ assert.false(true, 'request made for login settings and it should not have been');
+ });
+ await visit('/vault/auth');
+ assert.strictEqual(currentURL(), '/vault/auth');
+ });
+
test('it selects auth method if "with" query param is a supported auth method', async function (assert) {
const backends = supportedAuthBackends();
assert.expect(backends.length);
@@ -38,6 +55,16 @@ module('Acceptance | auth login form', function (hooks) {
}
});
+ test('it selects auth method if "with" query param ends in an unencoded a slash', async function (assert) {
+ await visit('/vault/auth?with=userpass/');
+ assert.dom(AUTH_FORM.selectMethod).hasValue('userpass');
+ });
+
+ test('it selects auth method if "with" query param ends in an encoded slash and matches an auth type', async function (assert) {
+ await visit('/vault/auth?with=userpass%2F');
+ assert.dom(AUTH_FORM.selectMethod).hasValue('userpass');
+ });
+
test('it redirects if "with" query param is not a supported auth method', async function (assert) {
await visit('/vault/auth?with=fake');
assert.strictEqual(currentURL(), '/vault/auth', 'invalid query param is cleared');
@@ -74,7 +101,7 @@ module('Acceptance | auth login form', function (hooks) {
module('listing visibility', function (hooks) {
hooks.beforeEach(async function () {
this.server.get('/sys/internal/ui/mounts', () => {
- return { data: { auth: VISIBLE_MOUNTS } };
+ return { data: { auth: SYS_INTERNAL_UI_MOUNTS } };
});
await logout(); // clear local storage
});
@@ -84,7 +111,7 @@ module('Acceptance | auth login form', function (hooks) {
const expectedTabs = [
{ type: 'userpass', display: 'Userpass' },
{ type: 'oidc', display: 'OIDC' },
- { type: 'token', display: 'Token' },
+ { type: 'ldap', display: 'LDAP' },
];
await visit('/vault/auth');
await waitFor(AUTH_FORM.tabs);
@@ -129,9 +156,9 @@ module('Acceptance | auth login form', function (hooks) {
});
test('it selects type from dropdown if query param is NOT a visible mount, but is a supported method', async function (assert) {
- await visit('/vault/auth?with=ldap');
+ await visit('/vault/auth?with=token');
await waitFor(GENERAL.selectByAttr('auth type'));
- assert.dom(GENERAL.selectByAttr('auth type')).hasValue('ldap');
+ assert.dom(GENERAL.selectByAttr('auth type')).hasValue('token');
assert.dom(GENERAL.backButton).exists('it renders "Back" button because tabs do exist');
assert
.dom(AUTH_FORM.otherMethodsBtn)
@@ -357,9 +384,7 @@ module('Acceptance | auth login form', function (hooks) {
await visit('/vault/auth');
this.server.get('/sys/internal/ui/mounts', (_, req) => {
- // sometimes the namespace header is "X-Vault-Namespace" and other times "x-vault-namespace"...haven't figured out why
- const key = Object.keys(req.requestHeaders).find((k) => k.toLowerCase().includes('namespace'));
- assert.strictEqual(req.requestHeaders[key], 'admin', `${key}: header contains namespace`);
+ assert.strictEqual(req.requestHeaders['x-vault-namespace'], 'admin', 'header contains namespace');
req.passthrough();
});
await typeIn(GENERAL.inputByAttr('namespace'), 'admin');
diff --git a/ui/tests/acceptance/auth/login-settings-test.js b/ui/tests/acceptance/auth/login-settings-test.js
new file mode 100644
index 0000000000..5d1db9eaa2
--- /dev/null
+++ b/ui/tests/acceptance/auth/login-settings-test.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupApplicationTest } from 'ember-qunit';
+import { click, fillIn, typeIn, visit, waitFor } from '@ember/test-helpers';
+import { runCmd } from 'vault/tests/helpers/commands';
+import { login, logout, rootToken } from 'vault/tests/helpers/auth/auth-helpers';
+import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+
+// Auth form login settings
+// This feature has thorough integration test coverage so only testing a few scenarios and direct link functionality
+// Tests for read/list views are in ui/tests/acceptance/config-ui/login-settings-test.js
+module('Acceptance | Enterprise | auth form custom login settings', function (hooks) {
+ setupApplicationTest(hooks);
+ hooks.beforeEach(async function () {
+ await login();
+ await runCmd([
+ `write sys/namespaces/test-ns -force`,
+ `write test-ns/sys/namespaces/child -force`,
+ `write sys/config/ui/login/default-auth/root-rule backup_auth_types=token default_auth_type=okta disable_inheritance=false namespace=""`,
+ `write sys/config/ui/login/default-auth/ns-rule default_auth_type=ldap disable_inheritance=true namespace=test-ns`,
+ `write sys/auth/my-oidc type=oidc`,
+ `write sys/auth/my-oidc/tune listing_visibility="unauth"`,
+ ]);
+ return await logout();
+ });
+
+ hooks.afterEach(async function () {
+ // cleanup login rules
+ await visit('/vault/auth?with=token');
+ await fillIn(GENERAL.inputByAttr('token'), rootToken);
+ await click(AUTH_FORM.login);
+ await runCmd([
+ 'delete sys/config/ui/login/default-auth/root-rule',
+ 'delete sys/config/ui/login/default-auth/ns-rule',
+ 'delete sys/auth/my-oidc',
+ 'delete test-ns/sys/namespaces/child',
+ 'delete sys/namespaces/test-ns',
+ ]);
+ });
+
+ 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 click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.authForm('token')).exists('it renders backup method');
+ });
+
+ test('it renders login settings for namespaces', async function (assert) {
+ await visit('/vault/auth');
+ await fillIn(GENERAL.inputByAttr('namespace'), 'test-ns');
+ await waitFor(AUTH_FORM.authForm('ldap'));
+ assert.dom(AUTH_FORM.authForm('ldap')).exists('it renders default method');
+ assert.dom(AUTH_FORM.advancedSettings).exists();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist('it does not render alternate view');
+
+ // type in so that the namespace is "test-ns/child"
+ await typeIn(GENERAL.inputByAttr('namespace'), '/child');
+ await waitFor(AUTH_FORM.authForm('okta'));
+ assert
+ .dom(AUTH_FORM.authForm('okta'))
+ .exists('it inherits view from root namespace because "test-ns" settings are not inheritable');
+ });
+
+ test('it ignores login settings if query param references a visible mount path', async function (assert) {
+ await visit('/vault/auth?with=my-oidc%2F');
+ await waitFor(AUTH_FORM.tabBtn('oidc'));
+ assert
+ .dom(AUTH_FORM.tabBtn('oidc'))
+ .hasAttribute('aria-selected', 'true', 'it selects tab matching query param');
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(GENERAL.selectByAttr('auth type')).exists('dropdown renders as fallback view');
+ });
+
+ test('it ignores login settings if query param references a valid type', async function (assert) {
+ await visit('/vault/auth?with=userpass');
+ assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass', 'dropdown selects userpass');
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.tabBtn('oidc')).exists('it renders tabs on "Back" because visible mounts exist');
+ });
+});
diff --git a/ui/tests/acceptance/config-ui/login-settings-test.js b/ui/tests/acceptance/config-ui/login-settings-test.js
index 802a5d4e33..402634029d 100644
--- a/ui/tests/acceptance/config-ui/login-settings-test.js
+++ b/ui/tests/acceptance/config-ui/login-settings-test.js
@@ -9,74 +9,121 @@ import { click, visit, currentRouteName } from '@ember/test-helpers';
import { login } 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';
+const SELECTORS = {
+ rule: (name) => (name ? `[data-test-rule="${name}"]` : '[data-test-rule]'),
+ popupMenu: (name) => `[data-test-rule="${name}"] ${GENERAL.menuTrigger}`,
+};
+// read view for custom login settings
module('Acceptance | Enterprise | config-ui/login-settings', function (hooks) {
setupApplicationTest(hooks);
+ setupMirage(hooks);
hooks.beforeEach(async function () {
- await login();
-
- // create login rules
- await runCmd([
- `write sys/config/ui/login/default-auth/testRule backup_auth_types=userpass default_auth_type=okta disable_inheritance=false namespace=ns1`,
- 'write sys/config/ui/login/default-auth/testRule2 backup_auth_types=oidc default_auth_type=ldap disable_inheritance=true namespace=ns2',
- ]);
+ return await login();
});
- hooks.afterEach(async function () {
- await login();
-
- // cleanup login rules
- await runCmd([
- 'delete sys/config/ui/login/default-auth/testRule',
- 'delete sys/config/ui/login/default-auth/testRule2',
- ]);
- });
-
- test('fetched login rule list renders', async function (assert) {
- // Visit the login settings list index page
+ test('it renders empty state if no login settings exist', async function (assert) {
await visit('vault/config-ui/login-settings');
- // verify fetched rules are rendered in list
- assert.dom('.linked-block-item').exists({ count: 2 });
- // verify rule data namespaces render
- assert.dom('[data-test-rule-path="ns1/"]').exists();
- assert.dom('[data-test-rule-path="ns2/"]').exists();
+ assert.dom(GENERAL.emptyStateTitle).hasText('No UI login rules yet');
+ assert
+ .dom(GENERAL.emptyStateMessage)
+ .hasText(
+ 'Login rules can be used to select default and back up login methods and customize which methods display in the web UI login form. Available to be created via the CLI or HTTP API.'
+ );
});
- test('delete rule from list view', async function (assert) {
- // Visit the login settings list index page
+ test('it falls back error template if no permission', async function (assert) {
+ this.server.get('/sys/config/ui/login/default-auth', () => overrideResponse(403));
await visit('vault/config-ui/login-settings');
-
- await click(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 testRule.');
- assert.dom('[data-test-rule-name="testRule"]').doesNotExist();
+ assert.dom(GENERAL.pageError.error).hasText('Error permission denied');
});
- test('navigate to rule details page and renders rule data', async function (assert) {
- // visit individual rule page
- await visit('vault/config-ui/login-settings');
+ module('list, read and delete', function (hooks) {
+ hooks.beforeEach(async function () {
+ await login();
- await click(GENERAL.menuTrigger);
- await click(GENERAL.menuItem('view-rule'));
+ // create login rules
+ await runCmd([
+ `write sys/config/ui/login/default-auth/testRule backup_auth_types=userpass default_auth_type=okta disable_inheritance=false namespace=ns1`,
+ 'write sys/config/ui/login/default-auth/testRule2 backup_auth_types=oidc default_auth_type=ldap disable_inheritance=true namespace=ns2',
+ ]);
+ });
- // 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'
- );
+ hooks.afterEach(async function () {
+ await login();
- // verify fetched rule data is rendered
- assert.dom(GENERAL.infoRowValue('Name')).hasText('testRule');
- assert.dom(GENERAL.infoRowValue('Namespace')).hasText('ns1/');
- assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('userpass');
- assert.dom(GENERAL.infoRowValue('Inheritance')).hasText('true');
+ // cleanup login rules
+ await runCmd([
+ 'delete sys/config/ui/login/default-auth/testRule',
+ 'delete sys/config/ui/login/default-auth/testRule2',
+ ]);
+ });
+
+ 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('testRule')).hasText('testRule ns1/ Inheritance enabled');
+ assert.dom(SELECTORS.rule('testRule2')).hasText('testRule2 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('testRule'));
+ 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 testRule.');
+ assert.dom(SELECTORS.rule('testRule')).doesNotExist();
+ assert.dom(SELECTORS.rule()).exists({ count: 1 });
+ });
+
+ test('navigate to rule details page and renders rule data', async function (assert) {
+ // visit individual rule page
+ await visit('vault/config-ui/login-settings');
+
+ await click(SELECTORS.popupMenu('testRule'));
+ 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')).hasText('ns1/');
+ assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('userpass');
+ assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('Yes');
+ });
+
+ test('it navigates to rule details from linked block', async function (assert) {
+ await visit('vault/config-ui/login-settings');
+ await click(SELECTORS.rule('testRule2'));
+ 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')).hasText('ns2/');
+ assert.dom(GENERAL.infoRowValue('Backup methods')).hasText('oidc');
+ assert.dom(GENERAL.infoRowValue('Inheritance enabled')).hasText('No');
+ });
});
});
diff --git a/ui/tests/helpers/auth/auth-helpers.ts b/ui/tests/helpers/auth/auth-helpers.ts
index d61c68fd10..f9a4614715 100644
--- a/ui/tests/helpers/auth/auth-helpers.ts
+++ b/ui/tests/helpers/auth/auth-helpers.ts
@@ -128,7 +128,8 @@ export const AUTH_METHOD_MAP = [
{ authType: 'saml', options: LOGIN_DATA.saml },
];
-export const VISIBLE_MOUNTS = {
+// Mock response for `sys/internal/ui/mounts`
+export const SYS_INTERNAL_UI_MOUNTS = {
'userpass/': {
description: '',
options: {},
@@ -144,9 +145,9 @@ export const VISIBLE_MOUNTS = {
options: {},
type: 'oidc',
},
- 'token/': {
- description: 'token based credentials',
+ 'ldap/': {
+ description: '',
options: null,
- type: 'token',
+ type: 'ldap',
},
};
diff --git a/ui/tests/integration/components/auth/form-template-test.js b/ui/tests/integration/components/auth/form-template-test.js
index ab1db62d7c..8e16bcc6e4 100644
--- a/ui/tests/integration/components/auth/form-template-test.js
+++ b/ui/tests/integration/components/auth/form-template-test.js
@@ -27,26 +27,29 @@ module('Integration | Component | auth | form template', function (hooks) {
hooks.beforeEach(function () {
window.localStorage.clear();
this.version = this.owner.lookup('service:version');
- this.visibleMountsByType = null;
this.cluster = { id: '1' };
- this.directLinkData = null;
+
+ this.alternateView = null;
+ this.defaultView = { view: 'dropdown', tabData: null };
this.handleNamespaceUpdate = sinon.spy();
+ this.initialFormState = { initialAuthType: 'token', showAlternate: false };
this.namespaceQueryParam = '';
this.oidcProviderQueryParam = '';
this.onSuccess = sinon.spy();
- this.canceledMfaAuth = '';
+ this.visibleMountTypes = null;
this.renderComponent = () => {
return render(hbs`
`);
};
});
@@ -57,31 +60,18 @@ module('Integration | Component | auth | form template', function (hooks) {
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('token');
});
- test('it selects @canceledMfaAuth by default', async function (assert) {
- this.canceledMfaAuth = 'ldap';
- await this.renderComponent();
- assert.dom(GENERAL.selectByAttr('auth type')).hasValue('ldap');
- assert.dom(GENERAL.inputByAttr('username')).exists();
- assert.dom(GENERAL.inputByAttr('password')).exists();
- });
-
- test('it selects type in the dropdown if @directLinkData data just contains type', async function (assert) {
- this.directLinkData = { type: 'oidc', isVisibleMount: false };
- await this.renderComponent();
- assert.dom(GENERAL.selectByAttr('auth type')).hasValue('oidc');
- assert.dom(GENERAL.inputByAttr('role')).exists();
- await click(AUTH_FORM.advancedSettings);
- assert.dom(GENERAL.inputByAttr('path')).exists();
- assert.dom(GENERAL.backButton).doesNotExist();
- assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist('"Sign in with other methods" does not render');
- });
-
- test('it does not show toggle buttons when listing visibility is not set', async function (assert) {
+ test('it does not show toggle buttons if @alternateView does not exist', async function (assert) {
await this.renderComponent();
assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render');
assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist('"Sign in with other methods" does not render');
});
+ test('it initializes with preset auth type', async function (assert) {
+ this.initialFormState = { initialAuthType: 'userpass' };
+ await this.renderComponent();
+ assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass');
+ });
+
test('it displays errors', async function (assert) {
const authenticateStub = sinon.stub(this.owner.lookup('service:auth'), 'authenticate');
authenticateStub.throws('permission denied');
@@ -95,7 +85,7 @@ module('Integration | Component | auth | form template', function (hooks) {
module('listing visibility', function (hooks) {
hooks.beforeEach(function () {
- this.visibleMountsByType = {
+ const defaultTabs = {
userpass: [
{
path: 'userpass/',
@@ -127,27 +117,12 @@ module('Integration | Component | auth | form template', function (hooks) {
},
],
};
- });
-
- test('it renders mounts configured with listing_visibility="unuath"', async function (assert) {
- const expectedTabs = [
- { type: 'userpass', display: 'Userpass' },
- { type: 'oidc', display: 'OIDC' },
- { type: 'token', display: 'Token' },
- ];
-
- await this.renderComponent();
- assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
- // there are 4 mount paths returned in the stubbed sys/internal/ui/mounts response above,
- // but two are of the same type so only expect 3 tabs
- assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'it groups mount paths by type and renders 3 tabs');
- expectedTabs.forEach((m) => {
- assert.dom(AUTH_FORM.tabBtn(m.type)).exists(`${m.type} renders as a tab`);
- assert.dom(AUTH_FORM.tabBtn(m.type)).hasText(m.display, `${m.type} renders expected display name`);
- });
- assert
- .dom(AUTH_FORM.tabBtn('userpass'))
- .hasAttribute('aria-selected', 'true', 'it selects the first type by default');
+ // all computed by the parent, in this case the initial tabs are the same as visible mount types
+ // but that isn't always the case
+ this.visibleMountTypes = Object.keys(defaultTabs);
+ this.defaultView = { type: 'tabs', tabData: defaultTabs };
+ this.alternateView = { type: 'dropdown', tabData: null };
+ this.initialFormState = { initialAuthType: 'userpass', showAlternate: false };
});
test('it selects each auth tab and renders form for that type', async function (assert) {
@@ -180,37 +155,14 @@ module('Integration | Component | auth | form template', function (hooks) {
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
});
- test('it renders the mount description', async function (assert) {
- await this.renderComponent();
- await click(AUTH_FORM.tabBtn('token'));
- assert.dom(AUTH_FORM.description).hasText('token based credentials');
- });
-
- test('it renders a dropdown if multiple mount paths are returned', async function (assert) {
- await this.renderComponent();
- await click(AUTH_FORM.tabBtn('userpass'));
- const dropdownOptions = findAll(`${GENERAL.selectByAttr('path')} option`).map((o) => o.value);
- const expectedPaths = ['userpass/', 'userpass2/'];
- expectedPaths.forEach((p) => {
- assert.true(dropdownOptions.includes(p), `dropdown includes path: ${p}`);
- });
- });
-
- test('it renders hidden input if only one mount path is returned', async function (assert) {
- await this.renderComponent();
- await click(AUTH_FORM.tabBtn('oidc'));
- assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
- assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
- });
-
- test('it clicks "Sign in with other methods"', async function (assert) {
+ test('it clicks "Sign in with other methods" and toggles to other view', async function (assert) {
await this.renderComponent();
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'tabs render by default');
assert.dom(GENERAL.backButton).doesNotExist();
await click(AUTH_FORM.otherMethodsBtn);
assert
.dom(AUTH_FORM.otherMethodsBtn)
- .doesNotExist('"Sign in with other methods" does not renderafter it is clicked');
+ .doesNotExist('"Sign in with other methods" does not render after it is clicked');
assert
.dom(GENERAL.selectByAttr('auth type'))
.exists('clicking "Sign in with other methods" renders dropdown instead of tabs');
@@ -239,15 +191,15 @@ module('Integration | Component | auth | form template', function (hooks) {
assert.dom(AUTH_FORM.tabBtn('token')).hasAttribute('aria-selected', 'false');
});
- test('it preselects tab if @canceledMfaAuth is a tab', async function (assert) {
- this.canceledMfaAuth = 'oidc';
+ test('it preselects tab from initialFormState', async function (assert) {
+ this.initialFormState = { initialAuthType: 'oidc', showAlternate: false };
await this.renderComponent();
assert.dom(AUTH_FORM.authForm('oidc')).exists('oidc form renders');
assert.dom(AUTH_FORM.tabBtn('oidc')).hasAttribute('aria-selected', 'true');
});
- test('if @canceledMfaAuth is NOT a tab, dropdown renders with type selected instead of tabs', async function (assert) {
- this.canceledMfaAuth = 'ldap';
+ test('it renders dropdown and preselects type if initialFormState is not a tab', async function (assert) {
+ this.initialFormState = { initialAuthType: 'ldap', showAlternate: true };
await this.renderComponent();
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('ldap');
assert.dom(GENERAL.inputByAttr('username')).exists();
@@ -256,41 +208,6 @@ module('Integration | Component | auth | form template', function (hooks) {
assert.dom(GENERAL.backButton).exists('"Back" button renders');
assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist('"Sign in with other methods" does not render');
});
-
- // if mount data exists, the mount has listing_visibility="unauth"
- test('it renders single mount view instead of tabs if @directLinkData data exists and includes mount data', async function (assert) {
- this.directLinkData = { path: 'my-oidc/', type: 'oidc', isVisibleMount: true };
- await this.renderComponent();
- assert.dom(AUTH_FORM.authForm('oidc')).exists;
- assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders auth type tab');
- assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
- assert.dom(GENERAL.inputByAttr('role')).exists();
- assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
- assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
- assert.dom(AUTH_FORM.otherMethodsBtn).exists('"Sign in with other methods" renders');
-
- assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
- assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
- assert.dom(GENERAL.backButton).doesNotExist();
- });
-
- test('it does not render tabs if @directLinkData data exists and just includes type', async function (assert) {
- // set a type that is NOT in a visible mount because mount data exists otherwise
- this.directLinkData = { type: 'ldap', isVisibleMount: false };
- await this.renderComponent();
-
- assert.dom(GENERAL.selectByAttr('auth type')).hasValue('ldap', 'dropdown has type selected');
- assert.dom(AUTH_FORM.authForm('ldap')).exists();
- assert.dom(GENERAL.inputByAttr('username')).exists();
- assert.dom(GENERAL.inputByAttr('password')).exists();
- await click(AUTH_FORM.advancedSettings);
- assert.dom(GENERAL.inputByAttr('path')).exists();
- assert.dom(AUTH_FORM.tabBtn('ldap')).doesNotExist('tab does not render');
- assert
- .dom(GENERAL.backButton)
- .exists('back button renders because listing_visibility="unauth" for other mounts');
- assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist('"Sign in with other methods" does not render');
- });
});
module('community', function (hooks) {
diff --git a/ui/tests/integration/components/auth/page-test.js b/ui/tests/integration/components/auth/page-test.js
index d855041ab5..106f902902 100644
--- a/ui/tests/integration/components/auth/page-test.js
+++ b/ui/tests/integration/components/auth/page-test.js
@@ -10,9 +10,10 @@ import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
-import { fillInLoginFields, VISIBLE_MOUNTS } from 'vault/tests/helpers/auth/auth-helpers';
+import { fillInLoginFields, SYS_INTERNAL_UI_MOUNTS } from 'vault/tests/helpers/auth/auth-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CSP_ERROR } from 'vault/components/auth/page';
+import { setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
module('Integration | Component | auth | page', function (hooks) {
setupRenderingTest(hooks);
@@ -21,16 +22,18 @@ module('Integration | Component | auth | page', function (hooks) {
hooks.beforeEach(function () {
this.version = this.owner.lookup('service:version');
this.cluster = { id: '1' };
+ this.directLinkData = null;
+ this.loginSettings = null;
this.onAuthSuccess = sinon.spy();
this.onNamespaceUpdate = sinon.spy();
this.visibleAuthMounts = false;
- this.directLinkData = null;
this.renderComponent = () => {
return render(hbs`
setupTotpMfaResponse(authType));
+
+ await this.renderComponent();
+ await click(AUTH_FORM.otherMethodsBtn);
+ await fillIn(AUTH_FORM.selectMethod, authType);
+ await fillInLoginFields(loginData);
+ await click(AUTH_FORM.login);
+ await waitFor('[data-test-mfa-description]'); // wait until MFA validation renders
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.selectMethod).hasValue(authType, 'Okta is selected in dropdown');
+ });
+
+ test('it prioritizes auth type from canceled mfa instead of direct link with type', async function (assert) {
+ assert.expect(1);
+ this.directLinkData = this.directLinkIsJustType;
+ const authType = 'userpass';
+ const { loginData, url } = REQUEST_DATA.username;
+ const requestUrl = url({ path: authType, username: loginData?.username });
+ this.server.post(requestUrl, () => setupTotpMfaResponse(authType));
+ await this.renderComponent();
+ await fillIn(AUTH_FORM.selectMethod, authType);
+ await fillInLoginFields(loginData);
+ await click(AUTH_FORM.login);
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ });
});
});
@@ -182,7 +267,6 @@ module('Integration | Component | auth | page', function (hooks) {
await this.renderComponent();
await fillIn(AUTH_FORM.selectMethod, authType);
- // await fillIn(AUTH_FORM.selectMethod, authType);
await fillInLoginFields(loginData);
await click(AUTH_FORM.login);
const [actual] = this.onAuthSuccess.lastCall.args;
@@ -218,6 +302,20 @@ module('Integration | Component | auth | page', function (hooks) {
};
assert.propEqual(actual, expected, `onAuthSuccess called with: ${JSON.stringify(actual)}`);
});
+
+ test('it preselects auth type from canceled mfa', async function (assert) {
+ assert.expect(1);
+ const { loginData, url } = options;
+ const requestUrl = url({ path: authType, username: loginData?.username });
+ this.server.post(requestUrl, () => setupTotpMfaResponse(authType));
+
+ await this.renderComponent();
+ await fillIn(AUTH_FORM.selectMethod, authType);
+ await fillInLoginFields(loginData);
+ await click(AUTH_FORM.login);
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.selectMethod).hasValue(authType, `${authType} is selected in dropdown`);
+ });
}
// token makes a GET request so test separately
@@ -240,4 +338,340 @@ module('Integration | Component | auth | page', function (hooks) {
};
assert.propEqual(actual, expected, `onAuthSuccess called with: ${JSON.stringify(actual)}`);
});
+
+ /*
+ Login settings are an enterprise only feature but the component is version agnostic (and subsequently so are these tests)
+ because fetching login settings happens in the route only for enterprise versions.
+ Each combination must be tested with and without visible mounts (i.e. tuned with listing_visibility="unauth")
+ 1. default+backups: default type set, backup types set
+ 2. default only: no backup types
+ 3. backup only: backup types set without a default
+ */
+ module('ent login settings', function (hooks) {
+ hooks.beforeEach(function () {
+ this.loginSettings = {
+ defaultType: 'oidc',
+ backupTypes: ['userpass', 'ldap'],
+ };
+
+ this.assertPathInput = async (assert, { isHidden = false, value = '' } = {}) => {
+ // the path input can render behind the "Advanced settings" toggle or as a hidden input.
+ // Assert it only renders once and is the expected input
+ if (!isHidden) {
+ await click(AUTH_FORM.advancedSettings);
+ assert.dom(GENERAL.inputByAttr('path')).exists('it renders mount path input');
+ }
+ if (isHidden) {
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
+ assert.dom(GENERAL.inputByAttr('path')).hasValue(value);
+ }
+ assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
+ };
+ });
+
+ test('(default+backups): it initially renders default type and toggles to view backup methods', async function (assert) {
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ assert.dom(GENERAL.backButton).doesNotExist();
+ await this.assertPathInput(assert);
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(GENERAL.backButton).exists();
+ assert.dom(AUTH_FORM.tabs).exists({ count: 2 }, 'it renders 2 backup type tabs');
+ assert
+ .dom(AUTH_FORM.tabBtn('userpass'))
+ .hasAttribute('aria-selected', 'true', 'it selects the first backup type');
+ await this.assertPathInput(assert);
+ await click(AUTH_FORM.tabBtn('ldap'));
+ assert.dom(AUTH_FORM.tabBtn('ldap')).hasAttribute('aria-selected', 'true', 'it selects ldap tab');
+ await this.assertPathInput(assert);
+ });
+
+ test('(default only): it renders default type without backup methods', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
+ assert.dom(GENERAL.backButton).doesNotExist();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ });
+
+ test('(backups only): it initially renders backup types if no default is set', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist();
+ assert.dom(AUTH_FORM.tabs).exists({ count: 2 }, 'it renders 2 backup type tabs');
+ assert
+ .dom(AUTH_FORM.tabBtn('userpass'))
+ .hasAttribute('aria-selected', 'true', 'it selects the first backup type');
+ await this.assertPathInput(assert);
+ assert.dom(GENERAL.backButton).doesNotExist();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ });
+
+ module('all methods have visible mounts', function (hooks) {
+ hooks.beforeEach(function () {
+ this.loginSettings = {
+ defaultType: 'oidc',
+ backupTypes: ['userpass', 'ldap'],
+ };
+ this.visibleAuthMounts = SYS_INTERNAL_UI_MOUNTS;
+ });
+
+ test('(default+backups): it hides advanced settings for both views', async function (assert) {
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
+ this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.tabs).exists({ count: 2 }, 'it renders 2 backup type tabs');
+ assert
+ .dom(AUTH_FORM.tabBtn('userpass'))
+ .hasAttribute('aria-selected', 'true', 'it selects the first backup type');
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ assert.dom(GENERAL.inputByAttr('path')).doesNotExist();
+ assert.dom(GENERAL.selectByAttr('path')).exists(); // dropdown renders because userpass has 2 mount paths
+ await click(AUTH_FORM.tabBtn('ldap'));
+ this.assertPathInput(assert, { isHidden: true, value: 'ldap/' });
+ });
+
+ test('(default only): it hides advanced settings and renders hidden input', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders default method');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
+ assert.dom(GENERAL.backButton).doesNotExist();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ });
+
+ test('(backups only): it hides advanced settings and renders hidden input', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabs).exists({ count: 2 }, 'it renders 2 backup type tabs');
+ assert
+ .dom(AUTH_FORM.tabBtn('userpass'))
+ .hasAttribute('aria-selected', 'true', 'it selects the first backup type');
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ assert.dom(GENERAL.backButton).doesNotExist();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ });
+ });
+
+ module('only some methods have visible mounts', function (hooks) {
+ hooks.beforeEach(function () {
+ this.loginSettings = {
+ defaultType: 'oidc',
+ backupTypes: ['userpass', 'ldap'],
+ };
+ this.mountData = (path) => ({ [path]: SYS_INTERNAL_UI_MOUNTS[path] });
+ });
+
+ test('(default+backups): it hides advanced settings for default with visible mount but it renders for backups', async function (assert) {
+ this.visibleAuthMounts = { ...this.mountData('my-oidc/') };
+ await this.renderComponent();
+ this.assertPathInput(assert, { isHidden: true, value: 'my-oidc/' });
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ await this.assertPathInput(assert);
+ await click(AUTH_FORM.tabBtn('ldap'));
+ await this.assertPathInput(assert);
+ });
+
+ test('(default+backups): it only renders advanced settings for method without mounts', async function (assert) {
+ // default and only one backup method have visible mounts
+ this.visibleAuthMounts = {
+ ...this.mountData('my-oidc/'),
+ ...this.mountData('userpass/'),
+ ...this.mountData('userpass2/'),
+ };
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ assert.dom(GENERAL.selectByAttr('path')).exists();
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ await click(AUTH_FORM.tabBtn('ldap'));
+ assert.dom(AUTH_FORM.advancedSettings).exists();
+ });
+
+ test('(default+backups): it hides advanced settings for single backup method with mounts', async function (assert) {
+ this.visibleAuthMounts = { ...this.mountData('ldap/') };
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ assert.dom(AUTH_FORM.advancedSettings).exists();
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ assert.dom(AUTH_FORM.advancedSettings).exists();
+ await click(AUTH_FORM.tabBtn('ldap'));
+ this.assertPathInput(assert, { isHidden: true, value: 'ldap/' });
+ });
+
+ test('(backups only): it hides advanced settings for single method with mounts', async function (assert) {
+ this.loginSettings.defaultType = '';
+ this.visibleAuthMounts = { ...this.mountData('ldap/') };
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ assert.dom(AUTH_FORM.advancedSettings).exists();
+ await click(AUTH_FORM.tabBtn('ldap'));
+ this.assertPathInput(assert, { isHidden: true, value: 'ldap/' });
+ });
+ });
+
+ module('@directLinkData overrides login settings', function (hooks) {
+ hooks.beforeEach(function () {
+ this.mountData = SYS_INTERNAL_UI_MOUNTS;
+ });
+
+ module('when there are no visible mounts at all', function (hooks) {
+ hooks.beforeEach(function () {
+ this.visibleAuthMounts = null;
+ this.directLinkData = { type: 'okta' };
+ });
+
+ const testHelper = (assert) => {
+ assert.dom(AUTH_FORM.selectMethod).hasValue('okta');
+ assert.dom(AUTH_FORM.authForm('okta')).exists();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ assert.dom(GENERAL.backButton).doesNotExist();
+ };
+
+ test('(default+backups): it renders standard view and selects @directLinkData type from dropdown', async function (assert) {
+ await this.renderComponent();
+ testHelper(assert);
+ });
+
+ test('(default only): it renders standard view and selects @directLinkData type from dropdown', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ testHelper(assert);
+ });
+
+ test('(backups only): it renders standard view and selects @directLinkData type from dropdown', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ testHelper(assert);
+ });
+ });
+
+ module('when param matches a visible mount path and other visible mounts exist', function (hooks) {
+ hooks.beforeEach(function () {
+ this.visibleAuthMounts = {
+ ...this.mountData,
+ 'my-okta/': {
+ description: '',
+ options: null,
+ type: 'okta',
+ },
+ };
+ this.directLinkData = { path: 'my-okta/', type: 'okta' };
+ });
+
+ const testHelper = async (assert) => {
+ assert.dom(AUTH_FORM.tabBtn('okta')).hasText('Okta', 'it renders preferred method');
+ assert.dom(AUTH_FORM.tabs).exists({ count: 1 }, 'only one tab renders');
+ assert.dom(AUTH_FORM.authForm('okta'));
+ assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
+ assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
+ assert.dom(GENERAL.inputByAttr('path')).hasValue('my-okta/');
+ assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert
+ .dom(GENERAL.selectByAttr('auth type'))
+ .exists('it renders dropdown after clicking "Sign in with other"');
+ };
+
+ test('(default+backups): it renders single mount view for @directLinkData', async function (assert) {
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(default only): it renders single mount view for @directLinkData', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(backups only): it renders single mount view for @directLinkData', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+ });
+
+ module('when param matches a type and other visible mounts exist', function (hooks) {
+ hooks.beforeEach(function () {
+ // only type is present in directLinkData because the query param does not match a path with listing_visibility="unauth"
+ this.directLinkData = { type: 'okta' };
+ this.visibleAuthMounts = this.mountData;
+ });
+
+ const testHelper = async (assert) => {
+ assert.dom(GENERAL.backButton).exists('back button renders because other methods have tabs');
+ assert.dom(AUTH_FORM.selectMethod).hasValue('okta');
+ assert.dom(AUTH_FORM.authForm('okta')).exists();
+ assert.dom(AUTH_FORM.otherMethodsBtn).doesNotExist();
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.selectMethod).exists('it toggles back to dropdown');
+ };
+
+ test('(default+backups): it selects @directLinkData type from dropdown and toggles to tab view', async function (assert) {
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(default only): it selects @directLinkData type from dropdown and toggles to tab view', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(backups only): it selects @directLinkData type from dropdown and toggles to tab view', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+ });
+
+ module('when param matches a type that matches other visible mounts', function (hooks) {
+ hooks.beforeEach(function () {
+ // only type exists because the query param does not match a path with listing_visibility="unauth"
+ this.directLinkData = { type: 'oidc' };
+ this.visibleAuthMounts = this.mountData;
+ });
+
+ const testHelper = async (assert) => {
+ assert.dom(AUTH_FORM.tabBtn('oidc')).hasAttribute('aria-selected', 'true');
+ assert.dom(AUTH_FORM.authForm('oidc')).exists();
+ assert.dom(GENERAL.backButton).doesNotExist();
+ await click(AUTH_FORM.otherMethodsBtn);
+ assert.dom(AUTH_FORM.selectMethod).exists('it toggles to view dropdown');
+ await click(GENERAL.backButton);
+ assert.dom(AUTH_FORM.tabs).exists('it toggles back to tabs');
+ };
+
+ test('(default+backups): it selects @directLinkData type tab and toggles to dropdown view', async function (assert) {
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(default only): it selects @directLinkData type tab and toggles to dropdown view', async function (assert) {
+ this.loginSettings.backupTypes = null;
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+
+ test('(backups only): it selects @directLinkData type tab and toggles to dropdown view', async function (assert) {
+ this.loginSettings.defaultType = '';
+ await this.renderComponent();
+ await testHelper(assert);
+ });
+ });
+ });
+ });
});
diff --git a/ui/tests/integration/components/auth/tabs-test.js b/ui/tests/integration/components/auth/tabs-test.js
new file mode 100644
index 0000000000..40fc14c0a1
--- /dev/null
+++ b/ui/tests/integration/components/auth/tabs-test.js
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { click, findAll, render } from '@ember/test-helpers';
+import hbs from 'htmlbars-inline-precompile';
+import sinon from 'sinon';
+import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+
+module('Integration | Component | auth | tabs', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.tabData = {
+ userpass: [
+ {
+ path: 'userpass/',
+ description: '',
+ options: {},
+ type: 'userpass',
+ },
+ {
+ path: 'userpass2/',
+ description: '',
+ options: {},
+ type: 'userpass',
+ },
+ ],
+ oidc: [
+ {
+ path: 'my-oidc/',
+ description: '',
+ options: {},
+ type: 'oidc',
+ },
+ ],
+ token: [
+ {
+ path: 'token/',
+ description: 'token based credentials',
+ options: null,
+ type: 'token',
+ },
+ ],
+ };
+ this.selectedAuthMethod = '';
+ this.handleTabClick = sinon.spy();
+ this.renderComponent = () => {
+ return render(hbs`
+ `);
+ };
+ });
+
+ test('it renders tabs', async function (assert) {
+ const expectedTabs = [
+ { type: 'userpass', display: 'Userpass' },
+ { type: 'oidc', display: 'OIDC' },
+ { type: 'token', display: 'Token' },
+ ];
+
+ await this.renderComponent();
+ expectedTabs.forEach((m) => {
+ assert.dom(AUTH_FORM.tabBtn(m.type)).exists(`${m.type} renders as a tab`);
+ assert.dom(AUTH_FORM.tabBtn(m.type)).hasText(m.display, `${m.type} renders expected display name`);
+ });
+ });
+
+ test('it selects first tab if no @selectedAuthMethod exists', async function (assert) {
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
+ });
+
+ test('it renders the mount description', async function (assert) {
+ this.selectedAuthMethod = 'token';
+ await this.renderComponent();
+ assert.dom(AUTH_FORM.description).hasText('token based credentials');
+ });
+
+ test('it renders a dropdown if multiple mount paths are returned', async function (assert) {
+ this.selectedAuthMethod = 'userpass';
+ await this.renderComponent();
+ const dropdownOptions = findAll(`${GENERAL.selectByAttr('path')} option`).map((o) => o.value);
+ const expectedPaths = ['userpass/', 'userpass2/'];
+ expectedPaths.forEach((p) => {
+ assert.true(dropdownOptions.includes(p), `dropdown includes path: ${p}`);
+ });
+ });
+
+ test('it renders hidden input if only one mount path is returned', async function (assert) {
+ this.selectedAuthMethod = 'oidc';
+ await this.renderComponent();
+ assert.dom(GENERAL.inputByAttr('path')).hasAttribute('type', 'hidden');
+ assert.dom(GENERAL.inputByAttr('path')).hasValue('my-oidc/');
+ });
+
+ test('it calls handleTabClick with tab method type', async function (assert) {
+ await this.renderComponent();
+ await click(AUTH_FORM.tabBtn('oidc'));
+ const [actual] = this.handleTabClick.lastCall.args;
+ assert.strictEqual(actual, 'oidc');
+ });
+});
diff --git a/ui/types/vault/auth/form.d.ts b/ui/types/vault/auth/form.d.ts
index cf32073d7f..ec3dd441ed 100644
--- a/ui/types/vault/auth/form.d.ts
+++ b/ui/types/vault/auth/form.d.ts
@@ -5,14 +5,15 @@
export interface UnauthMountsByType {
// key is the auth method type
- [key: string]: AuthTabMountData[];
+ // if the value is "null" there is no mount data for that type
+ [key: string]: AuthTabMountData[] | null;
}
export interface UnauthMountsResponse {
// key is the mount path
[key: string]: { type: string; description?: string; config?: object | null };
}
-export interface AuthTabMountData {
+interface AuthTabMountData {
path: string;
type: string;
description?: string;