Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('AddressVerificationMethodContextProvider', () => {
);
return (
<>
<div>Current value: {addressVerificationMethod}</div>
<div>Current value: {String(addressVerificationMethod)}</div>
<button
type="button"
onClick={() =>
Expand All @@ -32,7 +32,7 @@ describe('AddressVerificationMethodContextProvider', () => {
</AddressVerificationMethodContextProvider>,
);

expect(getByText('Current value: phone')).to.exist();
expect(getByText('Current value: null')).to.exist();
});

it('can be overridden with an initial method', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ReactNode } from 'react';
/**
* Mechanisms by which a user can verify their address.
*/
export type AddressVerificationMethod = 'phone' | 'gpo';
export type AddressVerificationMethod = 'phone' | 'gpo' | null;

/**
* Context provider props.
Expand Down Expand Up @@ -40,7 +40,7 @@ interface AddressVerificationMethodContextValue {
/**
* Default address verification method.
*/
const DEFAULT_METHOD: AddressVerificationMethod = 'phone';
const DEFAULT_METHOD: AddressVerificationMethod = null;

/**
* Address verification method context container.
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/packages/verify-flow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ export { default as FlowContext } from './context/flow-context';
export { SecretsContextProvider } from './context/secrets-context';
export { default as StartOverOrCancel } from './start-over-or-cancel';
export { default as VerifyFlow } from './verify-flow';
export { default as personalKeyStep } from './steps/personal-key';
export { default as personalKeyConfirmStep } from './steps/personal-key-confirm';

export type { SecretValues } from './context/secrets-context';
export type { AddressVerificationMethod } from './context/address-verification-method-context';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ function PersonalKeyStep({ value }: PersonalKeyStepProps) {

return (
<>
<Alert type="success" className="margin-bottom-4">
{addressVerificationMethod === 'phone'
? t('idv.messages.confirm')
: t('idv.messages.mail_sent')}
</Alert>
{addressVerificationMethod && (
<Alert type="success" className="margin-bottom-4">
{addressVerificationMethod === 'phone' && t('idv.messages.confirm')}
{addressVerificationMethod === 'gpo' && t('idv.messages.mail_sent')}
</Alert>
)}
<PageHeading>{t('headings.personal_key')}</PageHeading>
<p>{t('instructions.personal_key.info')}</p>
<div className="full-width-box margin-y-5">
Expand Down
12 changes: 8 additions & 4 deletions app/javascript/packages/verify-flow/verify-flow.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import userEvent from '@testing-library/user-event';
import * as analytics from '@18f/identity-analytics';
import { useSandbox } from '@18f/identity-test-helpers';
import { STEPS } from './steps';
import VerifyFlow from './verify-flow';
import VerifyFlow, { VerifyFlowProps } from './verify-flow';

describe('VerifyFlow', () => {
const sandbox = useSandbox();
const personalKey = '0000-0000-0000-0000';
const DEFAULT_PROPS = {
basePath: '/',
initialAddressVerificationMethod: 'phone',
onComplete: () => {},
} as VerifyFlowProps;

beforeEach(() => {
sandbox.spy(analytics, 'trackEvent');
Expand All @@ -23,7 +28,7 @@ describe('VerifyFlow', () => {
const onComplete = sandbox.spy();

const { getByText, findByText, getByLabelText } = render(
<VerifyFlow initialValues={{ personalKey }} onComplete={onComplete} basePath="/" />,
<VerifyFlow {...DEFAULT_PROPS} initialValues={{ personalKey }} onComplete={onComplete} />,
);

// Password confirm
Expand Down Expand Up @@ -62,10 +67,9 @@ describe('VerifyFlow', () => {
it('sets details according to the first enabled steps', () => {
render(
<VerifyFlow
{...DEFAULT_PROPS}
initialValues={{ personalKey }}
onComplete={() => {}}
enabledStepNames={[STEPS[1].name]}
basePath="/"
/>,
);

Expand Down
2 changes: 1 addition & 1 deletion app/javascript/packages/verify-flow/verify-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface VerifyFlowValues {
completionURL?: string;
}

interface VerifyFlowProps {
export interface VerifyFlowProps {
/**
* Initial values for the form, if applicable.
*/
Expand Down
24 changes: 24 additions & 0 deletions app/javascript/packs/verify-personal-key.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render } from 'react-dom';
import { FormSteps } from '@18f/identity-form-steps';
import { personalKeyStep, personalKeyConfirmStep } from '@18f/identity-verify-flow';

interface AppRootValues {
personalKey: string;
}

interface AppRootElement extends HTMLElement {
dataset: DOMStringMap & AppRootValues;
}

const appRoot = document.getElementById('app-root') as AppRootElement;
const appForm = document.getElementById('app-form') as HTMLFormElement;

render(
<FormSteps
steps={[personalKeyStep, personalKeyConfirmStep]}
initialValues={{ personalKey: appRoot.dataset.personalKey }}
promptOnNavigate={false}
onComplete={() => appForm.submit()}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

silly question, how do the values get in to this form before we submit it? it looks like the form lives outside of appRoot?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a silly question. The server-side controller doesn't actually care about any submitted values 🙃 The personal key is generated when the page is shown, and the form submission is basically just a redirect.

Page show:

def create
user_session[:personal_key] = create_new_code
analytics.profile_personal_key_create
create_user_event(:new_personal_key)
result = send_new_personal_key_notifications
analytics.profile_personal_key_create_notifications(**result.to_h)
flash[:info] = t('account.personal_key.old_key_will_not_work')
redirect_to manage_personal_key_url
end

Page submit:

def update
user_session.delete(:personal_key)
redirect_to next_step
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦 thanks, I forgot that checking the personal key was all JS before anyways

/>,
appRoot,
);
6 changes: 5 additions & 1 deletion app/views/users/personal_keys/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<% title t('titles.personal_key') %>

<%= render 'shared/personal_key', code: @code, update_path: manage_personal_key_path %>
<%= form_tag(manage_personal_key_path, method: 'post', id: 'app-form') %>
<%= content_tag(:div, '', id: 'app-root', data: { personal_key: @code }) do %>
<%= render 'shared/personal_key', code: @code, update_path: manage_personal_key_path %>
<% end %>
<%= javascript_packs_tag_once('verify-personal-key') %>
84 changes: 54 additions & 30 deletions spec/features/idv/steps/confirmation_step_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,78 @@
feature 'idv confirmation step', js: true do
include IdvStepHelper

it_behaves_like 'idv confirmation step'
it_behaves_like 'idv confirmation step', :oidc
it_behaves_like 'idv confirmation step', :saml
let(:idv_api_enabled_steps) { [] }
let(:sp) { nil }
let(:address_verification_mechanism) { :phone }

context 'personal key information and actions' do
before do
@user = sign_in_and_2fa_user
before do
allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).and_return(idv_api_enabled_steps)
start_idv_from_sp(sp)
complete_idv_steps_before_confirmation_step(address_verification_mechanism)
end

visit idv_path
it_behaves_like 'personal key page'

complete_idv_steps_before_confirmation_step(@user)
end
it 'shows status content for phone verification progress' do
expect(page).to have_content(t('idv.messages.confirm'))
expect(page).to have_css(
'.step-indicator__step--current',
text: t('step_indicator.flows.idv.secure_account'),
)
expect(page).to have_css(
'.step-indicator__step--complete',
text: t('step_indicator.flows.idv.verify_phone_or_address'),
)
expect(page).not_to have_css('.step-indicator__step--pending')
end

it 'allows the user to refresh and still displays the personal key' do
# Visit the current path is the same as refreshing
visit current_path
expect(page).to have_content(t('headings.personal_key'))
end

context 'with idv app feature enabled' do
let(:idv_api_enabled_steps) { ['password_confirm', 'personal_key', 'personal_key_confirm'] }

it_behaves_like 'personal key page'

it 'allows the user to refresh and still displays the personal key' do
# Visit the current path is the same as refreshing
visit current_path
expect(page).to have_content(t('headings.personal_key'))
end

it_behaves_like 'personal key page'
end

context 'with idv app feature enabled' do
before do
allow(IdentityConfig.store).to receive(:idv_api_enabled_steps).
and_return(['password_confirm', 'personal_key', 'personal_key_confirm'])
context 'verifying by gpo' do
let(:address_verification_mechanism) { :gpo }

it 'shows status content for gpo verification progress' do
expect(page).to have_content(t('idv.messages.mail_sent'))
expect(page).to have_css(
'.step-indicator__step--current',
text: t('step_indicator.flows.idv.secure_account'),
)
expect(page).to have_css(
'.step-indicator__step--pending',
text: t('step_indicator.flows.idv.verify_phone_or_address'),
)
end

it_behaves_like 'idv confirmation step'
it_behaves_like 'idv confirmation step', :oidc
it_behaves_like 'idv confirmation step', :saml
it_behaves_like 'personal key page', :gpo
end

context 'personal key information and actions' do
before do
@user = sign_in_and_2fa_user
context 'with associated sp' do
let(:sp) { :oidc }

visit idv_path
it 'redirects to the completions page and then to the SP' do
acknowledge_and_confirm_personal_key

complete_idv_steps_before_confirmation_step(@user)
end
expect(page).to have_current_path(sign_up_completed_path)

it 'allows the user to refresh and still displays the personal key' do
# Visit the current path is the same as refreshing
visit current_path
expect(page).to have_content(t('headings.personal_key'))
end
click_agree_and_continue

it_behaves_like 'personal key page'
expect(current_url).to start_with('http://localhost:7654/auth/result')
end
end
end
103 changes: 3 additions & 100 deletions spec/features/users/regenerate_personal_key_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require 'rails_helper'

feature 'View personal key' do
feature 'View personal key', js: true do
include XPathHelper
include PersonalKeyHelper
include SamlAuthHelper
Expand Down Expand Up @@ -31,7 +31,7 @@
end

context 'regenerating new code after canceling edit password action' do
scenario 'displays new code', js: true do
scenario 'displays new code' do
sign_in_and_2fa_user(user)
old_digest = user.encrypted_recovery_code_digest

Expand Down Expand Up @@ -59,7 +59,7 @@
end
end

context 'personal key actions and information' do
describe 'personal key actions and information' do
before do
sign_in_and_2fa_user(user)
visit account_two_factor_authentication_path
Expand All @@ -70,101 +70,4 @@
it_behaves_like 'personal key page'
end
end

context 'with javascript enabled', js: true do
it 'prompts the user to enter their personal key to confirm they have it' do
sign_in_and_2fa_user(user)
visit account_two_factor_authentication_path
click_on(t('account.links.regenerate_personal_key'), match: :prefer_exact)
click_continue

click_acknowledge_personal_key

expect_confirmation_modal_to_appear_with_first_code_field_in_focus

expect(page).to have_selector(:link_or_button, 'Back')

click_back_button

expect_to_be_back_on_manage_personal_key_page_with_continue_button_in_focus

click_acknowledge_personal_key
submit_form_without_entering_the_code
submit_form_with_the_wrong_code

expect(current_path).not_to eq account_path

visit manage_personal_key_path

acknowledge_and_confirm_personal_key

expect(current_path).to eq account_path
end

it 'confirms personal key on mobile', driver: :headless_chrome_mobile do
sign_in_and_2fa_user(user)
visit account_two_factor_authentication_path
click_on(t('account.links.regenerate_personal_key'), match: :prefer_exact)
click_continue

click_acknowledge_personal_key

expect_confirmation_modal_to_appear_with_first_code_field_in_focus

expect(page).to have_selector(:link_or_button, 'Back')

click_back_button

expect_to_be_back_on_manage_personal_key_page_with_continue_button_in_focus

click_acknowledge_personal_key
submit_form_without_entering_the_code

expect(current_path).not_to eq account_path

visit manage_personal_key_path
acknowledge_and_confirm_personal_key

expect(current_path).to eq account_path
end
end
end

def sign_up_and_view_personal_key
sign_up_and_set_password
select_2fa_option('phone')
fill_in 'new_phone_form_phone', with: '202-555-1212'
click_send_security_code
fill_in_code_with_last_phone_otp
click_submit_default
end

def expect_confirmation_modal_to_appear_with_first_code_field_in_focus
expect(page.find(':focus')).to eq page.find_field(t('forms.personal_key.confirmation_label'))
end

def click_back_button
click_on t('forms.buttons.back')
end

def expect_to_be_back_on_manage_personal_key_page_with_continue_button_in_focus
expect(page).to have_xpath(
"//div[@id='personal-key-confirm'][@class='display-none']", visible: false
)
expect(page.evaluate_script('document.activeElement.value')).to eq(
t('forms.buttons.continue'),
)
end

def submit_form_without_entering_the_code
within('[role=dialog]') { click_continue }
expect(page).to have_content(t('simple_form.required.text'))
expect(page).not_to have_content(t('users.personal_key.confirmation_error'))
end

def submit_form_with_the_wrong_code
fill_in 'personal_key', with: 'hellohellohello'
within('[role=dialog]') { click_continue }
expect(page).to have_content(t('users.personal_key.confirmation_error'))
expect(page).not_to have_content(t('simple_form.required.text'))
end
Loading