vault/ui/tests/integration/components/mfa-form-test.js
Jordan Reimer d4766766f2
Ember Upgrade to 4.4 (#17086)
* runs ember-cli-update to 4.4.0

* updates yarn.lock

* updates dependencies causing runtime errors (#17135)

* Inject Store Service When Accessed Implicitly (#17345)

* adds codemod for injecting store service

* adds custom babylon parser with decorators-legacy plugin for jscodeshift transforms

* updates inject-store-service codemod to only look for .extend object expressions and adds recast options

* runs inject-store-service codemod on js files

* replace query-params helper with hash (#17404)

* Updates/removes dependencies throwing errors in Ember 4.4 (#17396)

* updates ember-responsive to latest

* updates ember-composable-helpers to latest and uses includes helper since contains was removed

* updates ember-concurrency to latest

* updates ember-cli-clipboard to latest

* temporary workaround for toolbar-link component throwing errors for using params arg with LinkTo

* adds missing store injection to auth configure route

* fixes issue with string-list component throwing error for accessing prop in same computation

* fixes non-iterable query params issue in mfa methods controller

* refactors field-to-attrs to handle belongsTo rather than fragments

* converts mount-config fragment to belongsTo on auth-method model

* removes ember-api-actions and adds tune method to auth-method adapter

* converts cluster replication attributes from fragment to relationship

* updates ember-data, removes ember-data-fragments and updates yarn to latest

* removes fragments from secret-engine model

* removes fragment from test-form-model

* removes commented out code

* minor change to inject-store-service codemod and runs again on js files

* Remove LinkTo positional params (#17421)

* updates ember-cli-page-object to latest version

* update toolbar-link to support link-to args and not positional params

* adds replace arg to toolbar-link component

* Clean up js lint errors (#17426)

* replaces assert.equal to assert.strictEqual

* update eslint no-console to error and disables invididual intended uses of console

* cleans up hbs lint warnings (#17432)

* Upgrade bug and test fixes (#17500)

* updates inject-service codemod to take arg for service name and runs for flashMessages service

* fixes hbs lint error after merging main

* fixes flash messages

* updates more deps

* bug fixes

* test fixes

* updates ember-cli-content-security-policy and prevents default form submission throwing errors

* more bug and test fixes

* removes commented out code

* fixes issue with code-mirror modifier sending change event on setup causing same computation error

* Upgrade Clean Up (#17543)

* updates deprecation workflow and filter

* cleans up build errors, removes unused ivy-codemirror and sass and updates ember-cli-sass and node-sass to latest

* fixes control groups test that was skipped after upgrade

* updates control group service tests

* addresses review feedback

* updates control group service handleError method to use router.currentURL rather that transition.intent.url

* adds changelog entry
2022-10-18 09:46:02 -06:00

229 lines
8.5 KiB
JavaScript

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { fillIn, click, waitUntil } from '@ember/test-helpers';
import { _cancelTimers as cancelTimers, later } from '@ember/runloop';
import { TOTP_VALIDATION_ERROR } from 'vault/components/mfa/mfa-form';
module('Integration | Component | mfa-form', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.clusterId = '123456';
this.mfaAuthData = {
backend: 'userpass',
data: { username: 'foo', password: 'bar' },
};
this.authService = this.owner.lookup('service:auth');
// setup basic totp mfa_requirement
// override in tests that require different scenarios
this.totpConstraint = this.server.create('mfa-method', { type: 'totp' });
const { mfa_requirement } = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa: { any: [this.totpConstraint] } },
});
this.mfaAuthData.mfa_requirement = mfa_requirement;
});
test('it should render correct descriptions', async function (assert) {
const totpConstraint = this.server.create('mfa-method', { type: 'totp' });
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [totpConstraint] } },
}).mfa_requirement;
await render(
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Enter your authentication code to log in.',
'Correct description renders for single passcode'
);
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [duoConstraint, oktaConstraint] } },
}).mfa_requirement;
await render(
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Select the MFA method you wish to use.',
'Correct description renders for multiple methods'
);
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] }, test_mfa_2: { any: [duoConstraint] } },
}).mfa_requirement;
await render(
hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} @onError={{fn (mut this.error)}} />`
);
assert
.dom('[data-test-mfa-description]')
.includesText(
'Two methods are required for successful authentication.',
'Correct description renders for multiple constraints'
);
});
test('it should render method selects and passcode inputs', async function (assert) {
assert.expect(2);
const duoConstraint = this.server.create('mfa-method', { type: 'duo', uses_passcode: true });
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
const pingidConstraint = this.server.create('mfa-method', { type: 'pingid' });
const { mfa_requirement } = this.authService._parseMfaResponse({
mfa_request_id: 'test-mfa-id',
mfa_constraints: {
test_mfa_1: {
any: [pingidConstraint, oktaConstraint],
},
test_mfa_2: {
any: [duoConstraint],
},
},
});
this.mfaAuthData.mfa_requirement = mfa_requirement;
this.server.post('/sys/mfa/validate', (schema, req) => {
const json = JSON.parse(req.requestBody);
const payload = {
mfa_request_id: 'test-mfa-id',
mfa_payload: { [oktaConstraint.id]: [], [duoConstraint.id]: ['test-code'] },
};
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
return {};
});
this.owner.lookup('service:auth').reopen({
// override to avoid authSuccess method since it expects an auth payload
async totpValidate({ mfa_requirement }) {
await this.clusterAdapter().mfaValidate(mfa_requirement);
return 'test response';
},
});
this.onSuccess = (resp) =>
assert.strictEqual(resp, 'test response', 'Response is returned in onSuccess callback');
await render(hbs`
<Mfa::MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
@onSuccess={{this.onSuccess}}
/>
`);
await fillIn('[data-test-mfa-select="0"] select', oktaConstraint.id);
await fillIn('[data-test-mfa-passcode="1"]', 'test-code');
await click('[data-test-mfa-validate]');
});
test('it should validate mfa requirement', async function (assert) {
assert.expect(5);
this.server.post('/sys/mfa/validate', (schema, req) => {
const json = JSON.parse(req.requestBody);
const payload = {
mfa_request_id: 'test-mfa-id',
mfa_payload: { [this.totpConstraint.id]: ['test-code'] },
};
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
return {};
});
const expectedAuthData = { clusterId: this.clusterId, ...this.mfaAuthData };
this.owner.lookup('service:auth').reopen({
// override to avoid authSuccess method since it expects an auth payload
async totpValidate(authData) {
await waitUntil(() =>
assert.dom('[data-test-mfa-validate]').hasClass('is-loading', 'Loading class applied to button')
);
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled while loading');
assert.deepEqual(authData, expectedAuthData, 'Mfa auth data passed to validate method');
await this.clusterAdapter().mfaValidate(authData.mfa_requirement);
return 'test response';
},
});
this.onSuccess = (resp) =>
assert.strictEqual(resp, 'test response', 'Response is returned in onSuccess callback');
await render(hbs`
<Mfa::MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
@onSuccess={{this.onSuccess}}
/>
`);
await fillIn('[data-test-mfa-passcode]', 'test-code');
await click('[data-test-mfa-validate]');
});
test('it should show countdown on passcode already used and rate limit errors', async function (assert) {
const messages = {
used: 'code already used; new code is available in 45 seconds',
limit:
'maximum TOTP validation attempts 4 exceeded the allowed attempts 3. Please try again in 15 seconds',
};
const codes = ['used', 'limit'];
for (let code of codes) {
this.owner.lookup('service:auth').reopen({
totpValidate() {
throw { errors: [messages[code]] };
},
});
await render(hbs`
<Mfa::MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
/>
`);
await fillIn('[data-test-mfa-passcode]', code);
later(() => cancelTimers(), 50);
await click('[data-test-mfa-validate]');
assert
.dom('[data-test-mfa-countdown]')
.hasText(
code === 'used' ? '45' : '15',
'countdown renders with correct initial value from error response'
);
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled during countdown');
assert.dom('[data-test-mfa-passcode]').isDisabled('Input is disabled during countdown');
assert.dom('[data-test-inline-error-message]').exists('Alert message renders');
}
});
test('it should show error message for passcode invalid error', async function (assert) {
this.owner.lookup('service:auth').reopen({
totpValidate() {
throw { errors: ['failed to validate'] };
},
});
await render(hbs`
<Mfa::MfaForm
@clusterId={{this.clusterId}}
@authData={{this.mfaAuthData}}
/>
`);
await fillIn('[data-test-mfa-passcode]', 'test-code');
later(() => cancelTimers(), 50);
await click('[data-test-mfa-validate]');
assert
.dom('[data-test-error]')
.includesText(TOTP_VALIDATION_ERROR, 'Generic error message renders for passcode validation error');
});
});