diff --git a/lib/params/base_validator.rb b/lib/params/base_validator.rb index 964ad553..c394519a 100644 --- a/lib/params/base_validator.rb +++ b/lib/params/base_validator.rb @@ -2,6 +2,8 @@ module Params class BaseValidator + EMAIL_REGEXP = /\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i + InvalidParameterError = Class.new(StandardError) def self.call(...) @@ -65,6 +67,14 @@ module Params raise_error(message || "#{key} must follow the #{regexp.source} format") end + def email_format(params, key, message: nil) + return if params.blank? + return if params[key].blank? + return if params[key].to_s.strip.split(/\s*[;,]\s*/).all? { |email| email.match?(EMAIL_REGEXP) } + + raise_error(message || "#{key} must follow the email format") + end + def unique_value(params, key, message: nil) return unless params.is_a?(Array) return if params.none? diff --git a/lib/params/submission_create_validator.rb b/lib/params/submission_create_validator.rb index 0658aac2..eef14391 100644 --- a/lib/params/submission_create_validator.rb +++ b/lib/params/submission_create_validator.rb @@ -22,6 +22,7 @@ module Params required(params, %i[emails email]) type(params, :emails, String) + email_format(params, :emails, message: 'emails are invalid') boolean(params, :send_email) type(params, :message, Hash) @@ -43,8 +44,8 @@ module Params type(params, :completed_redirect_url, String) type(params, :bcc_completed, String) type(params, :reply_to, String) - format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid') - format(params, :reply_to, /@/, message: 'reply_to email is invalid') + email_format(params, :bcc_completed, message: 'bcc_completed email is invalid') + email_format(params, :reply_to, message: 'reply_to email is invalid') type(params, :message, Hash) type(params, :submitters, Array) @@ -75,8 +76,8 @@ module Params type(submitter_params, :name, String) type(submitter_params, :reply_to, String) type(submitter_params, :email, String) - format(submitter_params, :email, /@/, message: 'email is invalid') - format(submitter_params, :reply_to, /@/, message: 'reply_to email is invalid') + email_format(submitter_params, :email, message: 'email is invalid') + email_format(submitter_params, :reply_to, message: 'reply_to email is invalid') type(submitter_params, :phone, String) format(submitter_params, :phone, /\A\+\d+\z/, message: 'phone should start with + and contain only digits') @@ -106,7 +107,7 @@ module Params type(params, :order, String) type(params, :completed_redirect_url, String) type(params, :bcc_completed, String) - format(params, :bcc_completed, /@/, message: 'bcc_completed email is invalid') + email_format(params, :bcc_completed, message: 'bcc_completed email is invalid') type(params, :message, Hash) in_path(params, :message) do |message_params| diff --git a/spec/lib/params/base_validator_spec.rb b/spec/lib/params/base_validator_spec.rb new file mode 100644 index 00000000..193b9b33 --- /dev/null +++ b/spec/lib/params/base_validator_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Params::BaseValidator do + let(:validator) { described_class.new({}) } + + describe '#email_format' do + it 'when email is valid' do + emails = [ + ' john.doe@example.com ', + 'john.doe@example.com', + 'jane+newsletter@domain.org', + 'mike_smith@company.net', + 'lisa-wong@sub.example.co.uk', + 'peter@webmail.com', + 'anna.jones123@my-domain.com', + 'contact@company.email', + 'info@my-company123.org', + 'hello.world@business.info', + 'feedback@new-domain.com', + 'alerts+user@localdomain.net', + 'webmaster@industry.biz', + 'services@agency.example', + 'george123@consultant.pro', + 'sales-team@company.io' + ] + + emails.each do |email| + expect { validator.email_format({ email: }, :email) }.not_to raise_error + end + end + + it 'when signle email is invalid' do + emails = [ + 'jone.doe@', + 'mike.smith@', + 'jane.doe@@example.com', + '@example.com', + 'lisa.wong@example', + 'peter.parker..@example.com', + 'anna.jones@.com', + 'jack.brown@com', + 'john doe@example.com', + 'laura.martin@ example.com', + 'dave.clark@example .com', + 'susan.green@example,com', + 'chris.lee@example;com', + 'jenny.king@.example.com', + '.henry.ford@example.com', + 'amy.baker@sub_domain.com', + 'george.morris@-example.com', + 'nancy.davis@example..com', + 'kevin.white@.', + 'diana.robinson@.example..com', + 'oliver.scott@example.c', + 'email1@g.comemail@g.com', + 'user.name@subdomain.example@example.com', + 'double@at@sign.com', + 'user@@example.com', + 'email@123.123.123.123', + 'this...is@strange.but.valid.com', + 'mix-and.match@strangely-formed-email_address.com', + 'email@domain..com', + 'user@-weird-domain-.com', + 'user.name@[IPv6:2001:db8::1]', + 'tricky.email@sub.example-.com', + 'user@domain.c0m' + ] + + emails.each do |email| + expect do + validator.email_format({ email: }, :email) + end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format') + end + end + + it 'when multiple emails are valid' do + emails = [ + + 'john.doe@example.com, jane.doe+newsletter@domain.org', + 'joshua@automobile.car ; chloe+fashion@food.delivery', + 'mike-smith@company.net;lisa.wong-sales@sub.example.co.uk', + 'peter.parker+info@webmail.com,laura.martin-office@company.co', + 'anna.jones123@my-domain.com, jack.brown+work@college.edu', + 'susan.green@business-info.org; dave.clark+personal@nonprofit.org', + 'chris.lee+team@new-domain.com;jenny.king.marketing@localdomain.net', + 'george.morris@consultant.pro; nancy.davis-office@company.io', + 'joshua-jones@automobile.car; chloe.taylor+fashion@food.delivery', + 'ryan.moore+alerts@music-band.com,isabella.walker.design@fashion.design', + 'support-team@company.com, contact.us@domain.org', + 'admin.office@industry.biz, hr.department@service.pro', + 'feedback@agency-example.org; hello.world@creative-studio.net', + 'sales-team@e-commerce.shop, support.department@technology.co', + 'media.contact@financial.servicesl; events-coordinator@food.delivery', + 'order@music-band.com; info.support@creative.example', + 'design.team@webmail.com , admin-office@company.co', + 'contact.sales@sub-example.co.uk, support+info@legal.gov', + 'support@media.group;subscribe-updates@concert.events' + ] + + emails.each do |email| + expect { validator.email_format({ email: }, :email) }.not_to raise_error + end + end + + it 'when multiple emails are invalid' do + emails = [ + 'jone@gmail.com, ,mike@gmail.com', + 'john.doe@example.com dave@nonprofit.org', + '; oliver.scott@example.com', + 'amy.baker@ example.com, george.morris@ example.com', + 'jenny.king@example.com . diana.robinson@example.com', + 'nancy.davis@.com, henry.ford@.com', + 'jack.brown@example.com, laura.martin@example .com', + 'anna.jones@example,com lisa.wong@example.com', + 'dave.clark@example.com kevin.white@example;com', + 'susan.green@ example.com; john.doe@example.com', + 'amy.baker@sub_domain.com george.morris@-example.com', + 'nancy.davis@example..com john.doe@example.c', + 'peter.parker@example.com, .henry.ford@example.com', + 'diana.robinson@.example..com, mike.smith@.', + 'oliver.scott@example.com; laura.martin@ example.com, jane.doe@@example.com' + ] + + emails.each do |email| + expect do + validator.email_format({ email: }, :email) + end.to raise_error(described_class::InvalidParameterError, 'email must follow the email format') + end + end + + it 'when email is invalid with custom message' do + expect do + validator.email_format({ email: 'jone.doe@' }, :email, message: 'email is invalid') + end.to raise_error(described_class::InvalidParameterError, 'email is invalid') + end + end +end diff --git a/spec/requests/submissions_spec.rb b/spec/requests/submissions_spec.rb index 5062662c..3a9590d8 100644 --- a/spec/requests/submissions_spec.rb +++ b/spec/requests/submissions_spec.rb @@ -83,6 +83,20 @@ describe 'Submission API', type: :request do expect(response.parsed_body[2]['email']).to eq('mike.doe@example.com') end + it 'returns an error if the submitter email is invalid' do + post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + send_email: true, + submitters: [ + { role: 'First Role', email: 'john@example' } + ] + }.to_json + + expect(response).to have_http_status(:unprocessable_entity) + + expect(response.parsed_body).to eq({ 'error' => 'email is invalid in `submitters[0]`.' }) + end + it 'returns an error if the template fields are missing' do templates[0].update(fields: []) @@ -113,16 +127,28 @@ describe 'Submission API', type: :request do describe 'POST /api/submissions/emails' do it 'creates a submission using email' do - post '/api/submissions', headers: { 'x-auth-token': author.access_token.token }, params: { + post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: { template_id: templates[0].id, - emails: 'john.doe@example.com' + emails: 'john.doe@example.com,jane.doe@example.com' }.to_json expect(response).to have_http_status(:ok) - submission = Submission.last + submissions = Submission.last(2) + submissions_body = submissions.reduce([]) { |acc, submission| acc + create_submission_body(submission) } - expect(response.parsed_body).to eq(JSON.parse(create_submission_body(submission).to_json)) + expect(response.parsed_body).to eq(JSON.parse(submissions_body.to_json)) + end + + it 'returns an error if emails are invalid' do + post '/api/submissions/emails', headers: { 'x-auth-token': author.access_token.token }, params: { + template_id: templates[0].id, + emails: 'amy.baker@example.com, george.morris@example.com@gmail.com' + }.to_json + + expect(response).to have_http_status(:unprocessable_entity) + + expect(response.parsed_body).to eq({ 'error' => 'emails are invalid' }) end end