diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb
index ed86638e4..e5887fb98 100644
--- a/app/controllers/repp/v1/domains_controller.rb
+++ b/app/controllers/repp/v1/domains_controller.rb
@@ -80,7 +80,7 @@ module Repp
handle_errors(@domain) and return unless action.call
# rubocop:enable Style/AndOr
- render_success(data: { domain: { name: @domain.name,
+ render_success(message: message, data: { domain: { name: @domain.name,
transfer_code: @domain.transfer_code,
id: @domain.reload.uuid } })
end
@@ -104,7 +104,7 @@ module Repp
return
end
- render_success(data: { domain: { name: @domain.name } })
+ render_success(message: message, data: { domain: { name: @domain.name } })
end
api :GET, '/repp/v1/domains/:domain_name/transfer_info'
@@ -239,6 +239,10 @@ module Repp
index_params[:offset] || 0
end
+ def message
+ "Command completed successfully#{@domain.skipped_domain_contacts_validation if @domain.skipped_domain_contacts_validation.present?}"
+ end
+
def index_params
params.permit(:limit, :offset, :details, :simple, :q,
q: %i[s name_matches registrant_code_eq contacts_ident_eq
diff --git a/app/interactions/actions/domain_create.rb b/app/interactions/actions/domain_create.rb
index bd4f032d2..ef413a8db 100644
--- a/app/interactions/actions/domain_create.rb
+++ b/app/interactions/actions/domain_create.rb
@@ -15,7 +15,6 @@ module Actions
assign_registrant
assign_nameservers
assign_domain_contacts
- # domain.attach_default_contacts
assign_expiry_time
maybe_attach_legal_doc
@@ -38,7 +37,69 @@ module Actions
false
end
- # Check if domain is eligible for new registration
+ def check_for_cross_role_duplicates
+ @removed_duplicates = []
+
+ registrant_contact = domain.registrant
+ return true unless registrant_contact
+
+ @admin_contacts = remove_duplicate_contacts(@admin_contacts, registrant_contact, 'admin')
+ @tech_contacts = remove_duplicate_contacts(@tech_contacts, registrant_contact, 'tech')
+
+ @admin_contacts.each do |admin|
+ contact = Contact.find_by(id: admin[:contact_id])
+ next unless contact
+
+ @tech_contacts = remove_duplicate_contacts(@tech_contacts, contact, 'tech')
+ end
+
+ notify_about_removed_duplicates unless @removed_duplicates.empty?
+
+ true
+ end
+
+ def remove_duplicate_contacts(contacts_array, reference_contact, role)
+ return contacts_array unless reference_contact
+
+ non_duplicates = contacts_array.reject do |contact_hash|
+ contact = Contact.find_by(id: contact_hash[:contact_id])
+ next false unless contact
+
+ is_duplicate = duplicate_contact?(contact, reference_contact)
+ if is_duplicate
+ @removed_duplicates << {
+ role: role,
+ code: contact.code,
+ duplicate_of: reference_contact.code
+ }
+ end
+ is_duplicate
+ end
+
+ non_duplicates
+ end
+
+ def duplicate_contact?(contact1, contact2)
+ return false unless contact1 && contact2
+
+ contact1.code == contact2.code ||
+ (contact1.name == contact2.name &&
+ contact1.ident == contact2.ident &&
+ contact1.email == contact2.email &&
+ contact1.phone == contact2.phone)
+ end
+
+ def notify_about_removed_duplicates
+ return if @removed_duplicates.empty?
+
+ message = ''
+ @removed_duplicates.each do |duplicate|
+ message += ". #{duplicate[:role].capitalize} contact #{duplicate[:code]} was discarded as duplicate;"
+ end
+
+ domain.skipped_domain_contacts_validation = message
+ end
+
def validate_domain_integrity
return unless Domain.release_to_auction
@@ -132,9 +193,11 @@ module Actions
params[:admin_contacts]&.each { |c| assign_contact(c) }
params[:tech_contacts]&.each { |c| assign_contact(c, admin: false) }
+ check_contact_duplications
+ check_for_cross_role_duplicates
+
domain.admin_domain_contacts_attributes = @admin_contacts
domain.tech_domain_contacts_attributes = @tech_contacts
- check_contact_duplications
end
def assign_expiry_time
diff --git a/app/interactions/actions/domain_update.rb b/app/interactions/actions/domain_update.rb
index 36752b2a0..d035788d1 100644
--- a/app/interactions/actions/domain_update.rb
+++ b/app/interactions/actions/domain_update.rb
@@ -29,6 +29,7 @@ module Actions
assign_admin_contact_changes
assign_tech_contact_changes
+ check_for_cross_role_duplicates
end
def check_for_same_contacts(contacts, contact_type)
@@ -37,6 +38,150 @@ module Actions
domain.add_epp_error('2306', contact_type, nil, %i[domain_contacts invalid])
end
+ def check_for_cross_role_duplicates
+ @removed_duplicates = []
+ registrant_contact = domain.registrant
+ return true unless registrant_contact
+
+ current_admin_contacts = domain.admin_domain_contacts.map { |dc| { contact_id: dc.contact_id, contact_code: dc.contact.code } }
+ current_tech_contacts = domain.tech_domain_contacts.map { |dc| { contact_id: dc.contact_id, contact_code: dc.contact.code } }
+
+ updated_admin_contacts = remove_duplicate_contacts(current_admin_contacts, registrant_contact, 'admin')
+
+ updated_tech_contacts = remove_duplicate_contacts(current_tech_contacts, registrant_contact, 'tech')
+
+ if updated_admin_contacts.present?
+ updated_admin_contacts.each do |admin_hash|
+ admin_contact_object = Contact.find_by(id: admin_hash[:contact_id])
+ next unless admin_contact_object
+ updated_tech_contacts = remove_duplicate_contacts(updated_tech_contacts, admin_contact_object, 'tech')
+ end
+ end
+
+ admin_ids_after_filtering = updated_admin_contacts.map { |c| c[:contact_id] }
+ current_admin_ids_from_map = current_admin_contacts.map { |c| c[:contact_id] }
+ domain.admin_contact_ids = admin_ids_after_filtering if admin_ids_after_filtering.sort != current_admin_ids_from_map.sort
+
+ tech_ids_after_filtering = updated_tech_contacts.map { |c| c[:contact_id] }
+ current_tech_ids_from_map = current_tech_contacts.map { |c| c[:contact_id] }
+ domain.tech_contact_ids = tech_ids_after_filtering if tech_ids_after_filtering.sort != current_tech_ids_from_map.sort
+
+ notify_about_removed_duplicates unless @removed_duplicates.empty?
+
+ true
+ end
+
+ def remove_duplicate_contacts(contacts_array, reference_contact, role)
+ return contacts_array unless reference_contact && contacts_array.present?
+
+ contacts_array.reject do |contact_hash|
+ contact = Contact.find_by(id: contact_hash[:contact_id])
+ next false unless contact
+
+ is_duplicate = duplicate_contact?(contact, reference_contact)
+ if is_duplicate
+ @removed_duplicates << {
+ role: role,
+ code: contact.code,
+ duplicate_of: reference_contact.code
+ }
+ end
+ is_duplicate
+ end
+ end
+
+ def duplicate_contact?(contact1, contact2)
+ return false unless contact1 && contact2
+
+ contact1.code == contact2.code ||
+ (contact1.name == contact2.name &&
+ contact1.ident == contact2.ident &&
+ contact1.email == contact2.email &&
+ contact1.phone == contact2.phone)
+ end
+
+ def filter_duplicate_contacts_before_assignment(props, role)
+ @removed_duplicates ||= []
+ registrant = domain.registrant
+
+ # Get existing contacts
+ existing_admin_contacts = domain.admin_domain_contacts.map(&:contact)
+ existing_tech_contacts = domain.tech_domain_contacts.map(&:contact)
+
+ # Filter new contacts being added
+ filtered_props = props.select do |prop|
+ next true if prop[:_destroy] # Keep removal operations
+
+ new_contact = Contact.find_by(id: prop[:contact_id])
+ next false unless new_contact
+
+ # Check against registrant
+ if registrant && duplicate_contact?(new_contact, registrant)
+ @removed_duplicates << {
+ role: role,
+ code: new_contact.code,
+ duplicate_of: registrant.code
+ }
+ next false
+ end
+
+ # Check against existing admin contacts
+ is_duplicate = existing_admin_contacts.any? { |existing| duplicate_contact?(new_contact, existing) }
+ if is_duplicate && role == 'admin'
+ duplicate_of = existing_admin_contacts.find { |existing| duplicate_contact?(new_contact, existing) }
+ @removed_duplicates << {
+ role: role,
+ code: new_contact.code,
+ duplicate_of: duplicate_of.code
+ }
+ next false
+ end
+
+ # Check against existing tech contacts
+ is_duplicate = existing_tech_contacts.any? { |existing| duplicate_contact?(new_contact, existing) }
+ if is_duplicate && role == 'tech'
+ duplicate_of = existing_tech_contacts.find { |existing| duplicate_contact?(new_contact, existing) }
+ @removed_duplicates << {
+ role: role,
+ code: new_contact.code,
+ duplicate_of: duplicate_of.code
+ }
+ next false
+ end
+
+ # For tech contacts, also check against admin contacts
+ if role == 'tech'
+ is_duplicate = existing_admin_contacts.any? { |existing| duplicate_contact?(new_contact, existing) }
+ if is_duplicate
+ duplicate_of = existing_admin_contacts.find { |existing| duplicate_contact?(new_contact, existing) }
+ @removed_duplicates << {
+ role: role,
+ code: new_contact.code,
+ duplicate_of: duplicate_of.code
+ }
+ next false
+ end
+ end
+
+ true
+ end
+
+ notify_about_removed_duplicates unless @removed_duplicates.empty?
+ filtered_props
+ end
+
+ def notify_about_removed_duplicates
+ return if @removed_duplicates.empty?
+
+ # Template: Admin contact EE123:DFD39958 was discarded as duplicate
+ message = ''
+ @removed_duplicates.each do |duplicate|
+ message += ". #{duplicate[:role].capitalize} contact #{duplicate[:code]} was discarded as duplicate;"
+ end
+
+ domain.skipped_domain_contacts_validation = message
+ end
+
def validate_domain_integrity
domain.auth_info = params[:transfer_code] if params[:transfer_code]
@@ -161,8 +306,10 @@ module Actions
domain.add_epp_error('2304', 'admin', DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED,
I18n.t(:object_status_prohibits_operation))
elsif props.present?
- domain.admin_domain_contacts_attributes = props
- check_for_same_contacts(props, 'admin')
+ # Filter duplicates before assignment
+ props = filter_duplicate_contacts_before_assignment(props, 'admin')
+ domain.admin_domain_contacts_attributes = props if props.present?
+ check_for_same_contacts(props, 'admin') if props.present?
end
end
@@ -176,8 +323,10 @@ module Actions
domain.add_epp_error('2304', 'tech', DomainStatus::SERVER_TECH_CHANGE_PROHIBITED,
I18n.t(:object_status_prohibits_operation))
elsif props.present?
- domain.tech_domain_contacts_attributes = props
- check_for_same_contacts(props, 'tech')
+ # Filter duplicates before assignment
+ props = filter_duplicate_contacts_before_assignment(props, 'tech')
+ domain.tech_domain_contacts_attributes = props if props.present?
+ check_for_same_contacts(props, 'tech') if props.present?
end
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index 809d18615..f539d0b73 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -93,6 +93,7 @@ class Domain < ApplicationRecord
has_one :csync_record, dependent: :destroy
attribute :skip_whois_record_update, :boolean, default: false
+ attribute :skipped_domain_contacts_validation, :string, default: ''
after_initialize do
self.pending_json = {} if pending_json.blank?
diff --git a/app/views/epp/domains/create.xml.builder b/app/views/epp/domains/create.xml.builder
index 70710e685..f793d8698 100644
--- a/app/views/epp/domains/create.xml.builder
+++ b/app/views/epp/domains/create.xml.builder
@@ -1,7 +1,7 @@
xml.epp_head do
xml.response do
xml.result('code' => '1000') do
- xml.msg 'Command completed successfully'
+ xml.msg "Command completed successfully#{@domain.skipped_domain_contacts_validation if @domain.skipped_domain_contacts_validation.present?}"
end
xml.resData do
diff --git a/app/views/epp/domains/success.xml.builder b/app/views/epp/domains/success.xml.builder
index a111f92d0..f391c7427 100644
--- a/app/views/epp/domains/success.xml.builder
+++ b/app/views/epp/domains/success.xml.builder
@@ -1,7 +1,7 @@
xml.epp_head do
xml.response do
xml.result('code' => '1000') do
- xml.msg 'Command completed successfully'
+ xml.msg "Command completed successfully#{@domain.skipped_domain_contacts_validation if @domain && @domain.respond_to?(:skipped_domain_contacts_validation) && @domain.skipped_domain_contacts_validation.present?}"
end
render('epp/shared/trID', builder: xml)
diff --git a/test/integration/epp/domain/create/base_test.rb b/test/integration/epp/domain/create/base_test.rb
index 73b6f3abe..3f1d31f93 100644
--- a/test/integration/epp/domain/create/base_test.rb
+++ b/test/integration/epp/domain/create/base_test.rb
@@ -603,7 +603,8 @@ class EppDomainCreateBaseTest < EppTestCase
travel_to now
name = "new.#{dns_zones(:one).origin}"
contact = contacts(:john)
- registrant = contact.becomes(Registrant)
+ # registrant = contact.becomes(Registrant)
+ registrant = contacts(:william)
registrant.update!(ident_type: 'org')
registrant.reload
@@ -639,7 +640,7 @@ class EppDomainCreateBaseTest < EppTestCase
domain = Domain.find_by(name: name)
assert_equal name, domain.name
- assert_equal registrant, domain.registrant
+ assert_equal registrant.code, domain.registrant.code
assert_equal [contact], domain.admin_contacts
assert_empty domain.tech_contacts
assert_not_empty domain.transfer_code
@@ -1186,4 +1187,434 @@ class EppDomainCreateBaseTest < EppTestCase
assert_correct_against_schema response_xml
assert_epp_response :completed_successfully
end
+
+ def test_registers_domain_with_duplicate_registrant_and_admin
+ duplicate_contact = Contact.create!(
+ name: 'Duplicate Test',
+ code: 'duplicate-001',
+ email: 'duplicate@test.com',
+ phone: '+123.4567890',
+ ident: '12345X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ admin_contact = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-001',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: duplicate_contact.ident_type,
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ name = "domain-reg-admin-duplicate-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin_contact.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert response.body.include? "Admin contact #{admin_contact.code} was discarded as duplicate;"
+ end
+
+ def test_domain_with_duplicate_registrant_one_of_multiple_admins
+ duplicate_contact = Contact.create!(
+ name: 'Duplicate Test',
+ code: 'duplicate-002',
+ email: 'duplicate@test.com',
+ phone: '+123.4567890',
+ ident: '12345X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ admin1 = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-002',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: duplicate_contact.ident_type,
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ admin2 = contacts(:william)
+ name = "domain-reg-admin-multiple-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin1.code}
+ #{admin2.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert_equal 1, domain.admin_contacts.count, "Should have only one admin contact"
+ assert_equal admin2.code, domain.admin_contacts.first.code, "Should keep the non-duplicate admin"
+
+ assert response.body.include? "Admin contact #{admin1.code} was discarded as duplicate;"
+ end
+
+ def test_domain_with_duplicate_admin_and_tech
+ registrant = contacts(:acme_ltd).becomes(Registrant)
+
+ admin = Contact.create!(
+ name: 'Duplicate Admin Tech Test',
+ code: 'duplicate-admin-003',
+ email: 'admin-tech@test.com',
+ phone: '+123.4567890',
+ ident: '12346X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ tech = Contact.create!(
+ name: admin.name,
+ code: 'duplicate-tech-003',
+ email: admin.email,
+ phone: admin.phone,
+ ident: admin.ident,
+ ident_type: admin.ident_type,
+ ident_country_code: admin.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ name = "domain-admin-tech-duplicate-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin.code}
+ #{tech.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert_equal 1, domain.admin_contacts.count, "Should have one admin contact"
+ assert_equal admin.code, domain.admin_contacts.first.code, "Should keep the admin contact"
+ assert_empty domain.tech_contacts, "Tech contacts should be empty due to duplication with admin"
+
+ assert response.body.include? "Tech contact #{tech.code} was discarded as duplicate;"
+ end
+
+ def test_domain_with_duplicate_one_admin_one_tech
+ registrant = contacts(:acme_ltd).becomes(Registrant)
+
+ admin1 = Contact.create!(
+ name: 'First Admin',
+ code: 'duplicate-admin-004',
+ email: 'first-admin@test.com',
+ phone: '+123.4567890',
+ ident: '12347X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ admin2 = contacts(:william)
+
+ tech1 = Contact.create!(
+ name: admin1.name,
+ code: 'duplicate-tech-004',
+ email: admin1.email,
+ phone: admin1.phone,
+ ident: admin1.ident,
+ ident_type: admin1.ident_type,
+ ident_country_code: admin1.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ tech2 = contacts(:jack)
+
+ name = "domain-one-admin-one-tech-dup-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin1.code}
+ #{admin2.code}
+ #{tech1.code}
+ #{tech2.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert_equal 2, domain.admin_contacts.count, "Should have both admin contacts"
+
+ tech_contacts = domain.tech_contacts
+ assert_equal 1, tech_contacts.count, "Should have only the non-duplicate tech contact"
+ assert_equal tech2.code, tech_contacts.first.code, "Should keep the non-duplicate tech contact"
+
+ assert response.body.include? "Tech contact #{tech1.code} was discarded as duplicate;"
+ end
+
+ def test_domain_with_duplicate_registrant_admin_tech
+ duplicate_contact = Contact.create!(
+ name: 'Full Duplicate Test',
+ code: 'duplicate-005',
+ email: 'full-duplicate@test.com',
+ phone: '+123.5678901',
+ ident: '12348X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ admin = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-005',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: duplicate_contact.ident_type,
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ tech = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-tech-005',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: duplicate_contact.ident_type,
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ name = "domain-all-duplicates-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin.code}
+ #{tech.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert_empty domain.admin_contacts, "Admin contacts should be empty due to duplication"
+ assert_empty domain.tech_contacts, "Tech contacts should be empty due to duplication"
+ assert response.body.include? "Admin contact #{admin.code} was discarded as duplicate;"
+ assert response.body.include? "Tech contact #{tech.code} was discarded as duplicate;"
+ end
+
+ def test_domain_with_duplicate_registrant_one_admin_one_tech
+ duplicate_contact = Contact.create!(
+ name: 'Partial Duplicate Test',
+ code: 'duplicate-006',
+ email: 'partial-duplicate@test.com',
+ phone: '+123.6789012',
+ ident: '12349X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ admin1 = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-006',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: 'priv',
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ admin2 = contacts(:jack)
+ admin2.ident_type = 'priv'
+ admin2.save!
+
+ tech1 = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-tech-006',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: duplicate_contact.ident_type,
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ tech2 = contacts(:william)
+
+ name = "domain-partial-duplicates-#{Time.now.to_i}.#{dns_zones(:one).origin}"
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{name}
+ #{registrant.code}
+ #{admin1.code}
+ #{admin2.code}
+ #{tech1.code}
+ #{tech2.code}
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ assert_difference 'Domain.count', 1 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_not_nil domain, "Domain should have been created"
+ assert_equal 1, domain.admin_contacts.count, "Should have only the non-duplicate admin contact"
+ assert_equal admin2.code, domain.admin_contacts.first.code, "Should keep the non-duplicate admin contact"
+ assert_equal 1, domain.tech_contacts.count, "Should have only the non-duplicate tech contact"
+ assert_equal tech2.code, domain.tech_contacts.first.code, "Should keep the non-duplicate tech contact"
+
+ assert response.body.include? "Admin contact #{admin1.code} was discarded as duplicate;"
+ assert response.body.include? "Tech contact #{tech1.code} was discarded as duplicate;"
+ end
end
diff --git a/test/integration/epp/domain/update/base_test.rb b/test/integration/epp/domain/update/base_test.rb
index a8c2045c9..70b1fd355 100644
--- a/test/integration/epp/domain/update/base_test.rb
+++ b/test/integration/epp/domain/update/base_test.rb
@@ -1091,6 +1091,229 @@ class EppDomainUpdateBaseTest < EppTestCase
assert_epp_response :object_status_prohibits_operation
end
+ # UPDATE TESTS FOR DUPLICATE CONTACTS
+ def test_update_domain_with_duplicate_registrant_and_single_admin
+ domain = domains(:shop)
+
+ duplicate_contact = Contact.create!(
+ name: 'Partial Duplicate Test',
+ code: 'duplicate-006',
+ email: 'partial-duplicate@test.com',
+ phone: '+123.6789012',
+ ident: '12349X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ new_admin = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-006',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: 'priv',
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ domain.update(registrant: registrant) && domain.reload
+
+ old_admin = domain.admin_contacts.first
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{domain.name}
+
+
+ #{new_admin.code}
+
+
+ #{old_admin.code}
+
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ post epp_update_path(domain_name: domain.name), params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ domain.reload
+
+ assert_epp_response :completed_successfully
+ assert response.body.include? "Admin contact #{new_admin.code} was discarded as duplicate;"
+ end
+
+ def test_update_domain_with_registrant_admin_tech_all_duplicates
+ domain = domains(:airport)
+
+ initial_admin_contact = contacts(:jane)
+ initial_tech_contact = contacts(:william)
+ domain.admin_contacts = [initial_admin_contact]
+ domain.tech_contacts = [initial_tech_contact]
+ domain.save!
+ domain.reload
+
+ base_duplicate_data_contact = Contact.create!(
+ name: 'All Roles Are The Same Person',
+ code: "base-all-roles-#{SecureRandom.hex(3)}",
+ email: 'all.roles.same.person@example.com',
+ phone: '+1.999888777',
+ ident: 'ARSAMEP123',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: domain.registrar
+ )
+
+ new_registrant = base_duplicate_data_contact.becomes(Registrant)
+ domain.update!(registrant: new_registrant)
+ domain.reload
+
+ new_admin_being_added = Contact.create!(
+ name: base_duplicate_data_contact.name,
+ code: "admin-dup-all-roles-#{SecureRandom.hex(3)}",
+ email: base_duplicate_data_contact.email,
+ phone: base_duplicate_data_contact.phone,
+ ident: base_duplicate_data_contact.ident,
+ ident_type: base_duplicate_data_contact.ident_type,
+ ident_country_code: base_duplicate_data_contact.ident_country_code,
+ registrar: domain.registrar
+ )
+
+ new_tech_being_added = Contact.create!(
+ name: base_duplicate_data_contact.name,
+ code: "tech-dup-all-roles-#{SecureRandom.hex(3)}",
+ email: base_duplicate_data_contact.email,
+ phone: base_duplicate_data_contact.phone,
+ ident: base_duplicate_data_contact.ident,
+ ident_type: base_duplicate_data_contact.ident_type,
+ ident_country_code: base_duplicate_data_contact.ident_country_code,
+ registrar: domain.registrar
+ )
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{domain.name}
+
+
+ #{new_admin_being_added.code}
+ #{new_tech_being_added.code}
+
+
+ #{initial_admin_contact.code}
+ #{initial_tech_contact.code}
+
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ post epp_update_path(domain_name: domain.name), params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => "session=api_bestnames" }
+ domain.reload
+
+ assert_epp_response :completed_successfully
+ assert response.body.include? "Admin contact #{new_admin_being_added.code} was discarded as duplicate;"
+ assert response.body.include? "Tech contact #{new_tech_being_added.code} was discarded as duplicate;"
+ end
+
+ def test_update_domain_with_duplicate_admin_and_tech_registrant_is_different
+ domain = domains(:library)
+
+ initial_admin_contact = contacts(:john)
+ initial_tech_contact = contacts(:william)
+ domain.admin_contacts = [initial_admin_contact]
+ domain.tech_contacts = [initial_tech_contact]
+ domain.save!
+ domain.reload
+
+ admin_tech_duplicate_base = Contact.create!(
+ name: 'Admin And Tech Are Same Person',
+ code: "base-adm-tech-#{SecureRandom.hex(3)}",
+ email: 'admin.tech.same.person@example.com',
+ phone: '+1.555444333',
+ ident: 'ATSAMEP456',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: domain.registrar
+ )
+
+ new_admin_being_added = Contact.create!(
+ name: admin_tech_duplicate_base.name,
+ code: "admin-dup-adm-tech-#{SecureRandom.hex(3)}",
+ email: admin_tech_duplicate_base.email,
+ phone: admin_tech_duplicate_base.phone,
+ ident: admin_tech_duplicate_base.ident,
+ ident_type: admin_tech_duplicate_base.ident_type,
+ ident_country_code: admin_tech_duplicate_base.ident_country_code,
+ registrar: domain.registrar
+ )
+
+ new_tech_being_added_and_skipped = Contact.create!(
+ name: admin_tech_duplicate_base.name,
+ code: "tech-dup-adm-tech-#{SecureRandom.hex(3)}",
+ email: admin_tech_duplicate_base.email,
+ phone: admin_tech_duplicate_base.phone,
+ ident: admin_tech_duplicate_base.ident,
+ ident_type: admin_tech_duplicate_base.ident_type,
+ ident_country_code: admin_tech_duplicate_base.ident_country_code,
+ registrar: domain.registrar
+ )
+
+ request_xml = <<-XML
+
+
+
+
+
+ #{domain.name}
+
+
+ #{new_admin_being_added.code}
+ #{new_tech_being_added_and_skipped.code}
+
+
+ #{initial_admin_contact.code}
+ #{initial_tech_contact.code}
+
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ post epp_update_path(domain_name: domain.name), params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => "session=api_bestnames" }
+ domain.reload
+
+ assert_epp_response :completed_successfully
+ assert response.body.include? "Tech contact #{new_tech_being_added_and_skipped.code} was discarded as duplicate;"
+ end
+
private
def assert_verification_and_notification_emails
diff --git a/test/integration/repp/v1/domains/contacts_test.rb b/test/integration/repp/v1/domains/contacts_test.rb
index df472bad4..28025f2f0 100644
--- a/test/integration/repp/v1/domains/contacts_test.rb
+++ b/test/integration/repp/v1/domains/contacts_test.rb
@@ -51,7 +51,10 @@ class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
assert_response :ok
assert_equal 1000, json[:code]
- assert @domain.admin_contacts.find_by(code: new_contact.code).present?
+ # Becase we implemented feature which allows us avoid duplicates of contacts,
+ # I changed this value from assert to assert_not.
+ # PS! Tech contact and registrant are same, that's why tech contact is not added.
+ assert_not @domain.admin_contacts.find_by(code: new_contact.code).present?
end
def test_can_add_new_tech_contacts
@@ -66,13 +69,16 @@ class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
assert_equal 1000, json[:code]
@domain.reload
- assert @domain.tech_contacts.find_by(code: new_contact.code).present?
+ # Becase we implemented feature which allows us avoid duplicates of contacts,
+ # I changed this value from assert to assert_not.
+ # PS! Tech contact and registrant are same, that's why tech contact is not added.
+ assert_not @domain.tech_contacts.find_by(code: new_contact.code).present?
end
def test_can_remove_admin_contacts
Spy.on_instance_method(Actions::DomainUpdate, :validate_email).and_return(true)
- contact = contacts(:john)
+ contact = contacts(:william)
payload = { contacts: [ { code: contact.code, type: 'admin' } ] }
post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
assert @domain.admin_contacts.find_by(code: contact.code).present?
@@ -90,7 +96,7 @@ class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
def test_can_remove_tech_contacts
Spy.on_instance_method(Actions::DomainUpdate, :validate_email).and_return(true)
- contact = contacts(:john)
+ contact = contacts(:william)
payload = { contacts: [ { code: contact.code, type: 'tech' } ] }
post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
assert @domain.tech_contacts.find_by(code: contact.code).present?
@@ -240,4 +246,47 @@ class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
assert_equal 1000, json[:code]
assert_empty @domain.admin_contacts
end
+
+ def test_updates_add_duplicate_admin_contact
+ domain = domains(:shop)
+
+ duplicate_contact = Contact.create!(
+ name: 'Partial Duplicate Test',
+ code: 'duplicate-006',
+ email: 'partial-duplicate@test.com',
+ phone: '+123.6789012',
+ ident: '12349X',
+ ident_type: 'priv',
+ ident_country_code: 'US',
+ registrar: registrars(:bestnames)
+ )
+
+ registrant = duplicate_contact.becomes(Registrant)
+
+ new_admin = Contact.create!(
+ name: duplicate_contact.name,
+ code: 'duplicate-admin-006',
+ email: duplicate_contact.email,
+ phone: duplicate_contact.phone,
+ ident: duplicate_contact.ident,
+ ident_type: 'priv',
+ ident_country_code: duplicate_contact.ident_country_code,
+ registrar: registrars(:bestnames)
+ )
+
+ domain.update(registrant: registrant) && domain.reload
+
+ old_admin = domain.admin_contacts.first
+ assert_includes domain.admin_contacts, old_admin
+
+ payload = { contacts: [ { code: new_admin.code, type: 'admin' } ] }
+ post "/repp/v1/domains/#{domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+
+ domain.reload
+ assert_not domain.admin_contacts.find_by(code: new_admin.code).present?
+ end
end