From f7ef1a99ac596dfdfe8f09b7340333e7e7e91fe9 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Mon, 10 Aug 2026 11:13:07 +0300 Subject: [PATCH 1/2] Enable registrars to cancel a pending registrant change Once a registrant change went pending, the registrar had no way back: either wait 48 hours for the expiry cron or ask the registry to clear it by hand. Unlike pendingDelete, which a renew cancels, pendingUpdate had no escape hatch. Since the pending update never touches the domain in the database - the requested frame only lives in pending_json until the registrant confirms - cancelling is a matter of wiping the pending data, not rolling anything back. Domains::CancelPendingUpdate does that and is shared by both interfaces. EPP has no command for manipulating pending operations (RFC 3731 covers transfer only), so a domain:update asking for the registrant the domain already has is treated as a cancel request. Every other update of a pendingUpdate domain still gets 2304. This mirrors domain:renew cancelling pendingDelete. REPP gets an explicit endpoint instead: DELETE /repp/v1/domains/:domain_id/pending_update. Both registrants are notified by mail, since the confirmation link they received stops working. The registrant decision arrives through a background job, so it could land after the pending update was already cancelled - or cleaned up by the expiry cron - and replay an empty frame. ProcessAction now bails out when the domain is no longer pending. Several job tests were set up without pendingUpdate on the domain, which cannot happen in any real flow and left parts of them passing vacuously; they now reflect the actual state. Closes #2939 --- .gitignore | 1 + CHANGELOG.md | 3 + app/controllers/epp/domains_controller.rb | 36 +++++ .../v1/domains/pending_updates_controller.rb | 29 ++++ .../domains/cancel_pending_update.rb | 52 +++++++ .../domains/update_confirm/process_action.rb | 4 + app/mailers/registrant_change_mailer.rb | 9 ++ .../cancelled.html.erb | 18 +++ .../cancelled.text.erb | 20 +++ .../locales/mailers/registrant_change.en.yml | 6 +- config/routes.rb | 1 + .../epp/domain/update/cancel_pending_test.rb | 129 ++++++++++++++++++ .../v1/domains/cancel_pending_update_test.rb | 94 +++++++++++++ .../domains/cancel_pending_update_test.rb | 80 +++++++++++ test/jobs/domain_update_confirm_job_test.rb | 36 ++++- 15 files changed, 514 insertions(+), 4 deletions(-) create mode 100644 app/controllers/repp/v1/domains/pending_updates_controller.rb create mode 100644 app/interactions/domains/cancel_pending_update.rb create mode 100644 app/views/mailers/registrant_change_mailer/cancelled.html.erb create mode 100644 app/views/mailers/registrant_change_mailer/cancelled.text.erb create mode 100644 test/integration/epp/domain/update/cancel_pending_test.rb create mode 100644 test/integration/repp/v1/domains/cancel_pending_update_test.rb create mode 100644 test/interactions/domains/cancel_pending_update_test.rb diff --git a/.gitignore b/.gitignore index cb4bf2c3b5..51d4c2d0f5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ CLAUDE.md AGENT.md own AGENT.md +/docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b88ba60f11..a58f32aee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +10.08.2026 +* Registrars can now cancel a pending registrant change https://github.com/internetee/registry/issues/2939 + 23.07.2026 * Operations with pending status now return result code 1001 in REPP https://github.com/internetee/registry/issues/2940 * Fixed case sensitivity issue for REPP requests https://github.com/internetee/registry/issues/2943 diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb index 094fe595a7..e588e844c7 100644 --- a/app/controllers/epp/domains_controller.rb +++ b/app/controllers/epp/domains_controller.rb @@ -9,6 +9,12 @@ class DomainsController < BaseController THROTTLED_ACTIONS = %i[info create check renew update transfer delete].freeze include Shunter::Integration::Throttle + # Everything Deserializers::Xml::DomainUpdate can emit besides the registrant itself. + # :domain and :registrar_id are always present, :legal_document may be mandatory for + # the registrar even when cancelling. + UPDATE_KEYS_BESIDES_REGISTRANT = %i[contacts nameservers dns_keys statuses transfer_code + reserved_pw].freeze + def info authorize! :info, @domain @@ -47,6 +53,9 @@ def update registrar_id = current_user.registrar.id update_params = ::Deserializers::Xml::DomainUpdate.new(params[:parsed_frame], registrar_id).call + + return cancel_pending_update if cancels_pending_update?(update_params) + action = Actions::DomainUpdate.new(@domain, update_params, false) unless action.call handle_errors(@domain) @@ -134,6 +143,33 @@ def transfer private + # EPP has no command to manipulate pending operations (RFC 3731 covers transfer only), + # so a domain:update requesting the registrant the domain already has is treated as a + # request to cancel the pending registrant change. Same idea as domain:renew cancelling + # pendingDelete in Epp::Domain#renew. + def cancels_pending_update?(update_params) + return false unless @domain.pending_update? + return false if update_params[:registrant].blank? + + requested = Registrant.find_by(code: update_params[:registrant][:code]) + return false unless requested&.id == @domain.registrant_id + + (update_params.keys & UPDATE_KEYS_BESIDES_REGISTRANT).empty? + end + + def cancel_pending_update + result = ::Domains::CancelPendingUpdate.run(domain: @domain, + initiator: current_user.username) + unless result.valid? + @domain.add_epp_error('2304', 'status', DomainStatus::PENDING_UPDATE, + result.errors.full_messages.join(', ')) + handle_errors(@domain) + return + end + + render_epp_response('/epp/domains/success') + end + def validate_info @prefix = 'info > info >' requires('name') diff --git a/app/controllers/repp/v1/domains/pending_updates_controller.rb b/app/controllers/repp/v1/domains/pending_updates_controller.rb new file mode 100644 index 0000000000..0e36ec1847 --- /dev/null +++ b/app/controllers/repp/v1/domains/pending_updates_controller.rb @@ -0,0 +1,29 @@ +module Repp + module V1 + module Domains + class PendingUpdatesController < BaseController + before_action :set_domain + + THROTTLED_ACTIONS = %i[destroy].freeze + include Shunter::Integration::Throttle + + api :DELETE, '/repp/v1/domains/:domain_name/pending_update' + param :domain_name, String, desc: 'Domain name' + desc 'Cancel a pending registrant change of a specific domain' + def destroy + authorize!(:update, @domain) + + result = ::Domains::CancelPendingUpdate.run(domain: @domain, + initiator: current_user.username) + unless result.valid? + @domain.add_epp_error('2304', 'status', DomainStatus::PENDING_UPDATE, + result.errors.full_messages.join(', ')) + return handle_errors(@domain) + end + + render_success(data: { domain: { name: @domain.name } }) + end + end + end + end +end diff --git a/app/interactions/domains/cancel_pending_update.rb b/app/interactions/domains/cancel_pending_update.rb new file mode 100644 index 0000000000..1a740ec2b0 --- /dev/null +++ b/app/interactions/domains/cancel_pending_update.rb @@ -0,0 +1,52 @@ +module Domains + class CancelPendingUpdate < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain with a pending registrant change' + string :initiator, + default: nil + + validate :domain_has_pending_update + + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - cancelled by"\ + " #{initiator}" + + ActiveRecord::Base.transaction do + notify_registrants + clean_pendings! + end + + UpdateWhoisRecordJob.perform_later(domain.name, 'domain') + end + + private + + def domain_has_pending_update + return if domain&.pending_update? + + errors.add(:domain, I18n.t(:object_status_prohibits_operation)) + end + + # Both parties already got a confirmation link that is about to become invalid, + # so they are notified before the verification data is wiped. + def notify_registrants + RegistrantChangeMailer.cancelled(domain: domain, + registrar: domain.registrar, + registrant: domain.registrant, + send_to: [domain.new_registrant_email, + domain.registrant.email]).deliver_later + end + + def clean_pendings! + domain.is_admin = true + # Has to happen before save, otherwise before_update reinstates pendingUpdate + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + domain.pending_json = {} + domain.statuses.delete(DomainStatus::PENDING_UPDATE) + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.save! + end + end +end diff --git a/app/interactions/domains/update_confirm/process_action.rb b/app/interactions/domains/update_confirm/process_action.rb index 07c3a6dd31..03d2463476 100644 --- a/app/interactions/domains/update_confirm/process_action.rb +++ b/app/interactions/domains/update_confirm/process_action.rb @@ -2,6 +2,10 @@ module Domains module UpdateConfirm class ProcessAction < Base def execute + # The registrant decision arrives asynchronously, so the pending update may already + # be gone by now - cancelled by the registrar or cleaned up by the expiry cron. + return unless domain.pending_update? + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - #{action} by"\ " #{initiator}" diff --git a/app/mailers/registrant_change_mailer.rb b/app/mailers/registrant_change_mailer.rb index 101e6c3e17..dd0a8cb980 100644 --- a/app/mailers/registrant_change_mailer.rb +++ b/app/mailers/registrant_change_mailer.rb @@ -38,6 +38,15 @@ def rejected(domain:, registrar:, registrant:) mail(to: domain.new_registrant_email, subject: subject) end + def cancelled(domain:, registrar:, registrant:, send_to:) + @domain = DomainPresenter.new(domain: domain, view: view_context) + @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) + @registrant = RegistrantPresenter.new(registrant: registrant, view: view_context) + + subject = default_i18n_subject(domain_name: domain.name) + mail(to: send_to, subject: subject) + end + def expired(domain:, registrar:, registrant:, send_to:) @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) diff --git a/app/views/mailers/registrant_change_mailer/cancelled.html.erb b/app/views/mailers/registrant_change_mailer/cancelled.html.erb new file mode 100644 index 0000000000..9e12d4bde3 --- /dev/null +++ b/app/views/mailers/registrant_change_mailer/cancelled.html.erb @@ -0,0 +1,18 @@ +Tere +

+Registripidaja tühistas domeeni <%= @domain.name %> registreerija vahetuse taotluse. Varem saadetud kinnituslink ei kehti enam. +

+Küsimuste korral palun võtke ühendust oma registripidajaga: + +<%= render 'mailers/shared/registrar/registrar.et.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.html' %> +
+

+Hi, +

+The registrar has cancelled the registrant change request for the domain <%= @domain.name %>. The confirmation link sent earlier is no longer valid. +

+Please contact your registrar if you have any questions: + +<%= render 'mailers/shared/registrar/registrar.en.html', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.en.html' %> diff --git a/app/views/mailers/registrant_change_mailer/cancelled.text.erb b/app/views/mailers/registrant_change_mailer/cancelled.text.erb new file mode 100644 index 0000000000..2a15598ced --- /dev/null +++ b/app/views/mailers/registrant_change_mailer/cancelled.text.erb @@ -0,0 +1,20 @@ +Tere + +Registripidaja tühistas domeeni <%= @domain.name %> registreerija vahetuse taotluse. Varem saadetud kinnituslink ei kehti enam. + +Küsimuste korral palun võtke ühendust oma registripidajaga: + +<%= render 'mailers/shared/registrar/registrar.et.text', registrar: @registrar %> +<%= render 'mailers/shared/signatures/signature.et.text' %> + +-------------------------------------- + +Hi, + +The registrar has cancelled the registrant change request for the domain <%= @domain.name %>. The confirmation link sent earlier is no longer valid. + +Please contact your registrar if you have any questions: + +<%= render 'mailers/shared/registrar/registrar.en.text', registrar: @registrar %> + +<%= render 'mailers/shared/signatures/signature.en.text' %> diff --git a/config/locales/mailers/registrant_change.en.yml b/config/locales/mailers/registrant_change.en.yml index 744715807d..e3b8cf820e 100644 --- a/config/locales/mailers/registrant_change.en.yml +++ b/config/locales/mailers/registrant_change.en.yml @@ -19,4 +19,8 @@ en: expired: subject: >- Domeeni %{domain_name} registreerija vahetuse taotlus on tühistatud - / %{domain_name} registrant change cancelled \ No newline at end of file + / %{domain_name} registrant change cancelled + cancelled: + subject: >- + Registripidaja tühistas domeeni %{domain_name} registreerija vahetuse taotluse + / %{domain_name} registrant change request was cancelled by the registrar \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 05af94cda3..19d67731b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,6 +167,7 @@ resources :renew, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/renews' resources :transfer, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/transfers' resources :statuses, only: %i[update destroy], constraints: { id: /.*/ }, controller: 'domains/statuses' + resource :pending_update, only: %i[destroy], controller: 'domains/pending_updates' match 'dnssec', to: 'domains/dnssec#destroy', via: 'delete', defaults: { id: nil } match 'contacts', to: 'domains/contacts#destroy', via: 'delete', defaults: { id: nil } collection do diff --git a/test/integration/epp/domain/update/cancel_pending_test.rb b/test/integration/epp/domain/update/cancel_pending_test.rb new file mode 100644 index 0000000000..957157c969 --- /dev/null +++ b/test/integration/epp/domain/update/cancel_pending_test.rb @@ -0,0 +1,129 @@ +require 'test_helper' + +class EppDomainUpdateCancelPendingTest < EppTestCase + include ActionMailer::TestHelper + include ActiveJob::TestHelper + + setup do + @domain = domains(:shop) + @original_registrant_change_verification = + Setting.request_confirmation_on_registrant_change_enabled + Setting.request_confirmation_on_registrant_change_enabled = true + ActionMailer::Base.deliveries.clear + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + end + + teardown do + Setting.request_confirmation_on_registrant_change_enabled = + @original_registrant_change_verification + end + + def test_cancels_pending_update_when_current_registrant_is_requested_again + request_registrant_change + old_registrant = @domain.registrant + + post_domain_update(old_registrant) + + assert_epp_response :completed_successfully + assert_equal old_registrant, @domain.registrant + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_not @domain.registrant_verification_asked? + end + + def test_notifies_both_registrants_when_pending_update_is_cancelled + request_registrant_change + new_registrant_email = @domain.new_registrant_email + registrant_email = @domain.registrant.email + ActionMailer::Base.deliveries.clear + + perform_enqueued_jobs { post_domain_update(@domain.registrant) } + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, new_registrant_email + assert_includes email.to, registrant_email + end + + def test_rejects_update_of_pending_domain_when_another_registrant_is_requested + request_registrant_change + old_registrant = @domain.registrant + + post_domain_update(contacts(:jack)) + + assert_epp_response :object_status_prohibits_operation + assert_equal old_registrant, @domain.registrant + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def test_does_not_cancel_pending_update_when_other_changes_are_requested + request_registrant_change + old_transfer_code = @domain.transfer_code + + post_domain_update(@domain.registrant, transfer_code: 'new-transfer-code') + + assert_epp_response :object_status_prohibits_operation + assert_equal old_transfer_code, @domain.transfer_code + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def test_keeps_regular_update_intact_when_domain_has_no_pending_update + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + + post_domain_update(@domain.registrant) + + assert_epp_response :completed_successfully + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + private + + def request_registrant_change + new_registrant = contacts(:william) + assert_not_equal new_registrant, @domain.registrant + + post_domain_update(new_registrant) + + assert_epp_response :completed_successfully_action_pending + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def post_domain_update(registrant, transfer_code: nil) + post epp_update_path, + params: { frame: registrant_change_xml(registrant, transfer_code: transfer_code) }, + headers: { 'HTTP_COOKIE' => 'session=api_bestnames' } + + # assert_epp_response memoizes the parsed response, reset it between requests + @epp_response = nil + @domain.reload + end + + def registrant_change_xml(registrant, transfer_code: nil) + auth_info = if transfer_code + "#{transfer_code}" + end + + <<-XML + + + + + + #{@domain.name} + + #{registrant.code} + #{auth_info} + + + + + + #{'test' * 2000} + + + + + XML + end +end diff --git a/test/integration/repp/v1/domains/cancel_pending_update_test.rb b/test/integration/repp/v1/domains/cancel_pending_update_test.rb new file mode 100644 index 0000000000..a5474c8634 --- /dev/null +++ b/test/integration/repp/v1/domains/cancel_pending_update_test.rb @@ -0,0 +1,94 @@ +require 'test_helper' + +class ReppV1DomainsCancelPendingUpdateTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + @domain = domains(:shop) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + @auth_headers = { 'Authorization' => "Basic #{token}" } + Setting.request_confirmation_on_registrant_change_enabled = true + end + + def test_cancels_pending_registrant_change + request_registrant_change + + json = cancel_pending_update + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal @domain.name, json[:data][:domain][:name] + refute_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_nil @domain.registrant_verification_token + assert_nil @domain.registrant_verification_asked_at + end + + def test_keeps_current_registrant_on_cancel + old_registrant = @domain.registrant + request_registrant_change + + cancel_pending_update + + assert_equal old_registrant, @domain.registrant + end + + def test_notifies_both_registrants_on_cancel + request_registrant_change + new_registrant_email = @domain.new_registrant_email + registrant_email = @domain.registrant.email + ActionMailer::Base.deliveries.clear + + perform_enqueued_jobs { cancel_pending_update } + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, new_registrant_email + assert_includes email.to, registrant_email + end + + def test_returns_error_when_domain_has_no_pending_update + refute_includes @domain.statuses, DomainStatus::PENDING_UPDATE + + json = cancel_pending_update + + assert_response :bad_request + assert_equal 2304, json[:code] + end + + def test_does_not_cancel_pending_update_of_another_registrar + request_registrant_change + other_user = users(:api_goodnames) + token = Base64.encode64("#{other_user.username}:#{other_user.plain_text_password}") + @auth_headers = { 'Authorization' => "Basic #{token}" } + + json = cancel_pending_update + + assert_response :not_found + assert_equal 2303, json[:code] + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + private + + def request_registrant_change + new_registrant = contacts(:william) + refute_equal new_registrant, @domain.registrant + + put "/repp/v1/domains/#{@domain.name}", + headers: json_headers, + params: { domain: { registrant: { code: new_registrant.code } } }.to_json + + @domain.reload + assert_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + + def cancel_pending_update + delete "/repp/v1/domains/#{@domain.name}/pending_update", headers: json_headers + + @domain.reload + JSON.parse(response.body, symbolize_names: true) + end + + def json_headers + @auth_headers.merge('Content-Type' => 'application/json') + end +end diff --git a/test/interactions/domains/cancel_pending_update_test.rb b/test/interactions/domains/cancel_pending_update_test.rb new file mode 100644 index 0000000000..a9e8215814 --- /dev/null +++ b/test/interactions/domains/cancel_pending_update_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' + +module Domains + class CancelPendingUpdateTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + include ActiveJob::TestHelper + + setup do + @domain = domains(:shop) + @new_registrant = contacts(:william) + @domain.update!(registrant_verification_asked_at: Time.zone.now, + registrant_verification_token: 'test') + @domain.pending_json = { 'new_registrant_id' => @new_registrant.id, + 'new_registrant_email' => @new_registrant.email, + 'new_registrant_name' => @new_registrant.name } + @domain.statuses = [DomainStatus::PENDING_UPDATE] + @domain.save(validate: false) + ActionMailer::Base.deliveries.clear + end + + def test_clears_pending_update_data + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + assert_empty @domain.pending_json + assert_nil @domain.registrant_verification_token + assert_nil @domain.registrant_verification_asked_at + assert_equal '', @domain.status_notes[DomainStatus::PENDING_UPDATE] + end + + def test_keeps_registrant_untouched + old_registrant = @domain.registrant + + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_equal old_registrant, @domain.registrant + end + + def test_notifies_both_registrants + registrant_email = @domain.registrant.email + + perform_enqueued_jobs do + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + end + + email = ActionMailer::Base.deliveries.last + assert_includes email.to, @new_registrant.email + assert_includes email.to, registrant_email + end + + def test_updates_whois_record + assert_enqueued_with(job: UpdateWhoisRecordJob, args: [@domain.name, 'domain']) do + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + end + end + + def test_fails_when_domain_has_no_pending_update + @domain.statuses = [] + @domain.save(validate: false) + + result = CancelPendingUpdate.run(domain: @domain, initiator: 'test') + + assert_not result.valid? + assert_no_enqueued_emails + end + + def test_does_not_touch_other_statuses + @domain.statuses = [DomainStatus::PENDING_UPDATE, DomainStatus::CLIENT_HOLD] + @domain.save(validate: false) + + CancelPendingUpdate.run!(domain: @domain, initiator: 'test') + @domain.reload + + assert_includes @domain.statuses, DomainStatus::CLIENT_HOLD + assert_not_includes @domain.statuses, DomainStatus::PENDING_UPDATE + end + end +end diff --git a/test/jobs/domain_update_confirm_job_test.rb b/test/jobs/domain_update_confirm_job_test.rb index 158729ae3c..380ec494e7 100644 --- a/test/jobs/domain_update_confirm_job_test.rb +++ b/test/jobs/domain_update_confirm_job_test.rb @@ -36,7 +36,23 @@ def test_registrant_unlocked_domain assert_equal(@domain.registrar.notifications.last.text, "Domain #{@domain.name} has been unlocked by registrant") end + # The registrant decision may reach us after the registrar has already cancelled + # the pending update, or after the expiry cron has cleaned it up. + def test_skips_confirmation_when_pending_update_is_already_gone + set_pending_update + old_registrant_code = @domain.registrant.code + @domain.update!(statuses: [DomainStatus::OK]) + + assert_no_difference '@domain.registrar.notifications.count' do + DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) + end + @domain.reload + + assert_equal old_registrant_code, @domain.registrant.code + end + def test_rejected_registrant_verification_notifies_registrar + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) last_registrar_notification = @domain.registrar.notifications.last @@ -45,6 +61,7 @@ def test_rejected_registrant_verification_notifies_registrar end def test_accepted_registrant_verification_notifies_registrar + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) last_registrar_notification = @domain.registrar.notifications.last @@ -61,6 +78,7 @@ def test_changes_domain_registrant_after_approval @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) + set_pending_update @domain.reload DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @@ -79,6 +97,7 @@ def test_clears_pending_update_after_denial @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) + set_pending_update DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload @@ -96,7 +115,8 @@ def test_protects_statuses_after_denial @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload @@ -116,7 +136,8 @@ def test_protects_statuses_after_confirm @domain.pending_json['frame'] = parsed_frame @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @domain.reload @@ -137,7 +158,8 @@ def test_works_id_current_user_id_broken @domain.pending_json['frame'] = parsed_frame @domain.pending_json['current_user_id'] = { key: 'some_value'} @domain.update(pending_json: @domain.pending_json) - @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED]) + @domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED, + DomainStatus::PENDING_UPDATE]) assert_nothing_raised do DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @@ -191,4 +213,12 @@ def test_clears_pending_update_and_sets_ok_after_denial assert_not @domain.statuses.include? DomainStatus::PENDING_UPDATE assert @domain.statuses.include? DomainStatus::OK end + + private + + def set_pending_update + @domain.statuses = [DomainStatus::PENDING_UPDATE] + @domain.save(validate: false) + @domain.reload + end end From 6a06f6758f8a897f065519b71b0f3ca884c93d0a Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Tue, 11 Aug 2026 12:06:30 +0300 Subject: [PATCH 2/2] added repowidse to gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 51d4c2d0f5..d72d03e6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ AGENT.md own AGENT.md /docs/ - +.repowise