diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d7c1e24..0907d33e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +17.12.2020 +* New API for registering bounced emails [#1687](https://github.com/internetee/registry/pull/1687) + +16.12.2020 +* Refactored domain delete confirmation for interactors [#1769](https://github.com/internetee/registry/issues/1769) + +15.12.2020 +* Improved logic for domain list request in registrant API [#1750](https://github.com/internetee/registry/pull/1750) +* Refactored Whois update job for interactors [#1771](https://github.com/internetee/registry/issues/1771) + 14.12.2020 * Refactored domain cron jobs for interactors [#1767](https://github.com/internetee/registry/issues/1767) diff --git a/app/controllers/admin/bounced_mail_addresses_controller.rb b/app/controllers/admin/bounced_mail_addresses_controller.rb new file mode 100644 index 000000000..1c59acaa4 --- /dev/null +++ b/app/controllers/admin/bounced_mail_addresses_controller.rb @@ -0,0 +1,30 @@ +module Admin + class BouncedMailAddressesController < BaseController + before_action :set_bounced_mail_address, only: %i[show destroy] + load_and_authorize_resource + + # GET /bounced_mail_addresses + def index + @bounced_mail_addresses = BouncedMailAddress.all.order(created_at: :desc) + end + + # GET /bounced_mail_addresses/1 + def show; end + + # DELETE /bounced_mail_addresses/1 + def destroy + @bounced_mail_address.destroy + redirect_to( + admin_bounced_mail_addresses_url, + notice: 'Bounced mail address was successfully destroyed.' + ) + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_bounced_mail_address + @bounced_mail_address = BouncedMailAddress.find(params[:id]) + end + end +end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 54930edf9..b62b3e063 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -10,6 +10,11 @@ module Api head :unauthorized unless ip_allowed end + def authenticate_shared_key + api_key = "Basic #{ENV['api_shared_key']}" + head(:unauthorized) unless api_key == request.authorization + end + def not_found_error uuid = params['uuid'] json = { error: 'Not Found', uuid: uuid, message: 'Record not found' } diff --git a/app/controllers/api/v1/bounces_controller.rb b/app/controllers/api/v1/bounces_controller.rb new file mode 100644 index 000000000..fd10a3398 --- /dev/null +++ b/app/controllers/api/v1/bounces_controller.rb @@ -0,0 +1,25 @@ +module Api + module V1 + class BouncesController < BaseController + before_action :authenticate_shared_key + + # POST api/v1/bounces/ + def create + return head(:bad_request) unless bounce_params[:bounce][:bouncedRecipients].any? + + BouncedMailAddress.record(bounce_params) + head(:created) + rescue ActionController::ParameterMissing + head(:bad_request) + end + + def bounce_params + params.require(:data).require(:bounce).require(:bouncedRecipients).each do |r| + r.require(:emailAddress) + end + + params.require(:data) + end + end + end +end diff --git a/app/controllers/api/v1/registrant/contacts_controller.rb b/app/controllers/api/v1/registrant/contacts_controller.rb index 10f9abacf..e11458ff8 100644 --- a/app/controllers/api/v1/registrant/contacts_controller.rb +++ b/app/controllers/api/v1/registrant/contacts_controller.rb @@ -19,15 +19,16 @@ module Api end contacts = current_user_contacts.limit(limit).offset(offset) - serialized_contacts = contacts.collect { |contact| serialize_contact(contact) } + serialized_contacts = contacts.collect { |contact| serialize_contact(contact, false) } render json: serialized_contacts end def show contact = current_user_contacts.find_by(uuid: params[:uuid]) + links = params[:links] == 'true' if contact - render json: serialize_contact(contact) + render json: serialize_contact(contact, links) else render json: { errors: [{ base: ['Contact not found'] }] }, status: :not_found end @@ -85,7 +86,7 @@ module Api contact.registrar.notify(action) end - render json: serialize_contact(contact) + render json: serialize_contact(contact, false) end private @@ -96,8 +97,8 @@ module Api current_registrant_user.direct_contacts end - def serialize_contact(contact) - Serializers::RegistrantApi::Contact.new(contact).to_json + def serialize_contact(contact, links) + Serializers::RegistrantApi::Contact.new(contact, links).to_json end end end diff --git a/app/controllers/api/v1/registrant/domains_controller.rb b/app/controllers/api/v1/registrant/domains_controller.rb index b985ef390..73b534598 100644 --- a/app/controllers/api/v1/registrant/domains_controller.rb +++ b/app/controllers/api/v1/registrant/domains_controller.rb @@ -7,6 +7,7 @@ module Api def index limit = params[:limit] || 200 offset = params[:offset] || 0 + simple = params[:simple] == 'true' || false if limit.to_i > 200 || limit.to_i < 1 render(json: { errors: [{ limit: ['parameter is out of range'] }] }, @@ -18,21 +19,20 @@ module Api status: :bad_request) && return end - @domains = current_user_domains.limit(limit).offset(offset) - - serialized_domains = @domains.map do |item| - serializer = Serializers::RegistrantApi::Domain.new(item) + domains = current_user_domains + serialized_domains = domains.limit(limit).offset(offset).map do |item| + serializer = Serializers::RegistrantApi::Domain.new(item, simplify: simple) serializer.to_json end - render json: serialized_domains + render json: { count: domains.count, domains: serialized_domains } end def show @domain = current_user_domains.find_by(uuid: params[:uuid]) if @domain - serializer = Serializers::RegistrantApi::Domain.new(@domain) + serializer = Serializers::RegistrantApi::Domain.new(@domain, simplify: false) render json: serializer.to_json else render json: { errors: [{ base: ['Domain not found'] }] }, status: :not_found diff --git a/app/controllers/registrar/sessions_controller.rb b/app/controllers/registrar/sessions_controller.rb index 2fa69ca90..e413c4feb 100644 --- a/app/controllers/registrar/sessions_controller.rb +++ b/app/controllers/registrar/sessions_controller.rb @@ -62,6 +62,7 @@ class Registrar def find_user_by_idc_and_allowed(idc) return User.new unless idc + possible_users = ApiUser.where(identity_code: idc) || User.new possible_users.each do |selected_user| if selected_user.registrar.white_ips.registrar_area.include_ip?(request.ip) diff --git a/app/interactions/domains/delete_confirm/base.rb b/app/interactions/domains/delete_confirm/base.rb new file mode 100644 index 000000000..5bd07f40f --- /dev/null +++ b/app/interactions/domains/delete_confirm/base.rb @@ -0,0 +1,53 @@ +module Domains + module DeleteConfirm + class Base < ActiveInteraction::Base + object :domain, + class: Domain, + description: 'Domain to confirm release' + string :action + string :initiator, + default: nil + + validates :domain, :action, presence: true + validates :action, inclusion: { in: [RegistrantVerification::CONFIRMED, + RegistrantVerification::REJECTED] } + + def raise_errors!(domain) + return unless domain.errors.any? + + message = "domain #{domain.name} failed with errors #{domain.errors.full_messages}" + throw message + end + + def notify_registrar(message_key) + domain.registrar.notifications.create!( + text: "#{I18n.t(message_key)}: #{domain.name}", + attached_obj_id: domain.id, + attached_obj_type: domain.class.to_s + ) + end + + def preclean_pendings + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + end + + def clean_pendings! + domain.is_admin = true + domain.registrant_verification_token = nil + domain.registrant_verification_asked_at = nil + domain.pending_json = {} + clear_statuses + domain.save + end + + def clear_statuses + domain.statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) + domain.statuses.delete(DomainStatus::PENDING_UPDATE) + domain.statuses.delete(DomainStatus::PENDING_DELETE) + domain.status_notes[DomainStatus::PENDING_UPDATE] = '' + domain.status_notes[DomainStatus::PENDING_DELETE] = '' + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_action.rb b/app/interactions/domains/delete_confirm/process_action.rb new file mode 100644 index 000000000..59f23de67 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_action.rb @@ -0,0 +1,17 @@ +module Domains + module DeleteConfirm + class ProcessAction < Base + def execute + ::PaperTrail.request.whodunnit = "interaction - #{self.class.name} - #{action} by"\ + " #{initiator}" + + case action + when RegistrantVerification::CONFIRMED + compose(ProcessDeleteConfirmed, inputs) + when RegistrantVerification::REJECTED + compose(ProcessDeleteRejected, inputs) + end + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_delete_confirmed.rb b/app/interactions/domains/delete_confirm/process_delete_confirmed.rb new file mode 100644 index 000000000..1f3068107 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_delete_confirmed.rb @@ -0,0 +1,50 @@ +module Domains + module DeleteConfirm + class ProcessDeleteConfirmed < Base + def execute + notify_registrar(:poll_pending_delete_confirmed_by_registrant) + domain.apply_pending_delete! + raise_errors!(domain) + end + + def apply_pending_delete! + preclean_pendings + clean_pendings! + DomainDeleteMailer.accepted(domain).deliver_now + domain.set_pending_delete! + end + + def set_pending_delete! + unless domain.pending_deletable? + add_epp_error + return + end + + domain.delete_date = delete_date + domain.statuses << DomainStatus::PENDING_DELETE + set_server_hold if server_holdable? + domain.save(validate: false) + end + + def set_server_hold + domain.statuses << DomainStatus::SERVER_HOLD + domain.outzone_at = Time.current + end + + def server_holdable? + return false if domain.statuses.include?(DomainStatus::SERVER_HOLD) + return false if domain.statuses.include?(DomainStatus::SERVER_MANUAL_INZONE) + + true + end + + def delete_date + Time.zone.today + Setting.redemption_grace_period.days + 1.day + end + + def add_epp_error + domain.add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation)) + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/process_delete_rejected.rb b/app/interactions/domains/delete_confirm/process_delete_rejected.rb new file mode 100644 index 000000000..25b491660 --- /dev/null +++ b/app/interactions/domains/delete_confirm/process_delete_rejected.rb @@ -0,0 +1,32 @@ +module Domains + module DeleteConfirm + class ProcessDeleteRejected < Base + def execute + domain.cancel_pending_delete + notify_registrar(:poll_pending_delete_rejected_by_registrant) + domain.save(validate: false) + raise_errors!(domain) + + send_domain_delete_rejected_email + end + + def send_domain_delete_rejected_email + if domain.registrant_verification_token.blank? + warn "EMAIL NOT DELIVERED: registrant_verification_token is missing for #{domain.name}" + elsif domain.registrant_verification_asked_at.blank? + warn "EMAIL NOT DELIVERED: registrant_verification_asked_at is missing for #{domain.name}" + else + send_email + end + end + + def warn(message) + Rails.logger.warn(message) + end + + def send_email + DomainDeleteMailer.rejected(domain).deliver_now + end + end + end +end diff --git a/app/interactions/domains/delete_confirm/send_request.rb b/app/interactions/domains/delete_confirm_email/send_request.rb similarity index 96% rename from app/interactions/domains/delete_confirm/send_request.rb rename to app/interactions/domains/delete_confirm_email/send_request.rb index 91afaefb8..7070423a6 100644 --- a/app/interactions/domains/delete_confirm/send_request.rb +++ b/app/interactions/domains/delete_confirm_email/send_request.rb @@ -1,5 +1,5 @@ module Domains - module DeleteConfirm + module DeleteConfirmEmail class SendRequest < ActiveInteraction::Base object :domain, class: Domain, diff --git a/app/interactions/whois/delete_record.rb b/app/interactions/whois/delete_record.rb new file mode 100644 index 000000000..62e416d2f --- /dev/null +++ b/app/interactions/whois/delete_record.rb @@ -0,0 +1,48 @@ +module Whois + class DeleteRecord < ActiveInteraction::Base + string :name + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + send "delete_#{type}", name + end + + # 1. deleting own + # 2. trying to regenerate reserved in order domain is still in the list + def delete_domain(name) + WhoisRecord.where(name: name).destroy_all + + BlockedDomain.find_by(name: name).try(:generate_data) + ReservedDomain.find_by(name: name).try(:generate_data) + Dispute.active.find_by(domain_name: name).try(:generate_data) + end + + def delete_reserved(name) + remove_status_from_whois(domain_name: name, domain_status: 'Reserved') + end + + def delete_blocked(name) + delete_reserved(name) + end + + def delete_disputed(name) + return if Dispute.active.find_by(domain_name: name).present? + + remove_status_from_whois(domain_name: name, domain_status: 'disputed') + end + + def delete_zone(name) + WhoisRecord.where(name: name).destroy_all + Whois::Record.where(name: name).destroy_all + end + + def remove_status_from_whois(domain_name:, domain_status:) + Whois::Record.where(name: domain_name).each do |r| + r.json['status'] = r.json['status'].delete_if { |status| status == domain_status } + r.json['status'].blank? ? r.destroy : r.save + end + end + end +end diff --git a/app/interactions/whois/update.rb b/app/interactions/whois/update.rb new file mode 100644 index 000000000..bd37b4576 --- /dev/null +++ b/app/interactions/whois/update.rb @@ -0,0 +1,39 @@ +module Whois + class Update < ActiveInteraction::Base + array :names + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{type}" + + klass = determine_class + + Array(names).each do |name| + record = find_record(klass, name) + if record + Whois::UpdateRecord.run(record: record, type: type) + else + Whois::DeleteRecord.run(name: name, type: type) + end + end + end + + private + + def determine_class + case type + when 'reserved' then ReservedDomain + when 'blocked' then BlockedDomain + when 'domain' then Domain + when 'disputed' then Dispute.active + else DNS::Zone + end + end + + def find_record(klass, name) + klass == DNS::Zone ? klass.find_by(origin: name) : klass.find_by(name: name) + end + end +end diff --git a/app/interactions/whois/update_record.rb b/app/interactions/whois/update_record.rb new file mode 100644 index 000000000..6fc838968 --- /dev/null +++ b/app/interactions/whois/update_record.rb @@ -0,0 +1,32 @@ +module Whois + class UpdateRecord < ActiveInteraction::Base + interface :record + string :type + + validates :type, inclusion: { in: %w[reserved blocked domain disputed zone] } + + def execute + send "update_#{type}", record + end + + def update_domain(domain) + domain.whois_record ? domain.whois_record.save : domain.create_whois_record + end + + def update_reserved(record) + record.generate_data + end + + def update_blocked(record) + update_reserved(record) + end + + def update_disputed(record) + update_reserved(record) + end + + def update_zone(record) + update_reserved(record) + end + end +end diff --git a/app/jobs/domain_delete_confirm_job.rb b/app/jobs/domain_delete_confirm_job.rb index e94f2432c..afea3da59 100644 --- a/app/jobs/domain_delete_confirm_job.rb +++ b/app/jobs/domain_delete_confirm_job.rb @@ -1,39 +1,11 @@ -class DomainDeleteConfirmJob < Que::Job - def run(domain_id, action, initiator = nil) - ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{action} by #{initiator}" - # it's recommended to keep transaction against job table as short as possible. - ActiveRecord::Base.transaction do - domain = Epp::Domain.find(domain_id) +class DomainDeleteConfirmJob < ApplicationJob + queue_as :default - case action - when RegistrantVerification::CONFIRMED - domain.notify_registrar(:poll_pending_delete_confirmed_by_registrant) - domain.apply_pending_delete! - raise_errors!(domain) + def perform(domain_id, action, initiator = nil) + domain = Epp::Domain.find(domain_id) - when RegistrantVerification::REJECTED - domain.statuses.delete(DomainStatus::PENDING_DELETE_CONFIRMATION) - domain.notify_registrar(:poll_pending_delete_rejected_by_registrant) - - domain.cancel_pending_delete - domain.save(validate: false) - raise_errors!(domain) - - if domain.registrant_verification_token.blank? - Rails.logger.warn "EMAIL NOT DELIVERED: registrant_verification_token is missing for #{domain.name}" - elsif domain.registrant_verification_asked_at.blank? - Rails.logger.warn "EMAIL NOT DELIVERED: registrant_verification_asked_at is missing for #{domain.name}" - else - DomainDeleteMailer.rejected(domain).deliver_now - end - end - - destroy # it's best to destroy the job in the same transaction - end - end - - - def raise_errors!(domain) - throw "domain #{domain.name} failed with errors #{domain.errors.full_messages}" if domain.errors.any? + Domains::DeleteConfirm::ProcessAction.run(domain: domain, + action: action, + initiator: initiator) end end diff --git a/app/jobs/update_whois_record_job.rb b/app/jobs/update_whois_record_job.rb index 2973d5a6b..d067807a0 100644 --- a/app/jobs/update_whois_record_job.rb +++ b/app/jobs/update_whois_record_job.rb @@ -1,87 +1,5 @@ class UpdateWhoisRecordJob < Que::Job - def run(names, type) - ::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{type}" - - klass = determine_class(type) - - Array(names).each do |name| - record = find_record(klass, name) - if record - send "update_#{type}", record - else - send "delete_#{type}", name - end - end - end - - def find_record(klass, name) - klass == DNS::Zone ? klass.find_by(origin: name) : klass.find_by(name: name) - end - - def determine_class(type) - case type - when 'reserved' then ReservedDomain - when 'blocked' then BlockedDomain - when 'domain' then Domain - when 'disputed' then Dispute.active - when 'zone' then DNS::Zone - end - end - - def update_domain(domain) - domain.whois_record ? domain.whois_record.save : domain.create_whois_record - end - - def update_reserved(record) - record.generate_data - end - - def update_blocked(record) - update_reserved(record) - end - - def update_disputed(record) - update_reserved(record) - end - - def update_zone(record) - update_reserved(record) - end - - # 1. deleting own - # 2. trying to regenerate reserved in order domain is still in the list - def delete_domain(name) - WhoisRecord.where(name: name).destroy_all - - BlockedDomain.find_by(name: name).try(:generate_data) - ReservedDomain.find_by(name: name).try(:generate_data) - Dispute.active.find_by(domain_name: name).try(:generate_data) - end - - def delete_reserved(name) - remove_status_from_whois(domain_name: name, domain_status: 'Reserved') - end - - def delete_blocked(name) - delete_reserved(name) - end - - def delete_disputed(name) - return if Dispute.active.find_by(domain_name: name).present? - - remove_status_from_whois(domain_name: name, domain_status: 'disputed') - end - - def delete_zone(name) - WhoisRecord.where(name: name).destroy_all - Whois::Record.where(name: name).destroy_all - end - - def remove_status_from_whois(domain_name:, domain_status:) - Whois::Record.where(name: domain_name).each do |r| - r.json['status'] = r.json['status'].delete_if { |status| status == domain_status } - r.json['status'].blank? ? r.destroy : r.save - end + Whois::Update.run(names: [names].flatten, type: type) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index dce8a515b..31637b8ea 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -109,6 +109,7 @@ class Ability can :destroy, :pending can :create, :zonefile can :access, :settings_menu + can :manage, BouncedMailAddress end def static_registrant diff --git a/app/models/bounced_mail_address.rb b/app/models/bounced_mail_address.rb new file mode 100644 index 000000000..73c6a0941 --- /dev/null +++ b/app/models/bounced_mail_address.rb @@ -0,0 +1,28 @@ +class BouncedMailAddress < ApplicationRecord + validates :email, :message_id, :bounce_type, :bounce_subtype, :action, :status, presence: true + + def bounce_reason + "#{action} (#{status} #{diagnostic})" + end + + def self.record(json) + bounced_records = json['bounce']['bouncedRecipients'] + bounced_records.each do |record| + bounce_record = BouncedMailAddress.new(params_from_json(json, record)) + + bounce_record.save + end + end + + def self.params_from_json(json, bounced_record) + { + email: bounced_record['emailAddress'], + message_id: json['mail']['messageId'], + bounce_type: json['bounce']['bounceType'], + bounce_subtype: json['bounce']['bounceSubType'], + action: bounced_record['action'], + status: bounced_record['status'], + diagnostic: bounced_record['diagnosticCode'], + } + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index 8a154c50c..e30312b4a 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -347,19 +347,24 @@ class Contact < ApplicationRecord @desc = {} registrant_domains.each do |dom| - @desc[dom.name] ||= [] - @desc[dom.name] << :registrant + @desc[dom.name] ||= { id: dom.uuid, roles: [] } + @desc[dom.name][:roles] << :registrant end domain_contacts.each do |dc| - @desc[dc.domain.name] ||= [] - @desc[dc.domain.name] << dc.name.downcase.to_sym + @desc[dc.domain.name] ||= { id: dc.domain.uuid, roles: [] } + @desc[dc.domain.name][:roles] << dc.name.downcase.to_sym @desc[dc.domain.name] = @desc[dc.domain.name].compact end @desc end + def related_domains + a = related_domain_descriptions + a.keys.map { |d| { name: d, id: a[d][:id], roles: a[d][:roles] } } + end + def status_notes_array=(notes) self.status_notes = {} notes ||= [] diff --git a/app/models/domain.rb b/app/models/domain.rb index cdbbeb4af..49f18d9db 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -421,7 +421,7 @@ class Domain < ApplicationRecord pending_delete_confirmation! save(validate: false) # should check if this did succeed - Domains::DeleteConfirm::SendRequest.run(domain: self) + Domains::DeleteConfirmEmail::SendRequest.run(domain: self) end def cancel_pending_delete diff --git a/app/models/registrant_verification.rb b/app/models/registrant_verification.rb index 7e238cecc..4e8e6afe9 100644 --- a/app/models/registrant_verification.rb +++ b/app/models/registrant_verification.rb @@ -30,12 +30,12 @@ class RegistrantVerification < ApplicationRecord def domain_registrant_delete_confirm!(initiator) self.action_type = DOMAIN_DELETE self.action = CONFIRMED - DomainDeleteConfirmJob.enqueue domain.id, CONFIRMED, initiator if save + DomainDeleteConfirmJob.perform_later domain.id, CONFIRMED, initiator if save end def domain_registrant_delete_reject!(initiator) self.action_type = DOMAIN_DELETE self.action = REJECTED - DomainDeleteConfirmJob.enqueue domain.id, REJECTED, initiator if save + DomainDeleteConfirmJob.perform_later domain.id, REJECTED, initiator if save end end diff --git a/app/views/admin/base/_menu.haml b/app/views/admin/base/_menu.haml index a327419fd..5853bd3e6 100644 --- a/app/views/admin/base/_menu.haml +++ b/app/views/admin/base/_menu.haml @@ -33,6 +33,7 @@ %li= link_to t('.blocked_domains'), admin_blocked_domains_path %li= link_to t('.reserved_domains'), admin_reserved_domains_path %li= link_to t('.disputed_domains'), admin_disputes_path + %li= link_to t('.bounced_email_addresses'), admin_bounced_mail_addresses_path %li= link_to t('.epp_log'), admin_epp_logs_path(created_after: 'today') %li= link_to t('.repp_log'), admin_repp_logs_path(created_after: 'today') %li= link_to t('.que'), '/admin/que' diff --git a/app/views/admin/bounced_mail_addresses/index.html.erb b/app/views/admin/bounced_mail_addresses/index.html.erb new file mode 100644 index 000000000..913cbd19d --- /dev/null +++ b/app/views/admin/bounced_mail_addresses/index.html.erb @@ -0,0 +1,36 @@ +

Bounced Mail Addresses

+ +
+
+
+
+ + + + + + + + + + + + + + <% @bounced_mail_addresses.each do |mail_addr| %> + + + + + + + + + + <% end %> + +
EmailActionStatusDiagnosticTrackedActions
<%= mail_addr.email %><%= mail_addr.action %><%= mail_addr.status %><%= mail_addr.diagnostic %><%= mail_addr.created_at %><%= link_to 'Detailed', admin_bounced_mail_address_path(mail_addr) %><%= link_to 'Destroy', admin_bounced_mail_address_path(mail_addr), method: :delete, data: { confirm: 'Are you sure?' } %>
+
+
+
+
diff --git a/app/views/admin/bounced_mail_addresses/show.html.erb b/app/views/admin/bounced_mail_addresses/show.html.erb new file mode 100644 index 000000000..5183ae5a1 --- /dev/null +++ b/app/views/admin/bounced_mail_addresses/show.html.erb @@ -0,0 +1,27 @@ +

+ Email: + <%= @bounced_mail_address.email %> +

+ +

+ Bounced message ID: + <%= @bounced_mail_address.message_id %> +

+ +

+ Overall bounce type: + <%= @bounced_mail_address.bounce_type %> (<%= @bounced_mail_address.bounce_subtype %> ) +

+ +

+ Bounced recipient status: + <%= @bounced_mail_address.action %> (<%= @bounced_mail_address.status %>) +

+ +

+ Bounced recipient diagnostic: +

<%= @bounced_mail_address.diagnostic %>
+

+ +<%= link_to 'Back', admin_bounced_mail_addresses_path %> +<%= link_to 'Destroy', admin_bounced_mail_address_path(@bounced_mail_address), method: :delete, data: { confirm: 'Are you sure?' } %> diff --git a/config/application.yml.sample b/config/application.yml.sample index 82b45feb9..5885c47a2 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -87,6 +87,9 @@ sk_digi_doc_service_name: 'Testimine' registrant_api_base_url: registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas +# Bounces API +api_shared_key: testkey + # Base URL (inc. https://) of REST registrant portal # Leave blank to use internal registrant portal registrant_portal_verifications_base_url: '' diff --git a/config/locales/admin/menu.en.yml b/config/locales/admin/menu.en.yml index 617341c6a..cb1060e6f 100644 --- a/config/locales/admin/menu.en.yml +++ b/config/locales/admin/menu.en.yml @@ -14,6 +14,7 @@ en: blocked_domains: Blocked domains reserved_domains: Reserved domains disputed_domains: Disputed domains + bounced_email_addresses: Bounced emails epp_log: EPP log repp_log: REPP log que: Que diff --git a/config/routes.rb b/config/routes.rb index 2a2de1863..c31682a13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -91,7 +91,7 @@ Rails.application.routes.draw do end resources :auctions, only: %i[index show update], param: :uuid - + resources :bounces, only: %i[create] end match '*all', controller: 'cors', action: 'cors_preflight_check', via: [:options], @@ -322,6 +322,7 @@ Rails.application.routes.draw do resources :delayed_jobs resources :epp_logs resources :repp_logs + resources :bounced_mail_addresses, only: %i[index show destroy] authenticate :admin_user do mount Que::Web, at: 'que' diff --git a/db/migrate/20200916125326_create_bounced_mail_addresses.rb b/db/migrate/20200916125326_create_bounced_mail_addresses.rb new file mode 100644 index 000000000..e1744cc9a --- /dev/null +++ b/db/migrate/20200916125326_create_bounced_mail_addresses.rb @@ -0,0 +1,15 @@ +class CreateBouncedMailAddresses < ActiveRecord::Migration[6.0] + def change + create_table :bounced_mail_addresses do |t| + t.string :email, null: false + t.string :message_id, null: false + t.string :bounce_type, null: false + t.string :bounce_subtype, null: false + t.string :action, null: false + t.string :status, null: false + t.string :diagnostic, null: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 6a30fbc84..acf134a55 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -475,6 +475,43 @@ CREATE SEQUENCE public.blocked_domains_id_seq ALTER SEQUENCE public.blocked_domains_id_seq OWNED BY public.blocked_domains.id; +-- +-- Name: bounced_mail_addresses; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE public.bounced_mail_addresses ( + id bigint NOT NULL, + email character varying NOT NULL, + message_id character varying NOT NULL, + bounce_type character varying NOT NULL, + bounce_subtype character varying NOT NULL, + action character varying NOT NULL, + status character varying NOT NULL, + diagnostic character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: bounced_mail_addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.bounced_mail_addresses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: bounced_mail_addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.bounced_mail_addresses_id_seq OWNED BY public.bounced_mail_addresses.id; + + -- -- Name: certificates; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -2658,6 +2695,13 @@ ALTER TABLE ONLY public.bank_transactions ALTER COLUMN id SET DEFAULT nextval('p ALTER TABLE ONLY public.blocked_domains ALTER COLUMN id SET DEFAULT nextval('public.blocked_domains_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.bounced_mail_addresses ALTER COLUMN id SET DEFAULT nextval('public.bounced_mail_addresses_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -2876,14 +2920,14 @@ ALTER TABLE ONLY public.log_payment_orders ALTER COLUMN id SET DEFAULT nextval(' -- --- Name: log_prices id; Type: DEFAULT; Schema: public; Owner: - +-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_prices ALTER COLUMN id SET DEFAULT nextval('public.log_prices_id_seq'::regclass); -- --- Name: log_registrant_verifications id; Type: DEFAULT; Schema: public; Owner: - +-- Name: id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_registrant_verifications ALTER COLUMN id SET DEFAULT nextval('public.log_registrant_verifications_id_seq'::regclass); @@ -3100,6 +3144,14 @@ ALTER TABLE ONLY public.blocked_domains ADD CONSTRAINT blocked_domains_pkey PRIMARY KEY (id); +-- +-- Name: bounced_mail_addresses_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY public.bounced_mail_addresses + ADD CONSTRAINT bounced_mail_addresses_pkey PRIMARY KEY (id); + + -- -- Name: certificates_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -3349,7 +3401,7 @@ ALTER TABLE ONLY public.log_payment_orders -- --- Name: log_prices log_prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: log_prices_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_prices @@ -3357,7 +3409,7 @@ ALTER TABLE ONLY public.log_prices -- --- Name: log_registrant_verifications log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: log_registrant_verifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.log_registrant_verifications @@ -4906,5 +4958,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200902131603'), ('20200908131554'), ('20200910085157'), -('20200910102028'); +('20200910102028'), +('20200916125326'); + diff --git a/lib/serializers/registrant_api/contact.rb b/lib/serializers/registrant_api/contact.rb index dd36b4400..023544174 100644 --- a/lib/serializers/registrant_api/contact.rb +++ b/lib/serializers/registrant_api/contact.rb @@ -1,14 +1,15 @@ module Serializers module RegistrantApi class Contact - attr_reader :contact + attr_reader :contact, :links - def initialize(contact) + def initialize(contact, links) @contact = contact + @links = links end - def to_json - { + def to_json(_obj = nil) + obj = { id: contact.uuid, name: contact.name, code: contact.code, @@ -31,6 +32,10 @@ module Serializers statuses: contact.statuses, disclosed_attributes: contact.disclosed_attributes, } + + obj[:links] = contact.related_domains if @links + + obj end end end diff --git a/lib/serializers/registrant_api/domain.rb b/lib/serializers/registrant_api/domain.rb index 542f2d0de..2359d77c9 100644 --- a/lib/serializers/registrant_api/domain.rb +++ b/lib/serializers/registrant_api/domain.rb @@ -3,11 +3,14 @@ module Serializers class Domain attr_reader :domain - def initialize(domain) + def initialize(domain, simplify: false) @domain = domain + @simplify = simplify end - def to_json + def to_json(_obj = nil) + return simple_object if @simplify + { id: domain.uuid, name: domain.name, @@ -49,6 +52,17 @@ module Serializers private + def simple_object + { + id: domain.uuid, name: domain.name, registered_at: domain.registered_at, + valid_to: domain.valid_to, outzone_at: domain.outzone_at, statuses: domain.statuses, + registrant_verification_asked_at: domain.registrant_verification_asked_at, + registrar: { name: domain.registrar.name, website: domain.registrar.website }, + registrant: { name: domain.registrant.name, id: domain.registrant.uuid, + phone: domain.registrant.phone, email: domain.registrant.email } + } + end + def dnssec_keys domain.dnskeys.map do |key| "#{key.flags} #{key.protocol} #{key.alg} #{key.public_key}" diff --git a/test/fixtures/bounced_mail_addresses.yml b/test/fixtures/bounced_mail_addresses.yml new file mode 100644 index 000000000..cbfdff680 --- /dev/null +++ b/test/fixtures/bounced_mail_addresses.yml @@ -0,0 +1,10 @@ +one: + email: bounced@registry.test + message_id: 010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000 + bounce_type: Permanent + bounce_subtype: General + action: failed + status: '5.1.1' + diagnostic: 'smtp; 550 5.1.1 user unknown' + created_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %> + updated_at: <%= Time.zone.parse('2010-07-05').to_s(:db) %> diff --git a/test/integration/api/registrant/registrant_api_domains_test.rb b/test/integration/api/registrant/registrant_api_domains_test.rb index 22516fecc..61d635e5f 100644 --- a/test/integration/api/registrant/registrant_api_domains_test.rb +++ b/test/integration/api/registrant/registrant_api_domains_test.rb @@ -50,10 +50,10 @@ class RegistrantApiDomainsTest < ApplicationIntegrationTest assert_equal(200, response.status) response_json = JSON.parse(response.body, symbolize_names: true) - array_of_domain_names = response_json.map { |x| x[:name] } + array_of_domain_names = response_json[:domains].map { |x| x[:name] } assert(array_of_domain_names.include?('hospital.test')) - array_of_domain_registrars = response_json.map { |x| x[:registrar] } + array_of_domain_registrars = response_json[:domains].map { |x| x[:registrar] } assert(array_of_domain_registrars.include?({name: 'Good Names', website: nil})) end @@ -63,12 +63,12 @@ class RegistrantApiDomainsTest < ApplicationIntegrationTest response_json = JSON.parse(response.body, symbolize_names: true) assert_equal(200, response.status) - assert_equal(2, response_json.count) + assert_equal(2, response_json[:domains].count) get '/api/v1/registrant/domains', headers: @auth_headers response_json = JSON.parse(response.body, symbolize_names: true) - assert_equal(4, response_json.count) + assert_equal(4, response_json[:domains].count) end def test_root_does_not_accept_limit_higher_than_200 diff --git a/test/integration/api/v1/bounces/create_test.rb b/test/integration/api/v1/bounces/create_test.rb new file mode 100644 index 000000000..899b6c5c7 --- /dev/null +++ b/test/integration/api/v1/bounces/create_test.rb @@ -0,0 +1,75 @@ +require 'test_helper' + +class BouncesApiV1CreateTest < ActionDispatch::IntegrationTest + def setup + @api_key = "Basic #{ENV['api_shared_key']}" + @headers = { "Authorization": "#{@api_key}" } + @json_body = { "data": valid_bounce_request }.as_json + end + + def test_authorizes_api_request + post api_v1_bounces_path, params: @json_body, headers: @headers + assert_response :created + + invalid_headers = { "Authorization": "Basic invalid_api_key" } + post api_v1_bounces_path, params: @json_body, headers: invalid_headers + assert_response :unauthorized + end + + def test_returns_bad_request_if_invalid_payload + invalid_json_body = @json_body.dup + invalid_json_body['data']['bounce']['bouncedRecipients'] = nil + + post api_v1_bounces_path, params: invalid_json_body, headers: @headers + assert_response :bad_request + + invalid_json_body = 'aaaa' + post api_v1_bounces_path, params: invalid_json_body, headers: @headers + assert_response :bad_request + end + + def test_saves_new_bounce_object + request_body = @json_body.dup + random_mail = "#{rand(10000..99999)}@registry.test" + request_body['data']['bounce']['bouncedRecipients'][0]['emailAddress'] = random_mail + + post api_v1_bounces_path, params: request_body, headers: @headers + assert_response :created + + bounced_mail = BouncedMailAddress.last + assert bounced_mail.email = random_mail + assert '5.1.1', bounced_mail.status + assert 'failed', bounced_mail.action + end + + def valid_bounce_request + { + "notificationType": "Bounce", + "mail": { + "source": "noreply@registry.test", + "sourceIp": "195.43.86.5", + "messageId": "010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000", + "sourceArn": "arn:aws:ses:us-east-2:65026820000:identity/noreply@registry.test", + "timestamp": "2020-09-18T10:34:44.000Z", + "destination": [ "bounced@registry.test" ], + "sendingAccountId": "650268220000" + }, + "bounce": { + "timestamp": "2020-09-18T10:34:44.911Z", + "bounceType": "Permanent", + "feedbackId": "010f0174a0c7d4f9-27d59756-6111-4d5f-xxxx-26bee0d55fa2-000000", + "remoteMtaIp": "127.0.01", + "reportingMTA": "dsn; xxx.amazonses.com", + "bounceSubType": "General", + "bouncedRecipients": [ + { + "action": "failed", + "status": "5.1.1", + "emailAddress": "bounced@registry.test", + "diagnosticCode": "smtp; 550 5.1.1 user unknown" + } + ] + } + }.as_json + end +end diff --git a/test/jobs/domain_delete_confirm_job_test.rb b/test/jobs/domain_delete_confirm_job_test.rb index b999bd3c7..cbe4b87f1 100644 --- a/test/jobs/domain_delete_confirm_job_test.rb +++ b/test/jobs/domain_delete_confirm_job_test.rb @@ -18,7 +18,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase new_registrant_email: @new_registrant.email, current_user_id: @user.id }) - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -31,7 +31,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase new_registrant_email: @new_registrant.email, current_user_id: @user.id }) - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) last_registrar_notification = @domain.registrar.notifications.last assert_equal(last_registrar_notification.attached_obj_id, @domain.id) @@ -51,7 +51,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase assert @domain.registrant_delete_confirmable?(@domain.registrant_verification_token) assert_equal @user.id, @domain.pending_json['current_user_id'] - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::CONFIRMED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::CONFIRMED) @domain.reload assert @domain.statuses.include? DomainStatus::PENDING_DELETE @@ -72,7 +72,7 @@ class DomainDeleteConfirmJobTest < ActiveSupport::TestCase assert @domain.registrant_delete_confirmable?(@domain.registrant_verification_token) assert_equal @user.id, @domain.pending_json['current_user_id'] - DomainDeleteConfirmJob.enqueue(@domain.id, RegistrantVerification::REJECTED) + DomainDeleteConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED) @domain.reload assert_equal ['ok'], @domain.statuses diff --git a/test/lib/serializers/registrant_api/contact_test.rb b/test/lib/serializers/registrant_api/contact_test.rb index 165c91e00..8b84abd38 100644 --- a/test/lib/serializers/registrant_api/contact_test.rb +++ b/test/lib/serializers/registrant_api/contact_test.rb @@ -4,7 +4,7 @@ require 'serializers/registrant_api/contact' class SerializersRegistrantApiContactTest < ActiveSupport::TestCase def setup @contact = contacts(:william) - @serializer = Serializers::RegistrantApi::Contact.new(@contact) + @serializer = Serializers::RegistrantApi::Contact.new(@contact, false) @json = @serializer.to_json end diff --git a/test/models/bounced_mail_address_test.rb b/test/models/bounced_mail_address_test.rb new file mode 100644 index 000000000..4af401711 --- /dev/null +++ b/test/models/bounced_mail_address_test.rb @@ -0,0 +1,104 @@ +require 'test_helper' + +class BouncedMailAddressTest < ActiveSupport::TestCase + include ActionMailer::TestHelper + + def setup + @bounced_mail = BouncedMailAddress.new + @bounced_mail.email = 'recipient@registry.test' + @bounced_mail.message_id = '010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000' + @bounced_mail.bounce_type = 'Permanent' + @bounced_mail.bounce_subtype = 'General' + @bounced_mail.action = 'failed' + @bounced_mail.status = '5.1.1' + @bounced_mail.diagnostic = 'smtp; 550 5.1.1 user unknown' + end + + def test_email_is_required + assert @bounced_mail.valid? + @bounced_mail.email = nil + assert @bounced_mail.invalid? + end + + def test_message_id_is_required + assert @bounced_mail.valid? + @bounced_mail.message_id = nil + assert @bounced_mail.invalid? + end + + def test_bounce_type_is_required + assert @bounced_mail.valid? + @bounced_mail.bounce_type = nil + assert @bounced_mail.invalid? + end + + def test_bounce_subtype_is_required + assert @bounced_mail.valid? + @bounced_mail.bounce_subtype = nil + assert @bounced_mail.invalid? + end + + def test_action_is_required + assert @bounced_mail.valid? + @bounced_mail.action = nil + assert @bounced_mail.invalid? + end + + def test_status_is_required + assert @bounced_mail.valid? + @bounced_mail.status = nil + assert @bounced_mail.invalid? + end + + def test_diagnostic_is_not_required + assert @bounced_mail.valid? + @bounced_mail.diagnostic = nil + assert @bounced_mail.valid? + end + + def test_bounce_reason_is_determined_dynamically + assert @bounced_mail.valid? + assert_equal 'failed (5.1.1 smtp; 550 5.1.1 user unknown)', @bounced_mail.bounce_reason + end + + def test_creates_objects_from_sns_json + BouncedMailAddress.record(sns_bounce_payload) + + bounced_mail = BouncedMailAddress.last + assert_equal domains(:shop).registrant.email, bounced_mail.email + assert_equal 'failed', bounced_mail.action + assert_equal '5.1.1', bounced_mail.status + assert_equal 'smtp; 550 5.1.1 user unknown', bounced_mail.diagnostic + end + + def sns_bounce_payload + { + "notificationType": "Bounce", + "mail": { + "source": "noreply@registry.test", + "sourceIp": "195.43.86.5", + "messageId": "010f0174a0c7d348-ea6e2fc1-0854-4073-b71f-5cecf9b0d0b2-000000", + "sourceArn": "arn:aws:ses:us-east-2:65026820000:identity/noreply@registry.test", + "timestamp": "2020-09-18T10:34:44.000Z", + "destination": [ "#{domains(:shop).registrant.email}" ], + "sendingAccountId": "650268220000" + }, + "bounce": { + "timestamp": "2020-09-18T10:34:44.911Z", + "bounceType": "Permanent", + "feedbackId": "010f0174a0c7d4f9-27d59756-6111-4d5f-xxxx-26bee0d55fa2-000000", + "remoteMtaIp": "127.0.01", + "reportingMTA": "dsn; xxx.amazonses.com", + "bounceSubType": "General", + "bouncedRecipients": [ + { + "action": "failed", + "status": "5.1.1", + "emailAddress": "#{domains(:shop).registrant.email}", + "diagnosticCode": "smtp; 550 5.1.1 user unknown" + } + ] + } + }.as_json + end +end diff --git a/test/system/admin_area/bounced_mail_addresses_test.rb b/test/system/admin_area/bounced_mail_addresses_test.rb new file mode 100644 index 000000000..36b81f0f8 --- /dev/null +++ b/test/system/admin_area/bounced_mail_addresses_test.rb @@ -0,0 +1,40 @@ +require 'application_system_test_case' + +class AdminBouncedMailAddressesTest < ApplicationSystemTestCase + include ActionView::Helpers::NumberHelper + + def setup + @bounced_mail = bounced_mail_addresses(:one) + @original_default_language = Setting.default_language + sign_in users(:admin) + end + + def teardown + Setting.default_language = @original_default_language + end + + def test_shows_bounced_emails + visit admin_bounced_mail_addresses_path + assert_text @bounced_mail.status + assert_text @bounced_mail.action + assert_text @bounced_mail.diagnostic + assert_text @bounced_mail.email + end + + def test_shows_detailed_bounced_email + visit admin_bounced_mail_address_path(@bounced_mail) + assert_text @bounced_mail.status + assert_text @bounced_mail.action + assert_text @bounced_mail.diagnostic + assert_text @bounced_mail.email + + assert_text @bounced_mail.message_id + end + + def test_deletes_registrar + visit admin_bounced_mail_address_path(@bounced_mail) + click_on 'Destroy' + + assert_text 'Bounced mail address was successfully destroyed.' + end +end diff --git a/test/system/registrant_area/domains/domain_delete_confirms_test.rb b/test/system/registrant_area/domains/domain_delete_confirms_test.rb index 0eb61ada8..765cd0149 100644 --- a/test/system/registrant_area/domains/domain_delete_confirms_test.rb +++ b/test/system/registrant_area/domains/domain_delete_confirms_test.rb @@ -1,6 +1,7 @@ require 'application_system_test_case' class DomainDeleteConfirmsTest < ApplicationSystemTestCase + include ActionMailer::TestHelper setup do @user = users(:registrant) sign_in @user @@ -13,7 +14,9 @@ class DomainDeleteConfirmsTest < ApplicationSystemTestCase def test_enqueues_approve_job_after_verification visit registrant_domain_delete_confirm_url(@domain.id, token: @domain.registrant_verification_token) - click_on 'Confirm domain delete' + perform_enqueued_jobs do + click_on 'Confirm domain delete' + end assert_text 'Domain registrant change has successfully received.' @domain.reload @@ -23,7 +26,9 @@ class DomainDeleteConfirmsTest < ApplicationSystemTestCase def test_enqueues_reject_job_after_verification visit registrant_domain_delete_confirm_url(@domain.id, token: @domain.registrant_verification_token) - click_on 'Reject domain delete' + perform_enqueued_jobs do + click_on 'Reject domain delete' + end assert_text 'Domain registrant change has been rejected successfully.' @domain.reload