Input validations - thuy-econsys/rails_app GitHub Wiki

Devise

validates_format_of is used by Devise for validations in their codebase but opt for custom validations instead as the former validates every time a user is updated, causing failures such as presence validations for fields that do not exist in edit views that are needed for new views. In addition, custom validations allow for data that were valid prior to an implementation of new validations to pass as validations only occur at the point of signup/registration. Sessions will not be affected and users should still be able to sign-in after a change in password validation.

# app/models/user.rb
class User < ApplicationRecord

... 
 
  validate :phone_input, :password_input
  
  def password_input
    if password.present? && !password.match(/\A(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*\z/)
      errors.add(:password, 'must include 1 uppercase, 1 lowercase, 1 digit and 1 special character.')
    end
  end
  def phone_input
    if phone.present? && !phone.match(/\A[+]?[1]?[ \.\-]?([\(]?([0-9]{3})[\)]?[ \.\-]?([0-9]{3})[ \-\.]?([0-9]{4})[ ]?(;|#|x|extension|ext)?[ ]?([0-9]{0,6}))\z/i)
      errors.add(:phone, 'needs to be a valid number')
    end
  end

...
end

Devise validatable has configurations in config/initializers/devise.rb to validate password length and email format. Devise's default email_regexp only checks for presence of @ character. Update with a stronger regexp as well as the password length for stronger password strength:

Devise.setup do |config|
...

  # ==> Configuration for :validatable
  # Range for password length.
  config.password_length = 8..128

  # Email regex used to validate email formats. 
  config.email_regexp = /\A(([a-z0-9][\w\.\-]{0,62}[a-z0-9])@(([a-z0-9][a-z0-9\-]{0,62}[a-z0-9]\.){1,8})([a-z]{2,63}))\z/i
...
end

RSpec

Tests to ensure validations work

# /spec/models/user_spec.rb

require 'rails_helper'

describe User do 
  describe "validates attributes" do
    it { is_expected.to validate_presence_of(:email) }
    it { is_expected.to validate_presence_of(:password) }

    it { is_expected.to validate_uniqueness_of(:email).case_insensitive }

    it { is_expected.to validate_length_of(:password).is_at_least(8) }

    it do
      is_expected.to allow_values(
        "[email protected]"
      ).for(:email)
    end
    it do
      is_expected.not_to allow_values(
        "al.most.com", 
        "not@domain", 
        "@nowhere.com", 
        "not an email"
      ).for(:email)
    end

    it do
      is_expected.to allow_values(
        "P@ssw0rd",
        "siND8pvO2YlhT32UwWQMYQ**Q*sEp1UW#3CP6y$L9kV8Av2#aQG@hD79J25dMhsqUw8LorXd4iVIfy$Hl%YJBZrZEu1kQceqi&K*2nv$yd6xopnj3#F2nX2IHoB*#KrE"
      ).for(:password)
    end
    it do
      is_expected.not_to allow_values(
        # "N0      p@ss" # FIXME why are spaces allowed?
        "$h0rT",
        "mi$$ingNumber", 
        "12345678",
        "too2qui@t",
        "Y0L0ALLC@PS"
      ).for(:password)
    end

    it do
      is_expected.to allow_values(
        "297-736-6716 x09195",
        "1-758-235-5268",
        "1.781.882.7285",
        "267.628.2450 ext 986",
        "12134567890",
        "(697) 712-0620 x507",
        "1 (888) 342-5425 EXT 16290",
        "742.339.5512; 5878",
        "267-628-2450 #986"
      ).for(:phone)
    end
    it do
      is_expected.not_to allow_values(
        "1459-148-200",
        "2-338-612-5575",
        "909043751514462514262747251515",
        "255.178.249.36",
        "not a phone number"
      ).for(:phone)
    end
  end
end

RegEx

email

/\A(([a-z0-9][\w\.\-]{0,62}[a-z0-9])@(([a-z0-9][a-z0-9\-]{0,62}[a-z0-9]\.){1,8})([a-z]{2,63}))\z/i

password

/\A(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*\z/

phone

/\A[+]?[1]?[ \.\-]?([\(]?([0-9]{3})[\)]?[ \.\-]?([0-9]{3})[ \-\.]?([0-9]{4})[ ]?(;|#|x|extension|ext)?[ ]?([0-9]{0,6}))\z/i