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