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:
claire bontempo 2025-06-12 09:46:45 -07:00 committed by GitHub
parent 068d576425
commit e80d0ac68c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1792 additions and 1081 deletions

View File

@ -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 {

View File

@ -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>

View File

@ -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`);
}
});

View File

@ -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');

View File

@ -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);
});
});
}

View File

@ -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',
() =>

View File

@ -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

View File

@ -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');
});
});

View File

@ -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));

View File

@ -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');
});

View File

@ -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]',

View File

@ -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 = {

View 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: '',
},
};

View File

@ -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}"]`,

View File

@ -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);
};

View File

@ -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();

View File

@ -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);

View File

@ -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

View File

@ -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) {

View File

@ -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');
});

View File

@ -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) {

View File

@ -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);
});
});
});
});
});

View File

@ -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');
});
});
});

View 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);
});
});
});
});

View File

@ -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);
});
});

View 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);
});
});

View 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');
});
});

View File

@ -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}}
/>`);
};
};