vault/ui/tests/acceptance/mfa-method-test.js
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

308 lines
13 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { click, currentRouteName, currentURL, fillIn, visit } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth';
import { setupMirage } from 'ember-cli-mirage/test-support';
import ENV from 'vault/config/environment';
import { Response } from 'miragejs';
import { underscore } from '@ember/string';
module('Acceptance | mfa-method', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
hooks.before(function () {
ENV['ember-cli-mirage'].handler = 'mfaConfig';
});
hooks.beforeEach(async function () {
this.store = this.owner.lookup('service:store');
this.getMethods = () =>
['Totp', 'Duo', 'Okta', 'Pingid'].reduce((methods, type) => {
methods.addObjects(this.server.db[`mfa${type}Methods`].where({}));
return methods;
}, []);
return authPage.login();
});
hooks.after(function () {
ENV['ember-cli-mirage'].handler = null;
});
test('it should display landing page when no methods exist', async function (assert) {
this.server.get('/identity/mfa/method/', () => new Response(404, {}, { errors: [] }));
await visit('/vault/access/mfa/methods');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.index',
'Route redirects to mfa index when no methods exist'
);
await click('[data-test-mfa-configure]');
assert.strictEqual(currentRouteName(), 'vault.cluster.access.mfa.methods.create');
});
test('it should list methods', async function (assert) {
await visit('/vault/access/mfa');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.index',
'Parent route redirects to methods when some exist'
);
assert.dom('[data-test-tab="methods"]').hasClass('active', 'Methods tab is active');
assert.dom('.toolbar-link').exists({ count: 1 }, 'Correct number of toolbar links render');
assert.dom('[data-test-mfa-method-create]').includesText('New MFA method', 'New mfa link renders');
await click('[data-test-mfa-method-create]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.create',
'New method link transitions to create route'
);
await click('.breadcrumb a');
const methods = this.getMethods();
const model = this.store.peekRecord('mfa-method', methods[0].id);
assert.dom('[data-test-mfa-method-list-item]').exists({ count: methods.length }, 'Methods list renders');
assert.dom(`[data-test-mfa-method-list-icon="${model.type}"]`).exists('Icon renders for list item');
assert
.dom(`[data-test-mfa-method-list-item="${model.id}"]`)
.includesText(
`${model.name} ${model.id} Namespace: ${model.namespace_id}`,
'Copy renders for list item'
);
await click('[data-test-popup-menu-trigger]');
await click('[data-test-mfa-method-menu-link="details"]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.index',
'Details more menu action transitions to method route'
);
await click('.breadcrumb a');
await click('[data-test-popup-menu-trigger]');
await click('[data-test-mfa-method-menu-link="edit"]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.edit',
'Edit more menu action transitions to method edit route'
);
});
test('it should display method details', async function (assert) {
// ensure methods are tied to an enforcement
this.server.get('/identity/mfa/login-enforcement', () => {
const record = this.server.create('mfa-login-enforcement', {
mfa_method_ids: this.getMethods().mapBy('id'),
});
return {
data: {
key_info: { [record.name]: record },
keys: [record.name],
},
};
});
await visit('/vault/access/mfa/methods');
await click('[data-test-mfa-method-list-item]');
assert.dom('[data-test-tab="config"]').hasClass('active', 'Configuration tab is active by default');
assert
.dom('[data-test-confirm-action-trigger]')
.isDisabled('Delete toolbar action disabled when method is attached to an enforcement');
const fields = [
['Issuer', 'Period', 'Key size', 'QR size', 'Algorithm', 'Digits', 'Skew', 'Max validation attempts'],
['Duo API hostname', 'Passcode reminder'],
['Organization name', 'Base URL'],
['Use signature', 'Idp url', 'Admin url', 'Authenticator url', 'Org alias'],
];
for (const [index, labels] of fields.entries()) {
if (index) {
await click(`[data-test-mfa-method-list-item]:nth-of-type(${index + 2})`);
}
const url = currentURL();
const id = url.slice(url.lastIndexOf('/') + 1);
const model = this.store.peekRecord('mfa-method', id);
labels.forEach((label) => {
assert.dom(`[data-test-row-label="${label}"]`).hasText(label, `${label} field label renders`);
const key =
{
'Duo API hostname': 'api_hostname',
'Passcode reminder': 'use_passcode',
'Organization name': 'org_name',
}[label] || underscore(label);
const value = typeof model[key] === 'boolean' ? (model[key] ? 'Yes' : 'No') : model[key].toString();
assert.dom(`[data-test-value-div="${label}"]`).hasText(value, `${label} value renders`);
});
await click('.breadcrumb a');
}
await click('[data-test-mfa-method-list-item]');
await click('[data-test-mfa-method-edit]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.edit',
'Toolbar action transitions to edit route'
);
});
test('it should delete method that is not associated with any login enforcements', async function (assert) {
this.server.get('/identity/mfa/login-enforcement', () => new Response(404, {}, { errors: [] }));
await visit('/vault/access/mfa/methods');
const methodCount = this.element.querySelectorAll('[data-test-mfa-method-list-item]').length;
await click('[data-test-mfa-method-list-item]');
await click('[data-test-confirm-action-trigger]');
await click('[data-test-confirm-button]');
assert.dom('[data-test-mfa-method-list-item]').exists({ count: methodCount - 1 }, 'Method was deleted');
});
test('it should create methods', async function (assert) {
assert.expect(12);
await visit('/vault/access/mfa/methods');
const methodCount = this.element.querySelectorAll('[data-test-mfa-method-list-item]').length;
const methods = [
{ type: 'totp', required: ['issuer'] },
{ type: 'duo', required: ['secret_key', 'integration_key', 'api_hostname'] },
{ type: 'okta', required: ['org_name', 'api_token'] },
{ type: 'pingid', required: ['settings_file_base64'] },
];
for (const [index, method] of methods.entries()) {
const { type, required } = method;
await click('[data-test-mfa-method-create]');
await click(`[data-test-radio-card="${method.type}"]`);
await click('[data-test-mfa-create-next]');
await click('[data-test-mleh-radio="skip"]');
await click('[data-test-mfa-create-save]');
assert
.dom('[data-test-inline-error-message]')
.exists({ count: required.length }, `Required field validations display for ${type}`);
for (const [i, field] of required.entries()) {
let inputType = 'input';
// this is less than ideal but updating the test selectors in masked-input break a bunch of tests
// add value to the masked input text area data-test attributes for selection
if (['secret_key', 'integration_key'].includes(field)) {
inputType = 'textarea';
const textareas = this.element.querySelectorAll('[data-test-textarea]');
textareas[i].setAttribute('data-test-textarea', field);
}
await fillIn(`[data-test-${inputType}="${field}"]`, 'foo');
}
await click('[data-test-mfa-create-save]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.index',
`${type} method is displayed on save`
);
await click('.breadcrumb a');
assert
.dom('[data-test-mfa-method-list-item]')
.exists({ count: methodCount + index + 1 }, `List updates with new ${type} method`);
}
});
test('it should create method with new enforcement', async function (assert) {
await visit('/vault/access/mfa/methods/create');
await click('[data-test-radio-card="totp"]');
await click('[data-test-mfa-create-next]');
await fillIn('[data-test-input="issuer"]', 'foo');
await fillIn('[data-test-mlef-input="name"]', 'bar');
await fillIn('[data-test-mount-accessor-select]', 'auth_userpass_bb95c2b1');
await click('[data-test-mlef-add-target]');
await click('[data-test-mfa-create-save]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.index',
'Route transitions to method on save'
);
await click('[data-test-tab="enforcements"]');
assert.dom('[data-test-list-item]').hasText('bar', 'Enforcement is listed in method view');
await click('[data-test-sidebar-nav-link="Multi-Factor Authentication"]');
await click('[data-test-tab="enforcements"]');
assert.dom('[data-test-list-item="bar"]').hasText('bar', 'Enforcement is listed in enforcements view');
await click('[data-test-list-item="bar"]');
await click('[data-test-tab="methods"]');
assert
.dom('[data-test-mfa-method-list-item]')
.includesText('TOTP', 'TOTP method is listed in enforcement view');
});
test('it should create method and add it to existing enforcement', async function (assert) {
await visit('/vault/access/mfa/methods/create');
await click('[data-test-radio-card="totp"]');
await click('[data-test-mfa-create-next]');
await fillIn('[data-test-input="issuer"]', 'foo');
await click('[data-test-mleh-radio="existing"]');
await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger');
const enforcement = this.element.querySelector('.ember-power-select-option');
const name = enforcement.children[0].textContent.trim();
await click(enforcement);
await click('[data-test-mfa-create-save]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.methods.method.index',
'Route transitions to method on save'
);
await click('[data-test-tab="enforcements"]');
assert.dom('[data-test-list-item]').hasText(name, 'Enforcement is listed in method view');
});
test('it should edit methods', async function (assert) {
await visit('/vault/access/mfa/methods');
const id = this.element.querySelector('[data-test-mfa-method-list-item] .tag').textContent.trim();
const model = this.store.peekRecord('mfa-method', id);
await click('[data-test-mfa-method-list-item] .ember-basic-dropdown-trigger');
await click('[data-test-mfa-method-menu-link="edit"]');
const keys = ['issuer', 'period', 'key_size', 'qr_size', 'algorithm', 'digits', 'skew'];
keys.forEach((key) => {
if (key === 'period') {
assert
.dom('[data-test-ttl-value="Period"]')
.hasValue(model.period.toString(), 'Period form field is populated with model value');
assert.dom('[data-test-select="ttl-unit"]').hasValue('s', 'Correct time unit is shown for period');
} else if (key === 'algorithm' || key === 'digits' || key === 'skew') {
const radioElem = this.element.querySelector(`input[name=${key}]:checked`);
assert
.dom(radioElem)
.hasValue(model[key].toString(), `${key} form field is populated with model value`);
} else {
assert
.dom(`[data-test-input="${key}"]`)
.hasValue(model[key].toString(), `${key} form field is populated with model value`);
}
});
await fillIn('[data-test-input="issuer"]', 'foo');
const SHA1radioBtn = this.element.querySelectorAll('input[name=algorithm]')[0];
await click(SHA1radioBtn);
await fillIn('[data-test-input="max_validation_attempts"]', 10);
await click('[data-test-mfa-method-save]');
await fillIn('[data-test-confirmation-modal-input]', model.type);
await click('[data-test-confirm-button]');
assert.dom('[data-test-row-value="Issuer"]').hasText('foo', 'Issuer field is updated');
assert.dom('[data-test-row-value="Algorithm"]').hasText('SHA1', 'Algorithm field is updated');
assert
.dom('[data-test-row-value="Max validation attempts"]')
.hasText('10', 'Max validation attempts field is updated');
});
test('it should navigate to enforcements create route from method enforcement tab', async function (assert) {
await visit('/vault/access/mfa/methods');
await click('[data-test-mfa-method-list-item]');
await click('[data-test-tab="enforcements"]');
await click('[data-test-enforcement-create]');
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.mfa.enforcements.create',
'Navigates to enforcements create route from toolbar action'
);
});
});