diff --git a/src/data/constants.js b/src/data/constants.js index 2370cbebe0..61eef6478e 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -32,6 +32,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+ + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})' + '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$'; export const LETTER_REGEX = /[a-zA-Z]/; +export const USERNAME_REGEX = /^[a-zA-Z0-9_-]*$/i; export const NUMBER_REGEX = /\d/; export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape diff --git a/src/register/EmbeddableRegistrationPage.jsx b/src/register/EmbeddableRegistrationPage.jsx index 3eb1cc25bb..53e005879d 100644 --- a/src/register/EmbeddableRegistrationPage.jsx +++ b/src/register/EmbeddableRegistrationPage.jsx @@ -25,6 +25,9 @@ import { FORM_SUBMISSION_ERROR, } from './data/constants'; import { registrationErrorSelector, validationsSelector } from './data/selectors'; +import { + emailRegex, getSuggestionForInvalidEmail, urlRegex, validateCountryField, validateEmailAddress, +} from './data/utils'; import messages from './messages'; import RegistrationFailure from './RegistrationFailure'; import { EmailField, UsernameField } from './registrationFields'; @@ -36,7 +39,7 @@ import { fieldDescriptionSelector, } from '../common-components/data/selectors'; import { - DEFAULT_STATE, REDIRECT, + DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, REDIRECT, USERNAME_REGEX, } from '../data/constants'; import { getAllPossibleQueryParams, setCookie, @@ -173,16 +176,76 @@ const EmbeddableRegistrationPage = (props) => { } }, [registrationResult, host]); - const validateInput = (fieldName, value, payload, shouldValidateFromBackend) => { + const validateInput = (fieldName, value, payload, shouldValidateFromBackend, shouldSetErrors = true) => { + let fieldError = ''; + switch (fieldName) { - case 'name': - if (value && !payload.username.trim() && shouldValidateFromBackend) { - validateFromBackend(payload); + case 'name': + if (value && value.match(urlRegex)) { + fieldError = formatMessage(messages['name.validation.message']); + } else if (value && !payload.username.trim() && shouldValidateFromBackend) { + validateFromBackend(payload); + } + break; + case 'email': + if (value.length <= 2) { + fieldError = formatMessage(messages['email.invalid.format.error']); + } else { + const [username, domainName] = value.split('@'); + // Check if email address is invalid. If we have a suggestion for invalid email + // provide that along with the error message. + if (!emailRegex.test(value)) { + fieldError = formatMessage(messages['email.invalid.format.error']); + setEmailSuggestion({ + suggestion: getSuggestionForInvalidEmail(domainName, username), + type: 'error', + }); + } else { + const response = validateEmailAddress(value, username, domainName); + if (response.hasError) { + fieldError = formatMessage(messages['email.invalid.format.error']); + delete response.hasError; + } + setEmailSuggestion({ ...response }); } - break; - default: - break; + } + break; + case 'username': + if (!value.match(USERNAME_REGEX)) { + fieldError = formatMessage(messages['username.format.validation.message']); + } + break; + case 'password': + if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) { + fieldError = formatMessage(messages['password.validation.message']); + } + break; + case 'country': + if (flags.showConfigurableEdxFields || flags.showConfigurableRegistrationFields) { + const { + countryCode, displayValue, error, + } = validateCountryField(value.trim(), countryList, formatMessage(messages['empty.country.field.error'])); + fieldError = error; + setConfigurableFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } })); + } + break; + default: + if (flags.showConfigurableRegistrationFields) { + if (!value && fieldDescriptions[fieldName]?.error_message) { + fieldError = fieldDescriptions[fieldName].error_message; + } else if (fieldName === 'confirm_email' && formFields.email && value !== formFields.email) { + fieldError = formatMessage(messages['email.do.not.match']); + } + } + break; } + if (shouldSetErrors && fieldError) { + setErrors(prevErrors => ({ + ...prevErrors, + [fieldName]: fieldError, + })); + } + return fieldError; }; const isFormValid = (payload) => { @@ -226,6 +289,10 @@ const EmbeddableRegistrationPage = (props) => { event.preventDefault(); setErrors(prevErrors => ({ ...prevErrors, [fieldName]: '' })); switch (fieldName) { + case 'email': + setFormFields(prevState => ({ ...prevState, email: emailSuggestion.suggestion })); + setEmailSuggestion({ suggestion: '', type: '' }); + break; case 'username': setFormFields(prevState => ({ ...prevState, username: suggestion })); props.resetUsernameSuggestions(); @@ -267,8 +334,12 @@ const EmbeddableRegistrationPage = (props) => { value, { name: formFields.name, username: formFields.username, form_field_key: name }, !validationApiRateLimited, + false, ); } + if (name === 'email') { + validateInput(name, value, null, !validationApiRateLimited, false); + } }; const handleOnFocus = (event) => { @@ -294,7 +365,6 @@ const EmbeddableRegistrationPage = (props) => { e.preventDefault(); const totalRegistrationTime = (Date.now() - formStartTime) / 1000; let payload = { ...formFields }; - if (!isFormValid(payload)) { setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 })); return; @@ -307,12 +377,20 @@ const EmbeddableRegistrationPage = (props) => { payload[fieldName] = configurableFormFields[fieldName]; } }); - // Don't send the marketing email opt-in value if the flag is turned off if (!flags.showMarketingEmailOptInCheckbox) { delete payload.marketingEmailsOptIn; } - + let isValid = true; + Object.entries(payload).forEach(([key, value]) => { + if (validateInput(key, value, payload, false, true) !== '') { + isValid = false; + } + }); + if (!isValid) { + setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 })); + return; + } payload = snakeCaseObject(payload); payload.totalRegistrationTime = totalRegistrationTime; @@ -350,6 +428,7 @@ const EmbeddableRegistrationPage = (props) => { handleChange={handleOnChange} handleBlur={handleOnBlur} handleFocus={handleOnFocus} + handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')} handleOnClose={handleEmailSuggestionClosed} emailSuggestion={emailSuggestion} errorMessage={errors.email} diff --git a/src/register/tests/EmbeddableRegistrationPage.test.jsx b/src/register/tests/EmbeddableRegistrationPage.test.jsx index 26ae4b8919..7805fca029 100644 --- a/src/register/tests/EmbeddableRegistrationPage.test.jsx +++ b/src/register/tests/EmbeddableRegistrationPage.test.jsx @@ -234,6 +234,97 @@ describe('RegistrationPage', () => { expect(registrationPage.find('input#username').prop('value')).toEqual('test-user'); }); + it('should run username and email frontend validations', () => { + const payload = { + name: 'John Doe', + username: 'test@2u.com', + email: 'test@yopmail.test', + password: 'password1', + country: 'Pakistan', + honor_code: true, + totalRegistrationTime: 0, + marketing_emails_opt_in: true, + }; + + store.dispatch = jest.fn(store.dispatch); + const registrationPage = mount(reduxWrapper()); + populateRequiredFields(registrationPage, payload); + registrationPage.find('input[name="email"]').simulate('focus'); + registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@yopmail.test', name: 'email' } }); + expect(registrationPage.find('.email-suggestion__text').exists()).toBeTruthy(); + + registrationPage.find('input[name="email"]').simulate('focus'); + registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'asasasasas', name: 'email' } }); + + registrationPage.find('button.btn-brand').simulate('click'); + expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy(); + expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeTruthy(); + }); + it('should run email frontend validations when random string is input', () => { + const payload = { + name: 'John Doe', + username: 'testh@2u.com', + email: 'as', + password: 'password1', + country: 'Pakistan', + honor_code: true, + totalRegistrationTime: 0, + marketing_emails_opt_in: true, + }; + + const registrationPage = mount(reduxWrapper()); + populateRequiredFields(registrationPage, payload); + + registrationPage.find('button.btn-brand').simulate('click'); + expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy(); + }); + it('should run frontend validations for name field', () => { + const payload = { + name: 'https://localhost.com', + username: 'test@2u.com', + email: 'as', + password: 'password1', + country: 'Pakistan', + honor_code: true, + totalRegistrationTime: 0, + marketing_emails_opt_in: true, + }; + + const registrationPage = mount(reduxWrapper()); + populateRequiredFields(registrationPage, payload); + + registrationPage.find('button.btn-brand').simulate('click'); + expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy(); + }); + + it('should run frontend validations for password field', () => { + const payload = { + name: 'https://localhost.com', + username: 'test@2u.com', + email: 'as', + password: 'as', + country: 'Pakistan', + honor_code: true, + totalRegistrationTime: 0, + marketing_emails_opt_in: true, + }; + + const registrationPage = mount(reduxWrapper()); + populateRequiredFields(registrationPage, payload); + + registrationPage.find('button.btn-brand').simulate('click'); + expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeTruthy(); + }); + + it('should click on email suggestion in case suggestion is avialable', () => { + const registrationPage = mount(reduxWrapper()); + registrationPage.find('input[name="email"]').simulate('focus'); + registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@gmail.co', name: 'email' } }); + + registrationPage.find('a.email-suggestion-alert-warning').simulate('click'); + expect(registrationPage.find('input#email').prop('value')).toEqual('test@gmail.com'); + }); + it('should remove extra character if username is more than 30 character long', () => { const registrationPage = mount(reduxWrapper()); registrationPage.find('input#username').simulate('change', { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });