mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-06 04:46:25 +02:00
UI: Auth test cleanup to prep for auth service refactor (#30949)
* update test helpers, export DELAY_IN_MS, make window event a helper * simplify auth method map helpers, move page tests into separate files * use new buttons * finish separating page tests * move test helpers back to relevant files * remove redundant oidc test * move misplaced linked block AUTH_FORM selector * i definitely already addressed these.. * comment meant remove "trailing" forward slash...lol * cleanup stubs
This commit is contained in:
parent
068d576425
commit
e80d0ac68c
@ -348,9 +348,10 @@ export default Service.extend({
|
||||
displayName = (this.getTokenData(tokenName) || {}).displayName;
|
||||
}
|
||||
|
||||
// this is a workaround for OIDC/SAML methods WITH mfa configured. at this time mfa/validate endpoint does not
|
||||
// return display_name (or metadata that includes it) for this auth combination.
|
||||
// this if block can be removed if/when the API returns display_name on the mfa/validate response.
|
||||
// this is a fallback for any methods that don't return a display name from the initial auth request (i.e. JWT)
|
||||
// or for OIDC/SAML with mfa configured because the mfa/validate endpoint does not consistently
|
||||
// return display_name (or metadata that includes something to be used as such).
|
||||
// this if block can be removed if/when the API consistently returns a display_name.
|
||||
if (!displayName) {
|
||||
// if still nothing, request token data as a last resort
|
||||
try {
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<LinkedBlock
|
||||
@params={{array "vault.cluster.access.method" method.id}}
|
||||
class="list-item-row"
|
||||
data-test-auth-backend-link={{or method.id method.accessor}}
|
||||
data-test-linked-block={{or method.id method.accessor}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div>
|
||||
|
||||
@ -10,9 +10,9 @@ import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { mountAuthCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
|
||||
const { searchSelect } = GENERAL;
|
||||
|
||||
@ -79,7 +79,7 @@ module('Acceptance | auth-methods list view', function (hooks) {
|
||||
await visit('/vault/access/');
|
||||
for (const [key] of Object.entries(authPayload)) {
|
||||
assert
|
||||
.dom(AUTH_FORM.linkedBlockAuth(key.replace(/\/$/, ''))) // remove the forward slash
|
||||
.dom(GENERAL.linkedBlock(sanitizePath(key)))
|
||||
.exists({ count: 1 }, `auth method ${key} appears in list view`);
|
||||
}
|
||||
await visit('/vault/settings/auth/enable');
|
||||
@ -87,7 +87,7 @@ module('Acceptance | auth-methods list view', function (hooks) {
|
||||
await visit('/vault/access/');
|
||||
for (const [key] of Object.entries(authPayload)) {
|
||||
assert
|
||||
.dom(AUTH_FORM.linkedBlockAuth(key.replace(/\/$/, '')))
|
||||
.dom(GENERAL.linkedBlock(sanitizePath(key)))
|
||||
.exists({ count: 1 }, `auth method ${key} appears in list view after navigating from OIDC Provider`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -13,7 +13,6 @@ import { MANAGED_AUTH_BACKENDS } from 'vault/helpers/supported-managed-auth-back
|
||||
import { deleteAuthCmd, mountAuthCmd, runCmd, createNS } from 'vault/tests/helpers/commands';
|
||||
import { methods } from 'vault/helpers/mountable-auth-methods';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { MOUNT_BACKEND_FORM } from 'vault/tests/helpers/components/mount-backend-form-selectors';
|
||||
|
||||
const SELECTORS = {
|
||||
@ -44,7 +43,7 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
test('userpass secret backend', async function (assert) {
|
||||
// helper function to create a user in the specified backend
|
||||
async function createUser(backendPath, username) {
|
||||
await click(AUTH_FORM.linkedBlockAuth(backendPath));
|
||||
await click(GENERAL.linkedBlock(backendPath));
|
||||
assert.dom(GENERAL.emptyStateTitle).exists('shows empty state');
|
||||
await click(SELECTORS.createUser);
|
||||
await fillIn(GENERAL.inputByAttr('username'), username);
|
||||
@ -68,7 +67,7 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
|
||||
// check that switching back to the first auth method shows the first user
|
||||
await click(SELECTORS.methods);
|
||||
await click(AUTH_FORM.linkedBlockAuth(this.path1));
|
||||
await click(GENERAL.linkedBlock(this.path1));
|
||||
assert.dom(SELECTORS.listItem).hasText(this.user1, 'user1 exists in the list');
|
||||
});
|
||||
|
||||
@ -99,8 +98,8 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
|
||||
// check popup menu for auth method
|
||||
const itemCount = isTokenType ? 2 : 3;
|
||||
const triggerSelector = `${AUTH_FORM.linkedBlockAuth(path)} [data-test-popup-menu-trigger]`;
|
||||
const itemSelector = `${AUTH_FORM.linkedBlockAuth(path)} .hds-dropdown-list-item`;
|
||||
const triggerSelector = `${GENERAL.linkedBlock(path)} [data-test-popup-menu-trigger]`;
|
||||
const itemSelector = `${GENERAL.linkedBlock(path)} .hds-dropdown-list-item`;
|
||||
|
||||
await click(triggerSelector);
|
||||
assert
|
||||
@ -108,7 +107,7 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
.exists({ count: itemCount }, `shows ${itemCount} dropdown items for ${type}`);
|
||||
|
||||
// check that auth methods are linkable
|
||||
await click(AUTH_FORM.linkedBlockAuth(path));
|
||||
await click(GENERAL.linkedBlock(path));
|
||||
|
||||
if (!supportManaged.includes(type)) {
|
||||
assert.dom(GENERAL.linkTo('auth-tab')).exists({ count: 1 });
|
||||
@ -145,7 +144,7 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
await visit('/vault/access');
|
||||
|
||||
// all auth methods should be linkable
|
||||
await click(AUTH_FORM.linkedBlockAuth(path));
|
||||
await click(GENERAL.linkedBlock(path));
|
||||
assert.dom(GENERAL.linkTo('auth-tab')).exists({ count: 1 });
|
||||
assert
|
||||
.dom(GENERAL.linkTo('auth-tab'))
|
||||
@ -164,7 +163,7 @@ module('Acceptance | auth backend list', function (hooks) {
|
||||
await fillIn(GENERAL.inputByAttr('description'), 'My custom description');
|
||||
await click(GENERAL.submitButton);
|
||||
assert.strictEqual(currentURL(), '/vault/access', 'successfully saves and navigates away');
|
||||
await click(AUTH_FORM.linkedBlockAuth('token'));
|
||||
await click(GENERAL.linkedBlock('token'));
|
||||
assert
|
||||
.dom('[data-test-row-value="Description"]')
|
||||
.hasText('My custom description', 'description was saved');
|
||||
|
||||
@ -1,170 +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 } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { MFA_SELECTORS } from 'vault/tests/helpers/mfa/mfa-selectors';
|
||||
import { constraintId, setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
|
||||
import { AUTH_METHOD_MAP, fillInLoginFields } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { callbackData, windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
|
||||
const ENT_ONLY = ['saml'];
|
||||
const DELAY_IN_MS = 500;
|
||||
|
||||
for (const method of AUTH_METHOD_MAP) {
|
||||
const { authType, options } = method;
|
||||
// token doesn't support MFA
|
||||
if (authType === 'token') continue;
|
||||
|
||||
const isEntMethod = ENT_ONLY.includes(authType);
|
||||
// adding "enterprise" to the module title filters it out of the test runner for the CE repo
|
||||
module(`Acceptance | auth | mfa ${authType}${isEntMethod ? ' enterprise' : ''}`, function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
if (options?.hasPopupWindow) {
|
||||
this.windowStub = windowStub();
|
||||
}
|
||||
await visit('/vault/auth');
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
if (options?.hasPopupWindow) {
|
||||
this.windowStub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test(`${authType}: it displays mfa requirement for default paths`, async function (assert) {
|
||||
this.mountPath = authType;
|
||||
options.stubRequests(this.server, this.mountPath, setupTotpMfaResponse(this.mountPath));
|
||||
|
||||
const loginKeys = Object.keys(options.loginData);
|
||||
assert.expect(3 + loginKeys.length);
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields(options.loginData);
|
||||
|
||||
if (options?.hasPopupWindow) {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
setTimeout(() => {
|
||||
// set path which is used to set :mount param in the callback url => /auth/:mount/oidc/callback
|
||||
window.postMessage(callbackData({ path: this.mountPath }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(MFA_SELECTORS.mfaForm)
|
||||
.hasText(
|
||||
'Back Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify'
|
||||
);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue(authType, 'preserves method type on back');
|
||||
for (const field of loginKeys) {
|
||||
assert.dom(GENERAL.inputByAttr(field)).hasValue('', `${field} input clears on back`);
|
||||
}
|
||||
});
|
||||
|
||||
test(`${authType}: it displays mfa requirement for custom paths`, async function (assert) {
|
||||
this.mountPath = `${authType}-custom`;
|
||||
options.stubRequests(this.server, this.mountPath, setupTotpMfaResponse(this.mountPath));
|
||||
const loginKeys = Object.keys(options.loginData);
|
||||
assert.expect(3 + loginKeys.length);
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
// Toggle more options to input a custom mount path
|
||||
await fillInLoginFields({ ...options.loginData, path: this.mountPath }, { toggleOptions: true });
|
||||
|
||||
if (options?.hasPopupWindow) {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
setTimeout(() => {
|
||||
// set path which is used to set :mount param in the callback url => /auth/:mount/oidc/callback
|
||||
window.postMessage(callbackData({ path: this.mountPath }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom(MFA_SELECTORS.mfaForm)
|
||||
.hasText(
|
||||
'Back Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify'
|
||||
);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue(authType, 'preserves method type on back');
|
||||
for (const field of loginKeys) {
|
||||
assert.dom(GENERAL.inputByAttr(field)).hasValue('', `${field} input clears on back`);
|
||||
}
|
||||
});
|
||||
|
||||
test(`${authType}: it submits mfa requirement for default paths`, async function (assert) {
|
||||
assert.expect(2);
|
||||
this.mountPath = authType;
|
||||
options.stubRequests(this.server, this.mountPath, setupTotpMfaResponse(this.mountPath));
|
||||
|
||||
const expectedOtp = '12345';
|
||||
server.post('/sys/mfa/validate', async (_, req) => {
|
||||
const [actualOtp] = JSON.parse(req.requestBody).mfa_payload[constraintId];
|
||||
assert.true(true, 'it makes request to mfa validate endpoint');
|
||||
assert.strictEqual(actualOtp, expectedOtp, 'payload contains otp');
|
||||
});
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields(options.loginData);
|
||||
|
||||
if (options?.hasPopupWindow) {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
setTimeout(() => {
|
||||
// set path which is used to set :mount param in the callback url => /auth/:mount/oidc/callback
|
||||
window.postMessage(callbackData({ path: this.mountPath }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await fillIn(MFA_SELECTORS.passcode(0), expectedOtp);
|
||||
await click(MFA_SELECTORS.validate);
|
||||
});
|
||||
|
||||
test(`${authType}: it submits mfa requirement for custom paths`, async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.mountPath = `${authType}-custom`;
|
||||
options.stubRequests(this.server, this.mountPath, setupTotpMfaResponse(this.mountPath));
|
||||
|
||||
const expectedOtp = '12345';
|
||||
server.post('/sys/mfa/validate', async (_, req) => {
|
||||
const [actualOtp] = JSON.parse(req.requestBody).mfa_payload[constraintId];
|
||||
assert.true(true, 'it makes request to mfa validate endpoint');
|
||||
assert.strictEqual(actualOtp, expectedOtp, 'payload contains otp');
|
||||
});
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
// Toggle more options to input a custom mount path
|
||||
await fillInLoginFields({ ...options.loginData, path: this.mountPath }, { toggleOptions: true });
|
||||
|
||||
if (options?.hasPopupWindow) {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
setTimeout(() => {
|
||||
// set path which is used to set :mount param in the callback url => /auth/:mount/oidc/callback
|
||||
window.postMessage(callbackData({ path: this.mountPath }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await fillIn(MFA_SELECTORS.passcode(0), expectedOtp);
|
||||
await click(MFA_SELECTORS.validate);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -7,7 +7,6 @@ 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 sinon from 'sinon';
|
||||
import { Response } from 'miragejs';
|
||||
import { ERROR_JWT_LOGIN } from 'vault/components/auth/form/oidc-jwt';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
@ -20,7 +19,6 @@ module('Acceptance | jwt auth method', function (hooks) {
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
localStorage.clear(); // ensure that a token isn't stored otherwise visit('/vault/auth') will redirect to secrets
|
||||
this.stub = sinon.stub();
|
||||
this.server.post(
|
||||
'/auth/:path/oidc/auth_url',
|
||||
() =>
|
||||
|
||||
@ -4,19 +4,20 @@
|
||||
*/
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { click, fillIn, find, visit, waitFor, waitUntil } from '@ember/test-helpers';
|
||||
import { click, fillIn, visit, waitFor } from '@ember/test-helpers';
|
||||
import { logout } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { callbackData, windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
import sinon from 'sinon';
|
||||
import {
|
||||
callbackData,
|
||||
DELAY_IN_MS,
|
||||
triggerMessageEvent,
|
||||
windowStub,
|
||||
} from 'vault/tests/helpers/oidc-window-stub';
|
||||
import { Response } from 'miragejs';
|
||||
import { setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { ERROR_MISSING_PARAMS, ERROR_WINDOW_CLOSED } from 'vault/components/auth/form/oidc-jwt';
|
||||
|
||||
const DELAY_IN_MS = 500;
|
||||
|
||||
module('Acceptance | oidc auth method', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
@ -66,9 +67,7 @@ module('Acceptance | oidc auth method', function (hooks) {
|
||||
await logout();
|
||||
await this.selectMethod('oidc');
|
||||
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: 'oidc' }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
triggerMessageEvent('oidc');
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
@ -91,9 +90,7 @@ module('Acceptance | oidc auth method', function (hooks) {
|
||||
return { data: { auth_url: 'http://example.com' } };
|
||||
});
|
||||
await visit('/vault/auth');
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: 'oidc' }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
triggerMessageEvent('oidc');
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
@ -103,9 +100,7 @@ module('Acceptance | oidc auth method', function (hooks) {
|
||||
await logout();
|
||||
await this.selectMethod('oidc');
|
||||
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: 'oidc' }), window.origin);
|
||||
}, 500);
|
||||
triggerMessageEvent('oidc');
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitFor('[data-test-dashboard-card-header="Vault version"]');
|
||||
@ -157,52 +152,6 @@ module('Acceptance | oidc auth method', function (hooks) {
|
||||
.hasText('Authentication failed: Error fetching role: permission denied');
|
||||
});
|
||||
|
||||
test('it prompts mfa if configured', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.setupMocks(assert);
|
||||
this.server.get('/auth/oidc/oidc/callback', () => setupTotpMfaResponse('foo'));
|
||||
await logout();
|
||||
await this.selectMethod('oidc');
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: 'oidc' }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitUntil(() => find('[data-test-mfa-form]'));
|
||||
assert.dom('[data-test-mfa-form]').exists('it renders TOTP MFA form');
|
||||
});
|
||||
|
||||
test('auth service is called with client_token and cluster data', async function (assert) {
|
||||
const authSpy = sinon.spy(this.owner.lookup('service:auth'), 'authenticate');
|
||||
this.setupMocks();
|
||||
await logout();
|
||||
await this.selectMethod('oidc');
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: 'oidc' }), window.origin);
|
||||
}, DELAY_IN_MS);
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = authSpy.lastCall.args;
|
||||
const expected = {
|
||||
// even though this is the oidc auth method,
|
||||
// the callback has returned a token at this point of the login flow
|
||||
// and so the backend is 'token'
|
||||
backend: 'token',
|
||||
clusterId: '1',
|
||||
data: {
|
||||
// data from oidc/callback url
|
||||
token: 'root',
|
||||
},
|
||||
selectedAuth: 'oidc',
|
||||
};
|
||||
|
||||
assert.propEqual(
|
||||
actual,
|
||||
expected,
|
||||
`authenticate method called with correct args, ${JSON.stringify({ actual, expected })}`
|
||||
);
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
@ -8,16 +8,12 @@ import { setupApplicationTest } from 'ember-qunit';
|
||||
import { click, fillIn, find, visit, waitUntil } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { Response } from 'miragejs';
|
||||
import { windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
import { setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
|
||||
import { DELAY_IN_MS, windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { MFA_SELECTORS } from 'vault/tests/helpers/mfa/mfa-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
import { logout } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
|
||||
const DELAY_IN_MS = 500;
|
||||
|
||||
module('Acceptance | enterprise saml auth method', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
@ -143,15 +139,4 @@ module('Acceptance | enterprise saml auth method', function (hooks) {
|
||||
await click('#logout');
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue('saml', 'Previous auth method selected on logout');
|
||||
});
|
||||
|
||||
test('it prompts mfa if configured', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.put('/auth/saml/token', () => setupTotpMfaResponse('saml'));
|
||||
|
||||
await waitUntil(() => find(AUTH_FORM.selectMethod), { timeout: DELAY_IN_MS });
|
||||
await fillIn(AUTH_FORM.selectMethod, 'saml');
|
||||
await click(GENERAL.submitButton);
|
||||
await waitUntil(() => find(MFA_SELECTORS.mfaForm), { timeout: DELAY_IN_MS });
|
||||
assert.dom(MFA_SELECTORS.mfaForm).exists('it renders TOTP MFA form');
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,7 +11,6 @@ import { mountBackend } from 'vault/tests/helpers/components/mount-backend-form-
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { deleteAuthCmd, runCmd } from 'vault/tests/helpers/commands';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
|
||||
module('Acceptance | settings/auth/enable', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@ -38,7 +37,7 @@ module('Acceptance | settings/auth/enable', function (hooks) {
|
||||
);
|
||||
|
||||
await visit('/vault/access/');
|
||||
assert.dom(AUTH_FORM.linkedBlockAuth(path)).exists('mount is present in the list');
|
||||
assert.dom(GENERAL.linkedBlock(path)).exists('mount is present in the list');
|
||||
|
||||
// cleanup
|
||||
await runCmd(deleteAuthCmd(path));
|
||||
|
||||
@ -10,7 +10,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { login } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import modifyPassthroughResponse from 'vault/mirage/helpers/modify-passthrough-response';
|
||||
import { setRunOptions } from 'ember-a11y-testing/test-support';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
const link = (label) => `[data-test-sidebar-nav-link="${label}"]`;
|
||||
const panel = (label) => `[data-test-sidebar-nav-panel="${label}"]`;
|
||||
@ -142,7 +142,7 @@ module('Acceptance | sidebar navigation', function (hooks) {
|
||||
await click('[data-test-auth-enable]');
|
||||
assert.dom('[data-test-sidebar-nav-panel="Access"]').exists('Access nav panel renders');
|
||||
await click(link('Authentication Methods'));
|
||||
await click(AUTH_FORM.linkedBlockAuth('token'));
|
||||
await click(GENERAL.linkedBlock('token'));
|
||||
await click('[data-test-configure-link]');
|
||||
assert.dom('[data-test-sidebar-nav-panel="Access"]').exists('Access nav panel renders');
|
||||
});
|
||||
|
||||
@ -6,13 +6,9 @@
|
||||
export const AUTH_FORM = {
|
||||
description: '[data-test-description]',
|
||||
form: '[data-test-auth-form]',
|
||||
linkedBlockAuth: (path: string) => `[data-test-auth-backend-link="${path}"]`,
|
||||
selectMethod: '[data-test-select="auth type"]',
|
||||
tabBtn: (method: string) => `[data-test-auth-tab="${method}"] button`, // method is all lowercased
|
||||
tabs: '[data-test-auth-tab]',
|
||||
// old form toggle, will eventually be deleted
|
||||
moreOptions: '[data-test-auth-form-options-toggle]',
|
||||
// new toggle, hds component is a button
|
||||
advancedSettings: '[data-test-auth-form-options-toggle] button',
|
||||
authForm: (type: string) => `[data-test-auth-form="${type}"]`,
|
||||
helpText: '[data-test-auth-helptext]',
|
||||
|
||||
@ -7,7 +7,6 @@ import { click, fillIn, visit } from '@ember/test-helpers';
|
||||
import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { Server } from 'miragejs';
|
||||
|
||||
import type { LoginFields } from 'vault/vault/auth/form';
|
||||
|
||||
@ -70,63 +69,26 @@ export const fillInLoginFields = async (loginFields: LoginFields, { toggleOption
|
||||
}
|
||||
};
|
||||
|
||||
// See AUTH_METHOD_MAP for how login data maps to method types,
|
||||
// stubRequests are the requests made on submit for that method type
|
||||
export const LOGIN_DATA = {
|
||||
token: {
|
||||
loginData: { token: 'mytoken' },
|
||||
stubRequests: (server: Server, response: object) => server.get('/auth/token/lookup-self', () => response),
|
||||
},
|
||||
username: {
|
||||
loginData: { username: 'matilda', password: 'password' },
|
||||
stubRequests: (server: Server, path: string, response: object) =>
|
||||
server.post(`/auth/${path}/login/matilda`, () => response),
|
||||
},
|
||||
github: {
|
||||
loginData: { token: 'mysupersecuretoken' },
|
||||
stubRequests: (server: Server, path: string, response: object) =>
|
||||
server.post(`/auth/${path}/login`, () => response),
|
||||
},
|
||||
oidc: {
|
||||
loginData: { role: 'some-dev' },
|
||||
hasPopupWindow: true,
|
||||
stubRequests: (server: Server, path: string, response: object) => {
|
||||
server.get(`/auth/${path}/oidc/callback`, () => response);
|
||||
server.post(`/auth/${path}/oidc/auth_url`, () => {
|
||||
return { data: { auth_url: 'http://dev-foo-bar.com' } };
|
||||
});
|
||||
},
|
||||
},
|
||||
saml: {
|
||||
loginData: { role: 'some-dev' },
|
||||
hasPopupWindow: true,
|
||||
stubRequests: (server: Server, path: string, response: object) => {
|
||||
server.put(`/auth/${path}/token`, () => response);
|
||||
server.put(`/auth/${path}/sso_service_url`, () => {
|
||||
return { data: { sso_service_url: 'http://sso-url.hashicorp.com/service', token_poll_id: '1234' } };
|
||||
});
|
||||
},
|
||||
},
|
||||
const LOGIN_DATA = {
|
||||
token: { token: 'mysupersecuretoken' },
|
||||
username: { username: 'matilda', password: 'password' },
|
||||
role: { role: 'some-dev' },
|
||||
};
|
||||
|
||||
// maps auth type to request data
|
||||
export const AUTH_METHOD_MAP = [
|
||||
{ authType: 'token', options: LOGIN_DATA.token },
|
||||
{ authType: 'github', options: LOGIN_DATA.github },
|
||||
|
||||
// maps auth type to login input data
|
||||
export const AUTH_METHOD_LOGIN_DATA = {
|
||||
// token methods
|
||||
token: LOGIN_DATA.token,
|
||||
github: LOGIN_DATA.token,
|
||||
// username and password methods
|
||||
{ authType: 'userpass', options: LOGIN_DATA.username },
|
||||
{ authType: 'ldap', options: LOGIN_DATA.username },
|
||||
{ authType: 'okta', options: LOGIN_DATA.username },
|
||||
{ authType: 'radius', options: LOGIN_DATA.username },
|
||||
|
||||
// oidc
|
||||
{ authType: 'oidc', options: LOGIN_DATA.oidc },
|
||||
{ authType: 'jwt', options: LOGIN_DATA.oidc },
|
||||
|
||||
// ENTERPRISE ONLY
|
||||
{ authType: 'saml', options: LOGIN_DATA.saml },
|
||||
];
|
||||
userpass: LOGIN_DATA.username,
|
||||
ldap: LOGIN_DATA.username,
|
||||
okta: LOGIN_DATA.username,
|
||||
radius: LOGIN_DATA.username,
|
||||
// role
|
||||
oidc: LOGIN_DATA.role,
|
||||
jwt: LOGIN_DATA.role,
|
||||
saml: LOGIN_DATA.role,
|
||||
};
|
||||
|
||||
// Mock response for `sys/internal/ui/mounts`
|
||||
export const SYS_INTERNAL_UI_MOUNTS = {
|
||||
|
||||
492
ui/tests/helpers/auth/response-stubs.ts
Normal file
492
ui/tests/helpers/auth/response-stubs.ts
Normal file
@ -0,0 +1,492 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/*
|
||||
Authentication requests return authentication information in either the "auth" or "data" key,
|
||||
depending on the authentication method.
|
||||
|
||||
The stubbed responses below are used to test and compare authentication logic across different method types:
|
||||
- If the method has `{ mount_type: "token" }`, the authentication details are returned inside the "data" key.
|
||||
- Otherwise, `mount_type` is an empty string (""), and the authentication details are found in the "auth" key.
|
||||
|
||||
Some values depend on the method's mount configuration (such as `ttl` and `lease_duration`).
|
||||
For consistency, all methods stubbed here were mounted using default settings.
|
||||
*/
|
||||
|
||||
const BASE_REQUEST_DATA = {
|
||||
request_id: 'cbca58ca-8b53-76e7-3d07-6d72f7c5affe',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
};
|
||||
|
||||
export const RESPONSE_STUBS = {
|
||||
github: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedgithubtoken',
|
||||
accessor: 'm9UFOzTtVahYxs8nQe6Su73r',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
org: 'hashicorp',
|
||||
username: 'github-user99',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: 'd3396007-4d0e-33f9-7e4e-beb2e87c3518',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
jwt: {
|
||||
['lookup-self']: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: {
|
||||
accessor: 'MkjSR78ducuarJ6ypCDbHhBp',
|
||||
creation_time: 1749659696,
|
||||
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',
|
||||
explicit_max_ttl: 0,
|
||||
id: 'hvs.myvaultgeneratedjwttoken',
|
||||
issue_time: '2025-06-11T12:34:56.345113-04:00',
|
||||
meta: {
|
||||
role: 'reader',
|
||||
},
|
||||
num_uses: 0,
|
||||
orphan: true,
|
||||
path: 'auth/jwt/login',
|
||||
policies: ['default', 'reader'],
|
||||
renewable: true,
|
||||
ttl: 2764800,
|
||||
type: 'service',
|
||||
},
|
||||
auth: null,
|
||||
mount_type: 'token',
|
||||
},
|
||||
login: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedjwttoken',
|
||||
accessor: 'wzT0PZBYP0ZWyXI0Cst3UlsY',
|
||||
policies: ['default', 'reader'],
|
||||
token_policies: ['default', 'reader'],
|
||||
metadata: {
|
||||
role: 'reader',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: 'b6061dc8-a19e-195e-43a8-43d37f4625dd',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
},
|
||||
ldap: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: {}, // empty object instead of null
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedldaptoken',
|
||||
accessor: 'aIFJgy2Eo6qgIUx9bAuOKC6y',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
username: 'bob.johnson',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: '998d4fb7-c7db-8d81-c34d-dc1754103510',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
oidc: {
|
||||
// Response from the OIDC token exchange (happens first)
|
||||
['oidc/callback']: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedoidctoken',
|
||||
accessor: 'AjTM1Ec825ZJCg4xEVxbdPmf',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
role: 'reader',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: '18b57edf-acff-3e65-2ff2-6c772ce44924',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
// Response from token lookup (after OIDC token exchange)
|
||||
'lookup-self': {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: {
|
||||
accessor: 'ew50HTqF2xgsmaKIsdKpJtTc',
|
||||
creation_time: 1749584514,
|
||||
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',
|
||||
explicit_max_ttl: 0,
|
||||
id: 'hvs.myvaultgeneratedoidctoken',
|
||||
issue_time: '2025-06-10T15:41:54.961919-04:00',
|
||||
meta: {
|
||||
role: 'reader',
|
||||
},
|
||||
num_uses: 0,
|
||||
orphan: true,
|
||||
path: 'auth/my-oidc/oidc/callback',
|
||||
policies: ['default'],
|
||||
renewable: true,
|
||||
ttl: 2764799,
|
||||
type: 'service',
|
||||
},
|
||||
auth: null,
|
||||
mount_type: 'token',
|
||||
},
|
||||
},
|
||||
okta: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: {}, // empty object instead of null
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedoktatoken',
|
||||
accessor: 'bnCp5tEioxHJXgSXbKowYoZj',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
policies: '',
|
||||
username: 'vaultuser@gmail.com',
|
||||
},
|
||||
lease_duration: 2592000,
|
||||
renewable: true,
|
||||
entity_id: 'cb1ed44d-d3fb-5fd4-62cf-e027f84f35f6',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
radius: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedradiustoken',
|
||||
accessor: 'wx4Df1iktankETDXZ67tpGgo',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
policies: '',
|
||||
username: 'vaultuser',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: '7056887c-7e54-3f76-e498-1f76fc0d0e2c',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
token: {
|
||||
request_id: '6a7c7b72-6f0b-1700-5a4e-cd82e768b5d8',
|
||||
lease_id: '',
|
||||
renewable: false,
|
||||
lease_duration: 0,
|
||||
data: {
|
||||
accessor: '3tl0hAUwdDJVduSEnIca7Tr6',
|
||||
creation_time: 1744649084,
|
||||
creation_ttl: 2764800,
|
||||
display_name: 'token',
|
||||
entity_id: '',
|
||||
expire_time: '2025-05-16T09:44:44.837733-07:00',
|
||||
explicit_max_ttl: 0,
|
||||
id: 'hvs.myvaultgeneratedtoken',
|
||||
issue_time: '2025-04-14T09:44:44.837735-07:00',
|
||||
meta: null,
|
||||
num_uses: 0,
|
||||
orphan: false,
|
||||
path: 'auth/token/create',
|
||||
policies: ['default'],
|
||||
renewable: true,
|
||||
ttl: 2764785,
|
||||
type: 'service',
|
||||
},
|
||||
wrap_info: null,
|
||||
warnings: null,
|
||||
auth: null,
|
||||
mount_type: 'token',
|
||||
},
|
||||
userpass: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgenerateduserpasstoken',
|
||||
accessor: 'WSm7g8UzWEXhMO7g8C1zggDU',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
username: 'bob',
|
||||
},
|
||||
lease_duration: 2764800,
|
||||
renewable: true,
|
||||
entity_id: 'fa17f31c-41b0-c927-7b2b-d905200bb95c',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
// ENTERPRISE ONLY
|
||||
saml: {
|
||||
['saml/token']: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: null,
|
||||
auth: {
|
||||
client_token: 'hvs.myvaultgeneratedsamltoken',
|
||||
accessor: 'kHiH5wwqClnujASsKalca1T6',
|
||||
policies: ['default'],
|
||||
token_policies: ['default'],
|
||||
metadata: {
|
||||
role: 'dev',
|
||||
},
|
||||
lease_duration: 1800,
|
||||
renewable: true,
|
||||
entity_id: '81fc10e5-49a3-d0a2-9835-ac6b551ee266',
|
||||
token_type: 'service',
|
||||
orphan: true,
|
||||
mfa_requirement: null,
|
||||
num_uses: 0,
|
||||
},
|
||||
mount_type: '',
|
||||
},
|
||||
['lookup-self']: {
|
||||
...BASE_REQUEST_DATA,
|
||||
data: {
|
||||
accessor: 'H4fWtQaYX3aaEg1JIPSWiK9v',
|
||||
creation_time: 1749585309,
|
||||
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',
|
||||
explicit_max_ttl: 0,
|
||||
id: 'hvs.myvaultgeneratedsamltoken',
|
||||
issue_time: '2025-06-10T15:55:09.246666-04:00',
|
||||
meta: {
|
||||
role: 'dev',
|
||||
},
|
||||
num_uses: 0,
|
||||
orphan: true,
|
||||
path: 'auth/saml/token',
|
||||
policies: ['default'],
|
||||
renewable: true,
|
||||
ttl: 1800,
|
||||
type: 'service',
|
||||
},
|
||||
auth: null,
|
||||
mount_type: 'token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Once the auth service authentication method is simplified and no longer handles every auth type
|
||||
// the "backend" key should be completely removable
|
||||
export const TOKEN_DATA = {
|
||||
github: {
|
||||
backend: {
|
||||
description: 'GitHub authentication.',
|
||||
displayNamePath: ['metadata.org', 'metadata.username'],
|
||||
formAttributes: ['token'],
|
||||
mountPath: 'github',
|
||||
tokenPath: 'client_token',
|
||||
type: 'github',
|
||||
typeDisplay: 'GitHub',
|
||||
},
|
||||
displayName: `${RESPONSE_STUBS.github.auth.metadata.org}/${RESPONSE_STUBS.github.auth.metadata.username}`,
|
||||
entity_id: RESPONSE_STUBS.github.auth.entity_id,
|
||||
policies: RESPONSE_STUBS.github.auth.policies,
|
||||
renewable: RESPONSE_STUBS.github.auth.renewable,
|
||||
token: RESPONSE_STUBS.github.auth.client_token,
|
||||
tokenExpirationEpoch: 1752352843223,
|
||||
ttl: RESPONSE_STUBS.github.auth.lease_duration,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
ldap: {
|
||||
backend: {
|
||||
description: 'LDAP authentication.',
|
||||
displayNamePath: 'metadata.username',
|
||||
formAttributes: ['username', 'password'],
|
||||
mountPath: 'ldap',
|
||||
tokenPath: 'client_token',
|
||||
type: 'ldap',
|
||||
typeDisplay: 'LDAP',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.ldap.auth.metadata.username,
|
||||
entity_id: RESPONSE_STUBS.ldap.auth.entity_id,
|
||||
policies: RESPONSE_STUBS.ldap.auth.policies,
|
||||
renewable: RESPONSE_STUBS.ldap.auth.renewable,
|
||||
token: RESPONSE_STUBS.ldap.auth.client_token,
|
||||
tokenExpirationEpoch: 1752352843696,
|
||||
ttl: RESPONSE_STUBS.ldap.auth.lease_duration,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
jwt: {
|
||||
backend: {
|
||||
description: 'Authenticate using JWT or OIDC provider.',
|
||||
displayNamePath: 'display_name',
|
||||
formAttributes: ['role', 'jwt'],
|
||||
mountPath: 'jwt',
|
||||
tokenPath: 'client_token',
|
||||
type: 'jwt',
|
||||
typeDisplay: 'JWT',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.jwt['lookup-self'].data.display_name,
|
||||
entity_id: RESPONSE_STUBS.jwt['lookup-self'].data.entity_id,
|
||||
policies: RESPONSE_STUBS.jwt['lookup-self'].data.policies,
|
||||
renewable: RESPONSE_STUBS.jwt['lookup-self'].data.renewable,
|
||||
token: RESPONSE_STUBS.jwt['lookup-self'].data.id,
|
||||
tokenExpirationEpoch: 1752425319766,
|
||||
ttl: RESPONSE_STUBS.jwt['lookup-self'].data.ttl,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
oidc: {
|
||||
backend: {
|
||||
description: 'Token authentication.',
|
||||
displayNamePath: 'display_name',
|
||||
formAttributes: ['token'],
|
||||
mountPath: 'oidc',
|
||||
tokenPath: 'id',
|
||||
type: 'token',
|
||||
typeDisplay: 'Token',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.oidc['lookup-self'].data.display_name,
|
||||
entity_id: RESPONSE_STUBS.oidc['lookup-self'].data.entity_id,
|
||||
policies: RESPONSE_STUBS.oidc['lookup-self'].data.policies,
|
||||
renewable: RESPONSE_STUBS.oidc['lookup-self'].data.renewable,
|
||||
token: RESPONSE_STUBS.oidc['lookup-self'].data.id,
|
||||
tokenExpirationEpoch: 1752349314961,
|
||||
ttl: RESPONSE_STUBS.oidc['lookup-self'].data.ttl,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
okta: {
|
||||
backend: {
|
||||
description: 'Authenticate with your Okta username and password.',
|
||||
displayNamePath: 'metadata.username',
|
||||
formAttributes: ['username', 'password'],
|
||||
mountPath: 'okta',
|
||||
tokenPath: 'client_token',
|
||||
type: 'okta',
|
||||
typeDisplay: 'Okta',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.okta.auth.metadata.username,
|
||||
entity_id: RESPONSE_STUBS.okta.auth.entity_id,
|
||||
policies: RESPONSE_STUBS.okta.auth.policies,
|
||||
renewable: RESPONSE_STUBS.okta.auth.renewable,
|
||||
token: RESPONSE_STUBS.okta.auth.client_token,
|
||||
tokenExpirationEpoch: 1752180044950,
|
||||
ttl: RESPONSE_STUBS.okta.auth.lease_duration,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
radius: {
|
||||
backend: {
|
||||
description: 'Authenticate with your RADIUS username and password.',
|
||||
displayNamePath: 'metadata.username',
|
||||
formAttributes: ['username', 'password'],
|
||||
mountPath: 'radius',
|
||||
tokenPath: 'client_token',
|
||||
type: 'radius',
|
||||
typeDisplay: 'RADIUS',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.radius.auth.metadata.username,
|
||||
entity_id: RESPONSE_STUBS.radius.auth.entity_id,
|
||||
policies: RESPONSE_STUBS.radius.auth.policies,
|
||||
renewable: RESPONSE_STUBS.radius.auth.renewable,
|
||||
token: RESPONSE_STUBS.radius.auth.client_token,
|
||||
tokenExpirationEpoch: 1752352846180,
|
||||
ttl: RESPONSE_STUBS.radius.auth.lease_duration,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
token: {
|
||||
userRootNamespace: '',
|
||||
displayName: 'token',
|
||||
backend: {
|
||||
mountPath: 'token',
|
||||
type: 'token',
|
||||
typeDisplay: 'Token',
|
||||
description: 'Token authentication.',
|
||||
tokenPath: 'id',
|
||||
displayNamePath: 'display_name',
|
||||
formAttributes: ['token'],
|
||||
},
|
||||
token: RESPONSE_STUBS.token.data.id,
|
||||
policies: RESPONSE_STUBS.token.data.policies,
|
||||
renewable: RESPONSE_STUBS.token.data.renewable,
|
||||
entity_id: RESPONSE_STUBS.token.data.entity_id,
|
||||
ttl: RESPONSE_STUBS.token.data.ttl,
|
||||
tokenExpirationEpoch: 1747413884837,
|
||||
},
|
||||
userpass: {
|
||||
backend: {
|
||||
description: 'A simple username and password backend.',
|
||||
displayNamePath: 'metadata.username',
|
||||
formAttributes: ['username', 'password'],
|
||||
mountPath: 'userpass',
|
||||
tokenPath: 'client_token',
|
||||
type: 'userpass',
|
||||
typeDisplay: 'Username',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.userpass.auth.metadata.username,
|
||||
entity_id: RESPONSE_STUBS.userpass.auth.entity_id,
|
||||
policies: RESPONSE_STUBS.userpass.auth.policies,
|
||||
renewable: RESPONSE_STUBS.userpass.auth.renewable,
|
||||
token: RESPONSE_STUBS.userpass.auth.client_token,
|
||||
tokenExpirationEpoch: 1752352843463,
|
||||
ttl: RESPONSE_STUBS.userpass.auth.lease_duration,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
// ENTERPRISE ONLY
|
||||
saml: {
|
||||
backend: {
|
||||
description: 'Token authentication.',
|
||||
displayNamePath: 'display_name',
|
||||
formAttributes: ['token'],
|
||||
mountPath: 'saml',
|
||||
tokenPath: 'id',
|
||||
type: 'token',
|
||||
typeDisplay: 'Token',
|
||||
},
|
||||
displayName: RESPONSE_STUBS.saml['lookup-self'].data.display_name,
|
||||
entity_id: RESPONSE_STUBS.saml['lookup-self'].data.entity_id,
|
||||
policies: RESPONSE_STUBS.saml['lookup-self'].data.policies,
|
||||
renewable: RESPONSE_STUBS.saml['lookup-self'].data.renewable,
|
||||
token: RESPONSE_STUBS.saml['lookup-self'].data.id,
|
||||
tokenExpirationEpoch: 1749587109246,
|
||||
ttl: RESPONSE_STUBS.saml['lookup-self'].data.ttl,
|
||||
userRootNamespace: '',
|
||||
},
|
||||
};
|
||||
@ -36,6 +36,7 @@ export const GENERAL = {
|
||||
menuTrigger: '[data-test-popup-menu-trigger]',
|
||||
menuItem: (name: string) => `[data-test-popup-menu="${name}"]`,
|
||||
listItem: '[data-test-list-item-link]',
|
||||
linkedBlock: (item: string) => `[data-test-linked-block="${item}"]`,
|
||||
|
||||
/* ────── Inputs / Form Fields ────── */
|
||||
checkboxByAttr: (attr: string) => `[data-test-checkbox="${attr}"]`,
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
*/
|
||||
import sinon from 'sinon';
|
||||
|
||||
export const DELAY_IN_MS = 500;
|
||||
|
||||
// suggestions for a custom popup
|
||||
// passing { close: true } automatically closes popups opened from window.open()
|
||||
// passing { closed: true } sets value on popup window
|
||||
@ -30,3 +32,11 @@ export const callbackData = (data = {}) => ({
|
||||
code: 'code',
|
||||
...data,
|
||||
});
|
||||
|
||||
// Simulates the OIDC popup message event the OIDC auth method waits for
|
||||
// mountPath is necessary because it builds the callback URL
|
||||
export const triggerMessageEvent = (mountPath, delay = DELAY_IN_MS) => {
|
||||
setTimeout(() => {
|
||||
window.postMessage(callbackData({ path: mountPath }), window.origin);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
@ -10,13 +10,9 @@ 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 { AUTH_METHOD_MAP } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { AUTH_METHOD_LOGIN_DATA } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import {
|
||||
ALL_LOGIN_METHODS,
|
||||
BASE_LOGIN_METHODS,
|
||||
ENTERPRISE_LOGIN_METHODS,
|
||||
} from 'vault/utils/supported-login-methods';
|
||||
import { ENTERPRISE_LOGIN_METHODS, supportedTypes } from 'vault/utils/supported-login-methods';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import { ERROR_JWT_LOGIN } from 'vault/components/auth/form/oidc-jwt';
|
||||
|
||||
@ -81,7 +77,7 @@ module('Integration | Component | auth | form template', function (hooks) {
|
||||
|
||||
test('dropdown does not include enterprise methods on community versions', async function (assert) {
|
||||
this.version.type = 'community';
|
||||
const supported = BASE_LOGIN_METHODS.map((m) => m.type);
|
||||
const supported = supportedTypes(false);
|
||||
const unsupported = ENTERPRISE_LOGIN_METHODS.map((m) => m.type);
|
||||
assert.expect(supported.length + unsupported.length);
|
||||
await this.renderComponent();
|
||||
@ -234,21 +230,23 @@ module('Integration | Component | auth | form template', function (hooks) {
|
||||
this.namespaceQueryParam = '';
|
||||
});
|
||||
|
||||
// in th ent module to test ALL supported login methods
|
||||
// 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
|
||||
test('it selects each supported auth type and renders its form and relevant fields', async function (assert) {
|
||||
const fieldCount = AUTH_METHOD_MAP.map((m) => Object.keys(m.options.loginData).length);
|
||||
const sum = fieldCount.reduce((a, b) => a + b, 0);
|
||||
const methodCount = AUTH_METHOD_MAP.length;
|
||||
const authMethodTypes = supportedTypes(true);
|
||||
const totalFields = Object.values(AUTH_METHOD_LOGIN_DATA).reduce(
|
||||
(sum, obj) => sum + Object.keys(obj).length,
|
||||
0
|
||||
);
|
||||
// 3 assertions per method, plus an assertion for each expected field
|
||||
assert.expect(3 * methodCount + sum); // count at time of writing is 40
|
||||
assert.expect(3 * authMethodTypes.length + totalFields); // count at time of writing is 40
|
||||
|
||||
await this.renderComponent();
|
||||
for (const method of AUTH_METHOD_MAP) {
|
||||
const { authType, options } = method;
|
||||
for (const authType of authMethodTypes) {
|
||||
const loginData = AUTH_METHOD_LOGIN_DATA[authType];
|
||||
|
||||
const fields = Object.keys(options.loginData);
|
||||
const fields = Object.keys(loginData);
|
||||
await fillIn(GENERAL.selectByAttr('auth type'), authType);
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue(authType), `${authType}: it selects type`;
|
||||
@ -273,7 +271,7 @@ module('Integration | Component | auth | form template', function (hooks) {
|
||||
});
|
||||
|
||||
test('dropdown includes enterprise methods', async function (assert) {
|
||||
const supported = ALL_LOGIN_METHODS.map((m) => m.type);
|
||||
const supported = supportedTypes(true);
|
||||
assert.expect(supported.length);
|
||||
await this.renderComponent();
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { click, fillIn, findAll } from '@ember/test-helpers';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { AUTH_METHOD_MAP } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { AUTH_METHOD_LOGIN_DATA, fillInLoginFields } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
/*
|
||||
@ -51,12 +51,9 @@ export default (test, { standardSubmit = true } = {}) => {
|
||||
if (standardSubmit) {
|
||||
test('it submits form data with defaults', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const { options } = AUTH_METHOD_MAP.find((m) => m.authType === this.authType);
|
||||
const { loginData } = options;
|
||||
const loginData = AUTH_METHOD_LOGIN_DATA[this.authType];
|
||||
|
||||
for (const [field, value] of Object.entries(loginData)) {
|
||||
await fillIn(GENERAL.inputByAttr(field), value);
|
||||
}
|
||||
await fillInLoginFields(loginData);
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = this.authenticateStub.lastCall.args;
|
||||
assert.propEqual(
|
||||
@ -70,12 +67,9 @@ export default (test, { standardSubmit = true } = {}) => {
|
||||
// component here just yields <:advancedSettings> to test form submits data from yielded inputs
|
||||
test('it submits form data from yielded inputs', async function (assert) {
|
||||
await this.renderComponent({ yieldBlock: true });
|
||||
const { options } = AUTH_METHOD_MAP.find((m) => m.authType === this.authType);
|
||||
const { loginData } = options;
|
||||
const loginData = AUTH_METHOD_LOGIN_DATA[this.authType];
|
||||
|
||||
for (const [field, value] of Object.entries(loginData)) {
|
||||
await fillIn(GENERAL.inputByAttr(field), value);
|
||||
}
|
||||
await fillInLoginFields(loginData);
|
||||
await fillIn(GENERAL.inputByAttr('path'), `custom-${this.authType}`);
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
@ -8,7 +8,7 @@ import { setupRenderingTest } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { click, fillIn, find, render } from '@ember/test-helpers';
|
||||
import sinon from 'sinon';
|
||||
import testHelper from './test-helper';
|
||||
import testHelper from './auth-form-test-helper';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
|
||||
// These auth types all use the default methods in auth/form/base
|
||||
|
||||
@ -16,7 +16,7 @@ import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import * as parseURL from 'core/utils/parse-url';
|
||||
import sinon from 'sinon';
|
||||
import testHelper from './test-helper';
|
||||
import testHelper from './auth-form-test-helper';
|
||||
|
||||
/*
|
||||
The OIDC and JWT mounts call the same endpoint (see docs https://developer.hashicorp.com/vault/docs/auth/jwt )
|
||||
@ -311,6 +311,7 @@ module('Integration | Component | auth | form | oidc-jwt', function (hooks) {
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.authenticateStub.restore();
|
||||
this.routerStub.restore();
|
||||
});
|
||||
|
||||
test('it renders helper text', async function (assert) {
|
||||
|
||||
@ -9,8 +9,8 @@ import hbs from 'htmlbars-inline-precompile';
|
||||
import { click, fillIn, render } from '@ember/test-helpers';
|
||||
import sinon from 'sinon';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import testHelper from './test-helper';
|
||||
import { LOGIN_DATA } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import testHelper from './auth-form-test-helper';
|
||||
import { fillInLoginFields } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import * as uuid from 'core/utils/uuid';
|
||||
@ -38,13 +38,6 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
custom: { path: 'custom-okta', username: 'matilda', password: 'password', nonce: this.nonce },
|
||||
};
|
||||
|
||||
this.fillInForm = async () => {
|
||||
const { loginData } = LOGIN_DATA.username;
|
||||
for (const [field, value] of Object.entries(loginData)) {
|
||||
await fillIn(GENERAL.inputByAttr(field), value);
|
||||
}
|
||||
};
|
||||
|
||||
this.renderComponent = ({ yieldBlock = false } = {}) => {
|
||||
if (yieldBlock) {
|
||||
return render(hbs`
|
||||
@ -85,7 +78,8 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = this.authenticateStub.lastCall.args;
|
||||
assert.propEqual(
|
||||
@ -106,7 +100,7 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
});
|
||||
|
||||
await this.renderComponent({ yieldBlock: true });
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await fillIn(GENERAL.inputByAttr('path'), `custom-${this.authType}`);
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = this.authenticateStub.lastCall.args;
|
||||
@ -119,7 +113,7 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
|
||||
test('it displays okta number challenge answer', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
assert
|
||||
.dom('[data-test-okta-number-challenge]')
|
||||
@ -130,7 +124,7 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
|
||||
test('it returns to login when "Back to login" is clicked', async function (assert) {
|
||||
await this.renderComponent();
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
assert.dom('[data-test-okta-number-challenge]').exists();
|
||||
await click(GENERAL.backButton);
|
||||
@ -160,14 +154,14 @@ module('Integration | Component | auth | form | okta', function (hooks) {
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
});
|
||||
|
||||
test('it renders error message when okta verify request errors', async function (assert) {
|
||||
this.server.get(`/auth/okta/verify/${this.nonce}`, () => new Response(500));
|
||||
await this.renderComponent();
|
||||
await this.fillInForm();
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
assert.dom(GENERAL.messageError).hasText('Error An error occurred, please try again');
|
||||
});
|
||||
|
||||
@ -70,7 +70,7 @@ module('Integration | Component | auth | form | saml', function (hooks) {
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.windowStub.restore();
|
||||
sinon.restore();
|
||||
this.authenticateStub.restore();
|
||||
});
|
||||
|
||||
test('it renders helper text', async function (assert) {
|
||||
|
||||
@ -1,708 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, fillIn, render, waitFor } from '@ember/test-helpers';
|
||||
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, 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);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.cluster = { id: '1' };
|
||||
this.directLinkData = null;
|
||||
this.loginSettings = null;
|
||||
this.namespaceQueryParam = '';
|
||||
this.oidcProviderQueryParam = '';
|
||||
this.onAuthSuccess = sinon.spy();
|
||||
this.onNamespaceUpdate = sinon.spy();
|
||||
this.visibleAuthMounts = false;
|
||||
|
||||
this.renderComponent = () => {
|
||||
return render(hbs`
|
||||
<Auth::Page
|
||||
@cluster={{this.cluster}}
|
||||
@directLinkData={{this.directLinkData}}
|
||||
@loginSettings={{this.loginSettings}}
|
||||
@namespaceQueryParam={{this.namespaceQueryParam}}
|
||||
@oidcProviderQueryParam={{this.oidcProviderQueryParam}}
|
||||
@onAuthSuccess={{this.onAuthSuccess}}
|
||||
@onNamespaceUpdate={{this.onNamespaceUpdate}}
|
||||
@visibleAuthMounts={{this.visibleAuthMounts}}
|
||||
/>
|
||||
`);
|
||||
};
|
||||
|
||||
// in the real world more info is returned by auth requests
|
||||
// only including pertinent data for testing
|
||||
this.authRequest = (url) => this.server.post(url, () => ({ auth: { policies: ['default'] } }));
|
||||
});
|
||||
|
||||
test('it renders error on CSP violation', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.cluster.standby = true;
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.pageError.error).doesNotExist();
|
||||
this.owner.lookup('service:csp-event').handleEvent({ violatedDirective: 'connect-src' });
|
||||
await waitFor(GENERAL.pageError.error);
|
||||
assert.dom(GENERAL.pageError.error).hasText(CSP_ERROR);
|
||||
});
|
||||
|
||||
test('it renders splash logo and disables namespace input when oidc provider query param is present', async function (assert) {
|
||||
this.oidcProviderQueryParam = 'myprovider';
|
||||
this.version.features = ['Namespaces'];
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.logo).exists();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).isDisabled();
|
||||
assert
|
||||
.dom(AUTH_FORM.helpText)
|
||||
.hasText(
|
||||
'Once you log in, you will be redirected back to your application. If you require login credentials, contact your administrator.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it calls onNamespaceUpdate', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.version.features = ['Namespaces'];
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('namespace'), 'mynamespace');
|
||||
const [actual] = this.onNamespaceUpdate.lastCall.args;
|
||||
assert.strictEqual(actual, 'mynamespace', `onNamespaceUpdate called with: ${actual}`);
|
||||
});
|
||||
|
||||
test('it passes query param to namespace input', async function (assert) {
|
||||
this.version.features = ['Namespaces'];
|
||||
this.namespaceQueryParam = 'ns-1';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).hasValue(this.namespaceQueryParam);
|
||||
});
|
||||
|
||||
test('it does not render the namespace input on community', async function (assert) {
|
||||
this.version.type = 'community';
|
||||
this.version.features = [];
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it does not render the namespace input on enterprise without the "Namespaces" feature', async function (assert) {
|
||||
this.version.type = 'enterprise';
|
||||
this.version.features = [];
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it selects type in the dropdown if direct link just has type', async function (assert) {
|
||||
this.directLinkData = { type: 'oidc' };
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist('tab does not render');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('oidc', 'dropdown has type selected');
|
||||
assert.dom(AUTH_FORM.authForm('oidc')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('role')).exists();
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.backButton).doesNotExist();
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('"Sign in with other methods" does not render');
|
||||
});
|
||||
|
||||
module('listing visibility', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.visibleAuthMounts = SYS_INTERNAL_UI_MOUNTS;
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test('it formats and renders tabs if visible auth mounts exist', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const expectedTabs = [
|
||||
{ type: 'userpass', display: 'Userpass' },
|
||||
{ type: 'oidc', display: 'OIDC' },
|
||||
{ type: 'ldap', display: 'LDAP' },
|
||||
];
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
|
||||
// there are 4 mount paths returned in visibleAuthMounts 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');
|
||||
});
|
||||
|
||||
test('it renders dropdown as alternate 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(GENERAL.button('Sign in with other methods'));
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('button disappears after it is clicked');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass', 'dropdown has userpass selected');
|
||||
assert.dom(AUTH_FORM.advancedSettings).exists('toggle renders even though userpass has visible mounts');
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.inputByAttr('path')).hasValue('', 'it renders empty custom path input');
|
||||
|
||||
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
||||
assert.dom(AUTH_FORM.advancedSettings).exists('toggle renders even though oidc has a visible mount');
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.inputByAttr('path')).hasValue('', 'it renders empty custom path input');
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render after it is clicked');
|
||||
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'clicking "Back" renders tabs again');
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.exists('"Sign in with other methods" renders again');
|
||||
});
|
||||
|
||||
module('with a direct link', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
// if path exists, the mount has listing_visibility="unauth"
|
||||
this.directLinkIsVisibleMount = { path: 'my-oidc/', type: 'oidc' };
|
||||
this.directLinkIsJustType = { type: 'okta' };
|
||||
});
|
||||
|
||||
test('it selects type in the dropdown if direct link is just type', async function (assert) {
|
||||
this.directLinkData = this.directLinkIsJustType;
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.tabBtn('okta')).doesNotExist('tab does not render');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('okta', 'dropdown has type selected');
|
||||
assert.dom(AUTH_FORM.authForm('okta')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('username')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('password')).exists();
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('"Sign in with other methods" does not render');
|
||||
assert.dom(GENERAL.backButton).exists('back button renders because tabs exist for other methods');
|
||||
await click(GENERAL.backButton);
|
||||
assert
|
||||
.dom(AUTH_FORM.tabBtn('userpass'))
|
||||
.hasAttribute('aria-selected', 'true', 'first tab is selected on back');
|
||||
});
|
||||
|
||||
test('it renders single method view instead of tabs if direct link includes path', async function (assert) {
|
||||
this.directLinkData = this.directLinkIsVisibleMount;
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.authForm('oidc')).exists;
|
||||
assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders tab for type');
|
||||
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(GENERAL.button('Sign in with other methods'))
|
||||
.exists('"Sign in with other methods" renders');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
|
||||
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
|
||||
assert.dom(GENERAL.backButton).doesNotExist();
|
||||
});
|
||||
|
||||
test('it prioritizes auth type from canceled mfa instead of direct link for path', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.directLinkData = this.directLinkIsVisibleMount;
|
||||
const authType = 'okta';
|
||||
const { loginData, url } = REQUEST_DATA.username;
|
||||
const requestUrl = url({ path: authType, username: loginData?.username });
|
||||
this.server.post(requestUrl, () => setupTotpMfaResponse(authType));
|
||||
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields(loginData);
|
||||
await click(GENERAL.submitButton);
|
||||
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(GENERAL.submitButton);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const REQUEST_DATA = {
|
||||
username: {
|
||||
loginData: { username: 'matilda', password: 'password' },
|
||||
url: ({ path, username }) => `/auth/${path}/login/${username}`,
|
||||
},
|
||||
github: {
|
||||
loginData: { token: 'mysupersecuretoken' },
|
||||
url: ({ path }) => `/auth/${path}/login`,
|
||||
},
|
||||
};
|
||||
|
||||
// only testing methods that submit via AuthForm (and not separate, child component)
|
||||
const AUTH_METHOD_TEST_CASES = [
|
||||
{ authType: 'github', options: REQUEST_DATA.github },
|
||||
{ authType: 'userpass', options: REQUEST_DATA.username },
|
||||
{ authType: 'ldap', options: REQUEST_DATA.username },
|
||||
{ authType: 'okta', options: REQUEST_DATA.username },
|
||||
{ authType: 'radius', options: REQUEST_DATA.username },
|
||||
];
|
||||
|
||||
for (const { authType, options } of AUTH_METHOD_TEST_CASES) {
|
||||
test(`${authType}: it calls onAuthSuccess on submit for default path`, async function (assert) {
|
||||
assert.expect(1);
|
||||
const { loginData, url } = options;
|
||||
const requestUrl = url({ path: authType, username: loginData?.username });
|
||||
this.authRequest(requestUrl);
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields(loginData);
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = this.onAuthSuccess.lastCall.args;
|
||||
const expected = {
|
||||
namespace: '',
|
||||
token: `vault-${authType}☃1`,
|
||||
isRoot: false,
|
||||
};
|
||||
assert.propEqual(actual, expected, `onAuthSuccess called with: ${JSON.stringify(actual)}`);
|
||||
});
|
||||
|
||||
test(`${authType}: it calls onAuthSuccess on submit for custom path`, async function (assert) {
|
||||
const customPath = `${authType}-custom`;
|
||||
const { loginData, url } = options;
|
||||
const loginDataWithPath = { ...loginData, path: customPath };
|
||||
// pass custom path to request URL
|
||||
const requestUrl = url({ path: customPath, username: loginData?.username });
|
||||
this.authRequest(requestUrl);
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
// await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
// toggle mount path input to specify custom path
|
||||
await fillInLoginFields(loginDataWithPath, { toggleOptions: true });
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const [actual] = this.onAuthSuccess.lastCall.args;
|
||||
const expected = {
|
||||
namespace: '',
|
||||
token: `vault-${authType}☃1`,
|
||||
isRoot: false,
|
||||
};
|
||||
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(GENERAL.submitButton);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue(authType, `${authType} is selected in dropdown`);
|
||||
});
|
||||
}
|
||||
|
||||
// token makes a GET request so test separately
|
||||
test('token: it calls onAuthSuccess on submit', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.get('/auth/token/lookup-self', () => {
|
||||
return { data: { policies: ['default'] } };
|
||||
});
|
||||
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, 'token');
|
||||
// await fillIn(AUTH_FORM.selectMethod, 'token');
|
||||
await fillInLoginFields({ token: 'mysupersecuretoken' });
|
||||
await click(GENERAL.submitButton);
|
||||
const [actual] = this.onAuthSuccess.lastCall.args;
|
||||
const expected = {
|
||||
namespace: '',
|
||||
token: `vault-token☃1`,
|
||||
isRoot: false,
|
||||
};
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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+backups): it initially renders default type if backup types include the default method', async function (assert) {
|
||||
this.loginSettings = {
|
||||
defaultType: 'userpass',
|
||||
backupTypes: ['ldap', 'userpass', 'oidc'],
|
||||
};
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.backButton).doesNotExist('it renders default view');
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasText('Userpass', 'it renders default method');
|
||||
assert
|
||||
.dom(AUTH_FORM.tabs)
|
||||
.exists({ count: 1 }, 'it is rendering the default view because only one tab renders');
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
assert.dom(GENERAL.backButton).exists('it toggles to backup method view');
|
||||
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'it renders 3 backup type tabs');
|
||||
assert
|
||||
.dom(AUTH_FORM.tabBtn('ldap'))
|
||||
.hasAttribute('aria-selected', 'true', 'it selects the first backup type');
|
||||
});
|
||||
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).doesNotExist();
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { click, fillIn, waitFor } from '@ember/test-helpers';
|
||||
import { fillInLoginFields, SYS_INTERNAL_UI_MOUNTS } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
|
||||
import setupTestContext from './setup-test-context';
|
||||
|
||||
module('Integration | Component | auth | page | listing visibility', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
setupTestContext(this);
|
||||
this.visibleAuthMounts = SYS_INTERNAL_UI_MOUNTS;
|
||||
});
|
||||
|
||||
test('it formats and renders tabs if visible auth mounts exist', async function (assert) {
|
||||
await this.renderComponent();
|
||||
const expectedTabs = [
|
||||
{ type: 'userpass', display: 'Userpass' },
|
||||
{ type: 'oidc', display: 'OIDC' },
|
||||
{ type: 'ldap', display: 'LDAP' },
|
||||
];
|
||||
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist('dropdown does not render');
|
||||
// there are 4 mount paths returned in visibleAuthMounts 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');
|
||||
});
|
||||
|
||||
test('it renders dropdown as alternate 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(GENERAL.button('Sign in with other methods'));
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('button disappears after it is clicked');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('userpass', 'dropdown has userpass selected');
|
||||
assert.dom(AUTH_FORM.advancedSettings).exists('toggle renders even though userpass has visible mounts');
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.inputByAttr('path')).hasValue('', 'it renders empty custom path input');
|
||||
|
||||
await fillIn(GENERAL.selectByAttr('auth type'), 'oidc');
|
||||
assert.dom(AUTH_FORM.advancedSettings).exists('toggle renders even though oidc has a visible mount');
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.inputByAttr('path')).hasValue('', 'it renders empty custom path input');
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(GENERAL.backButton).doesNotExist('"Back" button does not render after it is clicked');
|
||||
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'clicking "Back" renders tabs again');
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.exists('"Sign in with other methods" renders again');
|
||||
});
|
||||
|
||||
// integration tests for ?with= query param
|
||||
module('with a direct link', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
// if path exists, the mount has listing_visibility="unauth"
|
||||
this.directLinkIsVisibleMount = { path: 'my-oidc/', type: 'oidc' };
|
||||
this.directLinkIsJustType = { type: 'okta' };
|
||||
});
|
||||
|
||||
test('it selects type in the dropdown if direct link is just type', async function (assert) {
|
||||
this.directLinkData = this.directLinkIsJustType;
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.tabBtn('okta')).doesNotExist('tab does not render');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('okta', 'dropdown has type selected');
|
||||
assert.dom(AUTH_FORM.authForm('okta')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('username')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('password')).exists();
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('"Sign in with other methods" does not render');
|
||||
assert.dom(GENERAL.backButton).exists('back button renders because tabs exist for other methods');
|
||||
await click(GENERAL.backButton);
|
||||
assert
|
||||
.dom(AUTH_FORM.tabBtn('userpass'))
|
||||
.hasAttribute('aria-selected', 'true', 'first tab is selected on back');
|
||||
});
|
||||
|
||||
test('it renders single method view instead of tabs if direct link includes path', async function (assert) {
|
||||
this.directLinkData = this.directLinkIsVisibleMount;
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.authForm('oidc')).exists;
|
||||
assert.dom(AUTH_FORM.tabBtn('oidc')).hasText('OIDC', 'it renders tab for type');
|
||||
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(GENERAL.button('Sign in with other methods')).exists('"Sign in with other methods" renders');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).doesNotExist();
|
||||
assert.dom(AUTH_FORM.advancedSettings).doesNotExist();
|
||||
assert.dom(GENERAL.backButton).doesNotExist();
|
||||
});
|
||||
|
||||
test('it prioritizes auth type from canceled mfa instead of direct link for path', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.directLinkData = this.directLinkIsVisibleMount;
|
||||
const authType = 'okta';
|
||||
this.server.post(`/auth/okta/login/matilda`, () => setupTotpMfaResponse(authType));
|
||||
await this.renderComponent();
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
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';
|
||||
this.server.post(`/auth/okta/login/matilda`, () => setupTotpMfaResponse(authType));
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, authType);
|
||||
await fillInLoginFields({ username: 'matilda', password: 'password' });
|
||||
await click(GENERAL.submitButton);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
370
ui/tests/integration/components/auth/page/login-settings-test.js
Normal file
370
ui/tests/integration/components/auth/page/login-settings-test.js
Normal file
@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { click } from '@ember/test-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { SYS_INTERNAL_UI_MOUNTS } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import setupTestContext from './setup-test-context';
|
||||
|
||||
/*
|
||||
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('Integration | Component | auth | page | ent login settings', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
setupTestContext(this);
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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+backups): it initially renders default type if backup types include the default method', async function (assert) {
|
||||
this.loginSettings = {
|
||||
defaultType: 'userpass',
|
||||
backupTypes: ['ldap', 'userpass', 'oidc'],
|
||||
};
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.backButton).doesNotExist('it renders default view');
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasText('Userpass', 'it renders default method');
|
||||
assert
|
||||
.dom(AUTH_FORM.tabs)
|
||||
.exists({ count: 1 }, 'it is rendering the default view because only one tab renders');
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
assert.dom(GENERAL.backButton).exists('it toggles to backup method view');
|
||||
assert.dom(AUTH_FORM.tabs).exists({ count: 3 }, 'it renders 3 backup type tabs');
|
||||
assert
|
||||
.dom(AUTH_FORM.tabBtn('ldap'))
|
||||
.hasAttribute('aria-selected', 'true', 'it selects the first backup type');
|
||||
});
|
||||
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).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(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods')).doesNotExist();
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.tabBtn('userpass')).hasAttribute('aria-selected', 'true');
|
||||
await click(GENERAL.button('Sign in with other methods'));
|
||||
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(GENERAL.button('Sign in with other methods'));
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { click, fillIn, waitUntil } from '@ember/test-helpers';
|
||||
import { ERROR_JWT_LOGIN } from 'vault/components/auth/form/oidc-jwt';
|
||||
import { fillInLoginFields } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { module, test } from 'qunit';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import { RESPONSE_STUBS, TOKEN_DATA } from 'vault/tests/helpers/auth/response-stubs';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { triggerMessageEvent, windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
import setupTestContext from './setup-test-context';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const methodAuthenticationTests = (test) => {
|
||||
test('it sets token data on login for default path', async function (assert) {
|
||||
assert.expect(5);
|
||||
// Setup
|
||||
this.stubRequests();
|
||||
// Render and log in
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
await fillInLoginFields(this.loginData);
|
||||
if (this.authType === 'oidc') {
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
await click(GENERAL.submitButton);
|
||||
await waitUntil(() => this.setTokenDataSpy.calledOnce);
|
||||
const [tokenName, persistedTokenData] = this.setTokenDataSpy.lastCall.args;
|
||||
|
||||
const expectedData = {
|
||||
...TOKEN_DATA[this.authType],
|
||||
// there are other tests that confirm this calculation happens as expected, just copy value from spy
|
||||
tokenExpirationEpoch: persistedTokenData.tokenExpirationEpoch,
|
||||
};
|
||||
|
||||
assert.strictEqual(tokenName, this.tokenName, 'setTokenData is called with expected token name');
|
||||
assert.propEqual(persistedTokenData, expectedData, 'setTokenData is called with expected data');
|
||||
|
||||
// propEqual failures are challenging to parse in CI so pulling out a couple of important attrs
|
||||
const { token, displayName, entity_id } = expectedData;
|
||||
assert.strictEqual(persistedTokenData.token, token, 'setTokenData has expected token');
|
||||
assert.strictEqual(persistedTokenData.displayName, displayName, 'setTokenData has expected display name');
|
||||
assert.strictEqual(persistedTokenData.entity_id, entity_id, 'setTokenData has expected entity_id');
|
||||
});
|
||||
|
||||
test('it calls onAuthSuccess on submit for custom path', async function (assert) {
|
||||
assert.expect(1);
|
||||
// Setup
|
||||
this.path = `${this.authType}-custom`;
|
||||
this.loginData = { ...this.loginData, path: this.path };
|
||||
this.stubRequests();
|
||||
// Render and log in
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
// toggle mount path input to specify custom path
|
||||
await fillInLoginFields(this.loginData, { toggleOptions: true });
|
||||
if (this.authType === 'oidc') {
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
await waitUntil(() => this.onAuthSuccess.calledOnce);
|
||||
const [actual] = this.onAuthSuccess.lastCall.args;
|
||||
const expected = { namespace: '', token: this.tokenName, isRoot: false };
|
||||
assert.propEqual(actual, expected, `onAuthSuccess called with: ${JSON.stringify(actual)}`);
|
||||
});
|
||||
};
|
||||
|
||||
module('Integration | Component | auth | page | method authentication', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
setupTestContext(this);
|
||||
this.auth = this.owner.lookup('service:auth');
|
||||
this.setTokenDataSpy = sinon.spy(this.auth, 'setTokenData');
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
module('github', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'github';
|
||||
this.loginData = { token: 'mysupersecuretoken' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.github;
|
||||
this.tokenName = 'vault-github☃1';
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login`, () => this.response);
|
||||
};
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('jwt', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'jwt';
|
||||
this.loginData = { role: 'some-dev', jwt: 'jwttoken' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.jwt.login;
|
||||
this.tokenName = 'vault-jwt☃1';
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'urlFor').returns('123-example.com');
|
||||
// Requests are stubbed in the order they are hit
|
||||
this.stubRequests = () => {
|
||||
// passing a dynamic path so that even the OIDC form renders the JWT token input
|
||||
// (there is test coverage elsewhere to assert switching between methods updates the form)
|
||||
this.server.post('/auth/:path/oidc/auth_url', () =>
|
||||
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']);
|
||||
};
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.routerStub.restore();
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('ldap', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'ldap';
|
||||
this.loginData = { username: 'matilda', password: 'password' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.ldap;
|
||||
this.tokenName = 'vault-ldap☃1';
|
||||
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login/${this.loginData.username}`, () => this.response);
|
||||
};
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('oidc', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'oidc';
|
||||
this.loginData = { role: 'some-dev' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.oidc['oidc/callback'];
|
||||
this.tokenName = 'vault-token☃1';
|
||||
// Requests are stubbed in the order they are hit
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/oidc/auth_url`, () => {
|
||||
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']);
|
||||
};
|
||||
|
||||
// additional OIDC setup
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'urlFor').returns('123-example.com');
|
||||
this.windowStub = windowStub();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.routerStub.restore();
|
||||
this.windowStub.restore();
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('okta', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'okta';
|
||||
this.loginData = { username: 'matilda', password: 'password' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.okta;
|
||||
this.tokenName = 'vault-okta☃1';
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login/${this.loginData.username}`, () => this.response);
|
||||
};
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('radius', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'radius';
|
||||
this.loginData = { username: 'matilda', password: 'password' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.radius;
|
||||
this.tokenName = 'vault-radius☃1';
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login/${this.loginData.username}`, () => this.response);
|
||||
};
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
module('token', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'token';
|
||||
this.tokenName = 'vault-token☃1';
|
||||
this.server.get('/auth/token/lookup-self', () => RESPONSE_STUBS.token);
|
||||
});
|
||||
|
||||
test('it sets token data and calls onAuthSuccess', async function (assert) {
|
||||
assert.expect(6);
|
||||
await this.renderComponent();
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
await fillInLoginFields({ token: 'mysupersecuretoken' });
|
||||
await click(GENERAL.submitButton);
|
||||
|
||||
const [actual] = this.onAuthSuccess.lastCall.args;
|
||||
const expected = { namespace: '', token: this.tokenName, isRoot: false };
|
||||
assert.propEqual(actual, expected, `onAuthSuccess called with: ${JSON.stringify(actual)}`);
|
||||
|
||||
const [tokenName, persistedTokenData] = this.setTokenDataSpy.lastCall.args;
|
||||
const expectedTokenData = {
|
||||
...TOKEN_DATA[this.authType],
|
||||
// there are other tests that confirm this calculation happens as expected, just copy value from spy
|
||||
tokenExpirationEpoch: persistedTokenData.tokenExpirationEpoch,
|
||||
};
|
||||
assert.strictEqual(tokenName, this.tokenName, 'setTokenData is called with expected token name');
|
||||
assert.propEqual(persistedTokenData, expectedTokenData, 'setTokenData is called with expected data');
|
||||
|
||||
// propEqual failures are challenging to parse in CI so pulling out a couple of important attrs
|
||||
const { token, displayName, entity_id } = expectedTokenData;
|
||||
assert.strictEqual(persistedTokenData.token, token, 'setTokenData has expected token');
|
||||
assert.strictEqual(
|
||||
persistedTokenData.displayName,
|
||||
displayName,
|
||||
'setTokenData has expected display name'
|
||||
);
|
||||
assert.strictEqual(persistedTokenData.entity_id, entity_id, 'setTokenData has expected entity_id');
|
||||
});
|
||||
});
|
||||
|
||||
module('userpass', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'userpass';
|
||||
this.loginData = { username: 'matilda', password: 'password' };
|
||||
this.path = this.authType;
|
||||
this.response = RESPONSE_STUBS.userpass;
|
||||
this.tokenName = 'vault-userpass☃1';
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login/${this.loginData.username}`, () => this.response);
|
||||
};
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
|
||||
// ENTERPRISE METHODS
|
||||
module('saml', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.version.type = 'enterprise';
|
||||
this.authType = 'saml';
|
||||
this.path = this.authType;
|
||||
this.loginData = { role: 'some-dev' };
|
||||
this.response = RESPONSE_STUBS.saml['saml/token'];
|
||||
this.tokenName = 'vault-token☃1';
|
||||
// Requests are stubbed in the order they are hit
|
||||
this.stubRequests = () => {
|
||||
this.server.put(`/auth/${this.path}/sso_service_url`, () => ({
|
||||
data: {
|
||||
sso_service_url: 'test/fake/sso/route',
|
||||
token_poll_id: '1234',
|
||||
},
|
||||
}));
|
||||
this.server.put(`/auth/${this.path}/token`, () => this.response);
|
||||
this.server.get(`/auth/token/lookup-self`, () => RESPONSE_STUBS.saml['lookup-self']);
|
||||
};
|
||||
this.windowStub = windowStub();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.windowStub.restore();
|
||||
});
|
||||
|
||||
methodAuthenticationTests(test);
|
||||
});
|
||||
});
|
||||
281
ui/tests/integration/components/auth/page/mfa-test.js
Normal file
281
ui/tests/integration/components/auth/page/mfa-test.js
Normal file
@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { constraintId, setupTotpMfaResponse } from 'vault/tests/helpers/mfa/mfa-helpers';
|
||||
import setupTestContext from './setup-test-context';
|
||||
import { ERROR_JWT_LOGIN } from 'vault/components/auth/form/oidc-jwt';
|
||||
import { overrideResponse } from 'vault/tests/helpers/stubs';
|
||||
import sinon from 'sinon';
|
||||
import { triggerMessageEvent, windowStub } from 'vault/tests/helpers/oidc-window-stub';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { MFA_SELECTORS } from 'vault/tests/helpers/mfa/mfa-selectors';
|
||||
import { fillInLoginFields } from 'vault/tests/helpers/auth/auth-helpers';
|
||||
import { click, fillIn, waitFor } from '@ember/test-helpers';
|
||||
|
||||
const mfaTests = (test) => {
|
||||
test('it displays mfa requirement for default paths', async function (assert) {
|
||||
const loginKeys = Object.keys(this.loginData);
|
||||
assert.expect(3 + loginKeys.length);
|
||||
this.stubRequests();
|
||||
await this.renderComponent();
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
await fillInLoginFields(this.loginData);
|
||||
|
||||
if (this.authType === 'oidc') {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
// pass mount path which is used to set :mount param in the callback url => /auth/:mount/oidc/callback
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitFor(MFA_SELECTORS.mfaForm);
|
||||
assert
|
||||
.dom(MFA_SELECTORS.mfaForm)
|
||||
.hasText(
|
||||
'Back Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify'
|
||||
);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue(this.authType, 'preserves method type on back');
|
||||
for (const field of loginKeys) {
|
||||
assert.dom(GENERAL.inputByAttr(field)).hasValue('', `${field} input clears on back`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it displays mfa requirement for custom paths', async function (assert) {
|
||||
this.path = `${this.authType}-custom`;
|
||||
const loginKeys = Object.keys(this.loginData);
|
||||
assert.expect(3 + loginKeys.length);
|
||||
this.stubRequests();
|
||||
await this.renderComponent();
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
// Toggle more options to input a custom mount path
|
||||
await fillInLoginFields({ ...this.loginData, path: this.path }, { toggleOptions: true });
|
||||
|
||||
if (this.authType === 'oidc') {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitFor(MFA_SELECTORS.mfaForm);
|
||||
assert
|
||||
.dom(MFA_SELECTORS.mfaForm)
|
||||
.hasText(
|
||||
'Back Multi-factor authentication is enabled for your account. Enter your authentication code to log in. TOTP passcode Verify'
|
||||
);
|
||||
await click(GENERAL.backButton);
|
||||
assert.dom(AUTH_FORM.form).exists('clicking back returns to auth form');
|
||||
assert.dom(AUTH_FORM.selectMethod).hasValue(this.authType, 'preserves method type on back');
|
||||
for (const field of loginKeys) {
|
||||
assert.dom(GENERAL.inputByAttr(field)).hasValue('', `${field} input clears on back`);
|
||||
}
|
||||
});
|
||||
|
||||
test('it submits mfa requirement for default paths', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.stubRequests();
|
||||
await this.renderComponent();
|
||||
|
||||
const expectedOtp = '12345';
|
||||
this.server.post('/sys/mfa/validate', async (_, req) => {
|
||||
const [actualOtp] = JSON.parse(req.requestBody).mfa_payload[constraintId];
|
||||
assert.true(true, 'it makes request to mfa validate endpoint');
|
||||
assert.strictEqual(actualOtp, expectedOtp, 'payload contains otp');
|
||||
});
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
await fillInLoginFields(this.loginData);
|
||||
|
||||
if (this.authType === 'oidc') {
|
||||
// fires "message" event which methods that rely on popup windows wait for
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitFor(MFA_SELECTORS.mfaForm);
|
||||
await fillIn(MFA_SELECTORS.passcode(0), expectedOtp);
|
||||
await click(MFA_SELECTORS.validate);
|
||||
});
|
||||
|
||||
test('it submits mfa requirement for custom paths', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.path = `${this.authType}-custom`;
|
||||
this.stubRequests();
|
||||
await this.renderComponent();
|
||||
|
||||
const expectedOtp = '12345';
|
||||
this.server.post('/sys/mfa/validate', async (_, req) => {
|
||||
const [actualOtp] = JSON.parse(req.requestBody).mfa_payload[constraintId];
|
||||
assert.true(true, 'it makes request to mfa validate endpoint');
|
||||
assert.strictEqual(actualOtp, expectedOtp, 'payload contains otp');
|
||||
});
|
||||
|
||||
// Fill in login form
|
||||
await fillIn(AUTH_FORM.selectMethod, this.authType);
|
||||
// Toggle more options to input a custom mount path
|
||||
await fillInLoginFields({ ...this.loginData, path: this.path }, { toggleOptions: true });
|
||||
|
||||
if (this.authType === 'oidc') {
|
||||
triggerMessageEvent(this.path);
|
||||
}
|
||||
|
||||
await click(GENERAL.submitButton);
|
||||
await waitFor(MFA_SELECTORS.mfaForm);
|
||||
await fillIn(MFA_SELECTORS.passcode(0), expectedOtp);
|
||||
await click(MFA_SELECTORS.validate);
|
||||
});
|
||||
};
|
||||
|
||||
module('Integration | Component | auth | page | mfa', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
setupTestContext(this);
|
||||
});
|
||||
|
||||
module('github', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'github';
|
||||
this.loginData = { token: 'mysupersecuretoken' };
|
||||
this.path = this.authType;
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login`, () => setupTotpMfaResponse(this.path));
|
||||
};
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('jwt', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'jwt';
|
||||
this.loginData = { role: 'some-dev', jwt: 'jwttoken' };
|
||||
this.path = this.authType;
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'urlFor').returns('123-example.com');
|
||||
|
||||
this.stubRequests = () => {
|
||||
this.server.post('/auth/:path/oidc/auth_url', () =>
|
||||
overrideResponse(400, { errors: [ERROR_JWT_LOGIN] })
|
||||
);
|
||||
this.server.post(`/auth/${this.path}/login`, () => setupTotpMfaResponse(this.path));
|
||||
};
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.routerStub.restore();
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('oidc', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'oidc';
|
||||
this.loginData = { role: 'some-dev' };
|
||||
this.path = this.authType;
|
||||
// Requests are stubbed in the order they are hit
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/oidc/auth_url`, () => {
|
||||
return { data: { auth_url: 'http://dev-foo-bar.com' } };
|
||||
});
|
||||
this.server.get(`/auth/${this.path}/oidc/callback`, () => setupTotpMfaResponse(this.path));
|
||||
};
|
||||
|
||||
// additional OIDC setup
|
||||
this.routerStub = sinon.stub(this.owner.lookup('service:router'), 'urlFor').returns('123-example.com');
|
||||
this.windowStub = windowStub();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.routerStub.restore();
|
||||
this.windowStub.restore();
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('username and password methods', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.loginData = { username: 'matilda', password: 'password' };
|
||||
this.stubRequests = () => {
|
||||
this.server.post(`/auth/${this.path}/login/matilda`, () => setupTotpMfaResponse(this.path));
|
||||
};
|
||||
});
|
||||
|
||||
module('ldap', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'ldap';
|
||||
this.path = this.authType;
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('okta', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'okta';
|
||||
this.path = this.authType;
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('radius', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'radius';
|
||||
this.path = this.authType;
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
|
||||
module('userpass', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.authType = 'userpass';
|
||||
this.path = this.authType;
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
});
|
||||
|
||||
// ENTERPRISE METHODS
|
||||
module('saml', function (hooks) {
|
||||
hooks.beforeEach(async function () {
|
||||
this.version.type = 'enterprise';
|
||||
this.authType = 'saml';
|
||||
this.path = this.authType;
|
||||
this.loginData = { role: 'some-dev' };
|
||||
// Requests are stubbed in the order they are hit
|
||||
this.stubRequests = () => {
|
||||
this.server.put(`/auth/${this.path}/sso_service_url`, () => ({
|
||||
data: {
|
||||
sso_service_url: 'test/fake/sso/route',
|
||||
token_poll_id: '1234',
|
||||
},
|
||||
}));
|
||||
this.server.put(`/auth/${this.path}/token`, () => setupTotpMfaResponse(this.authType));
|
||||
};
|
||||
this.windowStub = windowStub();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.windowStub.restore();
|
||||
});
|
||||
|
||||
mfaTests(test);
|
||||
});
|
||||
});
|
||||
93
ui/tests/integration/components/auth/page/page-test.js
Normal file
93
ui/tests/integration/components/auth/page/page-test.js
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, fillIn, waitFor } from '@ember/test-helpers';
|
||||
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
|
||||
import { GENERAL } from 'vault/tests/helpers/general-selectors';
|
||||
import { CSP_ERROR } from 'vault/components/auth/page';
|
||||
import setupTestContext from './setup-test-context';
|
||||
|
||||
/*
|
||||
The AuthPage parents much of the authentication workflow and so it can be used to test lots of auth functionality.
|
||||
This file tests the base component functionality. The other files test method authentication, listing visibility,
|
||||
login settings (enterprise feature), and mfa.
|
||||
*/
|
||||
module('Integration | Component | auth | page', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
setupTestContext(this);
|
||||
});
|
||||
|
||||
test('it renders error on CSP violation', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.cluster.standby = true;
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.pageError.error).doesNotExist();
|
||||
this.owner.lookup('service:csp-event').handleEvent({ violatedDirective: 'connect-src' });
|
||||
await waitFor(GENERAL.pageError.error);
|
||||
assert.dom(GENERAL.pageError.error).hasText(CSP_ERROR);
|
||||
});
|
||||
|
||||
test('it renders splash logo and disables namespace input when oidc provider query param is present', async function (assert) {
|
||||
this.oidcProviderQueryParam = 'myprovider';
|
||||
this.version.features = ['Namespaces'];
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.logo).exists();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).isDisabled();
|
||||
assert
|
||||
.dom(AUTH_FORM.helpText)
|
||||
.hasText(
|
||||
'Once you log in, you will be redirected back to your application. If you require login credentials, contact your administrator.'
|
||||
);
|
||||
});
|
||||
|
||||
test('it calls onNamespaceUpdate', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.version.features = ['Namespaces'];
|
||||
await this.renderComponent();
|
||||
await fillIn(GENERAL.inputByAttr('namespace'), 'mynamespace');
|
||||
const [actual] = this.onNamespaceUpdate.lastCall.args;
|
||||
assert.strictEqual(actual, 'mynamespace', `onNamespaceUpdate called with: ${actual}`);
|
||||
});
|
||||
|
||||
test('it passes query param to namespace input', async function (assert) {
|
||||
this.version.features = ['Namespaces'];
|
||||
this.namespaceQueryParam = 'ns-1';
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).hasValue(this.namespaceQueryParam);
|
||||
});
|
||||
|
||||
test('it does not render the namespace input on community', async function (assert) {
|
||||
this.version.type = 'community';
|
||||
this.version.features = [];
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it does not render the namespace input on enterprise without the "Namespaces" feature', async function (assert) {
|
||||
this.version.type = 'enterprise';
|
||||
this.version.features = [];
|
||||
await this.renderComponent();
|
||||
assert.dom(GENERAL.inputByAttr('namespace')).doesNotExist();
|
||||
});
|
||||
|
||||
test('it selects type in the dropdown if direct link just has type', async function (assert) {
|
||||
this.directLinkData = { type: 'oidc' };
|
||||
await this.renderComponent();
|
||||
assert.dom(AUTH_FORM.tabBtn('oidc')).doesNotExist('tab does not render');
|
||||
assert.dom(GENERAL.selectByAttr('auth type')).hasValue('oidc', 'dropdown has type selected');
|
||||
assert.dom(AUTH_FORM.authForm('oidc')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('role')).exists();
|
||||
await click(AUTH_FORM.advancedSettings);
|
||||
assert.dom(GENERAL.inputByAttr('path')).exists({ count: 1 });
|
||||
assert.dom(GENERAL.backButton).doesNotExist();
|
||||
assert
|
||||
.dom(GENERAL.button('Sign in with other methods'))
|
||||
.doesNotExist('"Sign in with other methods" does not render');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import sinon from 'sinon';
|
||||
|
||||
export default (context) => {
|
||||
context.version = context.owner.lookup('service:version');
|
||||
context.cluster = { id: '1' };
|
||||
context.directLinkData = null;
|
||||
context.loginSettings = null;
|
||||
context.namespaceQueryParam = '';
|
||||
context.oidcProviderQueryParam = '';
|
||||
context.onAuthSuccess = sinon.spy();
|
||||
context.onNamespaceUpdate = sinon.spy();
|
||||
context.visibleAuthMounts = false;
|
||||
|
||||
context.renderComponent = () => {
|
||||
return render(hbs`<Auth::Page
|
||||
@cluster={{this.cluster}}
|
||||
@directLinkData={{this.directLinkData}}
|
||||
@loginSettings={{this.loginSettings}}
|
||||
@namespaceQueryParam={{this.namespaceQueryParam}}
|
||||
@oidcProviderQueryParam={{this.oidcProviderQueryParam}}
|
||||
@onAuthSuccess={{this.onAuthSuccess}}
|
||||
@onNamespaceUpdate={{this.onNamespaceUpdate}}
|
||||
@visibleAuthMounts={{this.visibleAuthMounts}}
|
||||
/>`);
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user