diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx index 70d63c24fb6..828de0d5291 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.spec.tsx @@ -49,17 +49,17 @@ describe('PersonalKeyInput', () => { expect(input.value).to.equal('1234-1234-1234-1234'); }); - it('validates the input value against the expected value', async () => { - const { getByRole } = render(); + it('validates the input value against the expected value (case-insensitive, crockford)', async () => { + const { getByRole } = render(); const input = getByRole('textbox') as HTMLInputElement; - await userEvent.type(input, '0000-0000-0000-000'); + await userEvent.type(input, 'ABCDoOlL-defg-iI1'); input.checkValidity(); expect(input.validationMessage).to.equal('users.personal_key.confirmation_error'); - await userEvent.type(input, '0'); + await userEvent.type(input, '1'); input.checkValidity(); - expect(input.validationMessage).to.be.empty(); + expect(input.validity.valid).to.be.true(); }); }); diff --git a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx index 478244e64bb..03d2a835c3f 100644 --- a/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx +++ b/app/javascript/packages/verify-flow/steps/personal-key-confirm/personal-key-input.tsx @@ -17,13 +17,22 @@ interface PersonalKeyInputProps { onChange?: (nextValue: string) => void; } +/** + * Normalize an input value for validation comparison. + * + * @param string Denormalized value. + * + * @return Normalized value. + */ +const normalize = (string: string) => string.toLowerCase().replace(/o/g, '0').replace(/[il]/g, '1'); + function PersonalKeyInput( { expectedValue, onChange = () => {} }: PersonalKeyInputProps, ref: ForwardedRef, ) { const validate = useCallback( (value) => { - if (expectedValue && value !== expectedValue) { + if (expectedValue && normalize(value) !== normalize(expectedValue)) { throw new Error(t('users.personal_key.confirmation_error')); } }, diff --git a/spec/support/features/personal_key_helper.rb b/spec/support/features/personal_key_helper.rb index 7b95f69a9a6..3b2a651a25c 100644 --- a/spec/support/features/personal_key_helper.rb +++ b/spec/support/features/personal_key_helper.rb @@ -28,10 +28,6 @@ def trigger_reset_password_and_click_email_link(email) end def scrape_personal_key - new_personal_key_words = [] - page.all(:css, '[data-personal-key]').each do |node| - new_personal_key_words << node.text - end - new_personal_key_words.join('-') + page.all(:css, '.separator-text__code').map(&:text).join('-') end end diff --git a/spec/support/features/session_helper.rb b/spec/support/features/session_helper.rb index 931eabe6d07..a12a124dbff 100644 --- a/spec/support/features/session_helper.rb +++ b/spec/support/features/session_helper.rb @@ -2,6 +2,8 @@ module Features module SessionHelper + include PersonalKeyHelper + VALID_PASSWORD = 'Val!d Pass w0rd'.freeze def sign_up_with(email) @@ -307,18 +309,11 @@ def sign_in_with_totp_enabled_user end def acknowledge_and_confirm_personal_key(js: true) - extra_characters_get_ignored = 'abc123qwerty' - code_words = [] - - page.all(:css, '[data-personal-key]').map do |node| - code_words << node.text - end - button_text = t('forms.buttons.continue') click_on button_text, class: 'personal-key-continue' if js - fill_in 'personal_key', with: code_words.join.downcase + extra_characters_get_ignored + fill_in 'personal_key', with: scrape_personal_key find_all('.personal-key-confirm', text: button_text).first.click end diff --git a/spec/support/shared_examples_for_personal_keys.rb b/spec/support/shared_examples_for_personal_keys.rb index a6d7b950c77..ca655eaedd2 100644 --- a/spec/support/shared_examples_for_personal_keys.rb +++ b/spec/support/shared_examples_for_personal_keys.rb @@ -1,5 +1,5 @@ shared_examples_for 'personal key page' do - include XPathHelper + include PersonalKeyHelper context 'informational text' do context 'modal content' do @@ -33,5 +33,28 @@ code = page.all('[data-personal-key]').map(&:text).join('-') expect(copied_text).to eq(code) end + + it 'validates as case-insensitive, crockford-normalized, length-limited, dash-flexible' do + code_segments = scrape_personal_key.split('-') + + # Include dash between some segments and not others + code = code_segments[0..1].join('-') + code_segments[2..3].join + + # Randomize case + code = code.chars.map { |c| (rand 2) == 0 ? c.downcase : c.upcase }.join + + # De-normalize Crockford encoding + code = code.sub('1', 'l').sub('0', 'O') + + # Add extra characters + code += 'abc123qwerty' + + click_acknowledge_personal_key + page.find(':focus').fill_in with: code + + path_before_submit = current_path + within('[role=dialog]') { click_on t('forms.buttons.continue') } + expect(current_path).not_to eq path_before_submit + end end end