Make admin contacts optional for private registrants

This change makes admin contacts optional for private registrants while keeping them mandatory for organizations. The changes include:

- Updated Domain model validations to make admin and tech contacts optional (min=0) for private registrants
- Added validation rules methods to handle different requirements based on registrant type
- Modified EPP domain creation to support domains without admin contacts for private registrants
- Updated attach_default_contacts to skip adding contacts for private registrants
- Added comprehensive test coverage for:
  - Domain model validations with private/org registrants
  - EPP domain creation without admin contacts for private registrants
  - REPP API contact management for private registrants

This implements the requirement to make admin contacts optional for private registrations of .ee domains while maintaining the existing validation rules for organizations.
This commit is contained in:
oleghasjanov 2025-01-07 12:24:57 +02:00
parent bfe2a10889
commit 3c169bb00b
5 changed files with 173 additions and 8 deletions

View file

@ -164,15 +164,35 @@ class Domain < ApplicationRecord
max: -> { Setting.dnskeys_max_count }, max: -> { Setting.dnskeys_max_count },
} }
validates :admin_domain_contacts, object_count: { def self.admin_contacts_validation_rules(for_org:)
min: -> { Setting.admin_contacts_min_count }, {
max: -> { Setting.admin_contacts_max_count }, min: -> { for_org ? Setting.admin_contacts_min_count : 0 },
max: -> { Setting.admin_contacts_max_count }
} }
end
validates :tech_domain_contacts, object_count: { def self.tech_contacts_validation_rules(for_org:)
min: -> { Setting.tech_contacts_min_count }, {
max: -> { Setting.tech_contacts_max_count }, min: -> { for_org ? Setting.tech_contacts_min_count : 0 },
max: -> { Setting.tech_contacts_max_count }
} }
end
validates :admin_domain_contacts,
object_count: admin_contacts_validation_rules(for_org: true),
if: :require_admin_contacts?
validates :admin_domain_contacts,
object_count: admin_contacts_validation_rules(for_org: false),
unless: :require_admin_contacts?
validates :tech_domain_contacts,
object_count: tech_contacts_validation_rules(for_org: true),
if: :require_tech_contacts?
validates :tech_domain_contacts,
object_count: tech_contacts_validation_rules(for_org: false),
unless: :require_tech_contacts?
validates :nameservers, uniqueness_multi: { validates :nameservers, uniqueness_multi: {
attribute: 'hostname', attribute: 'hostname',
@ -835,4 +855,12 @@ class Domain < ApplicationRecord
end end
array array
end end
def require_admin_contacts?
registrant.present? && registrant.org?
end
def require_tech_contacts?
registrant.present? && registrant.org?
end
end end

View file

@ -114,7 +114,9 @@ class Epp::Domain < Domain
def attach_default_contacts def attach_default_contacts
return if registrant.blank? return if registrant.blank?
registrant_obj = Contact.find_by(code: registrant.code) registrant_obj = Contact.find_by(code: registrant.code)
return if registrant_obj.priv?
tech_contacts << registrant_obj if tech_domain_contacts.blank? tech_contacts << registrant_obj if tech_domain_contacts.blank?
admin_contacts << registrant_obj if admin_domain_contacts.blank? && !registrant.org? admin_contacts << registrant_obj if admin_domain_contacts.blank? && !registrant.org?

View file

@ -436,6 +436,9 @@ class EppDomainCreateBaseTest < EppTestCase
contact = contacts(:john) contact = contacts(:john)
registrant = contact.becomes(Registrant) registrant = contact.becomes(Registrant)
registrant.update!(ident_type: 'org')
registrant.reload
request_xml = <<-XML request_xml = <<-XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee', for_version: '1.0')}"> <epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee', for_version: '1.0')}">
@ -444,6 +447,7 @@ class EppDomainCreateBaseTest < EppTestCase
<domain:create xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}"> <domain:create xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{name}</domain:name> <domain:name>#{name}</domain:name>
<domain:registrant>#{registrant.code}</domain:registrant> <domain:registrant>#{registrant.code}</domain:registrant>
<domain:contact type="admin">#{contact.code}</domain:contact>
</domain:create> </domain:create>
</create> </create>
<extension> <extension>
@ -937,4 +941,54 @@ class EppDomainCreateBaseTest < EppTestCase
ENV["shunter_default_threshold"] = '10000' ENV["shunter_default_threshold"] = '10000'
ENV["shunter_enabled"] = 'false' ENV["shunter_enabled"] = 'false'
end end
def test_registers_new_domain_with_private_registrant_without_admin_contacts
now = Time.zone.parse('2010-07-05')
travel_to now
name = "new.#{dns_zones(:one).origin}"
contact = contacts(:john)
registrant = contact.becomes(Registrant)
registrant.update!(ident_type: 'priv')
registrant.reload
assert_not registrant.org?
request_xml = <<-XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="#{Xsd::Schema.filename(for_prefix: 'epp-ee', for_version: '1.0')}">
<command>
<create>
<domain:create xmlns:domain="#{Xsd::Schema.filename(for_prefix: 'domain-ee', for_version: '1.2')}">
<domain:name>#{name}</domain:name>
<domain:registrant>#{registrant.code}</domain:registrant>
</domain:create>
</create>
<extension>
<eis:extdata xmlns:eis="#{Xsd::Schema.filename(for_prefix: 'eis', for_version: '1.0')}">
<eis:legalDocument type="pdf">#{'test' * 2000}</eis:legalDocument>
</eis:extdata>
</extension>
</command>
</epp>
XML
assert_difference 'Domain.count' do
post epp_create_path, params: { frame: request_xml },
headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
end
response_xml = Nokogiri::XML(response.body)
assert_correct_against_schema response_xml
assert_epp_response :completed_successfully
domain = Domain.find_by(name: name)
assert_equal name, domain.name
assert_equal registrant, domain.registrant
assert_empty domain.admin_contacts
assert_empty domain.tech_contacts
assert_not_empty domain.transfer_code
default_registration_period = 1.year + 1.day
assert_equal now + default_registration_period, domain.expire_time
end
end end

View file

@ -108,9 +108,32 @@ class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
refute @domain.tech_contacts.find_by(code: contact.code).present? refute @domain.tech_contacts.find_by(code: contact.code).present?
end end
def test_can_remove_all_admin_contacts_for_private_registrant
Spy.on_instance_method(Actions::DomainUpdate, :validate_email).and_return(true)
@domain.registrant.update!(ident_type: 'priv')
@domain.reload
assert_not @domain.registrant.org?
contact = @domain.admin_contacts.last
payload = { contacts: [ { code: contact.code, type: 'admin' } ] }
delete "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
json = JSON.parse(response.body, symbolize_names: true)
@domain.reload
assert_response :ok
assert_equal 1000, json[:code]
assert_empty @domain.admin_contacts
end
def test_can_not_remove_one_and_only_contact def test_can_not_remove_one_and_only_contact
Spy.on_instance_method(Actions::DomainUpdate, :validate_email).and_return(true) Spy.on_instance_method(Actions::DomainUpdate, :validate_email).and_return(true)
@domain.registrant.update!(ident_type: 'org')
@domain.reload
contact = @domain.admin_contacts.last contact = @domain.admin_contacts.last
payload = { contacts: [ { code: contact.code, type: 'admin' } ] } payload = { contacts: [ { code: contact.code, type: 'admin' } ] }

View file

@ -221,6 +221,10 @@ class DomainTest < ActiveSupport::TestCase
assert domain.valid?, proc { domain.errors.full_messages } assert domain.valid?, proc { domain.errors.full_messages }
domain.admin_domain_contacts.clear domain.admin_domain_contacts.clear
domain.registrant.update!(ident_type: 'org')
domain.reload
assert domain.registrant.org?
assert domain.invalid? assert domain.invalid?
domain.admin_domain_contacts.clear domain.admin_domain_contacts.clear
@ -236,6 +240,10 @@ class DomainTest < ActiveSupport::TestCase
Setting.tech_contacts_min_count = min_count Setting.tech_contacts_min_count = min_count
Setting.tech_contacts_max_count = max_count Setting.tech_contacts_max_count = max_count
domain.registrant.update!(ident_type: 'org')
domain.reload
assert domain.registrant.org?
domain.tech_domain_contacts.clear domain.tech_domain_contacts.clear
min_count.times { domain.tech_domain_contacts.build(domain_contact_attributes) } min_count.times { domain.tech_domain_contacts.build(domain_contact_attributes) }
assert domain.valid?, proc { domain.errors.full_messages } assert domain.valid?, proc { domain.errors.full_messages }
@ -475,6 +483,56 @@ class DomainTest < ActiveSupport::TestCase
assert_not @domain.renewable? assert_not @domain.renewable?
end end
def test_validates_admin_contact_count_for_private_registrant
domain_contact_attributes = domain_contacts(:shop_jane).dup.attributes
domain = valid_domain
max_count = 2
Setting.admin_contacts_max_count = max_count
domain.registrant.update!(ident_type: 'priv')
domain.reload
assert_not domain.registrant.org?
# Valid without any admin contacts
domain.admin_domain_contacts.clear
assert domain.valid?, proc { domain.errors.full_messages }
# Valid with some admin contacts
domain.admin_domain_contacts.clear
max_count.pred.times { domain.admin_domain_contacts.build(domain_contact_attributes) }
assert domain.valid?, proc { domain.errors.full_messages }
# Invalid when exceeding max contacts
domain.admin_domain_contacts.clear
max_count.next.times { domain.admin_domain_contacts.build(domain_contact_attributes) }
assert domain.invalid?
end
def test_validates_tech_contact_count_for_private_registrant
domain_contact_attributes = domain_contacts(:shop_william).dup.attributes
domain = valid_domain
max_count = 2
Setting.tech_contacts_max_count = max_count
domain.registrant.update!(ident_type: 'priv')
domain.reload
assert_not domain.registrant.org?
# Valid without any tech contacts
domain.tech_domain_contacts.clear
assert domain.valid?, proc { domain.errors.full_messages }
# Valid with some tech contacts
domain.tech_domain_contacts.clear
max_count.pred.times { domain.tech_domain_contacts.build(domain_contact_attributes) }
assert domain.valid?, proc { domain.errors.full_messages }
# Invalid when exceeding max contacts
domain.tech_domain_contacts.clear
max_count.next.times { domain.tech_domain_contacts.build(domain_contact_attributes) }
assert domain.invalid?
end
private private
def valid_domain def valid_domain