diff --git a/app/models/admin_domain_contact.rb b/app/models/admin_domain_contact.rb index 99f0d02da..7e4b3b28a 100644 --- a/app/models/admin_domain_contact.rb +++ b/app/models/admin_domain_contact.rb @@ -1,4 +1,8 @@ class AdminDomainContact < DomainContact + include AgeValidation + + validate :validate_contact_age + # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength def self.replace(current_contact, new_contact) @@ -23,4 +27,14 @@ class AdminDomainContact < DomainContact end # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/MethodLength + + private + + def validate_contact_age + return unless contact&.underage? + + errors.add(:contact, I18n.t( + 'activerecord.errors.models.admin_domain_contact.contact_too_young' + )) + end end diff --git a/app/models/concerns/age_validation.rb b/app/models/concerns/age_validation.rb new file mode 100644 index 000000000..42e3d7ae7 --- /dev/null +++ b/app/models/concerns/age_validation.rb @@ -0,0 +1,53 @@ +module AgeValidation + extend ActiveSupport::Concern + + def underage? + case ident_type + when 'birthday' + underage_by_birthday? + when 'priv' + underage_by_estonian_id? + else + false + end + end + + private + + def underage_by_birthday? + birth_date = Date.parse(ident) + calculate_age(birth_date) < 18 + end + + def underage_by_estonian_id? + return false unless estonian_id? + + birth_date = parse_estonian_id_birth_date(ident) + calculate_age(birth_date) < 18 + end + + def estonian_id? + ident_country_code == 'EE' && ident.match?(/^\d{11}$/) + end + + def calculate_age(birth_date) + ((Time.zone.now - birth_date.to_time) / 1.year.seconds).floor + end + + def parse_estonian_id_birth_date(id_code) + century_number = id_code[0].to_i + year_digits = id_code[1..2] + month = id_code[3..4] + day = id_code[5..6] + + birth_year = case century_number + when 1, 2 then "18#{year_digits}" + when 3, 4 then "19#{year_digits}" + when 5, 6 then "20#{year_digits}" + else + raise ArgumentError, "Invalid century number in Estonian ID" + end + + Date.parse("#{birth_year}-#{month}-#{day}") + end +end \ No newline at end of file diff --git a/app/models/contact.rb b/app/models/contact.rb index 0e5d9de06..c95a355f5 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -10,6 +10,7 @@ class Contact < ApplicationRecord include Contact::Archivable include Contact::CompanyRegister include EmailVerifable + include AgeValidation belongs_to :original, class_name: 'Contact' belongs_to :registrar, required: true diff --git a/app/models/domain.rb b/app/models/domain.rb index 14536b764..d8c32790e 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -12,6 +12,7 @@ class Domain < ApplicationRecord include Domain::Releasable include Domain::Disputable include Domain::BulkUpdatable + include AgeValidation PERIODS = [ ['3 months', '3m'], @@ -866,7 +867,7 @@ class Domain < ApplicationRecord return true if registrant.org? && Setting.admin_contacts_required_for_org return false unless registrant.priv? - underage_registrant? && Setting.admin_contacts_required_for_minors + registrant.underage? && Setting.admin_contacts_required_for_minors end def require_tech_contacts? @@ -876,51 +877,7 @@ class Domain < ApplicationRecord private def underage_registrant? - case registrant.ident_type - when 'birthday' - underage_by_birthday? - when 'priv' - underage_by_estonian_id? - else - false - end - end - - def underage_by_birthday? - birth_date = Date.parse(registrant.ident) - calculate_age(birth_date) < 18 - end - - def underage_by_estonian_id? - return false unless estonian_id? - - birth_date = parse_estonian_id_birth_date(registrant.ident) - calculate_age(birth_date) < 18 - end - - def estonian_id? - registrant.ident_country_code == 'EE' && registrant.ident.match?(/^\d{11}$/) - end - - def calculate_age(birth_date) - ((Time.zone.now - birth_date.to_time) / 1.year.seconds).floor - end - - def parse_estonian_id_birth_date(id_code) - century_number = id_code[0].to_i - year_digits = id_code[1..2] - month = id_code[3..4] - day = id_code[5..6] - - birth_year = case century_number - when 1, 2 then "18#{year_digits}" - when 3, 4 then "19#{year_digits}" - when 5, 6 then "20#{year_digits}" - else - raise ArgumentError, "Invalid century number in Estonian ID" - end - - Date.parse("#{birth_year}-#{month}-#{day}") + registrant.underage? end def validate_admin_contacts_ident_type diff --git a/config/locales/en.yml b/config/locales/en.yml index 3b3cbb5b6..8cbc9d808 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -160,6 +160,8 @@ en: ipv6: taken: 'has already been taken' + admin_domain_contact: + contact_too_young: "Administrative contact must be at least 18 years old" attributes: epp_domain: &epp_domain_attributes diff --git a/test/integration/epp/domain/create/base_test.rb b/test/integration/epp/domain/create/base_test.rb index dbb2566ba..73b6f3abe 100644 --- a/test/integration/epp/domain/create/base_test.rb +++ b/test/integration/epp/domain/create/base_test.rb @@ -597,7 +597,7 @@ class EppDomainCreateBaseTest < EppTestCase end def test_registers_new_domain_with_required_attributes - Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true } + Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }.to_json now = Time.zone.parse('2010-07-05') travel_to now @@ -647,7 +647,7 @@ class EppDomainCreateBaseTest < EppTestCase default_registration_period = 1.year + 1.day assert_equal now + default_registration_period, domain.expire_time - Setting.admin_contacts_allowed_ident_type = { 'org' => false, 'priv' => true, 'birthday' => true } + Setting.admin_contacts_allowed_ident_type = { 'org' => false, 'priv' => true, 'birthday' => true }.to_json end def test_registers_domain_without_legaldoc_if_optout diff --git a/test/integration/epp/domain/update/base_test.rb b/test/integration/epp/domain/update/base_test.rb index 0c678afea..a8c2045c9 100644 --- a/test/integration/epp/domain/update/base_test.rb +++ b/test/integration/epp/domain/update/base_test.rb @@ -1032,7 +1032,7 @@ class EppDomainUpdateBaseTest < EppTestCase @domain.save! # Change allowed types after domain is created - Setting.admin_contacts_allowed_ident_type = { 'birthday' => true, 'priv' => true, 'org' => false } + Setting.admin_contacts_allowed_ident_type = { 'birthday' => true, 'priv' => true, 'org' => false }.to_json # Try to update domain with some other changes request_xml = <<-XML @@ -1061,6 +1061,36 @@ class EppDomainUpdateBaseTest < EppTestCase assert_epp_response :completed_successfully end + def test_does_not_allow_underage_admin_contact + admin_contact = contacts(:william) + admin_contact.update!( + ident_type: 'priv', + ident: '61203150222', + ident_country_code: 'EE' + ) + + request_xml = <<-XML + + + + + + #{@domain.name} + + #{admin_contact.code} + + + + + + XML + + post epp_update_path, params: { frame: request_xml }, + headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + + assert_epp_response :object_status_prohibits_operation + end + private def assert_verification_and_notification_emails diff --git a/test/models/domain/domain_version_test.rb b/test/models/domain/domain_version_test.rb index 7cfdae8d9..934bdaa86 100644 --- a/test/models/domain/domain_version_test.rb +++ b/test/models/domain/domain_version_test.rb @@ -14,7 +14,7 @@ class DomainVersionTest < ActiveSupport::TestCase end def test_assigns_creator_to_paper_trail_whodunnit - Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true } + Setting.admin_contacts_allowed_ident_type = { 'org' => true, 'priv' => true, 'birthday' => true }.to_json duplicate_domain = prepare_duplicate_domain PaperTrail.request.whodunnit = @user.id_role_username diff --git a/test/models/domain_contact_test.rb b/test/models/domain_contact_test.rb index 4bfefb7f1..c41a4243c 100644 --- a/test/models/domain_contact_test.rb +++ b/test/models/domain_contact_test.rb @@ -14,4 +14,70 @@ class DomainContactTest < ActiveSupport::TestCase assert @domain_contact.value_typeahead, 'Jane' end + def test_validates_admin_contact_age_with_birthday + admin_contact = contacts(:john) + admin_contact.update!( + ident_type: 'birthday', + ident: (Time.zone.now - 16.years).strftime('%Y-%m-%d') + ) + + domain_contact = AdminDomainContact.new( + domain: domains(:shop), + contact: admin_contact + ) + + assert_not domain_contact.valid? + assert_includes domain_contact.errors.full_messages, + 'Contact Administrative contact must be at least 18 years old' + end + + def test_validates_admin_contact_age_with_estonian_id + admin_contact = contacts(:john) + admin_contact.update!( + ident_type: 'priv', + ident: '61203150222', + ident_country_code: 'EE' + ) + + domain_contact = AdminDomainContact.new( + domain: domains(:shop), + contact: admin_contact + ) + + assert_not domain_contact.valid? + assert_includes domain_contact.errors.full_messages, + 'Contact Administrative contact must be at least 18 years old' + end + + def test_allows_adult_admin_contact_with_birthday + admin_contact = contacts(:john) + admin_contact.update!( + ident_type: 'birthday', + ident: (Time.zone.now - 20.years).strftime('%Y-%m-%d') + ) + + domain_contact = AdminDomainContact.new( + domain: domains(:shop), + contact: admin_contact + ) + + assert domain_contact.valid? + end + + def test_allows_adult_admin_contact_with_estonian_id + admin_contact = contacts(:john) + admin_contact.update!( + ident_type: 'priv', + ident: '38903111310', + ident_country_code: 'EE' + ) + + domain_contact = AdminDomainContact.new( + domain: domains(:shop), + contact: admin_contact + ) + + assert domain_contact.valid? + end + end \ No newline at end of file diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb index af25c535c..e94225343 100644 --- a/test/models/domain_test.rb +++ b/test/models/domain_test.rb @@ -587,7 +587,14 @@ class DomainTest < ActiveSupport::TestCase assert domain.invalid? assert_includes domain.errors.full_messages, 'Admin domain contacts Admin contacts count must be between 1-10' - domain.admin_domain_contacts.build(contact: contacts(:john)) + admin_contact = contacts(:john) + admin_contact.update!( + ident_type: 'priv', + ident: '37810166020', + ident_country_code: 'EE' + ) + + domain.admin_domain_contacts.build(contact: admin_contact) assert domain.valid? end