Add a rake task running job & test for job

This commit is contained in:
Alex Sherman 2021-07-01 16:00:35 +05:00
parent e110924968
commit 5f0c031410
8 changed files with 92 additions and 105 deletions

View file

@ -19,7 +19,7 @@ module Actions
private private
def check_email(parsed_email) def check_email(parsed_email)
Truemail.validate(parsed_email, with: check_level).result Truemail.validate(parsed_email, with: check_level.to_sym).result
end end
def save_result(result) def save_result(result)

View file

@ -1,47 +1,34 @@
class VerifyEmailsJob < ApplicationJob class VerifyEmailsJob < ApplicationJob
discard_on StandardError discard_on StandardError
VALID_CHECK_LEVELS = %w[regex mx smtp].freeze
def perform(verification_id) def perform(contact_id:, check_level: 'regex')
email_address_verification = EmailAddressVerification.find(verification_id) contact = Contact.find_by(id: contact_id)
return unless need_to_verify?(email_address_verification) contact_not_found(contact_id) unless contact
validate_check_level(check_level)
process(email_address_verification) action = Actions::EmailCheck.new(email: contact.email,
validation_eventable: contact,
check_level: check_level)
action.call
rescue StandardError => e rescue StandardError => e
log_error(verification: email_address_verification, error: e) logger.error e.message
raise e raise e
end end
private private
def need_to_verify?(email_address_verification) def contact_not_found(contact_id)
return false if email_address_verification.blank? raise StandardError, "Contact with contact_id #{contact_id} not found"
return false if email_address_verification.recently_verified?
true
end end
def process(email_address_verification) def validate_check_level(check_level)
email_address_verification.verify return if VALID_CHECK_LEVELS.include? check_level
log_success(email_address_verification)
raise StandardError, "Check level #{check_level} is invalid"
end end
def logger def logger
@logger ||= Logger.new(Rails.root.join('log/email_verification.log')) @logger ||= Rails.logger
end
def log_success(verification)
email = verification.try(:email) || verification
message = "Email address #{email} verification done"
logger.info message
end
def log_error(verification:, error:)
email = verification.try(:email) || verification
message = <<~TEXT.squish
There was an error verifying email #{email}.
The error message was the following: #{error}
This job will retry.
TEXT
logger.error message
end end
end end

View file

@ -1,6 +1,10 @@
module EmailVerifable module EmailVerifable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
scope :recently_not_validated, -> { where.not(id: ValidationEvent.validated_ids_by(name)) }
end
def email_verification def email_verification
EmailAddressVerification.find_or_create_by(email: unicode_email, domain: domain(email)) EmailAddressVerification.find_or_create_by(email: unicode_email, domain: domain(email))
end end
@ -16,6 +20,7 @@ module EmailVerifable
email_verification&.failed? email_verification&.failed?
end end
# TODO: The following methods are deprecated and need to be moved to ValidationEvent class
class_methods do class_methods do
def domain(email) def domain(email)
Mail::Address.new(email).domain&.downcase || 'not_found' Mail::Address.new(email).domain&.downcase || 'not_found'

View file

@ -6,11 +6,20 @@
# For email_validation event kind also check_level (regex/mx/smtp) is stored in the event_data # For email_validation event kind also check_level (regex/mx/smtp) is stored in the event_data
class ValidationEvent < ApplicationRecord class ValidationEvent < ApplicationRecord
enum event_type: ValidationEvent::EventType::TYPES, _suffix: true enum event_type: ValidationEvent::EventType::TYPES, _suffix: true
VALIDATION_PERIOD = 1.month.ago.freeze
store_accessor :event_data, :errors, :check_level, :email store_accessor :event_data, :errors, :check_level, :email
belongs_to :validation_eventable, polymorphic: true belongs_to :validation_eventable, polymorphic: true
scope :recent, -> { where('created_at > ?', VALIDATION_PERIOD) }
scope :successful, -> { where(success: true) }
def self.validated_ids_by(klass)
recent.successful.where('validation_eventable_type = ?', klass)
.pluck(:validation_eventable_id)
end
def event_type def event_type
@event_type ||= ValidationEvent::EventType.new(self[:event_kind]) @event_type ||= ValidationEvent::EventType.new(self[:event_kind])
end end

View file

@ -17,8 +17,6 @@ module EmailAddressConverter
"#{local}@#{domain}"&.downcase "#{local}@#{domain}"&.downcase
end end
private
def domain(email) def domain(email)
Mail::Address.new(email).domain&.downcase || 'not_found' Mail::Address.new(email).domain&.downcase || 'not_found'
rescue Mail::Field::IncompleteParseError rescue Mail::Field::IncompleteParseError

View file

@ -1,35 +1,16 @@
require 'optparse' require 'optparse'
require 'rake_option_parser_boilerplate' require 'rake_option_parser_boilerplate'
require 'syslog/logger'
namespace :verify_email do namespace :verify_email do
desc 'Stars verifying email jobs for all the domain'
task all_domains: :environment do
verifications_by_domain = EmailAddressVerification.not_verified_recently.group_by(&:domain)
verifications_by_domain.each do |_domain, verifications|
ver = verifications.sample # Verify random email to not to clog the SMTP servers
VerifyEmailsJob.perform_later(ver.id)
next
end
end
# Need to be run like 'bundle exec rake verify_email:domain['gmail.com']'
# In zsh syntax will be 'bundle exec rake verify_email:domain\['gmail.com'\]'
# Default 'bundle exec rake verify_email:domain' wil use 'internet.ee' domain
desc 'Stars verifying email jobs for domain stated in argument'
task :domain, [:domain_name] => [:environment] do |_task, args|
args.with_defaults(domain_name: 'internet.ee')
verifications_by_domain = EmailAddressVerification.not_verified_recently
.by_domain(args[:domain_name])
verifications_by_domain.map { |ver| VerifyEmailsJob.perform_later(ver.id) }
end
# bundle exec rake verify_email:check_all -- -d=shop.test --check_level=mx --spam_protect=true # bundle exec rake verify_email:check_all -- -d=shop.test --check_level=mx --spam_protect=true
# bundle exec rake verify_email:check_all -- -dshop.test -cmx -strue # bundle exec rake verify_email:check_all -- -dshop.test -cmx -strue
desc 'Starts verifying email jobs with optional check level and spam protection' desc 'Starts verifying email jobs with optional check level and spam protection'
task :check_all do task check_all: :environment do
SPAM_PROTECT_TIMEOUT = 30.seconds
options = { options = {
domain_name: 'shop.test', domain_name: nil,
check_level: 'regex', check_level: 'regex',
spam_protect: false, spam_protect: false,
} }
@ -37,7 +18,48 @@ namespace :verify_email do
options = RakeOptionParserBoilerplate.process_args(options: options, options = RakeOptionParserBoilerplate.process_args(options: options,
banner: banner, banner: banner,
hash: opts_hash) hash: opts_hash)
contacts = prepare_contacts(options)
logger.info 'No contacts to check email selected' and next if contacts.blank?
contacts.find_each do |contact|
VerifyEmailsJob.set(wait_until: spam_protect_timeout(options)).perform_later(
contact_id: contact.id,
check_level: check_level(options)
)
end end
end
end
def check_level(options)
options[:check_level]
end
def spam_protect(options)
options[:spam_protect]
end
def spam_protect_timeout(options)
spam_protect(options) ? 0.seconds : SPAM_PROTECT_TIMEOUT
end
def logger
@logger ||= ActiveSupport::TaggedLogging.new(Syslog::Logger.new('registry'))
end
def prepare_contacts(options)
if options[:domain_name].present?
contacts_by_domain(options[:domain_name])
else
Contact.recently_not_validated
end
end
def contacts_by_domain(domain_name)
domain = ::Domain.find_by(name: domain_name)
return unless domain
domain.contacts.recently_not_validated
end end
def opts_hash def opts_hash

View file

@ -4,9 +4,6 @@ class VerifyEmailsJobTest < ActiveJob::TestCase
def setup def setup
@contact = contacts(:john) @contact = contacts(:john)
@invalid_contact = contacts(:invalid_email) @invalid_contact = contacts(:invalid_email)
@contact_verification = @contact.email_verification
@invalid_contact_verification = @invalid_contact.email_verification
@default_whitelist = Truemail.configure.whitelisted_domains @default_whitelist = Truemail.configure.whitelisted_domains
@default_blacklist = Truemail.configure.blacklisted_domains @default_blacklist = Truemail.configure.blacklisted_domains
Truemail.configure.whitelisted_domains = whitelisted_domains Truemail.configure.whitelisted_domains = whitelisted_domains
@ -33,33 +30,21 @@ class VerifyEmailsJobTest < ActiveJob::TestCase
end end
def test_job_checks_if_email_valid def test_job_checks_if_email_valid
assert_difference 'ValidationEvent.successful.count', 1 do
perform_enqueued_jobs do perform_enqueued_jobs do
VerifyEmailsJob.perform_now(@contact_verification.id) VerifyEmailsJob.perform_now(contact_id: @contact.id, check_level: 'regex')
end end
@contact_verification.reload
assert @contact_verification.success
end end
assert ValidationEvent.validated_ids_by(Contact).include? @contact.id
def test_job_checks_does_not_run_if_recent
old_verified_at = Time.zone.now - 10.days
@contact_verification.update(success: true, verified_at: old_verified_at)
assert @contact_verification.recently_verified?
perform_enqueued_jobs do
VerifyEmailsJob.perform_now(@contact_verification.id)
end
@contact_verification.reload
assert_in_delta @contact_verification.verified_at.to_i, old_verified_at.to_i, 1
end end
def test_job_checks_if_email_invalid def test_job_checks_if_email_invalid
perform_enqueued_jobs do perform_enqueued_jobs do
VerifyEmailsJob.perform_now(@invalid_contact_verification.id) VerifyEmailsJob.perform_now(contact_id: @invalid_contact.id, check_level: 'regex')
end end
@contact_verification.reload @invalid_contact.reload
refute @contact_verification.success refute @invalid_contact.validation_events.last.success
refute ValidationEvent.validated_ids_by(Contact).include? @invalid_contact.id
end end
end end

View file

@ -5,8 +5,6 @@ class VerifyEmailTaskTest < ActiveJob::TestCase
def setup def setup
@contact = contacts(:john) @contact = contacts(:john)
@invalid_contact = contacts(:invalid_email) @invalid_contact = contacts(:invalid_email)
@contact_verification = @contact.email_verification
@invalid_contact_verification = @invalid_contact.email_verification
@default_whitelist = Truemail.configure.whitelisted_domains @default_whitelist = Truemail.configure.whitelisted_domains
@default_blacklist = Truemail.configure.blacklisted_domains @default_blacklist = Truemail.configure.blacklisted_domains
@ -36,32 +34,15 @@ class VerifyEmailTaskTest < ActiveJob::TestCase
def test_tasks_verifies_emails def test_tasks_verifies_emails
capture_io { run_task } capture_io { run_task }
@contact_verification.reload assert ValidationEvent.validated_ids_by(Contact).include? @contact.id
@invalid_contact_verification.reload assert @contact.validation_events.last.success
refute @invalid_contact.validation_events.last.success
assert @contact_verification.verified? refute ValidationEvent.validated_ids_by(Contact).include? @invalid_contact.id
assert @invalid_contact_verification.failed?
end
def test_domain_task_verifies_for_one_domain
capture_io { run_single_domain_task(@contact_verification.domain) }
@contact_verification.reload
@invalid_contact_verification.reload
assert @contact_verification.verified?
assert @invalid_contact_verification.not_verified?
end end
def run_task def run_task
perform_enqueued_jobs do perform_enqueued_jobs do
Rake::Task['verify_email:all_domains'].execute Rake::Task['verify_email:check_all'].execute
end
end
def run_single_domain_task(domain)
perform_enqueued_jobs do
Rake::Task["verify_email:domain"].invoke(domain)
end end
end end
end end