diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58ae259b2..44329c4d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,18 @@
+27.11.2020
+* Refactored delete confirmation for interactors [#1753](https://github.com/internetee/registry/issues/1753)
+
+24.11.2020
+* Added subnet support for list of allowed IPs [#983](https://github.com/internetee/registry/issues/983)
+* Added contact endpoint to Restful EPP API [#1580](https://github.com/internetee/registry/issues/1580)
+
+20.11.2020
+* Registrant confirmation over Registrant API [#1742](https://github.com/internetee/registry/pull/1742)
+* Refactored forceDelete cancellation for interactors [#1743](https://github.com/internetee/registry/issues/1743)
+
+19.11.2020
+* Only sponsoring registrar has access to private contact's details [#1745](https://github.com/internetee/registry/issues/1745)
+* Refactor ForceDelete [#1740](https://github.com/internetee/registry/issues/1740)
+
13.11.2020
* Fixed per registrar epp session limit [#729](https://github.com/internetee/registry/issues/729)
* Correct error code is returned on reaching session limit [#587](https://github.com/internetee/registry/issues/587)
diff --git a/Gemfile b/Gemfile
index 25c3eafff..7ae8d4ef6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,7 @@
source 'https://rubygems.org'
# core
+gem 'active_interaction', '~> 3.8'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'iso8601', '0.12.1' # for dates and times
gem 'rails', '~> 6.0'
@@ -35,8 +36,6 @@ gem 'select2-rails', '3.5.9.3' # for autocomplete
gem 'cancancan'
gem 'devise', '~> 4.7'
-gem 'grape'
-
# registry specfic
gem 'data_migrate', '~> 6.1'
gem 'isikukood' # for EE-id validation
diff --git a/Gemfile.lock b/Gemfile.lock
index c628257a2..72c716aae 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -112,6 +112,8 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
+ active_interaction (3.8.3)
+ activemodel (>= 4, < 7)
activejob (6.0.3.3)
activesupport (= 6.0.3.3)
globalid (>= 0.3.6)
@@ -196,28 +198,6 @@ GEM
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dry-configurable (0.11.6)
- concurrent-ruby (~> 1.0)
- dry-core (~> 0.4, >= 0.4.7)
- dry-equalizer (~> 0.2)
- dry-container (0.7.2)
- concurrent-ruby (~> 1.0)
- dry-configurable (~> 0.1, >= 0.1.3)
- dry-core (0.4.9)
- concurrent-ruby (~> 1.0)
- dry-equalizer (0.3.0)
- dry-inflector (0.2.0)
- dry-logic (1.0.7)
- concurrent-ruby (~> 1.0)
- dry-core (~> 0.2)
- dry-equalizer (~> 0.2)
- dry-types (1.4.0)
- concurrent-ruby (~> 1.0)
- dry-container (~> 0.3)
- dry-core (~> 0.4, >= 0.4.4)
- dry-equalizer (~> 0.3)
- dry-inflector (~> 0.1, >= 0.1.2)
- dry-logic (~> 1.0, >= 1.0.2)
erubi (1.9.0)
erubis (2.7.0)
execjs (2.7.0)
@@ -226,13 +206,6 @@ GEM
thor (~> 0.14)
globalid (0.4.2)
activesupport (>= 4.2.0)
- grape (1.4.0)
- activesupport
- builder
- dry-types (>= 1.1)
- mustermann-grape (~> 1.0.0)
- rack (>= 1.3.0)
- rack-accept
gyoku (1.3.1)
builder (>= 2.1.2)
haml (5.1.2)
@@ -312,8 +285,6 @@ GEM
multi_json (1.15.0)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
- mustermann-grape (1.0.1)
- mustermann (>= 1.0.0)
netrc (0.11.0)
nio4r (2.5.4)
nokogiri (1.10.10)
@@ -357,8 +328,6 @@ GEM
que (~> 0.8)
sinatra
rack (2.2.3)
- rack-accept (0.4.5)
- rack (>= 0.4)
rack-oauth2 (1.16.0)
activesupport
attr_required
@@ -521,6 +490,7 @@ PLATFORMS
ruby
DEPENDENCIES
+ active_interaction (~> 3.8)
activerecord-import
airbrake
bootsnap (>= 1.1.0)
@@ -542,7 +512,6 @@ DEPENDENCIES
epp!
epp-xml (= 1.1.0)!
figaro (= 1.1.1)
- grape
haml (~> 5.0)
isikukood
iso8601 (= 0.12.1)
diff --git a/app/api/repp/account_v1.rb b/app/api/repp/account_v1.rb
deleted file mode 100644
index fe3acd387..000000000
--- a/app/api/repp/account_v1.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module Repp
- class AccountV1 < Grape::API
- version 'v1', using: :path
-
- resource :accounts do
- desc 'Return current cash account balance'
-
- get 'balance' do
- @response = {
- balance: current_user.registrar.cash_account.balance,
- currency: current_user.registrar.cash_account.currency
- }
- end
- end
- end
-end
diff --git a/app/api/repp/api.rb b/app/api/repp/api.rb
deleted file mode 100644
index af6864cfa..000000000
--- a/app/api/repp/api.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module Repp
- class API < Grape::API
- format :json
- prefix :repp
-
- http_basic do |username, password|
- @current_user ||= ApiUser.find_by(username: username, plain_text_password: password)
- if @current_user
- true
- else
- error! I18n.t('api_user_not_found'), 401
- end
- end
-
- before do
- webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip)
- unless webclient_request
- error! I18n.t('api.authorization.ip_not_allowed', ip: request.ip), 401 unless @current_user.registrar.api_ip_white?(request.ip)
- end
-
- if @current_user.cannot?(:view, :repp)
- error! I18n.t('no_permission'), 401 unless @current_user.registrar.api_ip_white?(request.ip)
- end
-
- next if Rails.env.test? || Rails.env.development?
- message = 'Certificate mismatch! Cert common name should be:'
- request_name = env['HTTP_SSL_CLIENT_S_DN_CN']
-
- if webclient_request
- webclient_cert_name = ENV['webclient_cert_common_name'] || 'webclient'
- error! "Webclient #{message} #{webclient_cert_name}", 401 if webclient_cert_name != request_name
- else
- unless @current_user.pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'],
- request.env['HTTP_SSL_CLIENT_S_DN_CN'])
- error! "#{message} #{@current_user.username}", 401
- end
- end
- end
-
- helpers do
- attr_reader :current_user
- end
-
- after do
- ApiLog::ReppLog.create({
- request_path: request.path,
- request_method: request.request_method,
- request_params: request.params.except('route_info').to_json,
- response: @response.to_json,
- response_code: status,
- api_user_name: current_user.try(:username),
- api_user_registrar: current_user.try(:registrar).try(:to_s),
- ip: request.ip,
- uuid: request.try(:uuid)
- })
- end
-
- mount Repp::DomainV1
- mount Repp::ContactV1
- mount Repp::AccountV1
- mount Repp::DomainTransfersV1
- mount Repp::NameserversV1
- mount Repp::DomainContactsV1
- end
-end
diff --git a/app/api/repp/contact_v1.rb b/app/api/repp/contact_v1.rb
deleted file mode 100644
index 810829ef7..000000000
--- a/app/api/repp/contact_v1.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Repp
- class ContactV1 < Grape::API
- version 'v1', using: :path
-
- resource :contacts do
- desc 'Return list of contact'
- params do
- optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many contacts to show'
- optional :offset, type: Integer, desc: 'Contact number to start at'
- optional :details, type: String, values: %w(true false), desc: 'Whether to include details'
- end
-
- get '/' do
- limit = params[:limit] || 200
- offset = params[:offset] || 0
-
- if params[:details] == 'true'
- contacts = current_user.registrar.contacts.limit(limit).offset(offset)
-
- unless Contact.address_processing?
- attributes = Contact.attribute_names - Contact.address_attribute_names
- contacts = contacts.select(attributes)
- end
- else
- contacts = current_user.registrar.contacts.limit(limit).offset(offset).pluck(:code)
- end
-
- @response = {
- contacts: contacts,
- total_number_of_records: current_user.registrar.contacts.count
- }
- end
- end
- end
-end
diff --git a/app/api/repp/domain_contacts_v1.rb b/app/api/repp/domain_contacts_v1.rb
deleted file mode 100644
index 7f3e323ac..000000000
--- a/app/api/repp/domain_contacts_v1.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Repp
- class DomainContactsV1 < Grape::API
- version 'v1', using: :path
-
- resource :domains do
- resource :contacts do
- patch '/' do
- current_contact = current_user.registrar.contacts
- .find_by(code: params[:current_contact_id])
- new_contact = current_user.registrar.contacts.find_by(code: params[:new_contact_id])
-
- unless current_contact
- error!({ error: { type: 'invalid_request_error',
- param: 'current_contact_id',
- message: "No such contact: #{params[:current_contact_id]}"} },
- :bad_request)
- end
-
- unless new_contact
- error!({ error: { type: 'invalid_request_error',
- param: 'new_contact_id',
- message: "No such contact: #{params[:new_contact_id]}" } },
- :bad_request)
- end
-
- if new_contact.invalid?
- error!({ error: { type: 'invalid_request_error',
- param: 'new_contact_id',
- message: 'New contact must be valid' } },
- :bad_request)
- end
-
- if current_contact == new_contact
- error!({ error: { type: 'invalid_request_error',
- message: 'New contact ID must be different from current' \
- ' contact ID' } },
- :bad_request)
- end
-
- affected_domains, skipped_domains = TechDomainContact
- .replace(current_contact, new_contact)
- @response = { affected_domains: affected_domains, skipped_domains: skipped_domains }
- end
- end
- end
- end
-end
diff --git a/app/api/repp/domain_transfers_v1.rb b/app/api/repp/domain_transfers_v1.rb
deleted file mode 100644
index c6a48df6d..000000000
--- a/app/api/repp/domain_transfers_v1.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module Repp
- class DomainTransfersV1 < Grape::API
- version 'v1', using: :path
-
- resource :domain_transfers do
- post '/' do
- params do
- requires :data, type: Hash do
- requires :domainTransfers, type: Array do
- requires :domainName, type: String, allow_blank: false
- requires :transferCode, type: String, allow_blank: false
- end
- end
- end
-
- new_registrar = current_user.registrar
- domain_transfers = params['data']['domainTransfers']
- successful_domain_transfers = []
- errors = []
-
- domain_transfers.each do |domain_transfer|
- domain_name = domain_transfer['domainName']
- transfer_code = domain_transfer['transferCode']
- domain = Domain.find_by(name: domain_name)
-
- if domain
- if domain.transfer_code == transfer_code
- DomainTransfer.request(domain, new_registrar)
- successful_domain_transfers << { type: 'domain_transfer', attributes: { domain_name: domain.name } }
- else
- errors << { title: "#{domain_name} transfer code is wrong" }
- end
- else
- errors << { title: "#{domain_name} does not exist" }
- end
- end
-
- if errors.none?
- status 200
- @response = { data: successful_domain_transfers }
- else
- status 400
- @response = { errors: errors }
- end
- end
- end
- end
-end
diff --git a/app/api/repp/domain_v1.rb b/app/api/repp/domain_v1.rb
deleted file mode 100644
index cf45bfc6f..000000000
--- a/app/api/repp/domain_v1.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module Repp
- class DomainV1 < Grape::API
- version 'v1', using: :path
-
- resource :domains do
- desc 'Return list of domains'
- params do
- optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many domains to show'
- optional :offset, type: Integer, desc: 'Domain number to start at'
- optional :details, type: String, values: %w(true false), desc: 'Whether to include details'
- end
-
- get '/' do
- limit = params[:limit] || 200
- offset = params[:offset] || 0
-
- if params[:details] == 'true'
- domains = current_user.registrar.domains.limit(limit).offset(offset)
- else
- domains = current_user.registrar.domains.limit(limit).offset(offset).pluck(:name)
- end
-
- @response = {
- domains: domains,
- total_number_of_records: current_user.registrar.domains.count
- }
- end
-
- # example: curl -u registrar1:password localhost:3000/repp/v1/domains/1/transfer_info -H "Auth-Code: authinfopw1"
- get '/:id/transfer_info', requirements: { id: /.*/ } do
- ident = params[:id]
- domain = ident.match?(/\A[0-9]+\z/) ? Domain.find_by(id: ident) : Domain.find_by_idn(ident)
-
- error! I18n.t('errors.messages.epp_domain_not_found'), 404 unless domain
- error! I18n.t('errors.messages.epp_authorization_error'), 401 unless domain.transfer_code.eql? request.headers['Auth-Code']
-
- contact_repp_json = proc{|contact|
- contact.as_json.slice("code", "name", "ident", "ident_type", "ident_country_code", "phone", "email", "street", "city", "zip","country_code", "statuses")
- }
-
- @response = {
- domain: domain.name,
- registrant: contact_repp_json.call(domain.registrant),
- admin_contacts: domain.admin_contacts.map{|e| contact_repp_json.call(e)},
- tech_contacts: domain.tech_contacts.map{|e| contact_repp_json.call(e)}
- }
- end
- end
- end
-end
diff --git a/app/api/repp/nameservers_v1.rb b/app/api/repp/nameservers_v1.rb
deleted file mode 100644
index 56a893880..000000000
--- a/app/api/repp/nameservers_v1.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-module Repp
- class NameserversV1 < Grape::API
- version 'v1', using: :path
-
- resource 'registrar/nameservers' do
- put '/' do
- params do
- requires :data, type: Hash, allow_blank: false do
- requires :type, type: String, allow_blank: false
- requires :id, type: String, allow_blank: false
- optional :domains, type: Array
- requires :attributes, type: Hash, allow_blank: false do
- requires :hostname, type: String, allow_blank: false
- requires :ipv4, type: Array
- requires :ipv6, type: Array
- end
- end
- end
-
- hostname = params[:data][:id]
-
- unless current_user.registrar.nameservers.exists?(hostname: hostname)
- error!({ errors: [{ title: "Hostname #{hostname} does not exist" }] }, 404)
- end
-
- new_attributes = {
- hostname: params[:data][:attributes][:hostname],
- ipv4: params[:data][:attributes][:ipv4],
- ipv6: params[:data][:attributes][:ipv6],
- }
-
- domains = params[:data][:domains] || []
-
- begin
- affected_domains = current_user.registrar.replace_nameservers(hostname, new_attributes,
- domains: domains)
- rescue ActiveRecord::RecordInvalid => e
- error!({ errors: e.record.errors.full_messages.map { |error| { title: error } } }, 400)
- end
-
- status 200
- @response = { data: { type: 'nameserver',
- id: params[:data][:attributes][:hostname],
- attributes: params[:data][:attributes] },
- affected_domains: affected_domains }
- end
- end
- end
-end
diff --git a/app/controllers/admin/domains/force_delete_controller.rb b/app/controllers/admin/domains/force_delete_controller.rb
index 6a111926f..4fe85fa3b 100644
--- a/app/controllers/admin/domains/force_delete_controller.rb
+++ b/app/controllers/admin/domains/force_delete_controller.rb
@@ -4,26 +4,15 @@ module Admin
def create
authorize! :manage, domain
+ notice = t('.scheduled')
+
domain.transaction do
- domain.schedule_force_delete(type: force_delete_type)
- domain.registrar.notifications.create!(text: t('force_delete_set_on_domain',
- domain_name: domain.name,
- outzone_date: domain.outzone_date,
- purge_date: domain.purge_date))
-
- notify_by_email if notify_by_email?
+ result = domain.schedule_force_delete(type: force_delete_type,
+ notify_by_email: notify_by_email?)
+ notice = result.errors.messages[:domain].first unless result.valid?
end
- redirect_to edit_admin_domain_url(domain), notice: t('.scheduled')
- end
-
- def notify_by_email
- if force_delete_type == :fast_track
- send_email
- domain.update(contact_notification_sent_date: Time.zone.today)
- else
- domain.update(template_name: domain.notification_template)
- end
+ redirect_to edit_admin_domain_url(domain), notice: notice
end
def destroy
@@ -42,13 +31,6 @@ module Admin
ActiveRecord::Type::Boolean.new.cast(params[:notify_by_email])
end
- def send_email
- DomainDeleteMailer.forced(domain: domain,
- registrar: domain.registrar,
- registrant: domain.registrant,
- template_name: domain.notification_template).deliver_now
- end
-
def force_delete_type
soft_delete? ? :soft : :fast_track
end
diff --git a/app/controllers/api/v1/registrant/confirms_controller.rb b/app/controllers/api/v1/registrant/confirms_controller.rb
new file mode 100644
index 000000000..057400c8e
--- /dev/null
+++ b/app/controllers/api/v1/registrant/confirms_controller.rb
@@ -0,0 +1,119 @@
+require 'serializers/registrant_api/domain'
+
+module Api
+ module V1
+ module Registrant
+ class ConfirmsController < ::Api::V1::Registrant::BaseController
+ skip_before_action :authenticate, :set_paper_trail_whodunnit
+ before_action :set_domain, only: %i[index update]
+ before_action :verify_action, only: %i[index update]
+ before_action :verify_decision, only: %i[update]
+
+ def index
+ res = {
+ domain_name: @domain.name,
+ current_registrant: serialized_registrant(@domain.registrant),
+ }
+
+ unless delete_action?
+ res[:new_registrant] = serialized_registrant(@domain.pending_registrant)
+ end
+
+ render json: res, status: :ok
+ end
+
+ def update
+ verification = RegistrantVerification.new(domain_id: @domain.id,
+ verification_token: verify_params[:token])
+
+ unless delete_action? ? delete_action(verification) : change_action(verification)
+ head :bad_request
+ return
+ end
+
+ render json: { domain_name: @domain.name,
+ current_registrant: serialized_registrant(current_registrant),
+ status: params[:decision] }, status: :ok
+ end
+
+ private
+
+ def initiator
+ "email link, #{I18n.t(:user_not_authenticated)}"
+ end
+
+ def current_registrant
+ confirmed? && !delete_action? ? @domain.pending_registrant : @domain.registrant
+ end
+
+ def confirmed?
+ verify_params[:decision] == 'confirmed'
+ end
+
+ def change_action(verification)
+ if confirmed?
+ verification.domain_registrant_change_confirm!(initiator)
+ else
+ verification.domain_registrant_change_reject!(initiator)
+ end
+ end
+
+ def delete_action(verification)
+ if confirmed?
+ verification.domain_registrant_delete_confirm!(initiator)
+ else
+ verification.domain_registrant_delete_reject!(initiator)
+ end
+ end
+
+ def serialized_registrant(registrant)
+ {
+ name: registrant.try(:name),
+ ident: registrant.try(:ident),
+ country: registrant.try(:ident_country_code),
+ }
+ end
+
+ def verify_params
+ params do |p|
+ p.require(:name)
+ p.require(:token)
+ p.permit(:decision)
+ end
+ end
+
+ def delete_action?
+ return true if params[:template] == 'delete'
+
+ false
+ end
+
+ def verify_decision
+ return if %w[confirmed rejected].include?(params[:decision])
+
+ head :not_found
+ end
+
+ def set_domain
+ @domain = Domain.find_by(name: verify_params[:name])
+ @domain ||= Domain.find_by(name_puny: verify_params[:name])
+ return if @domain
+
+ render json: { error: 'Domain not found' }, status: :not_found
+ end
+
+ def verify_action
+ action = if params[:template] == 'change'
+ @domain.registrant_update_confirmable?(verify_params[:token])
+ elsif params[:template] == 'delete'
+ @domain.registrant_delete_confirmable?(verify_params[:token])
+ end
+
+ return if action
+
+ render json: { error: 'Application expired or not found' }, status: :unauthorized
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/epp/contacts_controller.rb b/app/controllers/epp/contacts_controller.rb
index df9755af6..85305213b 100644
--- a/app/controllers/epp/contacts_controller.rb
+++ b/app/controllers/epp/contacts_controller.rb
@@ -1,10 +1,9 @@
require 'deserializers/xml/contact_update'
-
+require 'deserializers/xml/contact_create'
module Epp
class ContactsController < BaseController
before_action :find_contact, only: [:info, :update, :delete]
before_action :find_password, only: [:info, :update, :delete]
- helper_method :address_processing?
def info
authorize! :info, @contact, @password
@@ -21,25 +20,13 @@ module Epp
def create
authorize! :create, Epp::Contact
- frame = params[:parsed_frame]
- @contact = Epp::Contact.new(frame, current_user.registrar)
- @contact.add_legal_file_to_new(frame)
- @contact.generate_code
+ @contact = Epp::Contact.new(params[:parsed_frame], current_user.registrar)
+ collected_data = ::Deserializers::Xml::ContactCreate.new(params[:parsed_frame])
+ action = Actions::ContactCreate.new(@contact, collected_data.legal_document,
+ collected_data.ident)
- if @contact.save
- if !address_processing? && address_given?
- @response_code = 1100
- @response_description = t('epp.contacts.completed_without_address')
- else
- @response_code = 1000
- @response_description = t('epp.contacts.completed')
- end
-
- render_epp_response '/epp/contacts/save'
- else
- handle_errors(@contact)
- end
+ action_call_response(action: action)
end
def update
@@ -52,29 +39,18 @@ module Epp
collected_data.ident,
current_user)
- if action.call
- if !address_processing? && address_given?
- @response_code = 1100
- @response_description = t('epp.contacts.completed_without_address')
- else
- @response_code = 1000
- @response_description = t('epp.contacts.completed')
- end
-
- render_epp_response 'epp/contacts/save'
- else
- handle_errors(@contact)
- end
+ action_call_response(action: action)
end
def delete
authorize! :delete, @contact, @password
-
- if @contact.destroy_and_clean(params[:parsed_frame])
- render_epp_response '/epp/contacts/delete'
- else
+ action = Actions::ContactDelete.new(@contact, params[:legal_document])
+ unless action.call
handle_errors(@contact)
+ return
end
+
+ render_epp_response '/epp/contacts/delete'
end
def renew
@@ -91,6 +67,26 @@ module Epp
private
+ def opt_addr?
+ !Contact.address_processing? && address_given?
+ end
+
+ def action_call_response(action:)
+ # rubocop:disable Style/AndOr
+ (handle_errors(@contact) and return) unless action.call
+ # rubocop:enable Style/AndOr
+
+ if opt_addr?
+ @response_code = 1100
+ @response_description = t('epp.contacts.completed_without_address')
+ else
+ @response_code = 1000
+ @response_description = t('epp.contacts.completed')
+ end
+
+ render_epp_response('epp/contacts/save')
+ end
+
def find_password
@password = params[:parsed_frame].css('authInfo pw').text
end
@@ -129,8 +125,7 @@ module Epp
'postalInfo > addr > cc',
]
- required_attributes.concat(address_attributes) if address_processing?
-
+ required_attributes.concat(address_attributes) if Contact.address_processing?
requires(*required_attributes)
ident = params[:parsed_frame].css('ident')
@@ -206,9 +201,5 @@ module Epp
def address_given?
params[:parsed_frame].css('postalInfo addr').size != 0
end
-
- def address_processing?
- Contact.address_processing?
- end
end
end
diff --git a/app/controllers/registrar/domain_transfers_controller.rb b/app/controllers/registrar/domain_transfers_controller.rb
index acacc3ef4..ca08c73cf 100644
--- a/app/controllers/registrar/domain_transfers_controller.rb
+++ b/app/controllers/registrar/domain_transfers_controller.rb
@@ -15,12 +15,12 @@ class Registrar
csv.each do |row|
domain_name = row['Domain']
transfer_code = row['Transfer code']
- domain_transfers << { 'domainName' => domain_name, 'transferCode' => transfer_code }
+ domain_transfers << { 'domain_name' => domain_name, 'transfer_code' => transfer_code }
end
- uri = URI.parse("#{ENV['repp_url']}domain_transfers")
+ uri = URI.parse("#{ENV['repp_url']}domains/transfer")
request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
- request.body = { data: { domainTransfers: domain_transfers } }.to_json
+ request.body = { data: { domain_transfers: domain_transfers } }.to_json
request.basic_auth(current_registrar_user.username,
current_registrar_user.plain_text_password)
diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb
new file mode 100644
index 000000000..89c14808f
--- /dev/null
+++ b/app/controllers/repp/v1/accounts_controller.rb
@@ -0,0 +1,11 @@
+module Repp
+ module V1
+ class AccountsController < BaseController
+ def balance
+ resp = { balance: current_user.registrar.cash_account.balance,
+ currency: current_user.registrar.cash_account.currency }
+ render_success(data: resp)
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/auctions_controller.rb b/app/controllers/repp/v1/auctions_controller.rb
index 4a5265d13..676dac266 100644
--- a/app/controllers/repp/v1/auctions_controller.rb
+++ b/app/controllers/repp/v1/auctions_controller.rb
@@ -3,9 +3,9 @@ module Repp
class AuctionsController < ActionController::API
def index
auctions = Auction.started
+ @response = { count: auctions.count, auctions: auctions_to_json(auctions) }
- render json: { count: auctions.count,
- auctions: auctions_to_json(auctions) }
+ render json: @response
end
private
diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb
new file mode 100644
index 000000000..ca9ef6fb5
--- /dev/null
+++ b/app/controllers/repp/v1/base_controller.rb
@@ -0,0 +1,111 @@
+module Repp
+ module V1
+ class BaseController < ActionController::API
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found_error
+ before_action :authenticate_user
+ before_action :check_ip_restriction
+ attr_reader :current_user
+
+ before_action :set_paper_trail_whodunnit
+
+ rescue_from ActionController::ParameterMissing do |exception|
+ render json: { code: 2003, message: exception }, status: :bad_request
+ end
+
+ after_action do
+ ApiLog::ReppLog.create(
+ request_path: request.path, request_method: request.request_method,
+ request_params: request.params.except('route_info').to_json, uuid: request.try(:uuid),
+ response: @response.to_json, response_code: status, ip: request.ip,
+ api_user_name: current_user.try(:username),
+ api_user_registrar: current_user.try(:registrar).try(:to_s)
+ )
+ end
+
+ private
+
+ def set_paper_trail_whodunnit
+ ::PaperTrail.request.whodunnit = current_user
+ end
+
+ def render_success(code: nil, message: nil, data: nil)
+ @response = { code: code || 1000, message: message || 'Command completed successfully',
+ data: data || {} }
+
+ render(json: @response, status: :ok)
+ end
+
+ def epp_errors
+ @epp_errors ||= []
+ end
+
+ def handle_errors(obj = nil, update: false)
+ @epp_errors ||= []
+
+ obj&.construct_epp_errors
+ @epp_errors += obj.errors[:epp_errors] if obj
+
+ format_epp_errors if update
+ @epp_errors.uniq!
+
+ render_epp_error
+ end
+
+ def format_epp_errors
+ @epp_errors.each_with_index do |error, index|
+ blocked_by_delete_prohibited?(error, index)
+ end
+ end
+
+ def blocked_by_delete_prohibited?(error, index)
+ if error[:code] == 2304 && error[:value][:val] == DomainStatus::SERVER_DELETE_PROHIBITED &&
+ error[:value][:obj] == 'status'
+
+ @epp_errors[index][:value][:val] = DomainStatus::PENDING_UPDATE
+ end
+ end
+
+ def render_epp_error(status = :bad_request, data = {})
+ @epp_errors ||= []
+ @epp_errors << { code: 2304, msg: 'Command failed' } if data != {}
+
+ @response = { code: @epp_errors[0][:code].to_i, message: @epp_errors[0][:msg], data: data }
+ render(json: @response, status: status)
+ end
+
+ def basic_token
+ pattern = /^Basic /
+ header = request.headers['Authorization']
+ header = header.gsub(pattern, '') if header&.match(pattern)
+ header.strip
+ end
+
+ def authenticate_user
+ username, password = Base64.urlsafe_decode64(basic_token).split(':')
+ @current_user ||= ApiUser.find_by(username: username, plain_text_password: password)
+
+ return if @current_user
+
+ raise(ArgumentError)
+ rescue NoMethodError, ArgumentError
+ @response = { code: 2202, message: 'Invalid authorization information' }
+ render(json: @response, status: :unauthorized)
+ end
+
+ def check_ip_restriction
+ allowed = @current_user.registrar.api_ip_white?(request.ip)
+
+ return if allowed
+
+ @response = { code: 2202,
+ message: I18n.t('registrar.authorization.ip_not_allowed', ip: request.ip) }
+ render(json: @response, status: :unauthorized)
+ end
+
+ def not_found_error
+ @response = { code: 2303, message: 'Object does not exist' }
+ render(json: @response, status: :not_found)
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb
new file mode 100644
index 000000000..acf275c47
--- /dev/null
+++ b/app/controllers/repp/v1/contacts_controller.rb
@@ -0,0 +1,137 @@
+require 'serializers/repp/contact'
+module Repp
+ module V1
+ class ContactsController < BaseController
+ before_action :find_contact, only: %i[show update destroy]
+
+ ## GET /repp/v1/contacts
+ def index
+ record_count = current_user.registrar.contacts.count
+ contacts = showable_contacts(params[:details], params[:limit] || 200,
+ params[:offset] || 0)
+ @response = { contacts: contacts, total_number_of_records: record_count }
+ render(json: @response, status: :ok)
+ end
+
+ ## GET /repp/v1/contacts/1
+ def show
+ serializer = ::Serializers::Repp::Contact.new(@contact,
+ show_address: Contact.address_processing?)
+ render_success(data: serializer.to_json)
+ end
+
+ ## GET /repp/v1/contacts/check/1
+ def check
+ contact = Epp::Contact.find_by(code: params[:id])
+ data = { contact: { id: params[:id], available: contact.nil? } }
+
+ render_success(data: data)
+ end
+
+ ## POST /repp/v1/contacts
+ def create
+ @contact = Epp::Contact.new(contact_params_with_address, current_user.registrar, epp: false)
+ action = Actions::ContactCreate.new(@contact, params[:legal_document],
+ contact_ident_params)
+
+ unless action.call
+ handle_errors(@contact)
+ return
+ end
+
+ render_success(create_update_success_body)
+ end
+
+ ## PUT /repp/v1/contacts/1
+ def update
+ action = Actions::ContactUpdate.new(@contact, contact_params_with_address(required: false),
+ params[:legal_document],
+ contact_ident_params(required: false), current_user)
+
+ unless action.call
+ handle_errors(@contact)
+ return
+ end
+
+ render_success(create_update_success_body)
+ end
+
+ def destroy
+ action = Actions::ContactDelete.new(@contact, params[:legal_document])
+ unless action.call
+ handle_errors(@contact)
+ return
+ end
+
+ render_success
+ end
+
+ def contact_addr_present?
+ return false unless contact_addr_params.key?(:addr)
+
+ contact_addr_params[:addr].keys.any?
+ end
+
+ def create_update_success_body
+ { code: opt_addr? ? 1100 : nil, data: { contact: { id: @contact.code } },
+ message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : nil }
+ end
+
+ def showable_contacts(details, limit, offset)
+ contacts = current_user.registrar.contacts.limit(limit).offset(offset)
+
+ return contacts.pluck(:code) unless details
+
+ contacts = contacts.map do |contact|
+ serializer = ::Serializers::Repp::Contact.new(contact,
+ show_address: Contact.address_processing?)
+ serializer.to_json
+ end
+
+ contacts
+ end
+
+ def opt_addr?
+ !Contact.address_processing? && contact_addr_present?
+ end
+
+ def find_contact
+ code = params[:id]
+ @contact = Epp::Contact.find_by!(code: code, registrar: current_user.registrar)
+ end
+
+ def contact_params_with_address(required: true)
+ return contact_create_params(required: required) unless contact_addr_params.key?(:addr)
+
+ addr = {}
+ contact_addr_params[:addr].each_key { |k| addr[k] = contact_addr_params[:addr][k] }
+ contact_create_params(required: required).merge(addr)
+ end
+
+ def contact_create_params(required: true)
+ params.require(:contact).require(%i[name email phone]) if required
+ params.require(:contact).permit(:name, :email, :phone, :id)
+ end
+
+ def contact_ident_params(required: true)
+ if required
+ params.require(:contact).require(:ident).require(%i[ident ident_type ident_country_code])
+ params.require(:contact).require(:ident).permit(:ident, :ident_type, :ident_country_code)
+ else
+ params.permit(contact: { ident: %i[ident ident_type ident_country_code] })
+ end
+
+ params[:contact][:ident]
+ end
+
+ def contact_addr_params
+ if Contact.address_processing?
+ params.require(:contact).require(:addr).require(%i[country_code city street zip])
+ params.require(:contact).require(:addr).permit(:country_code, :city, :street, :zip)
+ else
+ params.require(:contact).permit(addr: %i[country_code city street zip])
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/domains/contacts_controller.rb b/app/controllers/repp/v1/domains/contacts_controller.rb
new file mode 100644
index 000000000..75404e0c6
--- /dev/null
+++ b/app/controllers/repp/v1/domains/contacts_controller.rb
@@ -0,0 +1,42 @@
+module Repp
+ module V1
+ module Domains
+ class ContactsController < 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?
+
+ if @new_contact == @current_contact
+ @epp_errors << { code: 2304, msg: 'New contact must be different from current' }
+ end
+
+ return handle_errors if @epp_errors.any?
+
+ affected, skipped = TechDomainContact.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/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb
new file mode 100644
index 000000000..ba90f23f2
--- /dev/null
+++ b/app/controllers/repp/v1/domains_controller.rb
@@ -0,0 +1,94 @@
+module Repp
+ module V1
+ class DomainsController < BaseController
+ before_action :set_authorized_domain, only: [:transfer_info]
+
+ def index
+ records = current_user.registrar.domains
+ domains = records.limit(limit).offset(offset)
+ domains = domains.pluck(:name) unless index_params[:details] == 'true'
+
+ render_success(data: { domains: domains, total_number_of_records: records.count })
+ end
+
+ def transfer_info
+ contact_fields = %i[code name ident ident_type ident_country_code phone email street city
+ zip country_code statuses]
+
+ data = {
+ domain: @domain.name,
+ registrant: @domain.registrant.as_json(only: contact_fields),
+ admin_contacts: @domain.admin_contacts.map { |c| c.as_json(only: contact_fields) },
+ tech_contacts: @domain.tech_contacts.map { |c| c.as_json(only: contact_fields) },
+ }
+
+ render_success(data: data)
+ end
+
+ def transfer
+ @errors ||= []
+ @successful = []
+
+ transfer_params[:domain_transfers].each do |transfer|
+ initiate_transfer(transfer)
+ end
+
+ render_success(data: { success: @successful, failed: @errors })
+ end
+
+ def initiate_transfer(transfer)
+ domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name])
+ action = Actions::DomainTransfer.new(domain, transfer[:transfer_code],
+ current_user.registrar)
+
+ if action.call
+ @successful << { type: 'domain_transfer', domain_name: domain.name }
+ else
+ @errors << { type: 'domain_transfer', domain_name: domain.name,
+ errors: domain.errors[:epp_errors] }
+ end
+ end
+
+ private
+
+ def transfer_params
+ params.require(:data).require(:domain_transfers).each do |t|
+ t.require(:domain_name)
+ t.permit(:domain_name)
+ t.require(:transfer_code)
+ t.permit(:transfer_code)
+ end
+ params.require(:data).permit(domain_transfers: %i[domain_name transfer_code])
+ end
+
+ def transfer_info_params
+ params.require(:id)
+ params.permit(:id)
+ end
+
+ def set_authorized_domain
+ @epp_errors ||= []
+ h = {}
+ h[transfer_info_params[:id].match?(/\A[0-9]+\z/) ? :id : :name] = transfer_info_params[:id]
+ @domain = Domain.find_by!(h)
+
+ return if @domain.transfer_code.eql?(request.headers['Auth-Code'])
+
+ @epp_errors << { code: 2202, msg: I18n.t('errors.messages.epp_authorization_error') }
+ handle_errors
+ end
+
+ def limit
+ index_params[:limit] || 200
+ end
+
+ def offset
+ index_params[:offset] || 0
+ end
+
+ def index_params
+ params.permit(:limit, :offset, :details)
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/registrar/nameservers_controller.rb b/app/controllers/repp/v1/registrar/nameservers_controller.rb
new file mode 100644
index 000000000..47004d97b
--- /dev/null
+++ b/app/controllers/repp/v1/registrar/nameservers_controller.rb
@@ -0,0 +1,51 @@
+module Repp
+ module V1
+ module Registrar
+ class NameserversController < BaseController
+ before_action :verify_nameserver_existance, only: %i[update]
+
+ def update
+ domains = params[:data][:domains] || []
+ affected = current_user.registrar
+ .replace_nameservers(hostname,
+ hostname_params[:data][:attributes],
+ domains: domains)
+
+ render_success(data: data_format_for_success(affected))
+ rescue ActiveRecord::RecordInvalid => e
+ handle_errors(e.record)
+ end
+
+ private
+
+ def data_format_for_success(affected_domains)
+ {
+ type: 'nameserver',
+ id: params[:data][:attributes][:hostname],
+ attributes: params[:data][:attributes],
+ affected_domains: affected_domains,
+ }
+ end
+
+ def hostname_params
+ params.require(:data).require(%i[type id])
+ params.require(:data).require(:attributes).require([:hostname])
+
+ params.permit(data: [
+ :type, :id,
+ { domains: [],
+ attributes: [:hostname, { ipv4: [], ipv6: [] }] }
+ ])
+ end
+
+ def hostname
+ hostname_params[:data][:id]
+ end
+
+ def verify_nameserver_existance
+ current_user.registrar.nameservers.find_by!(hostname: hostname)
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/retained_domains_controller.rb b/app/controllers/repp/v1/retained_domains_controller.rb
index c1bb458e9..8edc32f5b 100644
--- a/app/controllers/repp/v1/retained_domains_controller.rb
+++ b/app/controllers/repp/v1/retained_domains_controller.rb
@@ -3,8 +3,9 @@ module Repp
class RetainedDomainsController < ActionController::API
def index
domains = RetainedDomains.new(query_params)
+ @response = { count: domains.count, domains: domains.to_jsonable }
- render json: { count: domains.count, domains: domains.to_jsonable }
+ render json: @response
end
def query_params
diff --git a/app/helpers/object_versions_helper.rb b/app/helpers/object_versions_helper.rb
index d8e00abbe..be8ef1217 100644
--- a/app/helpers/object_versions_helper.rb
+++ b/app/helpers/object_versions_helper.rb
@@ -3,7 +3,7 @@ module ObjectVersionsHelper
version.object_changes.to_h.each do |key, value|
method_name = "#{key}=".to_sym
if new_object.respond_to?(method_name)
- new_object.public_send(method_name, value.last)
+ new_object.public_send(method_name, event_value(version, value))
end
end
end
@@ -12,4 +12,10 @@ module ObjectVersionsHelper
field_names = model.column_names
version.object.to_h.select { |key, _value| field_names.include?(key) }
end
+
+ private
+
+ def event_value(version, val)
+ version.event == 'destroy' ? val.first : val.last
+ end
end
diff --git a/app/interactions/cancel_force_delete_interaction/base.rb b/app/interactions/cancel_force_delete_interaction/base.rb
new file mode 100644
index 000000000..595279d87
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/base.rb
@@ -0,0 +1,7 @@
+module CancelForceDeleteInteraction
+ class Base < ActiveInteraction::Base
+ object :domain,
+ class: Domain,
+ description: 'Domain to cancel ForceDelete on'
+ end
+end
diff --git a/app/interactions/cancel_force_delete_interaction/cancel_force_delete.rb b/app/interactions/cancel_force_delete_interaction/cancel_force_delete.rb
new file mode 100644
index 000000000..2f45bf0e4
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/cancel_force_delete.rb
@@ -0,0 +1,10 @@
+module CancelForceDeleteInteraction
+ class CancelForceDelete < Base
+ def execute
+ compose(RemoveForceDeleteStatuses, inputs)
+ compose(RestoreStatusesBeforeForceDelete, inputs)
+ compose(ClearForceDeleteData, inputs)
+ compose(NotifyRegistrar, inputs)
+ end
+ end
+end
diff --git a/app/interactions/cancel_force_delete_interaction/clear_force_delete_data.rb b/app/interactions/cancel_force_delete_interaction/clear_force_delete_data.rb
new file mode 100644
index 000000000..ccc0714f7
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/clear_force_delete_data.rb
@@ -0,0 +1,10 @@
+module CancelForceDeleteInteraction
+ class ClearForceDeleteData < Base
+ def execute
+ domain.force_delete_data = nil
+ domain.force_delete_date = nil
+ domain.force_delete_start = nil
+ domain.save(validate: false)
+ end
+ end
+end
diff --git a/app/interactions/cancel_force_delete_interaction/notify_registrar.rb b/app/interactions/cancel_force_delete_interaction/notify_registrar.rb
new file mode 100644
index 000000000..3ded4c489
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/notify_registrar.rb
@@ -0,0 +1,8 @@
+module CancelForceDeleteInteraction
+ class NotifyRegistrar < Base
+ def execute
+ domain.registrar.notifications.create!(text: I18n.t('force_delete_cancelled',
+ domain_name: domain.name))
+ end
+ end
+end
diff --git a/app/interactions/cancel_force_delete_interaction/remove_force_delete_statuses.rb b/app/interactions/cancel_force_delete_interaction/remove_force_delete_statuses.rb
new file mode 100644
index 000000000..1cc4989e5
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/remove_force_delete_statuses.rb
@@ -0,0 +1,11 @@
+module CancelForceDeleteInteraction
+ class RemoveForceDeleteStatuses < Base
+ def execute
+ domain.statuses.delete(DomainStatus::FORCE_DELETE)
+ domain.statuses.delete(DomainStatus::SERVER_RENEW_PROHIBITED)
+ domain.statuses.delete(DomainStatus::SERVER_TRANSFER_PROHIBITED)
+ domain.statuses.delete(DomainStatus::CLIENT_HOLD)
+ domain.save(validate: false)
+ end
+ end
+end
diff --git a/app/interactions/cancel_force_delete_interaction/restore_statuses_before_force_delete.rb b/app/interactions/cancel_force_delete_interaction/restore_statuses_before_force_delete.rb
new file mode 100644
index 000000000..06b6bbd18
--- /dev/null
+++ b/app/interactions/cancel_force_delete_interaction/restore_statuses_before_force_delete.rb
@@ -0,0 +1,9 @@
+module CancelForceDeleteInteraction
+ class RestoreStatusesBeforeForceDelete < Base
+ def execute
+ domain.statuses = domain.statuses_before_force_delete
+ domain.statuses_before_force_delete = nil
+ domain.save(validate: false)
+ end
+ end
+end
diff --git a/app/interactions/domain_delete_confirm_interaction/send_request.rb b/app/interactions/domain_delete_confirm_interaction/send_request.rb
new file mode 100644
index 000000000..bbf4c8d9e
--- /dev/null
+++ b/app/interactions/domain_delete_confirm_interaction/send_request.rb
@@ -0,0 +1,22 @@
+module DomainDeleteConfirmInteraction
+ class SendRequest < ActiveInteraction::Base
+ object :domain,
+ class: Domain,
+ description: 'Domain to send delete confirmation'
+
+ def execute
+ log
+ DomainDeleteMailer.confirmation_request(domain: domain,
+ registrar: domain.registrar,
+ registrant: domain.registrant).deliver_later
+ end
+
+ private
+
+ def log
+ message = "Send DomainDeleteMailer#confirm email for domain #{domain.name} (##{domain.id})" \
+ " to #{domain.registrant.email}"
+ Rails.logger.info(message)
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/base.rb b/app/interactions/force_delete_interaction/base.rb
new file mode 100644
index 000000000..4764ce8fe
--- /dev/null
+++ b/app/interactions/force_delete_interaction/base.rb
@@ -0,0 +1,16 @@
+module ForceDeleteInteraction
+ class Base < ActiveInteraction::Base
+ object :domain,
+ class: Domain,
+ description: 'Domain to set ForceDelete on'
+ symbol :type,
+ default: :fast_track,
+ description: 'Force delete type, might be :fast_track or :soft'
+ boolean :notify_by_email,
+ default: false,
+ description: 'Do we need to send email notification'
+
+ validates :type, inclusion: { in: %i[fast_track soft] }
+ end
+end
+
diff --git a/app/interactions/force_delete_interaction/check_discarded.rb b/app/interactions/force_delete_interaction/check_discarded.rb
new file mode 100644
index 000000000..e0c893294
--- /dev/null
+++ b/app/interactions/force_delete_interaction/check_discarded.rb
@@ -0,0 +1,11 @@
+module ForceDeleteInteraction
+ class CheckDiscarded < Base
+ def execute
+ return true unless domain.discarded?
+
+ message = 'Force delete procedure cannot be scheduled while a domain is discarded'
+ errors.add(:domain, message)
+ end
+ end
+end
+
diff --git a/app/interactions/force_delete_interaction/notify_by_email.rb b/app/interactions/force_delete_interaction/notify_by_email.rb
new file mode 100644
index 000000000..541df258d
--- /dev/null
+++ b/app/interactions/force_delete_interaction/notify_by_email.rb
@@ -0,0 +1,21 @@
+module ForceDeleteInteraction
+ class NotifyByEmail < Base
+ def execute
+ return unless notify_by_email
+
+ if type == :fast_track
+ send_email
+ domain.update(contact_notification_sent_date: Time.zone.today)
+ else
+ domain.update(template_name: domain.notification_template)
+ end
+ end
+
+ def send_email
+ DomainDeleteMailer.forced(domain: domain,
+ registrar: domain.registrar,
+ registrant: domain.registrant,
+ template_name: domain.notification_template).deliver_now
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/notify_registrar.rb b/app/interactions/force_delete_interaction/notify_registrar.rb
new file mode 100644
index 000000000..691e54f7e
--- /dev/null
+++ b/app/interactions/force_delete_interaction/notify_registrar.rb
@@ -0,0 +1,10 @@
+module ForceDeleteInteraction
+ class NotifyRegistrar < Base
+ def execute
+ domain.registrar.notifications.create!(text: I18n.t('force_delete_set_on_domain',
+ domain_name: domain.name,
+ outzone_date: domain.outzone_date,
+ purge_date: domain.purge_date))
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/post_set_process.rb b/app/interactions/force_delete_interaction/post_set_process.rb
new file mode 100644
index 000000000..e51acf9b7
--- /dev/null
+++ b/app/interactions/force_delete_interaction/post_set_process.rb
@@ -0,0 +1,17 @@
+module ForceDeleteInteraction
+ class PostSetProcess < Base
+ def execute
+ statuses = domain.statuses
+ # Stop all pending actions
+ statuses.delete(DomainStatus::PENDING_UPDATE)
+ statuses.delete(DomainStatus::PENDING_TRANSFER)
+ statuses.delete(DomainStatus::PENDING_RENEW)
+ statuses.delete(DomainStatus::PENDING_CREATE)
+
+ # Allow deletion
+ statuses.delete(DomainStatus::CLIENT_DELETE_PROHIBITED)
+ statuses.delete(DomainStatus::SERVER_DELETE_PROHIBITED)
+ domain.save(validate: false)
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/prepare_domain.rb b/app/interactions/force_delete_interaction/prepare_domain.rb
new file mode 100644
index 000000000..97f364145
--- /dev/null
+++ b/app/interactions/force_delete_interaction/prepare_domain.rb
@@ -0,0 +1,13 @@
+module ForceDeleteInteraction
+ class PrepareDomain < Base
+ STATUSES_TO_SET = [DomainStatus::FORCE_DELETE,
+ DomainStatus::SERVER_RENEW_PROHIBITED,
+ DomainStatus::SERVER_TRANSFER_PROHIBITED].freeze
+
+ def execute
+ domain.statuses_before_force_delete = domain.statuses
+ domain.statuses |= STATUSES_TO_SET
+ domain.save(validate: false)
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/set_force_delete.rb b/app/interactions/force_delete_interaction/set_force_delete.rb
new file mode 100644
index 000000000..4608b5127
--- /dev/null
+++ b/app/interactions/force_delete_interaction/set_force_delete.rb
@@ -0,0 +1,12 @@
+module ForceDeleteInteraction
+ class SetForceDelete < Base
+ def execute
+ compose(CheckDiscarded, inputs)
+ compose(PrepareDomain, inputs)
+ compose(SetStatus, inputs)
+ compose(PostSetProcess, inputs)
+ compose(NotifyRegistrar, inputs)
+ compose(NotifyByEmail, inputs)
+ end
+ end
+end
diff --git a/app/interactions/force_delete_interaction/set_status.rb b/app/interactions/force_delete_interaction/set_status.rb
new file mode 100644
index 000000000..7d104aeee
--- /dev/null
+++ b/app/interactions/force_delete_interaction/set_status.rb
@@ -0,0 +1,38 @@
+module ForceDeleteInteraction
+ class SetStatus < Base
+ def execute
+ domain.force_delete_type = type
+ type == :fast_track ? force_delete_fast_track : force_delete_soft
+ domain.save(validate: false)
+ end
+
+ def force_delete_fast_track
+ domain.force_delete_date = Time.zone.today +
+ expire_warning_period_days +
+ redemption_grace_period_days
+ domain.force_delete_start = Time.zone.today + 1.day
+ end
+
+ def force_delete_soft
+ years = (domain.valid_to.to_date - Time.zone.today).to_i / 365
+ soft_forcedelete_dates(years) if years.positive?
+ end
+
+ private
+
+ def soft_forcedelete_dates(years)
+ domain.force_delete_start = domain.valid_to - years.years
+ domain.force_delete_date = domain.force_delete_start +
+ Setting.expire_warning_period.days +
+ Setting.redemption_grace_period.days
+ end
+
+ def redemption_grace_period_days
+ Setting.redemption_grace_period.days + 1.day
+ end
+
+ def expire_warning_period_days
+ Setting.expire_warning_period.days
+ end
+ end
+end
diff --git a/app/jobs/domain_delete_confirm_email_job.rb b/app/jobs/domain_delete_confirm_email_job.rb
deleted file mode 100644
index ea5a9bf49..000000000
--- a/app/jobs/domain_delete_confirm_email_job.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-class DomainDeleteConfirmEmailJob < Que::Job
- def run(domain_id)
- domain = Domain.find(domain_id)
-
- log(domain)
- DomainDeleteMailer.confirmation_request(domain: domain,
- registrar: domain.registrar,
- registrant: domain.registrant).deliver_now
- end
-
- private
-
- def log(domain)
- message = "Send DomainDeleteMailer#confirm email for domain #{domain.name} (##{domain.id})" \
- " to #{domain.registrant.email}"
- logger.info(message)
- end
-
- def logger
- Rails.logger
- end
-end
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 9269a1102..af5d59d63 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,4 +1,15 @@
class ApplicationMailer < ActionMailer::Base
append_view_path Rails.root.join('app', 'views', 'mailers')
layout 'mailer'
-end
\ No newline at end of file
+
+ def registrant_confirm_url(domain:, method:)
+ token = domain.registrant_verification_token
+ base_url = ENV['registrant_portal_verifications_base_url']
+
+ url = registrant_domain_delete_confirm_url(domain, token: token) if method == 'delete'
+ url ||= registrant_domain_update_confirm_url(domain, token: token)
+ return url if base_url.blank?
+
+ "#{base_url}/confirmation/#{domain.name_puny}/#{method}/#{token}"
+ end
+end
diff --git a/app/mailers/domain_delete_mailer.rb b/app/mailers/domain_delete_mailer.rb
index dbacab68d..3e4507194 100644
--- a/app/mailers/domain_delete_mailer.rb
+++ b/app/mailers/domain_delete_mailer.rb
@@ -2,7 +2,7 @@ class DomainDeleteMailer < ApplicationMailer
def confirmation_request(domain:, registrar:, registrant:)
@domain = DomainPresenter.new(domain: domain, view: view_context)
@registrar = RegistrarPresenter.new(registrar: registrar, view: view_context)
- @confirmation_url = confirmation_url(domain)
+ @confirmation_url = registrant_confirm_url(domain: domain, method: 'delete')
subject = default_i18n_subject(domain_name: domain.name)
mail(to: registrant.email, subject: subject)
@@ -48,10 +48,6 @@ class DomainDeleteMailer < ApplicationMailer
private
- def confirmation_url(domain)
- registrant_domain_delete_confirm_url(domain, token: domain.registrant_verification_token)
- end
-
def forced_email_from
ENV['action_mailer_force_delete_from'] || self.class.default[:from]
end
diff --git a/app/mailers/registrant_change_mailer.rb b/app/mailers/registrant_change_mailer.rb
index ff3cfa18e..8f43f4ab5 100644
--- a/app/mailers/registrant_change_mailer.rb
+++ b/app/mailers/registrant_change_mailer.rb
@@ -5,7 +5,7 @@ class RegistrantChangeMailer < ApplicationMailer
@domain = DomainPresenter.new(domain: domain, view: view_context)
@registrar = RegistrarPresenter.new(registrar: registrar, view: view_context)
@new_registrant = RegistrantPresenter.new(registrant: new_registrant, view: view_context)
- @confirmation_url = confirmation_url(domain)
+ @confirmation_url = registrant_confirm_url(domain: domain, method: 'change')
subject = default_i18n_subject(domain_name: domain.name)
mail(to: current_registrant.email, subject: subject)
@@ -49,10 +49,6 @@ class RegistrantChangeMailer < ApplicationMailer
private
- def confirmation_url(domain)
- registrant_domain_update_confirm_url(domain, token: domain.registrant_verification_token)
- end
-
def address_processing
Contact.address_processing?
end
diff --git a/app/models/actions/contact_create.rb b/app/models/actions/contact_create.rb
new file mode 100644
index 000000000..22fabc7b9
--- /dev/null
+++ b/app/models/actions/contact_create.rb
@@ -0,0 +1,81 @@
+module Actions
+ class ContactCreate
+ attr_reader :contact, :legal_document, :ident
+
+ def initialize(contact, legal_document, ident)
+ @contact = contact
+ @legal_document = legal_document
+ @ident = ident
+ end
+
+ def call
+ maybe_remove_address
+ maybe_attach_legal_doc
+ validate_ident
+ commit
+ end
+
+ def maybe_remove_address
+ return if Contact.address_processing?
+
+ contact.city = nil
+ contact.zip = nil
+ contact.street = nil
+ contact.state = nil
+ contact.country_code = nil
+ end
+
+ def validate_ident
+ validate_ident_integrity
+ validate_ident_birthday
+
+ identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type],
+ country_code: ident[:ident_country_code])
+
+ identifier.validate
+ contact.identifier = identifier
+ end
+
+ def validate_ident_integrity
+ return if ident.blank?
+
+ if ident[:ident_type].blank?
+ contact.add_epp_error('2003', nil, 'ident_type',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ elsif !%w[priv org birthday].include?(ident[:ident_type])
+ contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type')
+ @error = true
+ end
+ end
+
+ def validate_ident_birthday
+ return if ident.blank?
+ return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank?
+
+ contact.add_epp_error('2003', nil, 'ident_country_code',
+ I18n.t('errors.messages.required_ident_attribute_missing'))
+ @error = true
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ doc = LegalDocument.create(
+ documentable_type: Contact,
+ document_type: legal_document[:type], body: legal_document[:body]
+ )
+
+ contact.legal_documents = [doc]
+ contact.legal_document_id = doc.id
+ end
+
+ def commit
+ contact.id = nil # new record
+ return false if @error
+
+ contact.generate_code
+ contact.save
+ end
+ end
+end
diff --git a/app/models/actions/contact_delete.rb b/app/models/actions/contact_delete.rb
new file mode 100644
index 000000000..59032d566
--- /dev/null
+++ b/app/models/actions/contact_delete.rb
@@ -0,0 +1,41 @@
+module Actions
+ class ContactDelete
+ attr_reader :contact
+ attr_reader :new_attributes
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(contact, legal_document = nil)
+ @legal_document = legal_document
+ @contact = contact
+ end
+
+ def call
+ maybe_attach_legal_doc
+
+ if contact.linked?
+ contact.errors.add(:domains, :exist)
+ return
+ end
+
+ commit
+ end
+
+ def maybe_attach_legal_doc
+ return unless legal_document
+
+ document = contact.legal_documents.create(
+ document_type: legal_document[:type],
+ body: legal_document[:body]
+ )
+
+ contact.legal_document_id = document.id
+ contact.save
+ end
+
+ def commit
+ contact.destroy
+ end
+ end
+end
diff --git a/app/models/actions/contact_update.rb b/app/models/actions/contact_update.rb
index f8b39ecb4..7ca7b6b04 100644
--- a/app/models/actions/contact_update.rb
+++ b/app/models/actions/contact_update.rb
@@ -17,7 +17,7 @@ module Actions
def call
maybe_remove_address
maybe_update_statuses
- maybe_update_ident
+ maybe_update_ident if ident.present?
maybe_attach_legal_doc
commit
end
@@ -53,7 +53,11 @@ module Actions
end
def maybe_update_ident
- return unless ident[:ident]
+ unless ident.is_a?(Hash)
+ contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident'))
+ @error = true
+ return
+ end
if contact.identifier.valid?
submitted_ident = ::Contact::Ident.new(code: ident[:ident],
diff --git a/app/models/actions/domain_transfer.rb b/app/models/actions/domain_transfer.rb
new file mode 100644
index 000000000..1ff9aafe9
--- /dev/null
+++ b/app/models/actions/domain_transfer.rb
@@ -0,0 +1,74 @@
+module Actions
+ class DomainTransfer
+ attr_reader :domain
+ attr_reader :transfer_code
+ attr_reader :legal_document
+ attr_reader :ident
+ attr_reader :user
+
+ def initialize(domain, transfer_code, user)
+ @domain = domain
+ @transfer_code = transfer_code
+ @user = user
+ end
+
+ def call
+ return unless domain_exists?
+ return unless valid_transfer_code?
+
+ run_validations
+
+ # return domain.pending_transfer if domain.pending_transfer
+ # attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call)
+
+ return if domain.errors[:epp_errors].any?
+
+ commit
+ end
+
+ def domain_exists?
+ return true if domain.persisted?
+
+ domain.add_epp_error('2303', nil, nil, 'Object does not exist')
+
+ false
+ end
+
+ def run_validations
+ validate_registrar
+ validate_eligilibty
+ validate_not_discarded
+ end
+
+ def valid_transfer_code?
+ return true if transfer_code == domain.transfer_code
+
+ domain.add_epp_error('2202', nil, nil, 'Invalid authorization information')
+ false
+ end
+
+ def validate_registrar
+ return unless user == domain.registrar
+
+ domain.add_epp_error('2002', nil, nil,
+ I18n.t(:domain_already_belongs_to_the_querying_registrar))
+ end
+
+ def validate_eligilibty
+ return unless domain.non_transferable?
+
+ domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ end
+
+ def validate_not_discarded
+ return unless domain.discarded?
+
+ domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
+ end
+
+ def commit
+ bare_domain = Domain.find(domain.id)
+ ::DomainTransfer.request(bare_domain, user)
+ end
+ end
+end
diff --git a/app/models/concerns/domain/force_delete.rb b/app/models/concerns/domain/force_delete.rb
index f3bf96975..ff869fc0a 100644
--- a/app/models/concerns/domain/force_delete.rb
+++ b/app/models/concerns/domain/force_delete.rb
@@ -52,51 +52,14 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength
force_delete_start + Setting.expire_warning_period.days <= valid_to
end
- def schedule_force_delete(type: :fast_track)
- if discarded?
- raise StandardError, 'Force delete procedure cannot be scheduled while a domain is discarded'
- end
-
- type == :fast_track ? force_delete_fast_track : force_delete_soft
- end
-
- def add_force_delete_type(force_delete_type)
- self.force_delete_type = force_delete_type
- end
-
- def force_delete_fast_track
- preserve_current_statuses_for_force_delete
- add_force_delete_statuses
- add_force_delete_type(:fast)
- self.force_delete_date = force_delete_fast_track_start_date + 1.day
- self.force_delete_start = Time.zone.today + 1.day
- stop_all_pending_actions
- allow_deletion
- save(validate: false)
- end
-
- def force_delete_soft
- preserve_current_statuses_for_force_delete
- add_force_delete_statuses
- add_force_delete_type(:soft)
- calculate_soft_delete_date
- stop_all_pending_actions
- allow_deletion
- save(validate: false)
- end
-
- def clear_force_delete_data
- self.force_delete_data = nil
+ def schedule_force_delete(type: :fast_track, notify_by_email: false)
+ ForceDeleteInteraction::SetForceDelete.run(domain: self,
+ type: type,
+ notify_by_email: notify_by_email)
end
def cancel_force_delete
- remove_force_delete_statuses
- restore_statuses_before_force_delete
- clear_force_delete_data
- self.force_delete_date = nil
- self.force_delete_start = nil
- save(validate: false)
- registrar.notifications.create!(text: I18n.t('force_delete_cancelled', domain_name: name))
+ CancelForceDeleteInteraction::CancelForceDelete.run(domain: self)
end
def outzone_date
@@ -107,55 +70,4 @@ module Concerns::Domain::ForceDelete # rubocop:disable Metrics/ModuleLength
(force_delete_date&.beginning_of_day || valid_to + Setting.expire_warning_period.days +
Setting.redemption_grace_period.days)
end
-
- private
-
- def calculate_soft_delete_date
- years = (valid_to.to_date - Time.zone.today).to_i / 365
- soft_delete_dates(years) if years.positive?
- end
-
- def soft_delete_dates(years)
- self.force_delete_start = valid_to - years.years
- self.force_delete_date = force_delete_start + Setting.expire_warning_period.days +
- Setting.redemption_grace_period.days
- end
-
- def stop_all_pending_actions
- statuses.delete(DomainStatus::PENDING_UPDATE)
- statuses.delete(DomainStatus::PENDING_TRANSFER)
- statuses.delete(DomainStatus::PENDING_RENEW)
- statuses.delete(DomainStatus::PENDING_CREATE)
- end
-
- def preserve_current_statuses_for_force_delete
- update(statuses_before_force_delete: statuses)
- end
-
- def restore_statuses_before_force_delete
- self.statuses = statuses_before_force_delete
- self.statuses_before_force_delete = nil
- end
-
- def add_force_delete_statuses
- self.statuses |= [DomainStatus::FORCE_DELETE,
- DomainStatus::SERVER_RENEW_PROHIBITED,
- DomainStatus::SERVER_TRANSFER_PROHIBITED]
- end
-
- def remove_force_delete_statuses
- statuses.delete(DomainStatus::FORCE_DELETE)
- statuses.delete(DomainStatus::SERVER_RENEW_PROHIBITED)
- statuses.delete(DomainStatus::SERVER_TRANSFER_PROHIBITED)
- statuses.delete(DomainStatus::CLIENT_HOLD)
- end
-
- def allow_deletion
- statuses.delete(DomainStatus::CLIENT_DELETE_PROHIBITED)
- statuses.delete(DomainStatus::SERVER_DELETE_PROHIBITED)
- end
-
- def force_delete_fast_track_start_date
- Time.zone.today + Setting.expire_warning_period.days + Setting.redemption_grace_period.days
- end
end
diff --git a/app/models/contact.rb b/app/models/contact.rb
index 9dc1e34a2..8a154c50c 100644
--- a/app/models/contact.rb
+++ b/app/models/contact.rb
@@ -333,31 +333,6 @@ class Contact < ApplicationRecord
Country.new(country_code)
end
- # TODO: refactor, it should not allow to destroy with normal destroy,
- # no need separate method
- # should use only in transaction
- def destroy_and_clean frame
- if linked?
- errors.add(:domains, :exist)
- return false
- end
-
- legal_document_data = ::Deserializers::Xml::LegalDocument.new(frame).call
-
- if legal_document_data
-
- doc = LegalDocument.create(
- documentable_type: Contact,
- document_type: legal_document_data[:type],
- body: legal_document_data[:body]
- )
- self.legal_documents = [doc]
- self.legal_document_id = doc.id
- self.save
- end
- destroy
- end
-
def to_upcase_country_code
self.ident_country_code = ident_country_code.upcase if ident_country_code
self.country_code = country_code.upcase if country_code
diff --git a/app/models/domain.rb b/app/models/domain.rb
index b15bb7c55..1ef66ffab 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -418,7 +418,7 @@ class Domain < ApplicationRecord
pending_delete_confirmation!
save(validate: false) # should check if this did succeed
- DomainDeleteConfirmEmailJob.enqueue(id)
+ DomainDeleteConfirmInteraction::SendRequest.run(domain: self)
end
def cancel_pending_delete
diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb
index 6867b037d..50ebac065 100644
--- a/app/models/epp/contact.rb
+++ b/app/models/epp/contact.rb
@@ -30,12 +30,13 @@ class Epp::Contact < Contact
at
end
- def new(frame, registrar)
+ def new(frame, registrar, epp: true)
return super if frame.blank?
+ attrs = epp ? attrs_from(frame, new_record: true) : frame
super(
- attrs_from(frame, new_record: true).merge(
- code: frame.css('id').text,
+ attrs.merge(
+ code: epp ? frame.css('id').text : frame[:id],
registrar: registrar
)
)
diff --git a/app/models/registrar.rb b/app/models/registrar.rb
index 040c7886b..149bb7541 100644
--- a/app/models/registrar.rb
+++ b/app/models/registrar.rb
@@ -137,7 +137,8 @@ class Registrar < ApplicationRecord
def api_ip_white?(ip)
return true unless Setting.api_ip_whitelist_enabled
- white_ips.api.pluck(:ipv4, :ipv6).flatten.include?(ip)
+
+ white_ips.api.include_ip?(ip)
end
# Audit log is needed, therefore no raw SQL
diff --git a/app/models/white_ip.rb b/app/models/white_ip.rb
index 303ff5886..417633b12 100644
--- a/app/models/white_ip.rb
+++ b/app/models/white_ip.rb
@@ -2,8 +2,8 @@ class WhiteIp < ApplicationRecord
include Versions
belongs_to :registrar
- validates :ipv4, format: { with: /\A(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\z/, allow_blank: true }
- validates :ipv6, format: { with: /(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/, allow_blank: true }
+ validate :valid_ipv4?
+ validate :valid_ipv6?
validate :validate_ipv4_and_ipv6
def validate_ipv4_and_ipv6
@@ -11,6 +11,22 @@ class WhiteIp < ApplicationRecord
errors.add(:base, I18n.t(:ipv4_or_ipv6_must_be_present))
end
+ def valid_ipv4?
+ return if ipv4.blank?
+
+ IPAddr.new(ipv4, Socket::AF_INET)
+ rescue StandardError => _e
+ errors.add(:ipv4, :invalid)
+ end
+
+ def valid_ipv6?
+ return if ipv6.blank?
+
+ IPAddr.new(ipv6, Socket::AF_INET6)
+ rescue StandardError => _e
+ errors.add(:ipv6, :invalid)
+ end
+
API = 'api'
REGISTRAR = 'registrar'
INTERFACES = [API, REGISTRAR]
@@ -23,8 +39,37 @@ class WhiteIp < ApplicationRecord
end
class << self
+ # rubocop:disable Style/CaseEquality
+ # rubocop:disable Metrics/AbcSize
def include_ip?(ip)
- where('ipv4 = :ip OR ipv6 = :ip', ip: ip).any?
+ return false if ip.blank?
+
+ where(id: ids_including(ip)).any?
+ end
+
+ def ids_including(ip)
+ ipv4 = ipv6 = []
+ if check_ip4(ip).present?
+ ipv4 = select { |white_ip| IPAddr.new(white_ip.ipv4, Socket::AF_INET) === check_ip4(ip) }
+ end
+ if check_ip6(ip).present?
+ ipv6 = select { |white_ip| IPAddr.new(white_ip.ipv6, Socket::AF_INET6) === check_ip6(ip) }
+ end
+ (ipv4 + ipv6).pluck(:id).flatten.uniq
+ end
+ # rubocop:enable Style/CaseEquality
+ # rubocop:enable Metrics/AbcSize
+
+ def check_ip4(ip)
+ IPAddr.new(ip, Socket::AF_INET)
+ rescue StandardError => _e
+ nil
+ end
+
+ def check_ip6(ip)
+ IPAddr.new(ip, Socket::AF_INET6)
+ rescue StandardError => _e
+ nil
end
end
end
diff --git a/app/services/registrant_change.rb b/app/services/registrant_change.rb
index 35b631fb6..fdee7654a 100644
--- a/app/services/registrant_change.rb
+++ b/app/services/registrant_change.rb
@@ -5,6 +5,7 @@ class RegistrantChange
end
def confirm
+ Dispute.close_by_domain(@domain.name) if @domain.disputed?
notify_registrant
end
diff --git a/app/views/epp/contacts/info.xml.builder b/app/views/epp/contacts/info.xml.builder
index 1945e7def..6ce0a17f4 100644
--- a/app/views/epp/contacts/info.xml.builder
+++ b/app/views/epp/contacts/info.xml.builder
@@ -14,11 +14,15 @@ xml.epp_head do
end
xml.tag!('contact:postalInfo', type: 'int') do
- xml.tag!('contact:name', @contact.name)
+ if can? :view_full_info, @contact, @password
+ xml.tag!('contact:name', @contact.name)
+ else
+ xml.tag!('contact:name', 'No access')
+ end
if can? :view_full_info, @contact, @password
xml.tag!('contact:org', @contact.org_name) if @contact.org_name.present?
- if address_processing?
+ if Contact.address_processing?
xml.tag!('contact:addr') do
xml.tag!('contact:street', @contact.street)
xml.tag!('contact:city', @contact.city)
@@ -31,7 +35,7 @@ xml.epp_head do
else
xml.tag!('contact:org', 'No access')
- if address_processing?
+ if Contact.address_processing?
xml.tag!('contact:addr') do
xml.tag!('contact:street', 'No access')
xml.tag!('contact:city', 'No access')
diff --git a/app/views/mailers/domain_expire_mailer/expired_soft.html.erb b/app/views/mailers/domain_expire_mailer/expired_soft.html.erb
index 1dd661a38..0bcfc6acd 100644
--- a/app/views/mailers/domain_expire_mailer/expired_soft.html.erb
+++ b/app/views/mailers/domain_expire_mailer/expired_soft.html.erb
@@ -2,7 +2,7 @@
Lugupeetud .ee domeeni registreerija/halduskontakt
-Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulike kontakti objekte, milles tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.
+Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulikke kontakti objekte, millest tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.
<%= @domain.name %> pikendamata jätmisel domeen kustub ja läheb <%= @domain.delete_date %> oksjonile .ee oksjonikeskkonda. Domeenioksjonite kohta loe lähemalt siit.
diff --git a/app/views/mailers/domain_expire_mailer/expired_soft.text.erb b/app/views/mailers/domain_expire_mailer/expired_soft.text.erb
index 0e6d9c953..7be32f73b 100644
--- a/app/views/mailers/domain_expire_mailer/expired_soft.text.erb
+++ b/app/views/mailers/domain_expire_mailer/expired_soft.text.erb
@@ -2,7 +2,7 @@ Domeen <%= @domain.name %> on aegunud ning suunatud kustutusmenetlusse kuna olem
Lugupeetud .ee domeeni registreerija/halduskontakt
-Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulike kontakti objekte, milles tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.
+Domeeninimi <%= @domain.name %> on aegunud ja ei ole alates <%= @domain.on_hold_date %> internetis kättesaadav. Domeeniga on seotud puudulikke kontakti objekte, millest tulenevalt on Eesti Interneti SA blokeerinud domeeni pikendamise ja registripidaja vahetuse, kuniks kontaktandmed korrastatakse. Andmete korrastamiseks ja registreeringu pikendamiseks pöörduge palun oma registripidaja poole.
<%= @domain.name %> pikendamata jätmisel domeen kustub ja läheb <%= @domain.delete_date %> oksjonile .ee oksjonikeskkonda. Domeenioksjonite kohta loe lähemalt siit https://www.internet.ee/domeenioksjonid.
diff --git a/app/views/registrar/contacts/_form.haml b/app/views/registrar/contacts/_form.haml
index cf8217e13..953c502e5 100644
--- a/app/views/registrar/contacts/_form.haml
+++ b/app/views/registrar/contacts/_form.haml
@@ -5,7 +5,7 @@
.col-md-8
= render 'registrar/contacts/form/general', f: f
-- if address_processing?
+- if Contact.address_processing?
.row
.col-md-8
= render 'registrar/contacts/form/address', f: f
diff --git a/app/views/registrar/domains/partials/_contacts.haml b/app/views/registrar/domains/partials/_contacts.haml
index 48d1ac21f..e6ef9aa8f 100644
--- a/app/views/registrar/domains/partials/_contacts.haml
+++ b/app/views/registrar/domains/partials/_contacts.haml
@@ -13,5 +13,5 @@
- registrant = Contact.find_by_code(x.text)
%tr
%td= x['type']
- %td= registrant.name
+ %td= registrant.registrar == current_registrar_user.registrar ? registrant.name : 'N/A'
%td= x.text
diff --git a/app/views/registrar/domains/partials/_general.html.erb b/app/views/registrar/domains/partials/_general.html.erb
index 3fb3a5df8..ff064857c 100644
--- a/app/views/registrar/domains/partials/_general.html.erb
+++ b/app/views/registrar/domains/partials/_general.html.erb
@@ -23,7 +23,7 @@
<% registrant = Contact.find_by_code(@data.css('registrant').text) %>
<%= t('.registrant') %>
- <%= "#{registrant.name} (#{@data.css('registrant').text})" %>
+ <%= registrant.registrar == current_registrar_user.registrar ? "#{registrant.name} (#{@data.css('registrant').text})" : @data.css('registrant').text %>
<%= t('.registered') %>
<%= @data.css('crDate').text %>
diff --git a/config/application.rb b/config/application.rb
index 5f4481512..a5fb17c9d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -36,6 +36,7 @@ module DomainNameRegistry
# Autoload all model subdirs
config.autoload_paths += Dir[Rails.root.join('app', 'models', '**/')]
+ config.autoload_paths += Dir[Rails.root.join('app', 'interactions', '**/')]
config.eager_load_paths << config.root.join('lib', 'validators')
config.watchable_dirs['lib'] = %i[rb]
diff --git a/config/application.yml.sample b/config/application.yml.sample
index e979e772a..82b45feb9 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
+# Base URL (inc. https://) of REST registrant portal
+# Leave blank to use internal registrant portal
+registrant_portal_verifications_base_url: ''
#
# MISC
diff --git a/config/routes.rb b/config/routes.rb
index f58063fae..0af3d95c9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -37,12 +37,35 @@ Rails.application.routes.draw do
get 'error/:command', to: 'errors#error'
end
- mount Repp::API => '/'
-
namespace :repp do
namespace :v1 do
+ resources :contacts do
+ collection do
+ get 'check/:id', to: 'contacts#check'
+ end
+ end
+
+ resources :accounts do
+ collection do
+ get 'balance'
+ end
+ end
resources :auctions, only: %i[index]
resources :retained_domains, only: %i[index]
+ namespace :registrar do
+ resources :nameservers do
+ collection do
+ put '/', to: 'nameservers#update'
+ end
+ end
+ end
+ resources :domains do
+ collection do
+ get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ }
+ post 'transfer', to: 'domains#transfer'
+ patch 'contacts', to: 'domains/contacts#update'
+ end
+ end
end
end
@@ -56,6 +79,8 @@ Rails.application.routes.draw do
namespace :v1 do
namespace :registrant do
post 'auth/eid', to: 'auth#eid'
+ get 'confirms/:name/:template/:token', to: 'confirms#index', constraints: { name: /[^\/]+/ }
+ post 'confirms/:name/:template/:token/:decision', to: 'confirms#update', constraints: { name: /[^\/]+/ }
resources :domains, only: %i[index show], param: :uuid do
resource :registry_lock, only: %i[create destroy]
diff --git a/doc/repp/v1/account.md b/doc/repp/v1/account.md
index 511c51382..80afabcdb 100644
--- a/doc/repp/v1/account.md
+++ b/doc/repp/v1/account.md
@@ -19,7 +19,11 @@ Content-Length: 37
Content-Type: application/json
{
- "balance": "324.45",
- "currency": "EUR"
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "balance": "356.0",
+ "currency": "EUR"
+ }
}
```
diff --git a/doc/repp/v1/contact.md b/doc/repp/v1/contact.md
index 41f45551f..aa6904b72 100644
--- a/doc/repp/v1/contact.md
+++ b/doc/repp/v1/contact.md
@@ -88,3 +88,141 @@ Content-Type: application/json
"total_number_of_records": 2
}
```
+
+## POST /repp/v1/contacts
+Creates new contact
+
+
+#### Request
+```
+POST /repp/v1/contacts HTTP/1.1
+Authorization: Basic dGVzdDp0ZXN0MTIz
+Content-Type: application/json
+
+{
+ "contact": {
+ "name": "John Doe",
+ "email": "john@doe.com",
+ "phone": "+371.1234567",
+ "ident": {
+ "ident": "12345678901",
+ "ident_type": "priv",
+ "ident_country_code": "EE"
+ }
+ }
+}
+```
+
+#### Response
+```
+HTTP/1.1 200
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "contact": {
+ "id": "ATSAA:20DCDCA1"
+ }
+ }
+}
+```
+
+#### Failed response
+```
+HTTP/1.1 400
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 2005,
+ "message": "Ident code does not conform to national identification number format of Estonia",
+ "data": {}
+}
+```
+
+## PUT /repp/v1/contacts/**contact id**
+Updates existing contact
+
+
+#### Request
+```
+PUT /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1
+Authorization: Basic dGVzdDp0ZXN0MTIz
+Content-Type: application/json
+
+{
+ "contact": {
+ "phone": "+372.123123123"
+ }
+}
+```
+
+#### Response
+```
+HTTP/1.1 200
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "contact": {
+ "id": "ATSAA:20DCDCA1"
+ }
+ }
+}
+```
+
+#### Failed response
+```
+HTTP/1.1 400
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 2005,
+ "message": "Phone nr is invalid [phone]",
+ "data": {}
+}
+```
+
+## DELETE /repp/v1/contacts/**contact id**
+Deletes existing contact
+
+
+#### Request
+```
+DELETE /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1
+Authorization: Basic dGVzdDp0ZXN0MTIz
+Content-Type: application/json
+```
+
+#### Response
+```
+HTTP/1.1 200
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {}
+}
+```
+
+#### Failed response
+```
+HTTP/1.1 400
+Cache-Control: max-age=0, private, must-revalidate
+Content-Type: application/json
+
+{
+ "code": 2305,
+ "message": "Object association prohibits operation [domains]",
+ "data": {}
+}
+```
diff --git a/doc/repp/v1/domain.md b/doc/repp/v1/domain.md
index c6734cbe2..20607f3b9 100644
--- a/doc/repp/v1/domain.md
+++ b/doc/repp/v1/domain.md
@@ -25,44 +25,52 @@ Content-Type: application/json
```
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
-Content-Length: 808
Content-Type: application/json
{
- "domains": [
- {
- "id": 1,
- "name": "domain0.ee",
- "registrar_id": 2,
- "registered_at": "2015-09-09T09:11:14.861Z",
- "status": null,
- "valid_from": "2015-09-09T09:11:14.861Z",
- "valid_to": "2016-09-09T09:11:14.861Z",
- "registrant_id": 1,
- "transfer_code": "98oiewslkfkd",
- "created_at": "2015-09-09T09:11:14.861Z",
- "updated_at": "2015-09-09T09:11:14.860Z",
- "name_dirty": "domain0.ee",
- "name_puny": "domain0.ee",
- "period": 1,
- "period_unit": "y",
- "creator_str": null,
- "updator_str": null,
- "outzone_at": "2016-09-24T09:11:14.861Z",
- "delete_date": "2016-10-24",
- "registrant_verification_asked_at": null,
- "registrant_verification_token": null,
- "pending_json": {
- },
- "force_delete_date": null,
- "statuses": [
- "ok"
- ],
- "status_notes": {
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "domains": [
+ {
+ "id": 7,
+ "name": "private.ee",
+ "registrar_id": 2,
+ "valid_to": "2022-09-23T00:00:00.000+03:00",
+ "registrant_id": 11,
+ "created_at": "2020-09-22T14:16:47.420+03:00",
+ "updated_at": "2020-10-21T13:31:43.733+03:00",
+ "name_dirty": "private.ee",
+ "name_puny": "private.ee",
+ "period": 1,
+ "period_unit": "y",
+ "creator_str": "2-ApiUser: test",
+ "updator_str": null,
+ "outzone_at": null,
+ "delete_date": null,
+ "registrant_verification_asked_at": null,
+ "registrant_verification_token": null,
+ "pending_json": {},
+ "force_delete_date": null,
+ "statuses": [
+ "serverRenewProhibited"
+ ],
+ "status_notes": {
+ "ok": "",
+ "serverRenewProhibited": ""
+ },
+ "upid": null,
+ "up_date": null,
+ "uuid": "6b6affa7-1449-4bd8-acf5-8b4752406705",
+ "locked_by_registrant_at": null,
+ "force_delete_start": null,
+ "force_delete_data": null,
+ "auth_info": "367b1e6d1f0d9aa190971ad8f571cd4d",
+ "valid_from": "2020-09-22T14:16:47.420+03:00"
}
- }
- ],
- "total_number_of_records": 2
+ ],
+ "total_number_of_records": 10
+ }
}
```
@@ -83,14 +91,17 @@ Content-Type: application/json
```
HTTP/1.1 200
Cache-Control: max-age=0, private, must-revalidate
-Content-Length: 54
Content-Type: application/json
{
- "domains": [
- "domain1.ee"
- ],
- "total_number_of_records": 2
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "domains": [
+ "private.ee",
+ ],
+ "total_number_of_records": 1
+ }
}
```
@@ -117,65 +128,68 @@ Please note that domain transfer/authorisation code must be placed in header - *
```
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
-Content-Length: 784
Content-Type: application/json
-
{
- "domain":"ee-test.ee",
- "registrant":{
- "code":"EE:R1",
- "name":"Registrant",
- "ident":"17612535",
- "ident_type":"org",
- "ident_country_code":"EE",
- "phone":"+372.1234567",
- "email":"registrant@cache.ee",
- "street":"Businesstreet 1",
- "city":"Tallinn",
- "zip":"10101",
- "country_code":"EE",
- "statuses":[
- "ok",
- "linked"
- ]
- },
- "admin_contacts":[
- {
- "code":"EE:A1",
- "name":"Admin Contact",
- "ident":"17612535376",
- "ident_type":"priv",
- "ident_country_code":"EE",
- "phone":"+372.7654321",
- "email":"admin@cache.ee",
- "street":"Adminstreet 2",
- "city":"Tallinn",
- "zip":"12345",
- "country_code":"EE",
- "statuses":[
- "ok",
- "linked"
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "domain":"ee-test.ee",
+ "registrant":{
+ "code":"EE:R1",
+ "name":"Registrant",
+ "ident":"17612535",
+ "ident_type":"org",
+ "ident_country_code":"EE",
+ "phone":"+372.1234567",
+ "email":"registrant@cache.ee",
+ "street":"Businesstreet 1",
+ "city":"Tallinn",
+ "zip":"10101",
+ "country_code":"EE",
+ "statuses":[
+ "ok",
+ "linked"
+ ]
+ },
+ "admin_contacts":[
+ {
+ "code":"EE:A1",
+ "name":"Admin Contact",
+ "ident":"17612535376",
+ "ident_type":"priv",
+ "ident_country_code":"EE",
+ "phone":"+372.7654321",
+ "email":"admin@cache.ee",
+ "street":"Adminstreet 2",
+ "city":"Tallinn",
+ "zip":"12345",
+ "country_code":"EE",
+ "statuses":[
+ "ok",
+ "linked"
+ ]
+ }
+ ],
+ "tech_contacts":[
+ {
+ "code":"EE:T1",
+ "name":"Tech Contact",
+ "ident":"17612536",
+ "ident_type":"org",
+ "ident_country_code":"EE",
+ "phone":"+372.7654321",
+ "email":"tech@cache.ee",
+ "street":"Techstreet 1",
+ "city":"Tallinn",
+ "zip":"12345",
+ "country_code":"EE",
+ "statuses":[
+ "ok",
+ "linked"
+ ]
+ }
]
}
- ],
- "tech_contacts":[
- {
- "code":"EE:T1",
- "name":"Tech Contact",
- "ident":"17612536",
- "ident_type":"org",
- "ident_country_code":"EE",
- "phone":"+372.7654321",
- "email":"tech@cache.ee",
- "street":"Techstreet 1",
- "city":"Tallinn",
- "zip":"12345",
- "country_code":"EE",
- "statuses":[
- "ok",
- "linked"
- ]
- }
- ]
+ }
}
```
diff --git a/doc/repp/v1/domain_contacts.md b/doc/repp/v1/domain_contacts.md
index b412e9f73..2e542bf81 100644
--- a/doc/repp/v1/domain_contacts.md
+++ b/doc/repp/v1/domain_contacts.md
@@ -5,15 +5,26 @@ Replaces all domain contacts of the current registrar.
### Example request
```
-$ curl https://repp.internet.ee/v1/domains/contacts \
- -X PATCH \
- -u username:password \
- -d current_contact_id=foo \
- -d new_contact_id=bar
+PATCH /repp/v1/domains/contacts HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Basic dGVzdDp0ZXN0dGVzdA==
+
+{
+ "current_contact_id": "ATSAA:749AA80F",
+ "new_contact_id": "ATSAA:E36957D7"
+}
```
### Example response
```
{
- "affected_domains": ["example.com", "example.org"]
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "affected_domains": [
+ "private.ee",
+ ],
+ "skipped_domains": []
+ }
}
```
diff --git a/doc/repp/v1/domain_transfers.md b/doc/repp/v1/domain_transfers.md
index a6eb4683c..a11c0a852 100644
--- a/doc/repp/v1/domain_transfers.md
+++ b/doc/repp/v1/domain_transfers.md
@@ -1,28 +1,28 @@
# Domain transfers
-## POST /repp/v1/domain_transfers
+## POST /repp/v1/domains/transfer
Transfers domains.
#### Request
```
-POST /repp/v1/domain_transfers
+POST /repp/v1/domains/transfer
Accept: application/json
Content-Type: application/json
Authorization: Basic dGVzdDp0ZXN0dGVzdA==
{
- "data":{
- "domainTransfers":[
- {
- "domainName":"example.com",
- "transferCode":"63e7"
- },
- {
- "domainName":"example.org",
- "transferCode":"15f9"
- }
- ]
- }
+ "data": {
+ "domain_transfers": [
+ {
+ "domain_name":"example.com",
+ "transferCode":"63e7"
+ },
+ {
+ "domain_name":"example.org",
+ "transferCode":"15f9"
+ }
+ ]
+ }
}
```
@@ -31,14 +31,21 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA==
HTTP/1.1 200
Content-Type: application/json
{
- "data":[
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "success": [
{
- "type":"domain_transfer"
+ "type": "domain_transfer",
+ "domain_name": "example.com"
},
{
- "type":"domain_transfer"
+ "type": "domain_transfer",
+ "domain_name": "example.org"
}
- ]
+ ],
+ "failed": []
+ }
}
```
@@ -48,13 +55,32 @@ Content-Type: application/json
HTTP/1.1 400
Content-Type: application/json
{
- "errors":[
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
+ "success": [],
+ "failed": [
{
- "title":"example.com transfer code is wrong"
+ "type": "domain_transfer",
+ "domain_name": "example.com",
+ "errors": [
+ {
+ "code": "2202",
+ "msg": "Invalid authorization information"
+ }
+ ]
},
{
- "title":"example.org does not exist"
+ "type": "domain_transfer",
+ "domain_name": "example.org",
+ "errors": [
+ {
+ "code": "2304",
+ "msg": "Object status prohibits operation"
+ }
+ ]
}
- ]
+ ]
+ }
}
```
diff --git a/doc/repp/v1/nameservers.md b/doc/repp/v1/nameservers.md
index 8190530d7..ab53c72df 100644
--- a/doc/repp/v1/nameservers.md
+++ b/doc/repp/v1/nameservers.md
@@ -10,15 +10,15 @@ Accept: application/json
Content-Type: application/json
Authorization: Basic dGVzdDp0ZXN0dGVzdA==
{
- "data":{
- "type": "nameserver",
- "id": "ns1.example.com",
- "attributes": {
- "hostname": "new-ns1.example.com",
- "ipv4": ["192.0.2.1", "192.0.2.2"],
- "ipv6": ["2001:db8::1", "2001:db8::2"]
- },
+ "data": {
+ "type": "nameserver",
+ "id": "ns1.example.com",
+ "attributes": {
+ "hostname": "new-ns1.example.com",
+ "ipv4": ["192.0.2.1", "192.0.2.2"],
+ "ipv6": ["2001:db8::1", "2001:db8::2"]
}
+ }
}
```
@@ -27,16 +27,26 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA==
HTTP/1.1 200
Content-Type: application/json
{
- "data":{
+ "code": 1000,
+ "message": "Command completed successfully",
+ "data": {
"type": "nameserver",
"id": "new-ns1.example.com",
"attributes": {
"hostname": "new-ns1.example.com",
- "ipv4": ["192.0.2.1", "192.0.2.2"],
- "ipv6": ["2001:db8::1", "2001:db8::2"]
- }
- },
- "affected_domains": ["example.com", "example.org"]
+ "ipv4": [
+ "192.0.2.1",
+ "192.0.2.2"
+ ],
+ "ipv6": [
+ "2001:db8::1",
+ "2001:db8::2"
+ ]
+ },
+ "affected_domains": [
+ "private.ee"
+ ]
+ }
}
```
@@ -44,14 +54,10 @@ Content-Type: application/json
```
HTTP/1.1 400
Content-Type: application/json
+
{
- "errors":[
- {
- "title":"ns1.example.com does not exist"
- },
- {
- "title":"192.0.2.1 is not a valid IPv4 address"
- }
- ]
+ "code": 2005,
+ "message": "IPv4 is invalid [ipv4]",
+ "data": {}
}
```
diff --git a/lib/deserializers/xml/contact_create.rb b/lib/deserializers/xml/contact_create.rb
new file mode 100644
index 000000000..5dfa32ef7
--- /dev/null
+++ b/lib/deserializers/xml/contact_create.rb
@@ -0,0 +1,10 @@
+require 'deserializers/xml/legal_document'
+require 'deserializers/xml/ident'
+require 'deserializers/xml/contact'
+
+module Deserializers
+ module Xml
+ class ContactCreate < ContactUpdate
+ end
+ end
+end
diff --git a/lib/serializers/registrant_api/.DS_Store b/lib/serializers/registrant_api/.DS_Store
new file mode 100644
index 000000000..5008ddfcf
Binary files /dev/null and b/lib/serializers/registrant_api/.DS_Store differ
diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb
new file mode 100644
index 000000000..834402359
--- /dev/null
+++ b/lib/serializers/repp/contact.rb
@@ -0,0 +1,36 @@
+module Serializers
+ module Repp
+ class Contact
+ attr_reader :contact
+
+ def initialize(contact, show_address:)
+ @contact = contact
+ @show_address = show_address
+ end
+
+ def to_json(obj = contact)
+ json = { id: obj.code, name: obj.name, ident: ident,
+ email: obj.email, phone: obj.phone, fax: obj.fax,
+ auth_info: obj.auth_info, statuses: obj.statuses,
+ disclosed_attributes: obj.disclosed_attributes }
+
+ json[:address] = address if @show_address
+
+ json
+ end
+
+ def ident
+ {
+ code: contact.ident,
+ type: contact.ident_type,
+ country_code: contact.ident_country_code,
+ }
+ end
+
+ def address
+ { street: contact.street, zip: contact.zip, city: contact.city,
+ state: contact.state, country_code: contact.country_code }
+ end
+ end
+ end
+end
diff --git a/test/integration/api/domain_contacts_test.rb b/test/integration/api/domain_contacts_test.rb
index 5336cc10a..6704739d1 100644
--- a/test/integration/api/domain_contacts_test.rb
+++ b/test/integration/api/domain_contacts_test.rb
@@ -27,8 +27,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :ok
- assert_equal ({ affected_domains: %w[airport.test shop.test],
- skipped_domains: [] }),
+ assert_equal ({ code: 1000, message: 'Command completed successfully', data: { affected_domains: %w[airport.test shop.test],
+ skipped_domains: [] }}),
JSON.parse(response.body, symbolize_names: true)
end
@@ -42,7 +42,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest
assert_response :ok
assert_equal %w[airport.test shop.test], JSON.parse(response.body,
- symbolize_names: true)[:skipped_domains]
+ symbolize_names: true)[:data][:skipped_domains]
end
def test_keep_other_tech_contacts_intact
@@ -66,10 +66,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest
new_contact_id: 'william-002' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response :bad_request
- assert_equal ({ error: { type: 'invalid_request_error',
- param: 'current_contact_id',
- message: 'No such contact: jack-001' } }),
+ assert_response :not_found
+ assert_equal ({ code: 2303, message: 'Object does not exist' }),
JSON.parse(response.body, symbolize_names: true)
end
@@ -77,10 +75,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest
patch '/repp/v1/domains/contacts', params: { current_contact_id: 'non-existent',
new_contact_id: 'john-001' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response :bad_request
- assert_equal ({ error: { type: 'invalid_request_error',
- param: 'current_contact_id',
- message: 'No such contact: non-existent' } }),
+ assert_response :not_found
+ assert_equal ({ code: 2303, message: 'Object does not exist' }),
JSON.parse(response.body, symbolize_names: true)
end
@@ -88,10 +84,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest
patch '/repp/v1/domains/contacts', params: { current_contact_id: 'william-001',
new_contact_id: 'non-existent' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response :bad_request
- assert_equal ({ error: { type: 'invalid_request_error',
- param: 'new_contact_id',
- message: 'No such contact: non-existent' } }),
+ assert_response :not_found
+ assert_equal ({code: 2303, message: 'Object does not exist'}),
JSON.parse(response.body, symbolize_names: true)
end
@@ -100,9 +94,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest
new_contact_id: 'invalid' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
- assert_equal ({ error: { type: 'invalid_request_error',
- param: 'new_contact_id',
- message: 'New contact must be valid' } }),
+ assert_equal ({ code: 2304, message: 'New contact must be valid', data: {} }),
JSON.parse(response.body, symbolize_names: true)
end
@@ -111,8 +103,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest
new_contact_id: 'william-001' },
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response :bad_request
- assert_equal ({ error: { type: 'invalid_request_error',
- message: 'New contact ID must be different from current contact ID' } }),
+ assert_equal ({ code: 2304, message: 'New contact must be different from current', data: {} }),
JSON.parse(response.body, symbolize_names: true)
end
diff --git a/test/integration/api/domain_transfers_test.rb b/test/integration/api/domain_transfers_test.rb
index aabaeb728..3e9c10100 100644
--- a/test/integration/api/domain_transfers_test.rb
+++ b/test/integration/api/domain_transfers_test.rb
@@ -12,34 +12,21 @@ class APIDomainTransfersTest < ApplicationIntegrationTest
Setting.transfer_wait_time = @original_transfer_wait_time
end
- def test_returns_domain_transfers
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
- headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response 200
- assert_equal ({ data: [{
- type: 'domain_transfer',
- attributes: {
- domain_name: 'shop.test'
- },
- }] }),
- JSON.parse(response.body, symbolize_names: true)
- end
-
def test_creates_new_domain_transfer
assert_difference -> { @domain.transfers.size } do
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
end
end
def test_approves_automatically_if_auto_approval_is_enabled
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert @domain.transfers.last.approved?
end
def test_assigns_new_registrar
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
@domain.reload
assert_equal @new_registrar, @domain.registrar
@@ -48,7 +35,7 @@ class APIDomainTransfersTest < ApplicationIntegrationTest
def test_regenerates_transfer_code
@old_transfer_code = @domain.transfer_code
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
@domain.reload
refute_equal @domain.transfer_code, @old_transfer_code
@@ -58,51 +45,28 @@ class APIDomainTransfersTest < ApplicationIntegrationTest
@old_registrar = @domain.registrar
assert_difference -> { @old_registrar.notifications.count } do
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
end
end
def test_duplicates_registrant_admin_and_tech_contacts
assert_difference -> { @new_registrar.contacts.size }, 3 do
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
end
end
def test_reuses_identical_contact
- post '/repp/v1/domain_transfers', params: request_params, as: :json,
+ post '/repp/v1/domains/transfer', params: request_params, as: :json,
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_equal 1, @new_registrar.contacts.where(name: 'William').size
end
- def test_fails_if_domain_does_not_exist
- post '/repp/v1/domain_transfers',
- params: { data: { domainTransfers: [{ domainName: 'non-existent.test',
- transferCode: 'any' }] } },
- as: :json,
- headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response 400
- assert_equal ({ errors: [{ title: 'non-existent.test does not exist' }] }),
- JSON.parse(response.body, symbolize_names: true)
- end
-
- def test_fails_if_transfer_code_is_wrong
- post '/repp/v1/domain_transfers',
- params: { data: { domainTransfers: [{ domainName: 'shop.test',
- transferCode: 'wrong' }] } },
- as: :json,
- headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
- assert_response 400
- refute_equal @new_registrar, @domain.registrar
- assert_equal ({ errors: [{ title: 'shop.test transfer code is wrong' }] }),
- JSON.parse(response.body, symbolize_names: true)
- end
-
private
def request_params
- { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } }
+ { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } }
end
def http_auth_key
diff --git a/test/integration/api/nameservers/put_test.rb b/test/integration/api/nameservers/put_test.rb
index 853a20549..3ab4f4dd4 100644
--- a/test/integration/api/nameservers/put_test.rb
+++ b/test/integration/api/nameservers/put_test.rb
@@ -60,12 +60,14 @@ class APINameserversPutTest < ApplicationIntegrationTest
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response 200
- assert_equal ({ data: { type: 'nameserver',
+ assert_equal ({ code: 1000,
+ message: 'Command completed successfully',
+ data: { type: 'nameserver',
id: 'ns55.bestnames.test',
attributes: { hostname: 'ns55.bestnames.test',
ipv4: ['192.0.2.55'],
- ipv6: ['2001:db8::55'] } },
- affected_domains: ["airport.test", "shop.test"] }),
+ ipv6: ['2001:db8::55'] },
+ affected_domains: ["airport.test", "shop.test"] }}),
JSON.parse(response.body, symbolize_names: true)
end
@@ -85,7 +87,7 @@ class APINameserversPutTest < ApplicationIntegrationTest
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response 404
- assert_equal ({ errors: [{ title: 'Hostname non-existent.test does not exist' }] }),
+ assert_equal ({code: 2303, message: 'Object does not exist' }),
JSON.parse(response.body, symbolize_names: true)
end
@@ -96,7 +98,8 @@ class APINameserversPutTest < ApplicationIntegrationTest
headers: { 'HTTP_AUTHORIZATION' => http_auth_key }
assert_response 400
- assert_equal ({ errors: [{ title: 'Hostname is missing' }] }),
+ assert_equal ({ code: 2003,
+ message: 'param is missing or the value is empty: hostname' }),
JSON.parse(response.body, symbolize_names: true)
end
diff --git a/test/integration/api/registrant/registrant_api_verifications_test.rb b/test/integration/api/registrant/registrant_api_verifications_test.rb
new file mode 100644
index 000000000..821d0dee0
--- /dev/null
+++ b/test/integration/api/registrant/registrant_api_verifications_test.rb
@@ -0,0 +1,297 @@
+require 'test_helper'
+require 'auth_token/auth_token_creator'
+
+class RegistrantApiVerificationsTest < ApplicationIntegrationTest
+ def setup
+ super
+
+ @domain = domains(:hospital)
+ @registrant = @domain.registrant
+ @new_registrant = contacts(:jack)
+ @user = users(:api_bestnames)
+
+ @token = 'verysecrettoken'
+
+ @domain.update!(statuses: [DomainStatus::PENDING_UPDATE],
+ registrant_verification_asked_at: Time.zone.now - 1.day,
+ registrant_verification_token: @token)
+
+ end
+
+ def test_fetches_registrant_change_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json)
+ @domain.reload
+
+ assert @domain.registrant_update_confirmable?(@token)
+
+ get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: "hospital.test",
+ current_registrant: {
+ name: @registrant.name,
+ ident: @registrant.ident,
+ country: @registrant.ident_country_code
+ },
+ new_registrant: {
+ name: @new_registrant.name,
+ ident: @new_registrant.ident,
+ country: @new_registrant.ident_country_code
+ }
+ }
+
+ assert_equal expected_body, res
+ end
+
+ def test_approves_registrant_change_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update!(pending_json: pending_json)
+ @domain.reload
+
+ assert @domain.registrant_update_confirmable?(@token)
+
+ perform_enqueued_jobs do
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/confirmed"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: @domain.name,
+ current_registrant: {
+ name: @new_registrant.name,
+ ident: @new_registrant.ident,
+ country: @new_registrant.ident_country_code
+ },
+ status: 'confirmed'
+ }
+ assert_equal expected_body, res
+ end
+ end
+
+ def test_rejects_registrant_change_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json)
+ @domain.reload
+
+ assert @domain.registrant_update_confirmable?(@token)
+
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/rejected"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: @domain.name,
+ current_registrant: {
+ name: @registrant.name,
+ ident: @registrant.ident,
+ country: @registrant.ident_country_code
+ },
+ status: 'rejected'
+ }
+
+ assert_equal expected_body, res
+ end
+
+ def test_registrant_change_requires_valid_attributes
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json)
+ @domain.reload
+
+ get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/123"
+ assert_equal 401, response.status
+
+ get "/api/v1/registrant/confirms/aohldfjg.ee/change/123"
+ assert_equal 404, response.status
+
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/invalidaction"
+ assert_equal 404, response.status
+ end
+
+ def test_fetches_domain_delete_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION])
+ @domain.reload
+
+ assert @domain.registrant_delete_confirmable?(@token)
+
+ get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: "hospital.test",
+ current_registrant: {
+ name: @registrant.name,
+ ident: @registrant.ident,
+ country: @registrant.ident_country_code
+ }
+ }
+
+ assert_equal expected_body, res
+ end
+
+ def test_approves_domain_delete_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION])
+ @domain.reload
+
+ assert @domain.registrant_delete_confirmable?(@token)
+
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/confirmed"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: @domain.name,
+ current_registrant: {
+ name: @registrant.name,
+ ident: @registrant.ident,
+ country: @registrant.ident_country_code
+ },
+ status: 'confirmed'
+ }
+
+ assert_equal expected_body, res
+ end
+
+ def test_rejects_domain_delete_request
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION])
+ @domain.reload
+
+ assert @domain.registrant_delete_confirmable?(@token)
+
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/rejected"
+ assert_equal(200, response.status)
+
+ res = JSON.parse(response.body, symbolize_names: true)
+ expected_body = {
+ domain_name: @domain.name,
+ current_registrant: {
+ name: @registrant.name,
+ ident: @registrant.ident,
+ country: @registrant.ident_country_code
+ },
+ status: 'rejected'
+ }
+
+ assert_equal expected_body, res
+ end
+
+ def test_domain_delete_requires_valid_attributes
+ pending_json = { new_registrant_id: @new_registrant.id,
+ new_registrant_name: @new_registrant.name,
+ new_registrant_email: @new_registrant.email,
+ current_user_id: @user.id }
+
+ @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION])
+ @domain.reload
+
+ get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/123"
+ assert_equal 401, response.status
+
+ get "/api/v1/registrant/confirms/aohldfjg.ee/delete/123"
+ assert_equal 404, response.status
+
+ post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/invalidaction"
+ assert_equal 404, response.status
+ end
+ #def test_get_non_existent_domain_details_by_uuid
+ # get '/api/v1/registrant/domains/random-uuid', headers: @auth_headers
+ # assert_equal(404, response.status)
+
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+ # assert_equal({ errors: [base: ['Domain not found']] }, response_json)
+ #end
+
+ #def test_root_returns_domain_list
+ # get '/api/v1/registrant/domains', headers: @auth_headers
+ # assert_equal(200, response.status)
+
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+ # array_of_domain_names = response_json.map { |x| x[:name] }
+ # assert(array_of_domain_names.include?('hospital.test'))
+
+ # array_of_domain_registrars = response_json.map { |x| x[:registrar] }
+ # assert(array_of_domain_registrars.include?({name: 'Good Names', website: nil}))
+ #end
+
+ #def test_root_accepts_limit_and_offset_parameters
+ # get '/api/v1/registrant/domains', params: { 'limit' => 2, 'offset' => 0 },
+ # headers: @auth_headers
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+
+ # assert_equal(200, response.status)
+ # assert_equal(2, response_json.count)
+
+ # get '/api/v1/registrant/domains', headers: @auth_headers
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+
+ # assert_equal(4, response_json.count)
+ #end
+
+ #def test_root_does_not_accept_limit_higher_than_200
+ # get '/api/v1/registrant/domains', params: { 'limit' => 400, 'offset' => 0 },
+ # headers: @auth_headers
+
+ # assert_equal(400, response.status)
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+ # assert_equal({ errors: [{ limit: ['parameter is out of range'] }] }, response_json)
+ #end
+
+ #def test_root_does_not_accept_offset_lower_than_0
+ # get '/api/v1/registrant/domains', params: { 'limit' => 200, 'offset' => "-10" },
+ # headers: @auth_headers
+
+ # assert_equal(400, response.status)
+ # response_json = JSON.parse(response.body, symbolize_names: true)
+ # assert_equal({ errors: [{ offset: ['parameter is out of range'] }] }, response_json)
+ #end
+
+ #def test_root_returns_401_without_authorization
+ # get '/api/v1/registrant/domains'
+ # assert_equal(401, response.status)
+ # json_body = JSON.parse(response.body, symbolize_names: true)
+
+ # assert_equal({ errors: [base: ['Not authorized']] }, json_body)
+ #end
+
+ #def test_details_returns_401_without_authorization
+ # get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37'
+ # assert_equal(401, response.status)
+ # json_body = JSON.parse(response.body, symbolize_names: true)
+
+ # assert_equal({ errors: [base: ['Not authorized']] }, json_body)
+ #end
+end
diff --git a/test/integration/epp/contact/info/base_test.rb b/test/integration/epp/contact/info/base_test.rb
index 80dad97e8..4e4a9190e 100644
--- a/test/integration/epp/contact/info/base_test.rb
+++ b/test/integration/epp/contact/info/base_test.rb
@@ -44,7 +44,7 @@ class EppContactInfoBaseTest < EppTestCase
contact: xml_schema).text
end
- def test_hides_password_when_current_registrar_is_not_sponsoring
+ def test_hides_password_and_name_when_current_registrar_is_not_sponsoring
non_sponsoring_registrar = registrars(:goodnames)
@contact.update!(registrar: non_sponsoring_registrar)
@@ -70,6 +70,7 @@ class EppContactInfoBaseTest < EppTestCase
assert_epp_response :completed_successfully
response_xml = Nokogiri::XML(response.body)
assert_nil response_xml.at_xpath('//contact:authInfo', contact: xml_schema)
+ assert_equal 'No access', response_xml.at_xpath('//contact:name', contact: xml_schema).text
end
private
diff --git a/test/integration/epp/domain/delete/base_test.rb b/test/integration/epp/domain/delete/base_test.rb
index bfdfa9f75..56a3cc31e 100644
--- a/test/integration/epp/domain/delete/base_test.rb
+++ b/test/integration/epp/domain/delete/base_test.rb
@@ -35,7 +35,6 @@ class EppDomainDeleteBaseTest < EppTestCase
XML
post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
- # binding.pry
assert_includes Domain.find_by(name: 'invalid.test').statuses, DomainStatus::PENDING_DELETE_CONFIRMATION
assert_epp_response :completed_successfully_action_pending
end
@@ -90,7 +89,9 @@ class EppDomainDeleteBaseTest < EppTestCase
XML
- post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ perform_enqueued_jobs do
+ post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ end
@domain.reload
assert @domain.registrant_verification_asked?
@@ -121,7 +122,9 @@ class EppDomainDeleteBaseTest < EppTestCase
XML
- post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ perform_enqueued_jobs do
+ post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ end
@domain.reload
assert_not @domain.registrant_verification_asked?
@@ -152,7 +155,9 @@ class EppDomainDeleteBaseTest < EppTestCase
XML
- post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ perform_enqueued_jobs do
+ post epp_delete_path, params: { frame: request_xml }, headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ end
@domain.reload
assert_not @domain.registrant_verification_asked?
diff --git a/test/integration/repp/v1/accounts/balance_test.rb b/test/integration/repp/v1/accounts/balance_test.rb
new file mode 100644
index 000000000..785e0aee8
--- /dev/null
+++ b/test/integration/repp/v1/accounts/balance_test.rb
@@ -0,0 +1,22 @@
+require 'test_helper'
+
+class ReppV1BalanceTest < ActionDispatch::IntegrationTest
+ def setup
+ @registrar = users(:api_bestnames)
+ token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_can_query_balance
+ get '/repp/v1/accounts/balance', headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+ assert_equal @registrar.registrar.cash_account.balance.to_s, json[:data][:balance]
+ assert_equal @registrar.registrar.cash_account.currency, json[:data][:currency]
+ end
+end
diff --git a/test/integration/repp/auctions_test.rb b/test/integration/repp/v1/auctions_test.rb
similarity index 100%
rename from test/integration/repp/auctions_test.rb
rename to test/integration/repp/v1/auctions_test.rb
diff --git a/test/integration/repp/v1/base_test.rb b/test/integration/repp/v1/base_test.rb
new file mode 100644
index 000000000..d0baed30e
--- /dev/null
+++ b/test/integration/repp/v1/base_test.rb
@@ -0,0 +1,63 @@
+require 'test_helper'
+
+class ReppV1BaseTest < ActionDispatch::IntegrationTest
+ def setup
+ @registrar = users(:api_bestnames)
+ token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_unauthorized_user_has_no_access
+ get repp_v1_contacts_path
+ response_json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :unauthorized
+ assert_equal 'Invalid authorization information', response_json[:message]
+
+ invalid_token = Base64.encode64("nonexistant:user")
+ headers = { 'Authorization' => "Basic #{invalid_token}" }
+
+ get repp_v1_contacts_path, headers: headers
+ response_json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :unauthorized
+ assert_equal 'Invalid authorization information', response_json[:message]
+ end
+
+ def test_authenticates_valid_user
+ get repp_v1_contacts_path, headers: @auth_headers
+ response_json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ end
+
+ def test_processes_invalid_base64_token_format_properly
+ token = '??as8d9sf kjsdjh klsdfjjf'
+ headers = { 'Authorization' => "Basic #{token}"}
+ get repp_v1_contacts_path, headers: headers
+ response_json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :unauthorized
+ assert_equal 'Invalid authorization information', response_json[:message]
+ end
+
+ def test_takes_ip_whitelist_into_account
+ Setting.api_ip_whitelist_enabled = true
+ Setting.registrar_ip_whitelist_enabled = true
+
+ whiteip = white_ips(:one)
+ whiteip.update(ipv4: '1.1.1.1')
+
+ get repp_v1_contacts_path, headers: @auth_headers
+ response_json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :unauthorized
+ assert_equal 2202, response_json[:code]
+ assert response_json[:message].include? 'Access denied from IP'
+
+ Setting.api_ip_whitelist_enabled = false
+ Setting.registrar_ip_whitelist_enabled = false
+ end
+end
diff --git a/test/integration/repp/v1/contacts/check_test.rb b/test/integration/repp/v1/contacts/check_test.rb
new file mode 100644
index 000000000..be0d979b1
--- /dev/null
+++ b/test/integration/repp/v1/contacts/check_test.rb
@@ -0,0 +1,30 @@
+require 'test_helper'
+
+class ReppV1ContactsCheckTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_code_based_check_returns_true_for_available_contact
+ get '/repp/v1/contacts/check/nonexistant:code', headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 'nonexistant:code', json[:data][:contact][:id]
+ assert_equal true, json[:data][:contact][:available]
+ end
+
+ def test_code_based_check_returns_true_for_available_contact
+ contact = contacts(:jack)
+ get "/repp/v1/contacts/check/#{contact.code}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal contact.code, json[:data][:contact][:id]
+ assert_equal false, json[:data][:contact][:available]
+ end
+end
diff --git a/test/integration/repp/v1/contacts/create_test.rb b/test/integration/repp/v1/contacts/create_test.rb
new file mode 100644
index 000000000..f30bc368f
--- /dev/null
+++ b/test/integration/repp/v1/contacts/create_test.rb
@@ -0,0 +1,156 @@
+require 'test_helper'
+
+class ReppV1ContactsCreateTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_creates_new_contact
+ request_body = {
+ "contact": {
+ "name": "Donald Trump",
+ "phone": "+372.51111112",
+ "email": "donald@trumptower.com",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "EE",
+ "ident": "39708290069"
+ }
+ }
+ }
+
+ post '/repp/v1/contacts', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ contact = Contact.find_by(code: json[:data][:contact][:id])
+ assert contact.present?
+
+ assert_equal(request_body[:contact][:name], contact.name)
+ assert_equal(request_body[:contact][:phone], contact.phone)
+ assert_equal(request_body[:contact][:email], contact.email)
+ assert_equal(request_body[:contact][:ident][:ident_type], contact.ident_type)
+ assert_equal(request_body[:contact][:ident][:ident_country_code], contact.ident_country_code)
+ assert_equal(request_body[:contact][:ident][:ident], contact.ident)
+ end
+
+ def test_removes_postal_info_when_contact_created
+ request_body = {
+ "contact": {
+ "name": "Donald Trump",
+ "phone": "+372.51111111",
+ "email": "donald@trump.com",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "EE",
+ "ident": "39708290069"
+ },
+ "addr": {
+ "city": "Tallinn",
+ "street": "Wismari 13",
+ "zip": "12345",
+ "country_code": "EE"
+ }
+ }
+ }
+
+ post '/repp/v1/contacts', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1100, json[:code]
+ assert_equal 'Command completed successfully; Postal address data discarded', json[:message]
+
+ contact = Contact.find_by(code: json[:data][:contact][:id])
+ assert contact.present?
+
+ assert_nil contact.city
+ assert_nil contact.street
+ assert_nil contact.zip
+ assert_nil contact.country_code
+ end
+
+ def test_requires_contact_address_when_processing_enabled
+ Setting.address_processing = true
+
+ request_body = {
+ "contact": {
+ "name": "Donald Trump",
+ "phone": "+372.51111112",
+ "email": "donald@trumptower.com",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "EE",
+ "ident": "39708290069"
+ }
+ }
+ }
+
+ post '/repp/v1/contacts', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2003, json[:code]
+ assert json[:message].include? 'param is missing or the value is empty'
+
+ Setting.address_processing = false
+ end
+
+ def test_validates_ident_code
+ request_body = {
+ "contact": {
+ "name": "Donald Trump",
+ "phone": "+372.51111112",
+ "email": "donald@trumptower.com",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "EE",
+ "ident": "123123123"
+ }
+ }
+ }
+
+ post '/repp/v1/contacts', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2005, json[:code]
+ assert json[:message].include? 'Ident code does not conform to national identification number format'
+ end
+
+ def test_attaches_legaldoc_if_present
+ request_body = {
+ "contact": {
+ "name": "Donald Trump",
+ "phone": "+372.51111112",
+ "email": "donald@trumptower.com",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "EE",
+ "ident": "39708290069"
+ },
+ },
+ "legal_document": {
+ "type": "pdf",
+ "body": "#{'test' * 2000}"
+ }
+ }
+
+ post '/repp/v1/contacts', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ contact = Contact.find_by(code: json[:data][:contact][:id])
+ assert contact.legal_documents.any?
+ end
+end
diff --git a/test/integration/repp/v1/contacts/delete_test.rb b/test/integration/repp/v1/contacts/delete_test.rb
new file mode 100644
index 000000000..07438d8af
--- /dev/null
+++ b/test/integration/repp/v1/contacts/delete_test.rb
@@ -0,0 +1,47 @@
+require 'test_helper'
+
+class ReppV1ContactsDeleteTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_deletes_unassociated_contact
+ contact = contacts(:invalid_email)
+ delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+ end
+
+ def test_can_not_delete_associated_contact
+ contact = contacts(:john)
+ delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2305, json[:code]
+ assert_equal 'Object association prohibits operation [domains]', json[:message]
+ end
+
+ def test_handles_unknown_contact
+ delete "/repp/v1/contacts/definitely:unexistant", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ end
+
+ def test_can_not_destroy_other_registrar_contact
+ contact = contacts(:jack)
+
+ delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ end
+end
diff --git a/test/integration/repp/v1/contacts/list_test.rb b/test/integration/repp/v1/contacts/list_test.rb
new file mode 100644
index 000000000..31c4baaf9
--- /dev/null
+++ b/test/integration/repp/v1/contacts/list_test.rb
@@ -0,0 +1,55 @@
+require 'test_helper'
+
+class ReppV1ContactsListTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_returns_registrar_contacts
+ get repp_v1_contacts_path, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal @user.registrar.contacts.count, json[:total_number_of_records]
+ assert_equal @user.registrar.contacts.count, json[:contacts].length
+
+ assert json[:contacts][0].is_a? String
+ end
+
+
+ def test_returns_detailed_registrar_contacts
+ get repp_v1_contacts_path(details: true), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal @user.registrar.contacts.count, json[:total_number_of_records]
+ assert_equal @user.registrar.contacts.count, json[:contacts].length
+
+ assert json[:contacts][0].is_a? Hash
+ end
+
+ def test_respects_limit
+ get repp_v1_contacts_path(details: true, limit: 2), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal 2, json[:contacts].length
+ end
+
+ def test_respects_offset
+ offset = 1
+ get repp_v1_contacts_path(details: true, offset: offset), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal (@user.registrar.contacts.count - offset), json[:contacts].length
+ end
+end
diff --git a/test/integration/repp/v1/contacts/show_test.rb b/test/integration/repp/v1/contacts/show_test.rb
new file mode 100644
index 000000000..4a6f5b615
--- /dev/null
+++ b/test/integration/repp/v1/contacts/show_test.rb
@@ -0,0 +1,45 @@
+require 'test_helper'
+
+class ReppV1ContactsShowTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_returns_error_when_not_found
+ get repp_v1_contact_path(id: 'definitelynotexistant'), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ assert_equal 2303, json[:code]
+ assert_equal 'Object does not exist', json[:message]
+ end
+
+ def test_shows_existing_contact
+ contact = @user.registrar.contacts.first
+
+ get repp_v1_contact_path(id: contact.code), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal contact.code, json[:data][:id]
+ end
+
+ def test_can_not_access_out_of_scope_contacts
+ # Contact of registrar goodnames, we're using bestnames API credentials
+ contact = contacts(:jack)
+
+ get repp_v1_contact_path(id: contact.code), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ assert_equal 2303, json[:code]
+ assert_equal 'Object does not exist', json[:message]
+ end
+end
diff --git a/test/integration/repp/v1/contacts/update_test.rb b/test/integration/repp/v1/contacts/update_test.rb
new file mode 100644
index 000000000..cf27f98da
--- /dev/null
+++ b/test/integration/repp/v1/contacts/update_test.rb
@@ -0,0 +1,119 @@
+require 'test_helper'
+
+class ReppV1ContactsUpdateTest < ActionDispatch::IntegrationTest
+ def setup
+ @contact = contacts(:john)
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_updates_contact
+ request_body = {
+ "contact": {
+ "email": "donaldtrump@yandex.ru"
+ }
+ }
+
+ put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ contact = Contact.find_by(code: json[:data][:contact][:id])
+ assert contact.present?
+
+ assert_equal(request_body[:contact][:email], contact.email)
+ end
+
+ def test_removes_postal_info_when_updated
+ request_body = {
+ "contact": {
+ "addr": {
+ "city": "Tallinn",
+ "street": "Wismari 13",
+ "zip": "12345",
+ "country_code": "EE"
+ }
+ }
+ }
+
+ put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1100, json[:code]
+ assert_equal 'Command completed successfully; Postal address data discarded', json[:message]
+
+ contact = Contact.find_by(code: json[:data][:contact][:id])
+ assert contact.present?
+
+ assert_nil contact.city
+ assert_nil contact.street
+ assert_nil contact.zip
+ assert_nil contact.country_code
+ end
+
+ def test_can_not_change_ident_code
+ request_body = {
+ "contact": {
+ "name": "Donald Trumpster",
+ "ident": {
+ "ident_type": "priv",
+ "ident_country_code": "US",
+ "ident": "12345"
+ }
+ }
+ }
+
+ put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ @contact.reload
+ assert_not @contact.ident == 12345
+ assert_response :bad_request
+ assert_equal 2308, json[:code]
+ assert json[:message].include? 'Ident update is not allowed. Consider creating new contact object'
+ end
+
+ def test_attaches_legaldoc_if_present
+ request_body = {
+ "contact": {
+ "email": "donaldtrump@yandex.ru"
+ },
+ "legal_document": {
+ "type": "pdf",
+ "body": "#{'test' * 2000}"
+ }
+ }
+
+ put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ @contact.reload
+ assert @contact.legal_documents.any?
+ end
+
+ def test_returns_error_if_ident_wrong_format
+ request_body = {
+ "contact": {
+ "ident": "123"
+ }
+ }
+
+ put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2308, json[:code]
+ assert_equal 'Ident update is not allowed. Consider creating new contact object', json[:message]
+ end
+end
diff --git a/test/integration/repp/v1/domains/contact_replacement_test.rb b/test/integration/repp/v1/domains/contact_replacement_test.rb
new file mode 100644
index 000000000..3cbd9eb8e
--- /dev/null
+++ b/test/integration/repp/v1/domains/contact_replacement_test.rb
@@ -0,0 +1,65 @@
+require 'test_helper'
+
+class ReppV1DomainsContactReplacementTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_replaces_tech_contact_with_new_one
+ replaceable_contact = contacts(:william)
+ replacing_contact = contacts(:jane)
+
+ payload = {
+ "current_contact_id": replaceable_contact.code,
+ "new_contact_id": replacing_contact.code
+ }
+
+ patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert json[:data][:affected_domains].include? 'airport.test'
+ assert json[:data][:affected_domains].include? 'shop.test'
+
+ assert_empty json[:data][:skipped_domains]
+ end
+
+ def test_tech_contact_id_must_differ
+ replaceable_contact = contacts(:william)
+ replacing_contact = contacts(:william)
+
+ payload = {
+ "current_contact_id": replaceable_contact.code,
+ "new_contact_id": replacing_contact.code
+ }
+
+ patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2304, json[:code]
+ assert_equal 'New contact must be different from current', json[:message]
+ end
+
+ def test_contact_codes_must_be_valid
+ payload = {
+ "current_contact_id": 'dfgsdfg',
+ "new_contact_id": 'vvv'
+ }
+
+ patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ assert_equal 2303, json[:code]
+ assert_equal 'Object does not exist', json[:message]
+ end
+
+end
diff --git a/test/integration/repp/v1/domains/list_test.rb b/test/integration/repp/v1/domains/list_test.rb
new file mode 100644
index 000000000..bee390e5c
--- /dev/null
+++ b/test/integration/repp/v1/domains/list_test.rb
@@ -0,0 +1,54 @@
+require 'test_helper'
+
+class ReppV1DomainsListTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_returns_registrar_domains
+ get repp_v1_domains_path, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records]
+ assert_equal @user.registrar.domains.count, json[:data][:domains].length
+
+ assert json[:data][:domains][0].is_a? String
+ end
+
+ def test_returns_detailed_registrar_domains
+ get repp_v1_domains_path(details: true), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records]
+ assert_equal @user.registrar.domains.count, json[:data][:domains].length
+
+ assert json[:data][:domains][0].is_a? Hash
+ end
+
+ def test_respects_limit
+ get repp_v1_domains_path(details: true, limit: 2), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal 2, json[:data][:domains].length
+ end
+
+ def test_respects_offset
+ offset = 1
+ get repp_v1_domains_path(details: true, offset: offset), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+
+ assert_equal (@user.registrar.domains.count - offset), json[:data][:domains].length
+ end
+end
diff --git a/test/integration/repp/v1/domains/transfer_info_test.rb b/test/integration/repp/v1/domains/transfer_info_test.rb
new file mode 100644
index 000000000..f57675b06
--- /dev/null
+++ b/test/integration/repp/v1/domains/transfer_info_test.rb
@@ -0,0 +1,40 @@
+require 'test_helper'
+
+class ReppV1DomainsTransferInfoTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+ @domain = domains(:shop)
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_can_query_domain_info
+ headers = @auth_headers
+ headers['Auth-Code'] = @domain.transfer_code
+
+ get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+ assert_equal @domain.name, json[:data][:domain]
+ assert json[:data][:registrant].present?
+ assert json[:data][:admin_contacts].present?
+ assert json[:data][:tech_contacts].present?
+ end
+
+ def test_respects_domain_authorization_code
+ headers = @auth_headers
+ headers['Auth-Code'] = 'jhfgifhdg'
+
+ get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2202, json[:code]
+ assert_equal 'Authorization error', json[:message]
+ assert_empty json[:data]
+ end
+end
diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb
new file mode 100644
index 000000000..46d5c30c4
--- /dev/null
+++ b/test/integration/repp/v1/domains/transfer_test.rb
@@ -0,0 +1,127 @@
+require 'test_helper'
+
+class ReppV1DomainsTransferTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+ @domain = domains(:hospital)
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_transfers_domain
+ payload = {
+ "data": {
+ "domain_transfers": [
+ { "domain_name": @domain.name, "transfer_code": @domain.transfer_code }
+ ]
+ }
+ }
+ post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal @domain.name, json[:data][:success][0][:domain_name]
+
+ @domain.reload
+
+ assert @domain.registrar = @user.registrar
+ end
+
+ def test_does_not_transfer_domain_if_not_transferable
+ @domain.schedule_force_delete(type: :fast_track)
+
+ payload = {
+ "data": {
+ "domain_transfers": [
+ { "domain_name": @domain.name, "transfer_code": @domain.transfer_code }
+ ]
+ }
+ }
+
+ post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal 'Object status prohibits operation', json[:data][:failed][0][:errors][0][:msg]
+
+ @domain.reload
+
+ assert_not @domain.registrar == @user.registrar
+ end
+
+ def test_does_not_transfer_domain_with_invalid_auth_code
+ payload = {
+ "data": {
+ "domain_transfers": [
+ { "domain_name": @domain.name, "transfer_code": "sdfgsdfg" }
+ ]
+ }
+ }
+ post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal "Invalid authorization information", json[:data][:failed][0][:errors][0][:msg]
+ end
+
+ def test_does_not_transfer_domain_to_same_registrar
+ @domain.update!(registrar: @user.registrar)
+
+ payload = {
+ "data": {
+ "domain_transfers": [
+ { "domain_name": @domain.name, "transfer_code": @domain.transfer_code }
+ ]
+ }
+ }
+
+ post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal 'Domain already belongs to the querying registrar', json[:data][:failed][0][:errors][0][:msg]
+
+ @domain.reload
+
+ assert @domain.registrar == @user.registrar
+ end
+
+ def test_does_not_transfer_domain_if_discarded
+ @domain.update!(statuses: [DomainStatus::DELETE_CANDIDATE])
+
+ payload = {
+ "data": {
+ "domain_transfers": [
+ { "domain_name": @domain.name, "transfer_code": @domain.transfer_code }
+ ]
+ }
+ }
+
+ post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal 'Object is not eligible for transfer', json[:data][:failed][0][:errors][0][:msg]
+
+ @domain.reload
+
+ assert_not @domain.registrar == @user.registrar
+ end
+end
diff --git a/test/integration/repp/v1/registrar/nameservers_test.rb b/test/integration/repp/v1/registrar/nameservers_test.rb
new file mode 100644
index 000000000..f01769dfb
--- /dev/null
+++ b/test/integration/repp/v1/registrar/nameservers_test.rb
@@ -0,0 +1,77 @@
+require 'test_helper'
+
+class ReppV1RegistrarNameserversTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_updates_nameserver_values
+ nameserver = nameservers(:shop_ns1)
+ payload = {
+ "data": {
+ "id": nameserver.hostname,
+ "type": "nameserver",
+ "attributes": {
+ "hostname": "#{nameserver.hostname}.test",
+ "ipv4": ["1.1.1.1"]
+ }
+ }
+ }
+
+ put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+ assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes])
+ assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes])
+ assert json[:data][:affected_domains].include? 'airport.test'
+ assert json[:data][:affected_domains].include? 'shop.test'
+ end
+
+ def test_nameserver_with_hostname_must_exist
+ payload = {
+ "data": {
+ "id": 'ns.nonexistant.test',
+ "type": "nameserver",
+ "attributes": {
+ "hostname": "ns1.dn.test",
+ "ipv4": ["1.1.1.1"]
+ }
+ }
+ }
+
+ put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :not_found
+ assert_equal 2303, json[:code]
+ assert_equal 'Object does not exist', json[:message]
+ end
+
+ def test_ip_must_be_in_correct_format
+ nameserver = nameservers(:shop_ns1)
+ payload = {
+ "data": {
+ "id": nameserver.hostname,
+ "type": "nameserver",
+ "attributes": {
+ "hostname": "#{nameserver.hostname}.test",
+ "ipv6": ["1.1.1.1"]
+ }
+ }
+ }
+
+ put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2005, json[:code]
+ assert_equal 'IPv6 is invalid [ipv6]', json[:message]
+ end
+end
diff --git a/test/integration/repp/retained_domains_test.rb b/test/integration/repp/v1/retained_domains_test.rb
similarity index 100%
rename from test/integration/repp/retained_domains_test.rb
rename to test/integration/repp/v1/retained_domains_test.rb
diff --git a/test/models/domain/force_delete_test.rb b/test/models/domain/force_delete_test.rb
index f0723c326..7c205fa74 100644
--- a/test/models/domain/force_delete_test.rb
+++ b/test/models/domain/force_delete_test.rb
@@ -1,18 +1,20 @@
require 'test_helper'
-class NewDomainForceDeleteTest < ActiveSupport::TestCase
+class ForceDeleteTest < ActionMailer::TestCase
setup do
@domain = domains(:shop)
Setting.redemption_grace_period = 30
+ ActionMailer::Base.deliveries.clear
end
def test_schedules_force_delete_fast_track
assert_not @domain.force_delete_scheduled?
travel_to Time.zone.parse('2010-07-05')
- @domain.schedule_force_delete(type: :fast_track)
+ @domain.schedule_force_delete(type: :fast_track, notify_by_email: true)
@domain.reload
+ assert_emails 1
assert @domain.force_delete_scheduled?
assert_equal Date.parse('2010-08-20'), @domain.force_delete_date.to_date
assert_equal Date.parse('2010-07-06'), @domain.force_delete_start.to_date
@@ -111,9 +113,12 @@ class NewDomainForceDeleteTest < ActiveSupport::TestCase
def test_force_delete_cannot_be_scheduled_when_a_domain_is_discarded
@domain.update!(statuses: [DomainStatus::DELETE_CANDIDATE])
- assert_raises StandardError do
- @domain.schedule_force_delete(type: :fast_track)
- end
+ result = ForceDeleteInteraction::SetForceDelete.run(domain: @domain, type: :fast_track)
+
+ assert_not result.valid?
+ assert_not @domain.force_delete_scheduled?
+ message = ["Force delete procedure cannot be scheduled while a domain is discarded"]
+ assert_equal message, result.errors.messages[:domain]
end
def test_cancels_force_delete
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index 4a9240f57..cc88cf35f 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -414,7 +414,7 @@ class DomainTest < ActiveSupport::TestCase
force_delete_date: nil)
@domain.update(template_name: 'legal_person')
travel_to Time.zone.parse('2010-07-05')
- @domain.schedule_force_delete(type: :fast_track)
+ ForceDeleteInteraction::SetForceDelete.run!(domain: @domain, type: :fast_track)
assert(@domain.force_delete_scheduled?)
other_registrant = Registrant.find_by(code: 'jane-001')
@domain.pending_json['new_registrant_id'] = other_registrant.id
diff --git a/test/models/whois/record_test.rb b/test/models/whois/record_test.rb
index e900a4965..d06b23cae 100644
--- a/test/models/whois/record_test.rb
+++ b/test/models/whois/record_test.rb
@@ -70,6 +70,6 @@ class Whois::RecordTest < ActiveSupport::TestCase
end
def registration_deadline
- Time.zone.now + 10.days
+ @registration_deadline ||= Time.zone.now + 10.days
end
end
diff --git a/test/system/registrar_area/base_test.rb b/test/system/registrar_area/base_test.rb
index 67b19a044..2529b009a 100644
--- a/test/system/registrar_area/base_test.rb
+++ b/test/system/registrar_area/base_test.rb
@@ -29,6 +29,16 @@ class RegistrarAreaBaseTestTest < ApplicationSystemTestCase
assert_button 'Login'
end
+ def test_user_can_access_when_ip_is_whitelisted_with_subnet
+ white_ips(:one).update!(ipv4: '127.0.0.1/32', interfaces: [WhiteIp::REGISTRAR])
+ Setting.registrar_ip_whitelist_enabled = true
+
+ visit new_registrar_user_session_url
+
+ assert_no_text 'Access denied from IP 127.0.0.1'
+ assert_button 'Login'
+ end
+
def test_user_can_access_when_ip_is_not_whitelisted_and_whitelist_is_disabled
Setting.registrar_ip_whitelist_enabled = false
WhiteIp.delete_all
diff --git a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb
index 69b755499..a531ff4dc 100644
--- a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb
+++ b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb
@@ -6,9 +6,9 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase
end
def test_transfer_multiple_domains_in_bulk
- request_body = { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } }
+ request_body = { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } }
headers = { 'Content-type' => Mime[:json] }
- request_stub = stub_request(:post, /domain_transfers/).with(body: request_body,
+ request_stub = stub_request(:post, /domains\/transfer/).with(body: request_body,
headers: headers,
basic_auth: ['test_goodnames', 'testtest'])
.to_return(body: { data: [{
@@ -29,7 +29,7 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase
def test_fail_gracefully
body = { errors: [{ title: 'epic fail' }] }.to_json
headers = { 'Content-type' => Mime[:json] }
- stub_request(:post, /domain_transfers/).to_return(status: 400, body: body, headers: headers)
+ stub_request(:post, /domains\/transfer/).to_return(status: 400, body: body, headers: headers)
visit registrar_domains_url
click_link 'Bulk change'