diff --git a/app/models/user.rb b/app/models/user.rb index 612c8a8d..1f175539 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -61,7 +61,7 @@ class User < ApplicationRecord screen_name.strip! end - validates :email, fake_email: true + validates :email, fake_email: true, typoed_email: true validates :screen_name, presence: true, format: { with: SCREEN_NAME_REGEX }, uniqueness: { case_sensitive: false }, screen_name: true mount_uploader :profile_picture, ProfilePictureUploader, mount_on: :profile_picture_file_name diff --git a/app/validators/typoed_email_validator.rb b/app/validators/typoed_email_validator.rb new file mode 100644 index 00000000..6c680a36 --- /dev/null +++ b/app/validators/typoed_email_validator.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class TypoedEmailValidator < ActiveModel::EachValidator + # this array contains "forbidden" email address endings + INVALID_ENDINGS = [ + # without @: + *%w[ + .con + .coom + ], + + # with @: + *%w[ + fmail.com + gemail.com + gmail.co + gmaile.com + gmaill.com + icluod.com + proton.mail + ].map { "@#{_1}" } + ].freeze + + def validate_each(record, attribute, value) + return if valid?(value) + + record.errors[attribute] << "contains a typo" + end + + private + + def valid?(value) + # needs an @ + return false unless value.include?('@') + + # part after the @ needs to have at least one period + return false if value.split('@', 2).last.count('.') == 0 + + # finally, common typos + return false if INVALID_ENDINGS.any? { value.end_with?(_1) } + + true + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index df17aa45..b5ed854a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,6 +33,61 @@ RSpec.describe User, type: :model do end end + describe 'email validation' do + subject do + FactoryBot.build(:user, email: email).tap(&:validate).errors[:email] + end + + shared_examples_for 'valid email' do |example_email| + context "when email is #{example_email}" do + let(:email) { example_email } + + it "does not have validation errors" do + expect(subject).to be_empty + end + end + end + + shared_examples_for 'invalid email' do |example_email| + context "when email is #{example_email}" do + let(:email) { example_email } + + it "has validation errors" do + expect(subject).not_to be_empty + end + end + end + + include_examples 'valid email', 'ifyouusethismailyouarebanned@nilsding.org' + include_examples 'valid email', 'fritz.fantom@gmail.com' + include_examples 'valid email', 'fritz.fantom@columbiamail.co' + include_examples 'valid email', 'fritz.fantom@protonmail.com' + include_examples 'valid email', 'fritz.fantom@enterprise.k8s.420stripes.k8s.needs.more.k8s.jira.atlassian.k8s.eu-central-1.s3.amazonaws.com' + include_examples 'invalid email', '@jack' + + # examples from the real world: + + # .con is not a valid TLD + include_examples 'invalid email', 'fritz.fantom@gmail.con' + include_examples 'invalid email', 'fritz.fantom@protonmail.con' + # neither is .coom + include_examples 'invalid email', 'fritz.fantom@gmail.coom' + # common typos: + include_examples 'invalid email', 'fritz.fantom@fmail.com' + include_examples 'invalid email', 'fritz.fantom@gemail.com' + include_examples 'invalid email', 'fritz.fantom@gmail.co' + include_examples 'invalid email', 'fritz.fantom@gmailcom' + include_examples 'invalid email', 'fritz.fantom@gmaile.com' + include_examples 'invalid email', 'fritz.fantom@gmaill.com' + include_examples 'invalid email', 'fritz.fantom@hotmailcom' + include_examples 'invalid email', 'fritz.fantom@icluod.com' + # no TLD + include_examples 'invalid email', 'fritz.fantom@gmail' + include_examples 'invalid email', 'fritz.fantom@protonmail' + # not registered as of 2022-01-11 + include_examples 'invalid email', 'fritz.fantom@proton.mail' + end + # -- User::TimelineMethods -- shared_examples_for 'result is blank' do