diff --git a/app/controllers/repp/v1/domains/admin_contacts_controller.rb b/app/controllers/repp/v1/domains/admin_contacts_controller.rb new file mode 100644 index 000000000..035cd36fd --- /dev/null +++ b/app/controllers/repp/v1/domains/admin_contacts_controller.rb @@ -0,0 +1,40 @@ +module Repp + module V1 + module Domains + class AdminContactsController < BaseController + before_action :set_current_contact, only: [:update] + before_action :set_new_contact, only: [:update] + + def set_current_contact + @current_contact = current_user.registrar.contacts.find_by!(code: contact_params[:current_contact_id]) + end + + def set_new_contact + @new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id]) + end + + def update + @epp_errors ||= [] + @epp_errors << { code: 2304, msg: 'New contact must be valid' } if @new_contact.invalid? + + unless @new_contact.identical_to?(@current_contact) + @epp_errors << { code: 2304, msg: 'Admin contacts must be identical' } + end + + return handle_errors if @epp_errors.any? + + affected, skipped = AdminDomainContact.replace(@current_contact, @new_contact) + @response = { affected_domains: affected, skipped_domains: skipped } + render_success(data: @response) + end + + private + + def contact_params + params.require(%i[current_contact_id new_contact_id]) + params.permit(:current_contact_id, :new_contact_id) + end + end + end + end +end diff --git a/app/models/admin_domain_contact.rb b/app/models/admin_domain_contact.rb index 14907403d..9fb8166af 100644 --- a/app/models/admin_domain_contact.rb +++ b/app/models/admin_domain_contact.rb @@ -1,2 +1,23 @@ class AdminDomainContact < DomainContact + def self.replace(current_contact, new_contact) + affected_domains = [] + skipped_domains = [] + admin_contacts = where(contact: current_contact) + + admin_contacts.each do |admin_contact| + if admin_contact.domain.discarded? + skipped_domains << admin_contact.domain.name + next + end + begin + admin_contact.contact = new_contact + admin_contact.save! + affected_domains << admin_contact.domain.name + rescue ActiveRecord::RecordNotUnique + skipped_domains << admin_contact.domain.name + end + end + + [affected_domains.sort, skipped_domains.sort] + end end diff --git a/app/models/concerns/contact/identical.rb b/app/models/concerns/contact/identical.rb index f529e09ac..aa527f723 100644 --- a/app/models/concerns/contact/identical.rb +++ b/app/models/concerns/contact/identical.rb @@ -20,6 +20,12 @@ module Concerns::Contact::Identical .where.not(id: id).take end + def identical_to?(contact) + IDENTIFIABLE_ATTRIBUTES.all? do |attribute| + self.attributes[attribute] == contact.attributes[attribute] + end + end + private def identifiable_hash diff --git a/config/routes.rb b/config/routes.rb index 3042eced4..16102f2b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,7 @@ Rails.application.routes.draw do get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ } post 'transfer', to: 'domains#transfer' patch 'contacts', to: 'domains/contacts#update' + patch 'admin_contacts', to: 'domains/admin_contacts#update' post 'renew/bulk', to: 'domains/renews#bulk_renew' end end diff --git a/test/integration/api/domain_admin_contacts_test.rb b/test/integration/api/domain_admin_contacts_test.rb index ff56096d2..f1ad9ff27 100644 --- a/test/integration/api/domain_admin_contacts_test.rb +++ b/test/integration/api/domain_admin_contacts_test.rb @@ -5,48 +5,48 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest @admin_current = domains(:shop).admin_contacts.find_by(code: 'jane-001') @admin_new = contacts(:william) - @admin_new.update(ident: @admin_current.ident, - ident_type: @admin_current.ident_type, + @admin_new.update(ident: @admin_current.ident, + ident_type: @admin_current.ident_type, ident_country_code: @admin_current.ident_country_code) end def test_replace_all_admin_contacts_when_ident_data_doesnt_match @admin_new.update(ident: '777' , - ident_type: 'priv', + ident_type: 'priv', ident_country_code: 'LV') - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request - assert_equal ({ code: 2304, message: 'New admin contact must have same ident' }), + assert_equal ({ code: 2304, message: 'Admin contacts must be identical', data: {} }), JSON.parse(response.body, symbolize_names: true) end def test_replace_all_admin_contacts_of_the_current_registrar - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_nil domains(:shop).admin_contacts.find_by(code: @admin_current) - assert domains(:shop).admin_contacts.find_by(code: @admin_new) - assert domains(:airport).admin_contacts.find_by(code: @admin_new) + assert_nil domains(:shop).admin_contacts.find_by(code: @admin_current.code) + assert domains(:shop).admin_contacts.find_by(code: @admin_new.code) + assert domains(:airport).admin_contacts.find_by(code: @admin_new.code) end def test_skip_discarded_domains domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE]) - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert domains(:shop).admin_contacts.find_by(code: @admin_current) + assert domains(:shop).admin_contacts.find_by(code: @admin_current.code) end def test_return_affected_domains_in_alphabetical_order - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :ok @@ -59,8 +59,8 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest domains(:shop).update!(statuses: [DomainStatus::DELETE_CANDIDATE]) domains(:airport).update!(statuses: [DomainStatus::DELETE_CANDIDATE]) - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :ok @@ -69,23 +69,23 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest end def test_keep_other_admin_contacts_intact - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert domains(:airport).admin_contacts.find_by(code: 'john-001') end def test_keep_tech_contacts_intact - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_new }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_new.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert domains(:airport).tech_contacts.find_by(code: 'william-001') end def test_restrict_contacts_to_the_current_registrar - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, new_contact_id: 'william-002' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } @@ -96,7 +96,7 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest def test_non_existent_current_contact patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: 'non-existent', - new_contact_id: @admin_new}, + new_contact_id: @admin_new.code}, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :not_found assert_equal ({ code: 2303, message: 'Object does not exist' }), @@ -104,7 +104,7 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest end def test_non_existent_new_contact - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, new_contact_id: 'non-existent' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :not_found @@ -113,7 +113,7 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest end def test_disallow_invalid_new_contact - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, new_contact_id: 'invalid' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request @@ -122,8 +122,8 @@ class APIDomainAdminContactsTest < ApplicationIntegrationTest end def test_disallow_self_replacement - patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current, - new_contact_id: @admin_current }, + patch '/repp/v1/domains/admin_contacts', params: { current_contact_id: @admin_current.code, + new_contact_id: @admin_current.code }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request assert_equal ({ code: 2304, message: 'New contact must be different from current', data: {} }),