Skip to content

Commit

Permalink
backport of commit 4e02a7a (#27575)
Browse files Browse the repository at this point in the history
Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com>
  • Loading branch information
1 parent bd2c2a2 commit 12da388
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 48 deletions.
5 changes: 3 additions & 2 deletions ui/app/components/auth-jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Ember from 'ember';
import { service } from '@ember/service';
// ARG NOTE: Once you remove outer-html after glimmerizing you can remove the outer-html component
import Component from './outer-html';
import { task, timeout, waitForEvent } from 'ember-concurrency';
import { debounce } from '@ember/runloop';

const WAIT_TIME = 500;
const ERROR_WINDOW_CLOSED =
'The provider window was closed before authentication was complete. Your web browser may have blocked or closed a pop-up window. Please check your settings and click Sign In to try again.';
const ERROR_MISSING_PARAMS =
Expand Down Expand Up @@ -109,6 +108,8 @@ export default Component.extend({

watchPopup: task(function* (oidcWindow) {
while (true) {
const WAIT_TIME = Ember.testing ? 50 : 500;

yield timeout(WAIT_TIME);
if (!oidcWindow || oidcWindow.closed) {
return this.handleOIDCError(ERROR_WINDOW_CLOSED);
Expand Down
8 changes: 6 additions & 2 deletions ui/app/components/mfa/mfa-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/

import Ember from 'ember';
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
Expand All @@ -29,7 +30,7 @@ export const TOTP_VALIDATION_ERROR =
export default class MfaForm extends Component {
@service auth;

@tracked countdown;
@tracked countdown = 0;
@tracked error;
@tracked codeDelayMessage;

Expand Down Expand Up @@ -104,7 +105,10 @@ export default class MfaForm extends Component {
@task *newCodeDelay(message) {
// parse validity period from error string to initialize countdown
this.countdown = parseInt(message.match(/(\d\w seconds)/)[0].split(' ')[0]);
while (this.countdown) {

if (Ember.testing) return;

while (this.countdown > 0) {
yield timeout(1000);
this.countdown--;
}
Expand Down
8 changes: 4 additions & 4 deletions ui/app/templates/components/mfa/mfa-form.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
placeholder={{if (gt constraint.methods.length 1) "Enter passcode"}}
spellcheck="false"
autofocus="true"
disabled={{or this.validate.isRunning this.newCodeDelay.isRunning}}
disabled={{or this.validate.isRunning this.countdown}}
@value={{constraint.passcode}}
data-test-mfa-passcode={{index}}
/>
Expand All @@ -56,7 +56,7 @@
{{/if}}
{{/each}}
</div>
{{#if this.newCodeDelay.isRunning}}
{{#if this.countdown}}
<div>
<AlertInline @type="danger" @message={{this.codeDelayMessage}} />
</div>
Expand All @@ -66,10 +66,10 @@
@icon={{if this.validate.isRunning "loading"}}
id="validate"
type="submit"
disabled={{or this.validate.isRunning this.newCodeDelay.isRunning}}
disabled={{or this.validate.isRunning this.countdown}}
data-test-mfa-validate
/>
{{#if this.newCodeDelay.isRunning}}
{{#if this.countdown}}
<Icon @name="delay" class="has-text-grey" />
<span class="has-text-grey is-v-centered" data-test-mfa-countdown>{{this.countdown}}</span>
{{/if}}
Expand Down
3 changes: 3 additions & 0 deletions ui/tests/acceptance/oidc-auth-method-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module('Acceptance | oidc auth method', function (hooks) {
window.postMessage(buildMessage().data, window.origin);
cancelTimers();
}, 100);

await click('[data-test-auth-submit]');
});

Expand Down Expand Up @@ -98,6 +99,7 @@ module('Acceptance | oidc auth method', function (hooks) {
window.postMessage(buildMessage().data, window.origin);
cancelTimers();
}, 50);

await click('[data-test-auth-submit]');
});

Expand All @@ -109,6 +111,7 @@ module('Acceptance | oidc auth method', function (hooks) {
window.postMessage(buildMessage().data, window.origin);
cancelTimers();
}, 50);

await click('[data-test-auth-submit]');
await waitUntil(() => find('[data-test-user-menu-trigger]'));
await click('[data-test-user-menu-trigger]');
Expand Down
1 change: 0 additions & 1 deletion ui/tests/integration/components/auth-form-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ module('Integration | Component | auth form', function (hooks) {
this.set('wrappedToken', '54321');
await render(hbs`<AuthForm @cluster={{this.cluster}} @wrappedToken={{this.wrappedToken}} />`);
later(() => cancelTimers(), 50);

await settled();
assert.strictEqual(
component.errorText,
Expand Down
12 changes: 10 additions & 2 deletions ui/tests/integration/components/auth-jwt-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ module('Integration | Component | auth jwt', function (hooks) {
await waitUntil(() => {
return this.openSpy.calledOnce;
});

cancelTimers();
await settled();

const call = this.openSpy.getCall(0);
assert.deepEqual(
call.args,
Expand Down Expand Up @@ -201,6 +204,8 @@ module('Integration | Component | auth jwt', function (hooks) {
buildMessage({ data: { source: 'oidc-callback', state: 'state', foo: 'bar' } })
);
cancelTimers();
await settled();

assert.strictEqual(this.error, ERROR_MISSING_PARAMS, 'calls onError with params missing error');
});

Expand All @@ -226,9 +231,11 @@ module('Integration | Component | auth jwt', function (hooks) {
return this.openSpy.calledOnce;
});
this.window.trigger('message', buildMessage({ origin: 'http://hackerz.com' }));

cancelTimers();
await settled();
assert.notOk(this.handler.called, 'should not call the submit handler');

assert.false(this.handler.called, 'should not call the submit handler');
});

test('oidc: fails silently when event is not trusted', async function (assert) {
Expand All @@ -242,7 +249,8 @@ module('Integration | Component | auth jwt', function (hooks) {
this.window.trigger('message', buildMessage({ isTrusted: false }));
cancelTimers();
await settled();
assert.notOk(this.handler.called, 'should not call the submit handler');

assert.false(this.handler.called, 'should not call the submit handler');
});

test('oidc: it should trigger error callback when role is not found', async function (assert) {
Expand Down
22 changes: 13 additions & 9 deletions ui/tests/integration/components/control-group-success-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,29 @@ module('Integration | Component | control group success', function (hooks) {
this.set('model', MODEL);
this.set('response', response);
await render(hbs`<ControlGroupSuccess @model={{this.model}} @controlGroupResponse={{this.response}} />`);
assert.ok(component.showsNavigateMessage, 'shows unwrap message');

assert.true(component.showsNavigateMessage, 'shows unwrap message');

await component.navigate();
later(() => cancelTimers(), 50);
return settled().then(() => {
assert.ok(this.controlGroup.markTokenForUnwrap.calledOnce, 'marks token for unwrap');
assert.ok(this.router.transitionTo.calledOnce, 'calls router transition');
});
await settled();

assert.true(this.controlGroup.markTokenForUnwrap.calledOnce, 'marks token for unwrap');
assert.true(this.router.transitionTo.calledOnce, 'calls router transition');
});

test('render without token', async function (assert) {
assert.expect(2);
this.set('model', MODEL);
await render(hbs`<ControlGroupSuccess @model={{this.model}} />`);
assert.ok(component.showsUnwrapForm, 'shows unwrap form');

assert.true(component.showsUnwrapForm, 'shows unwrap form');

await component.token('token');
component.unwrap();
later(() => cancelTimers(), 50);
return settled().then(() => {
assert.ok(component.showsJsonViewer, 'shows unwrapped data');
});
await settled();

assert.true(component.showsJsonViewer, 'shows unwrapped data');
});
});
18 changes: 9 additions & 9 deletions ui/tests/integration/components/edit-form-kmip-role-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,15 @@ module('Integration | Component | edit form kmip role', function (hooks) {
click('[data-test-edit-form-submit]');

later(() => cancelTimers(), 50);
return settled().then(() => {
for (const afterStateKey of Object.keys(stateAfterSave)) {
assert.strictEqual(
model.get(afterStateKey),
stateAfterSave[afterStateKey],
`sets ${afterStateKey} on save`
);
}
});
await settled();

for (const afterStateKey of Object.keys(stateAfterSave)) {
assert.strictEqual(
model.get(afterStateKey),
stateAfterSave[afterStateKey],
`sets ${afterStateKey} on save`
);
}
});
}
});
14 changes: 7 additions & 7 deletions ui/tests/integration/components/edit-form-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ module('Integration | Component | edit form', function (hooks) {

component.submit();
later(() => cancelTimers(), 50);
return settled().then(() => {
assert.ok(saveSpy.calledOnce, 'calls passed onSave');
assert.strictEqual(saveSpy.getCall(0).args[0].saveType, 'save');
assert.deepEqual(saveSpy.getCall(0).args[0].model, this.model, 'passes model to onSave');
const flash = this.owner.lookup('service:flash-messages');
assert.strictEqual(flash.success.callCount, 1, 'calls flash message success');
});
await settled();

assert.true(saveSpy.calledOnce, 'calls passed onSave');
assert.strictEqual(saveSpy.getCall(0).args[0].saveType, 'save');
assert.deepEqual(saveSpy.getCall(0).args[0].model, this.model, 'passes model to onSave');
const flash = this.owner.lookup('service:flash-messages');
assert.strictEqual(flash.success.callCount, 1, 'calls flash message success');
});
});
20 changes: 15 additions & 5 deletions ui/tests/integration/components/mfa-form-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, settled, fillIn, click, waitUntil, waitFor } 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';

Expand Down Expand Up @@ -84,6 +83,12 @@ module('Integration | Component | mfa-form', function (hooks) {
);
});

test('it should render a submit button', async function (assert) {
await render(hbs`<Mfa::MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);

assert.dom('[data-test-mfa-validate]').isNotDisabled('Button is not disabled by default');
});

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 });
Expand Down Expand Up @@ -170,7 +175,6 @@ module('Integration | Component | mfa-form', function (hooks) {
await click('[data-test-mfa-validate]');
});

// TODO JLR: It doesn't appear that cancelTimers is working and tests wait for the full countdown
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',
Expand All @@ -184,12 +188,16 @@ module('Integration | Component | mfa-form', function (hooks) {
throw { errors: [messages[code]] };
},
});
const expectedTime = code === 'used' ? 45 : 15;

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]');
const expectedTime = code === 'used' ? '45' : '15';

await waitFor('[data-test-mfa-countdown]');

assert
.dom('[data-test-mfa-countdown]')
.includesText(expectedTime, 'countdown renders with correct initial value from error response');
Expand All @@ -209,6 +217,8 @@ module('Integration | Component | mfa-form', function (hooks) {

await fillIn('[data-test-mfa-passcode]', 'test-code');
later(() => cancelTimers(), 50);
await settled();

await click('[data-test-mfa-validate]');
assert
.dom('[data-test-message-error]')
Expand Down
8 changes: 4 additions & 4 deletions ui/tests/integration/components/mount-backend-form-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ module('Integration | Component | mount backend form', function (hooks) {
later(() => cancelTimers(), 50);
await settled();

assert.ok(spy.calledOnce, 'calls the passed success method');
assert.ok(
assert.true(spy.calledOnce, 'calls the passed success method');
assert.true(
this.flashSuccessSpy.calledWith('Successfully mounted the approle auth method at foo.'),
'Renders correct flash message'
);
Expand Down Expand Up @@ -184,8 +184,8 @@ module('Integration | Component | mount backend form', function (hooks) {
later(() => cancelTimers(), 50);
await settled();

assert.ok(spy.calledOnce, 'calls the passed success method');
assert.ok(
assert.true(spy.calledOnce, 'calls the passed success method');
assert.true(
this.flashSuccessSpy.calledWith('Successfully mounted the ssh secrets engine at foo.'),
'Renders correct flash message'
);
Expand Down
18 changes: 15 additions & 3 deletions ui/tests/unit/components/auth-jwt-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { settled } from '@ember/test-helpers';
import EmberObject from '@ember/object';
import Evented from '@ember/object/evented';
import sinon from 'sinon';
Expand All @@ -29,9 +30,14 @@ module('Unit | Component | auth-jwt', function (hooks) {
this.component.prepareForOIDC.perform(mockWindow.create());
this.component.window.trigger('message', { origin: 'http://anotherdomain.com', isTrusted: true });

assert.ok(this.errorSpy.notCalled, 'Error handler not triggered while waiting for oidc callback message');
assert.true(
this.errorSpy.notCalled,
'Error handler not triggered while waiting for oidc callback message'
);
assert.strictEqual(this.component.exchangeOIDC.performCount, 0, 'exchangeOIDC method not fired');

cancelTimers();
await settled();
});

test('it should ignore untrusted messages while waiting for oidc callback', async function (assert) {
Expand All @@ -40,10 +46,11 @@ module('Unit | Component | auth-jwt', function (hooks) {
this.component.window.trigger('message', { origin: 'http://localhost:4200', isTrusted: false });
assert.ok(this.errorSpy.notCalled, 'Error handler not triggered while waiting for oidc callback message');
assert.strictEqual(this.component.exchangeOIDC.performCount, 0, 'exchangeOIDC method not fired');

cancelTimers();
await settled();
});

// TODO: Flaky
// 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(2);
Expand All @@ -65,12 +72,17 @@ module('Unit | Component | auth-jwt', function (hooks) {
message.data.source = 'oidc-callback';
this.component.window.trigger('message', message);

assert.ok(this.errorSpy.notCalled, 'Error handler not triggered while waiting for oidc callback message');
assert.true(
this.errorSpy.notCalled,
'Error handler not triggered while waiting for oidc callback message'
);
assert.strictEqual(
this.component.exchangeOIDC.performCount,
1,
'exchangeOIDC method fires when oidc callback message is received'
);

cancelTimers();
await settled();
});
});

0 comments on commit 12da388

Please sign in to comment.