diff --git a/.codeclimate.yml b/.codeclimate.yml
index 33801cc17..77b7917a2 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -15,6 +15,7 @@ plugins:
duplication:
enabled: true
config:
+ count_threshold: 3
languages:
ruby:
mass_threshold: 100
@@ -38,6 +39,9 @@ checks:
method-lines:
config:
threshold: 40
+ method-count:
+ config:
+ threshold: 25
exclude_patterns:
- "app/models/version/"
- "bin/"
diff --git a/Gemfile b/Gemfile
index de523bf4f..be4a9756b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,6 +2,7 @@ source 'https://rubygems.org'
# core
gem 'active_interaction', '~> 3.8'
+gem 'apipie-rails', '~> 0.5.18'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'iso8601', '0.12.1' # for dates and times
gem 'rails', '~> 6.0'
@@ -78,7 +79,6 @@ gem 'wkhtmltopdf-binary', '~> 0.12.5.1'
gem 'directo', github: 'internetee/directo', branch: 'master'
-
group :development, :test do
gem 'pry', '0.10.1'
gem 'puma'
diff --git a/Gemfile.lock b/Gemfile.lock
index 089c6660f..563c39fdc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -145,6 +145,8 @@ GEM
akami (1.3.1)
gyoku (>= 0.4.0)
nokogiri
+ apipie-rails (0.5.18)
+ rails (>= 4.1)
attr_required (1.0.1)
autoprefixer-rails (10.2.4.0)
execjs
@@ -495,11 +497,13 @@ GEM
PLATFORMS
ruby
+ x86_64-linux
DEPENDENCIES
active_interaction (~> 3.8)
activerecord-import
airbrake
+ apipie-rails (~> 0.5.18)
aws-sdk-sesv2 (~> 1.16)
bootsnap (>= 1.1.0)
bootstrap-sass (~> 3.4)
@@ -558,4 +562,4 @@ DEPENDENCIES
wkhtmltopdf-binary (~> 0.12.5.1)
BUNDLED WITH
- 2.1.4
+ 2.2.2
diff --git a/app/controllers/epp/domains_controller.rb b/app/controllers/epp/domains_controller.rb
index d9a8b2b5d..47a40857a 100644
--- a/app/controllers/epp/domains_controller.rb
+++ b/app/controllers/epp/domains_controller.rb
@@ -1,3 +1,4 @@
+require 'deserializers/xml/domain_delete'
module Epp
class DomainsController < BaseController
before_action :find_domain, only: %i[info renew update transfer delete]
@@ -26,104 +27,39 @@ module Epp
end
def create
- authorize! :create, Epp::Domain
+ authorize!(:create, Epp::Domain)
- if Domain.release_to_auction
- request_domain_name = params[:parsed_frame].css('name').text.strip.downcase
- domain_name = DNS::DomainName.new(SimpleIDN.to_unicode(request_domain_name))
+ registrar_id = current_user.registrar.id
+ @domain = Epp::Domain.new
+ data = ::Deserializers::Xml::DomainCreate.new(params[:parsed_frame], registrar_id).call
+ action = Actions::DomainCreate.new(@domain, data)
- if domain_name.at_auction?
- epp_errors << {
- code: '2306',
- msg: 'Parameter value policy error: domain is at auction',
- }
- handle_errors
- return
- elsif domain_name.awaiting_payment?
- epp_errors << {
- code: '2003',
- msg: 'Required parameter missing; reserved>pw element required for reserved domains',
- }
- handle_errors
- return
- elsif domain_name.pending_registration?
- registration_code = params[:parsed_frame].css('reserved > pw').text
-
- if registration_code.empty?
- epp_errors << {
- code: '2003',
- msg: 'Required parameter missing; reserved>pw element is required',
- }
- handle_errors
- return
- end
-
- unless domain_name.available_with_code?(registration_code)
- epp_errors << {
- code: '2202',
- msg: 'Invalid authorization information; invalid reserved>pw value',
- }
- handle_errors
- return
- end
- end
- end
-
- @domain = Epp::Domain.new_from_epp(params[:parsed_frame], current_user)
- handle_errors(@domain) and return if @domain.errors.any?
- @domain.valid?
- @domain.errors.delete(:name_dirty) if @domain.errors[:puny_label].any?
- handle_errors(@domain) and return if @domain.errors.any?
- handle_errors and return unless balance_ok?('create') # loads pricelist in this method
-
- ActiveRecord::Base.transaction do
- @domain.add_legal_file_to_new(params[:parsed_frame])
-
- if @domain.save # TODO: Maybe use validate: false here because we have already validated the domain?
- current_user.registrar.debit!({
- sum: @domain_pricelist.price.amount,
- description: "#{I18n.t('create')} #{@domain.name}",
- activity_type: AccountActivity::CREATE,
- price: @domain_pricelist
- })
-
- if Domain.release_to_auction && domain_name.pending_registration?
- active_auction = Auction.find_by(domain: domain_name.to_s,
- status: Auction.statuses[:payment_received])
- active_auction.domain_registered!
- end
- Dispute.close_by_domain(@domain.name)
- render_epp_response '/epp/domains/create'
- else
- handle_errors(@domain)
- end
- end
+ action.call ? render_epp_response('/epp/domains/create') : handle_errors(@domain)
end
def update
- authorize! :update, @domain, @password
+ authorize!(:update, @domain, @password)
- updated = @domain.update(params[:parsed_frame], current_user)
- (handle_errors(@domain) && return) unless updated
+ registrar_id = current_user.registrar.id
+ update_params = ::Deserializers::Xml::DomainUpdate.new(params[:parsed_frame],
+ registrar_id).call
+ action = Actions::DomainUpdate.new(@domain, update_params, false)
+ (handle_errors(@domain) and return) unless action.call
pending = @domain.epp_pending_update.present?
- render_epp_response "/epp/domains/success#{'_pending' if pending}"
+ render_epp_response("/epp/domains/success#{'_pending' if pending}")
end
def delete
- authorize! :delete, @domain, @password
+ authorize!(:delete, @domain, @password)
+ frame = params[:parsed_frame]
+ delete_params = ::Deserializers::Xml::DomainDelete.new(frame).call
+ action = Actions::DomainDelete.new(@domain, delete_params, current_user.registrar)
- (handle_errors(@domain) && return) unless @domain.can_be_deleted?
+ (handle_errors(@domain) and return) unless action.call
- if @domain.epp_destroy(params[:parsed_frame], current_user.id)
- if @domain.epp_pending_delete.present?
- render_epp_response '/epp/domains/success_pending'
- else
- render_epp_response '/epp/domains/success'
- end
- else
- handle_errors(@domain)
- end
+ pending = @domain.epp_pending_delete.present?
+ render_epp_response("/epp/domains/success#{'_pending' if pending}")
end
def check
@@ -137,42 +73,15 @@ module Epp
def renew
authorize! :renew, @domain
- period_element = params[:parsed_frame].css('period').text
- period = (period_element.to_i == 0) ? 1 : period_element.to_i
- period_unit = Epp::Domain.parse_period_unit_from_frame(params[:parsed_frame]) || 'y'
+ registrar_id = current_user.registrar.id
+ renew_params = ::Deserializers::Xml::Domain.new(params[:parsed_frame],
+ registrar_id).call
- balance_ok?('renew', period, period_unit) # loading pricelist
-
- begin
- ActiveRecord::Base.transaction(isolation: :serializable) do
- @domain.reload
-
- success = @domain.renew(
- params[:parsed_frame].css('curExpDate').text,
- period, period_unit
- )
-
- if success
- unless balance_ok?('renew', period, period_unit)
- handle_errors
- fail ActiveRecord::Rollback
- end
-
- current_user.registrar.debit!({
- sum: @domain_pricelist.price.amount,
- description: "#{I18n.t('renew')} #{@domain.name}",
- activity_type: AccountActivity::RENEW,
- price: @domain_pricelist
- })
-
- render_epp_response '/epp/domains/renew'
- else
- handle_errors(@domain)
- end
- end
- rescue ActiveRecord::StatementInvalid => e
- sleep rand / 100
- retry
+ action = Actions::DomainRenew.new(@domain, renew_params, current_user.registrar)
+ if action.call
+ render_epp_response '/epp/domains/renew'
+ else
+ handle_errors(@domain)
end
end
diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb
index 1e0fde466..388bc9a94 100644
--- a/app/controllers/repp/v1/accounts_controller.rb
+++ b/app/controllers/repp/v1/accounts_controller.rb
@@ -1,6 +1,8 @@
module Repp
module V1
class AccountsController < BaseController
+ api :GET, '/repp/v1/accounts/balance'
+ desc "Get account's balance"
def balance
resp = { balance: current_user.registrar.cash_account.balance,
currency: current_user.registrar.cash_account.currency }
diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb
index a194caf1a..53519195c 100644
--- a/app/controllers/repp/v1/base_controller.rb
+++ b/app/controllers/repp/v1/base_controller.rb
@@ -1,6 +1,6 @@
module Repp
module V1
- class BaseController < ActionController::API
+ class BaseController < ActionController::API # rubocop:disable Metrics/ClassLength
around_action :log_request
before_action :authenticate_user
before_action :validate_webclient_ca
@@ -16,9 +16,12 @@ module Repp
rescue ActiveRecord::RecordNotFound
@response = { code: 2303, message: 'Object does not exist' }
render(json: @response, status: :not_found)
- rescue ActionController::ParameterMissing => e
+ rescue ActionController::ParameterMissing, Apipie::ParamMissing => e
@response = { code: 2003, message: e }
render(json: @response, status: :bad_request)
+ rescue Apipie::ParamInvalid => e
+ @response = { code: 2005, message: e }
+ render(json: @response, status: :bad_request)
ensure
create_repp_log
end
@@ -35,6 +38,16 @@ module Repp
end
# rubocop:enable Metrics/AbcSize
+ def set_domain
+ registrar = current_user.registrar
+ @domain = Epp::Domain.find_by(registrar: registrar, name: params[:domain_id])
+ @domain ||= Epp::Domain.find_by!(registrar: registrar, name_puny: params[:domain_id])
+
+ return @domain if @domain
+
+ raise ActiveRecord::RecordNotFound
+ end
+
def set_paper_trail_whodunnit
::PaperTrail.request.whodunnit = current_user
end
diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb
index acf275c47..92f89a160 100644
--- a/app/controllers/repp/v1/contacts_controller.rb
+++ b/app/controllers/repp/v1/contacts_controller.rb
@@ -1,10 +1,11 @@
require 'serializers/repp/contact'
module Repp
module V1
- class ContactsController < BaseController
+ class ContactsController < BaseController # rubocop:disable Metrics/ClassLength
before_action :find_contact, only: %i[show update destroy]
- ## GET /repp/v1/contacts
+ api :get, '/repp/v1/contacts'
+ desc 'Get all existing contacts'
def index
record_count = current_user.registrar.contacts.count
contacts = showable_contacts(params[:details], params[:limit] || 200,
@@ -13,14 +14,16 @@ module Repp
render(json: @response, status: :ok)
end
- ## GET /repp/v1/contacts/1
+ api :get, '/repp/v1/contacts/:contact_code'
+ desc 'Get a specific contact'
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
+ api :get, '/repp/v1/contacts/check/:contact_code'
+ desc 'Check contact code availability'
def check
contact = Epp::Contact.find_by(code: params[:id])
data = { contact: { id: params[:id], available: contact.nil? } }
@@ -28,7 +31,8 @@ module Repp
render_success(data: data)
end
- ## POST /repp/v1/contacts
+ api :POST, '/repp/v1/contacts'
+ desc 'Create a new contact'
def create
@contact = Epp::Contact.new(contact_params_with_address, current_user.registrar, epp: false)
action = Actions::ContactCreate.new(@contact, params[:legal_document],
@@ -42,7 +46,8 @@ module Repp
render_success(create_update_success_body)
end
- ## PUT /repp/v1/contacts/1
+ api :PUT, '/repp/v1/contacts/:contact_code'
+ desc 'Update existing contact'
def update
action = Actions::ContactUpdate.new(@contact, contact_params_with_address(required: false),
params[:legal_document],
@@ -56,6 +61,8 @@ module Repp
render_success(create_update_success_body)
end
+ api :DELETE, '/repp/v1/contacts/:contact_code'
+ desc 'Delete a specific contact'
def destroy
action = Actions::ContactDelete.new(@contact, params[:legal_document])
unless action.call
diff --git a/app/controllers/repp/v1/domains/contacts_controller.rb b/app/controllers/repp/v1/domains/contacts_controller.rb
index 131615570..3bde107d1 100644
--- a/app/controllers/repp/v1/domains/contacts_controller.rb
+++ b/app/controllers/repp/v1/domains/contacts_controller.rb
@@ -2,6 +2,50 @@ module Repp
module V1
module Domains
class ContactsController < BaseContactsController
+ before_action :set_domain, only: %i[index create destroy]
+
+ def_param_group :contacts_apidoc do
+ param :contacts, Array, required: true, desc: 'Array of new linked contacts' do
+ param :code, String, required: true, desc: 'Contact code'
+ param :type, String, required: true, desc: 'Role of contact (admin/tech)'
+ end
+ end
+
+ api :GET, '/repp/v1/domains/:domain_name/contacts'
+ desc "View domain's admin and tech contacts"
+ def index
+ admin_contacts = @domain.admin_domain_contacts.pluck(:contact_code_cache)
+ tech_contacts = @domain.tech_domain_contacts.pluck(:contact_code_cache)
+
+ data = { admin_contacts: admin_contacts, tech_contacts: tech_contacts }
+ render_success(data: data)
+ end
+
+ api :POST, '/repp/v1/domains/:domain_name/contacts'
+ desc 'Link new contact(s) to domain'
+ param_group :contacts_apidoc
+ def create
+ cta('add')
+ end
+
+ api :DELETE, '/repp/v1/domains/:domain_name/contacts'
+ desc 'Remove contact(s) from domain'
+ param_group :contacts_apidoc
+ def destroy
+ cta('rem')
+ end
+
+ def cta(action = 'add')
+ params[:contacts].each { |c| c[:action] = action }
+ action = Actions::DomainUpdate.new(@domain, contact_create_params, false)
+
+ # rubocop:disable Style/AndOr
+ handle_errors(@domain) and return unless action.call
+ # rubocop:enable Style/AndOr
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
def update
super
@@ -15,6 +59,12 @@ module Repp
@response = { affected_domains: affected, skipped_domains: skipped }
render_success(data: @response)
end
+
+ private
+
+ def contact_create_params
+ params.permit(:domain_id, contacts: [%i[action code type]])
+ end
end
end
end
diff --git a/app/controllers/repp/v1/domains/dnssec_controller.rb b/app/controllers/repp/v1/domains/dnssec_controller.rb
new file mode 100644
index 000000000..fcfaa991a
--- /dev/null
+++ b/app/controllers/repp/v1/domains/dnssec_controller.rb
@@ -0,0 +1,58 @@
+module Repp
+ module V1
+ module Domains
+ class DnssecController < BaseController
+ before_action :set_domain, only: %i[index create destroy]
+
+ def_param_group :dns_keys_apidoc do
+ param :flags, String, required: true, desc: '256 (KSK) or 257 (ZSK)'
+ param :protocol, String, required: true, desc: 'Key protocol (3)'
+ param :alg, String, required: true, desc: 'DNSSEC key algorithm (3,5,6,7,8,10,13,14)'
+ param :public_key, String, required: true, desc: 'DNSSEC public key'
+ end
+
+ api :GET, '/repp/v1/domains/:domain_name/dnssec'
+ desc "View specific domain's DNSSEC keys"
+ def index
+ dnssec_keys = @domain.dnskeys
+ data = { dns_keys: dnssec_keys.as_json(only: %i[flags alg protocol public_key]) }
+ render_success(data: data)
+ end
+
+ api :POST, '/repp/v1/domains/:domain_name/dnssec'
+ desc 'Create a new DNSSEC key(s) for domain'
+ param :dns_keys, Array, required: true, desc: 'Array of new DNSSEC keys' do
+ param_group :dns_keys_apidoc, DnssecController
+ end
+ def create
+ cta('add')
+ end
+
+ api :DELETE, 'repp/v1/domains/:domain_name/dnssec'
+ param :dns_keys, Array, required: true, desc: 'Array of new DNSSEC keys' do
+ param_group :dns_keys_apidoc, DnssecController
+ end
+ def destroy
+ cta('rem')
+ end
+
+ def cta(action = 'add')
+ params[:dns_keys].each { |n| n[:action] = action }
+ action = Actions::DomainUpdate.new(@domain, dnssec_params, false)
+
+ # rubocop:disable Style/AndOr
+ (handle_errors(@domain) and return) unless action.call
+ # rubocop:enable Style/AndOr
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ private
+
+ def dnssec_params
+ params.permit(:domain_id, dns_keys: [%i[action flags protocol alg public_key]])
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb
new file mode 100644
index 000000000..044e36a20
--- /dev/null
+++ b/app/controllers/repp/v1/domains/nameservers_controller.rb
@@ -0,0 +1,61 @@
+module Repp
+ module V1
+ module Domains
+ class NameserversController < BaseController
+ before_action :set_domain, only: %i[index create destroy]
+ before_action :set_nameserver, only: %i[destroy]
+
+ api :GET, '/repp/v1/domains/:domain_name/nameservers'
+ desc "Get domain's nameservers"
+ def index
+ nameservers = @domain.nameservers
+ data = { nameservers: nameservers.as_json(only: %i[hostname ipv4 ipv6]) }
+ render_success(data: data)
+ end
+
+ api :POST, '/repp/v1/domains/:domain_name/nameservers'
+ desc 'Create new nameserver for domain'
+ param :nameservers, Array, required: true, desc: 'Array of new nameservers' do
+ param :hostname, String, required: true, desc: 'Nameserver hostname'
+ param :ipv4, Array, required: false, desc: 'Array of IPv4 values'
+ param :ipv6, Array, required: false, desc: 'Array of IPv6 values'
+ end
+ def create
+ params[:nameservers].each { |n| n[:action] = 'add' }
+ action = Actions::DomainUpdate.new(@domain, nameserver_params, current_user)
+
+ unless action.call
+ handle_errors(@domain)
+ return
+ end
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ api :DELETE, '/repp/v1/domains/:domain/nameservers/:nameserver'
+ desc 'Delete specific nameserver from domain'
+ def destroy
+ nameserver = { nameservers: [{ hostname: params[:id], action: 'rem' }] }
+ action = Actions::DomainUpdate.new(@domain, nameserver, false)
+
+ unless action.call
+ handle_errors(@domain)
+ return
+ end
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ private
+
+ def set_nameserver
+ @nameserver = @domain.nameservers.find_by!(hostname: params[:id])
+ end
+
+ def nameserver_params
+ params.permit(:domain_id, nameservers: [[:hostname, :action, ipv4: [], ipv6: []]])
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/domains/renews_controller.rb b/app/controllers/repp/v1/domains/renews_controller.rb
index 6b016dd86..c356d799a 100644
--- a/app/controllers/repp/v1/domains/renews_controller.rb
+++ b/app/controllers/repp/v1/domains/renews_controller.rb
@@ -4,6 +4,26 @@ module Repp
class RenewsController < BaseController
before_action :validate_renew_period, only: [:bulk_renew]
before_action :select_renewable_domains, only: [:bulk_renew]
+ before_action :set_domain, only: [:create]
+
+ api :POST, 'repp/v1/domains/:domain_name/renew'
+ desc 'Renew domain'
+ param :renew, Hash, required: true, desc: 'Renew parameters' do
+ param :period, Integer, required: true, desc: 'Renew period. Month (m) or year (y)'
+ param :period_unit, String, required: true, desc: 'For how many months or years to renew'
+ param :exp_date, String, required: true, desc: 'Current expiry date for domain'
+ end
+ def create
+ authorize!(:renew, @domain)
+ action = Actions::DomainRenew.new(@domain, renew_params[:renew], current_user.registrar)
+
+ unless action.call
+ handle_errors(@domain)
+ return
+ end
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
def bulk_renew
renew = run_bulk_renew_task(@domains, bulk_renew_params[:renew_period])
@@ -16,6 +36,10 @@ module Repp
private
+ def renew_params
+ params.permit(:domain_id, renew: %i[period period_unit exp_date])
+ end
+
def validate_renew_period
@epp_errors ||= []
periods = Depp::Domain::PERIODS.map { |p| p[1] }
diff --git a/app/controllers/repp/v1/domains/statuses_controller.rb b/app/controllers/repp/v1/domains/statuses_controller.rb
new file mode 100644
index 000000000..ee15655df
--- /dev/null
+++ b/app/controllers/repp/v1/domains/statuses_controller.rb
@@ -0,0 +1,65 @@
+module Repp
+ module V1
+ module Domains
+ class StatusesController < BaseController
+ before_action :set_domain, only: %i[update destroy]
+ before_action :verify_status
+
+ api :DELETE, '/repp/v1/domains/:domain_name/statuses/:status'
+ param :domain_name, String, desc: 'Domain name'
+ param :status, String, desc: 'Status to be removed'
+ desc 'Remove status from specific domain'
+ def destroy
+ return editing_failed unless domain_with_status?(params[:id])
+
+ @domain.statuses = @domain.statuses.delete(params[:id])
+ if @domain.save
+ render_success
+ else
+ handle_errors(@domain)
+ end
+ end
+
+ api :PUT, '/repp/v1/domains/:domain_name/statuses/:status'
+ param :domain_name, String, desc: 'Domain name'
+ param :status, String, desc: 'Status to be added'
+ desc 'Add status to specific domain'
+ def update
+ return editing_failed if domain_with_status?(params[:id])
+
+ @domain.statuses << params[:id]
+ if @domain.save
+ render_success(data: { domain: @domain.name, status: params[:id] })
+ else
+ handle_errors(@domain)
+ end
+ end
+
+ private
+
+ def domain_with_status?(status)
+ @domain.statuses.include?(status)
+ end
+
+ def verify_status
+ allowed_statuses = [DomainStatus::CLIENT_HOLD].freeze
+ stat = params[:id]
+
+ return if allowed_statuses.include?(stat)
+
+ @domain.add_epp_error('2306', nil, nil,
+ "#{I18n.t(:client_side_status_editing_error)}: status #{stat}")
+ handle_errors(@domain)
+ end
+
+ def editing_failed
+ stat = params[:id]
+
+ @domain.add_epp_error('2306', nil, nil,
+ "#{I18n.t(:client_side_status_editing_error)}: status #{stat}")
+ handle_errors(@domain)
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/domains/transfers_controller.rb b/app/controllers/repp/v1/domains/transfers_controller.rb
new file mode 100644
index 000000000..e9474d94d
--- /dev/null
+++ b/app/controllers/repp/v1/domains/transfers_controller.rb
@@ -0,0 +1,39 @@
+module Repp
+ module V1
+ module Domains
+ class TransfersController < BaseController
+ before_action :set_domain, only: [:create]
+
+ api :POST, 'repp/v1/domains/:domain_name/transfer'
+ desc 'Transfer a specific domain'
+ param :transfer, Hash, required: true, desc: 'Renew parameters' do
+ param :transfer_code, String, required: true, desc: 'Renew period. Month (m) or year (y)'
+ end
+ def create
+ action = Actions::DomainTransfer.new(@domain, transfer_params[:transfer][:transfer_code],
+ current_user.registrar)
+
+ unless action.call
+ handle_errors(@domain)
+ return
+ end
+
+ render_success(data: { domain: { name: @domain.name, type: 'domain_transfer' } })
+ end
+
+ private
+
+ def set_domain
+ domain_id = transfer_params[:domain_id]
+ h = {}
+ h[domain_id.match?(/\A[0-9]+\z/) ? :id : :name] = domain_id
+ @domain = Epp::Domain.find_by!(h)
+ end
+
+ def transfer_params
+ params.permit(:domain_id, transfer: [:transfer_code])
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb
index 3715e78ed..b058f4505 100644
--- a/app/controllers/repp/v1/domains_controller.rb
+++ b/app/controllers/repp/v1/domains_controller.rb
@@ -1,16 +1,96 @@
+require 'serializers/repp/domain'
module Repp
module V1
- class DomainsController < BaseController
- before_action :set_authorized_domain, only: [:transfer_info]
+ class DomainsController < BaseController # rubocop:disable Metrics/ClassLength
+ before_action :set_authorized_domain, only: %i[transfer_info destroy]
+ before_action :validate_registrar_authorization, only: %i[transfer_info destroy]
+ before_action :forward_registrar_id, only: %i[create update destroy]
+ before_action :set_domain, only: %i[update]
+ api :GET, '/repp/v1/domains'
+ desc 'Get all existing domains'
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 })
+ render_success(data: { domains: serialized_domains(domains),
+ total_number_of_records: records.count })
end
+ api :GET, '/repp/v1/domains/:domain_name'
+ desc 'Get a specific domain'
+ def show
+ @domain = Epp::Domain.find_by!(name: params[:id])
+ sponsor = @domain.registrar == current_user.registrar
+ render_success(data: { domain: Serializers::Repp::Domain.new(@domain,
+ sponsored: sponsor).to_json })
+ end
+
+ api :POST, '/repp/v1/domains'
+ desc 'Create a new domain'
+ param :domain, Hash, required: true, desc: 'Parameters for new domain' do
+ param :name, String, required: true, desc: 'Domain name to be registered'
+ param :registrant, String, required: true, desc: 'Registrant contact code'
+ param :reserved_pw, String, required: false, desc: 'Reserved password for domain'
+ param :transfer_code, String, required: false, desc: 'Desired transfer code for domain'
+ param :period, Integer, required: true, desc: 'Registration period in months or years'
+ param :period_unit, String, required: true, desc: 'Period type (month m) or (year y)'
+ param :nameservers_attributes, Array, required: false, desc: 'Domain nameservers' do
+ param :hostname, String, required: true, desc: 'Nameserver hostname'
+ param :ipv4, Array, desc: 'Array of IPv4 addresses'
+ param :ipv6, Array, desc: 'Array of IPv4 addresses'
+ end
+ param :admin_contacts, Array, required: false, desc: 'Admin domain contacts codes'
+ param :tech_contacts, Array, required: false, desc: 'Tech domain contacts codes'
+ param :dnskeys_attributes, Array, required: false, desc: 'DNSSEC keys for domain' do
+ param_group :dns_keys_apidoc, Repp::V1::Domains::DnssecController
+ end
+ end
+ returns code: 200, desc: 'Successful domain registration response' do
+ property :code, Integer, desc: 'EPP code'
+ property :message, String, desc: 'EPP code explanation'
+ property :data, Hash do
+ property :domain, Hash do
+ property :name, String, 'Domain name'
+ end
+ end
+ end
+ def create
+ authorize!(:create, Epp::Domain)
+ @domain = Epp::Domain.new
+ action = Actions::DomainCreate.new(@domain, domain_create_params)
+
+ # rubocop:disable Style/AndOr
+ handle_errors(@domain) and return unless action.call
+ # rubocop:enable Style/AndOr
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ api :PUT, '/repp/v1/domains/:domain_name'
+ desc 'Update existing domain'
+ param :id, String, desc: 'Domain name in IDN / Puny format'
+ param :domain, Hash, required: true, desc: 'Changes of domain object' do
+ param :registrant, Hash, required: false, desc: 'New registrant object' do
+ param :code, String, required: true, desc: 'New registrant contact code'
+ param :verified, [true, false], required: false,
+ desc: 'Registrant change is already verified'
+ end
+ param :transfer_code, String, required: false, desc: 'New authorization code'
+ end
+ def update
+ action = Actions::DomainUpdate.new(@domain, params[:domain], false)
+
+ unless action.call
+ handle_errors(@domain)
+ return
+ end
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ api :GET, '/repp/v1/domains/:domain_name/transfer_info'
+ desc "Retrieve specific domain's transfer info"
def transfer_info
contact_fields = %i[code name ident ident_type ident_country_code phone email street city
zip country_code statuses]
@@ -25,6 +105,8 @@ module Repp
render_success(data: data)
end
+ api :POST, '/repp/v1/domains/transfer'
+ desc 'Transfer multiple domains'
def transfer
@errors ||= []
@successful = []
@@ -36,6 +118,30 @@ module Repp
render_success(data: { success: @successful, failed: @errors })
end
+ api :DELETE, '/repp/v1/domains/:domain_name'
+ desc 'Delete specific domain'
+ param :delete, Hash, required: true, desc: 'Object holding verified key' do
+ param :verified, [true, false], required: true,
+ desc: 'Whether to ask registrant verification or not'
+ end
+ def destroy
+ action = Actions::DomainDelete.new(@domain, params, current_user.registrar)
+
+ # rubocop:disable Style/AndOr
+ handle_errors(@domain) and return unless action.call
+ # rubocop:enable Style/AndOr
+
+ render_success(data: { domain: { name: @domain.name } })
+ end
+
+ private
+
+ def serialized_domains(domains)
+ return domains.pluck(:name) unless index_params[:details] == 'true'
+
+ domains.map { |d| Serializers::Repp::Domain.new(d).to_json }
+ end
+
def initiate_transfer(transfer)
domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name])
action = Actions::DomainTransfer.new(domain, transfer[:transfer_code],
@@ -49,8 +155,6 @@ module Repp
end
end
- private
-
def transfer_params
params.require(:data).require(:domain_transfers).each do |t|
t.require(:domain_name)
@@ -66,10 +170,29 @@ module Repp
params.permit(:id)
end
+ def forward_registrar_id
+ return unless params[:domain]
+
+ params[:domain][:registrar] = current_user.registrar.id
+ end
+
+ def set_domain
+ registrar = current_user.registrar
+ @domain = Epp::Domain.find_by(registrar: registrar, name: params[:id])
+ @domain ||= Epp::Domain.find_by!(registrar: registrar, name_puny: params[:id])
+
+ return @domain if @domain
+
+ raise ActiveRecord::RecordNotFound
+ end
+
def set_authorized_domain
@epp_errors ||= []
@domain = domain_from_url_hash
+ end
+ def validate_registrar_authorization
+ return if @domain.registrar == current_user.registrar
return if @domain.transfer_code.eql?(request.headers['Auth-Code'])
@epp_errors << { code: 2202, msg: I18n.t('errors.messages.epp_authorization_error') }
@@ -78,9 +201,9 @@ module Repp
def domain_from_url_hash
entry = transfer_info_params[:id]
- return Domain.find(entry) if entry.match?(/\A[0-9]+\z/)
+ return Epp::Domain.find(entry) if entry.match?(/\A[0-9]+\z/)
- Domain.find_by!('name = ? OR name_puny = ?', entry, entry)
+ Epp::Domain.find_by!('name = ? OR name_puny = ?', entry, entry)
end
def limit
@@ -94,6 +217,14 @@ module Repp
def index_params
params.permit(:limit, :offset, :details)
end
+
+ def domain_create_params
+ params.require(:domain).permit(:name, :registrant, :period, :period_unit, :registrar,
+ :transfer_code, :reserved_pw,
+ dnskeys_attributes: [%i[flags alg protocol public_key]],
+ nameservers_attributes: [[:hostname, ipv4: [], ipv6: []]],
+ admin_contacts: [], tech_contacts: [])
+ end
end
end
end
diff --git a/app/controllers/repp/v1/registrar/nameservers_controller.rb b/app/controllers/repp/v1/registrar/nameservers_controller.rb
index 39f076e9b..0d03bf2a9 100644
--- a/app/controllers/repp/v1/registrar/nameservers_controller.rb
+++ b/app/controllers/repp/v1/registrar/nameservers_controller.rb
@@ -4,6 +4,19 @@ module Repp
class NameserversController < BaseController
before_action :verify_nameserver_existance, only: %i[update]
+ api :PUT, 'repp/v1/registrar/nameservers'
+ desc 'bulk nameserver change'
+ param :data, Hash, required: true, desc: 'Object holding nameserver changes' do
+ param :type, String, required: true, desc: 'Always set as "nameserver"'
+ param :id, String, required: true, desc: 'Hostname of replacable nameserver'
+ param :domains, Array, required: false, desc: 'Array of domain names qualified for ' \
+ 'nameserver replacement'
+ param :attributes, Hash, required: true, desc: 'Object holding new nameserver values' do
+ param :hostname, String, required: true, desc: 'New hostname of nameserver'
+ param :ipv4, Array, of: String, required: false, desc: 'Array of fixed IPv4 addresses'
+ param :ipv6, Array, of: String, required: false, desc: 'Array of fixed IPv6 addresses'
+ end
+ end
def update
affected, errored = current_user.registrar
.replace_nameservers(hostname,
diff --git a/app/controllers/repp/v1/registrar/notifications_controller.rb b/app/controllers/repp/v1/registrar/notifications_controller.rb
new file mode 100644
index 000000000..4e00a9321
--- /dev/null
+++ b/app/controllers/repp/v1/registrar/notifications_controller.rb
@@ -0,0 +1,51 @@
+module Repp
+ module V1
+ module Registrar
+ class NotificationsController < BaseController
+ before_action :set_notification, only: [:update]
+
+ api :GET, '/repp/v1/registrar/notifications'
+ desc 'Get the latest unread poll message'
+ def index
+ @notification = current_user.unread_notifications.order('created_at DESC').take
+
+ # rubocop:disable Style/AndOr
+ render_success(data: nil) and return unless @notification
+ # rubocop:enable Style/AndOr
+
+ data = @notification.as_json(only: %i[id text attached_obj_id attached_obj_type])
+
+ render_success(data: data)
+ end
+
+ api :GET, '/repp/v1/registrar/notifications/:notification_id'
+ desc 'Get a specific poll message'
+ def show
+ @notification = current_user.registrar.notifications.find(params[:id])
+ data = @notification.as_json(only: %i[id text attached_obj_id attached_obj_type read])
+
+ render_success(data: data)
+ end
+
+ api :PUT, '/repp/v1/registrar/notifications'
+ desc 'Mark poll message as read'
+ param :notification, Hash, required: true do
+ param :read, [true], required: true, desc: 'Set as true to mark as read'
+ end
+ def update
+ # rubocop:disable Style/AndOr
+ handle_errors(@notification) and return unless @notification.mark_as_read
+ # rubocop:enable Style/AndOr
+
+ render_success(data: { notification_id: @notification.id, read: true })
+ end
+
+ private
+
+ def set_notification
+ @notification = current_user.unread_notifications.find(params[:id])
+ end
+ end
+ end
+ end
+end
diff --git a/app/interactions/actions/base_action.rb b/app/interactions/actions/base_action.rb
new file mode 100644
index 000000000..f29526753
--- /dev/null
+++ b/app/interactions/actions/base_action.rb
@@ -0,0 +1,24 @@
+module Actions
+ class BaseAction
+ def self.maybe_attach_legal_doc(entity, legal_doc)
+ return unless legal_doc
+ return if legal_doc[:body].starts_with?(ENV['legal_documents_dir'])
+
+ entity.legal_documents.create(
+ document_type: legal_doc[:type],
+ body: legal_doc[:body]
+ )
+ end
+
+ def self.attach_legal_doc_to_new(entity, legal_doc, domain: true)
+ return unless legal_doc
+
+ doc = LegalDocument.create(
+ documentable_type: domain ? Domain : Contact,
+ document_type: legal_doc[:type],
+ body: legal_doc[:body]
+ )
+ entity.legal_documents = [doc]
+ end
+ end
+end
diff --git a/app/models/actions/contact_create.rb b/app/interactions/actions/contact_create.rb
similarity index 87%
rename from app/models/actions/contact_create.rb
rename to app/interactions/actions/contact_create.rb
index 22fabc7b9..8c6cb2282 100644
--- a/app/models/actions/contact_create.rb
+++ b/app/interactions/actions/contact_create.rb
@@ -59,15 +59,7 @@ module Actions
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
+ ::Actions::BaseAction.attach_legal_doc_to_new(contact, legal_document, domain: false)
end
def commit
diff --git a/app/models/actions/contact_delete.rb b/app/interactions/actions/contact_delete.rb
similarity index 73%
rename from app/models/actions/contact_delete.rb
rename to app/interactions/actions/contact_delete.rb
index 60d3252c4..69f803f6e 100644
--- a/app/models/actions/contact_delete.rb
+++ b/app/interactions/actions/contact_delete.rb
@@ -28,15 +28,7 @@ module Actions
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
+ ::Actions::BaseAction.maybe_attach_legal_doc(contact, legal_document)
end
def commit
diff --git a/app/models/actions/contact_update.rb b/app/interactions/actions/contact_update.rb
similarity index 92%
rename from app/models/actions/contact_update.rb
rename to app/interactions/actions/contact_update.rb
index 7ca7b6b04..e96ea8afb 100644
--- a/app/models/actions/contact_update.rb
+++ b/app/interactions/actions/contact_update.rb
@@ -42,14 +42,7 @@ module Actions
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
+ ::Actions::BaseAction.maybe_attach_legal_doc(contact, legal_document)
end
def maybe_update_ident
diff --git a/app/interactions/actions/domain_create.rb b/app/interactions/actions/domain_create.rb
new file mode 100644
index 000000000..2e735bcce
--- /dev/null
+++ b/app/interactions/actions/domain_create.rb
@@ -0,0 +1,214 @@
+module Actions
+ class DomainCreate # rubocop:disable Metrics/ClassLength
+ attr_reader :domain, :params
+
+ def initialize(domain, params)
+ @domain = domain
+ @params = params
+ end
+
+ def call
+ assign_domain_attributes
+ validate_domain_integrity
+ return false if domain.errors[:epp_errors].any?
+
+ assign_registrant
+ assign_nameservers
+ assign_domain_contacts
+ domain.attach_default_contacts
+ assign_expiry_time
+ maybe_attach_legal_doc
+
+ commit
+ end
+
+ def check_contact_duplications
+ if check_for_same_contacts(@admin_contacts, 'admin') &&
+ check_for_same_contacts(@tech_contacts, 'tech')
+ true
+ else
+ false
+ end
+ end
+
+ def check_for_same_contacts(contacts, contact_type)
+ return true unless contacts.uniq.count != contacts.count
+
+ domain.add_epp_error('2306', contact_type, nil, %i[domain_contacts invalid])
+ false
+ end
+
+ # Check if domain is eligible for new registration
+ def validate_domain_integrity
+ return unless Domain.release_to_auction
+
+ dn = DNS::DomainName.new(domain.name)
+ if dn.at_auction?
+ domain.add_epp_error('2306', nil, nil, 'Parameter value policy error: domain is at auction')
+ elsif dn.awaiting_payment?
+ domain.add_epp_error('2003', nil, nil, 'Required parameter missing; reserved>pw element' \
+ ' required for reserved domains')
+ elsif dn.pending_registration?
+ validate_reserved_password(dn)
+ end
+ end
+
+ def validate_reserved_password(domain_name)
+ if params[:reserved_pw].blank?
+ domain.add_epp_error('2003', nil, nil, 'Required parameter missing; reserved>pw ' \
+ 'element is required')
+ else
+ unless domain_name.available_with_code?(params[:reserved_pw])
+ domain.add_epp_error('2202', nil, nil, 'Invalid authorization information; invalid ' \
+ 'reserved>pw value')
+ end
+ end
+ end
+
+ def assign_registrant
+ unless params[:registrant]
+ domain.add_epp_error('2306', nil, nil, %i[registrant cannot_be_missing])
+ return
+ end
+
+ regt = Registrant.find_by(code: params[:registrant])
+ if regt
+ domain.registrant = regt
+ else
+ domain.add_epp_error('2303', 'registrant', params[:registrant], %i[registrant not_found])
+ end
+ end
+
+ def assign_domain_attributes
+ domain.name = params[:name].strip.downcase
+ domain.registrar = current_registrar
+ assign_domain_period
+ assign_domain_auth_codes
+ assign_dnskeys
+ end
+
+ def assign_dnskeys
+ return unless params[:dnskeys_attributes]&.any?
+
+ params[:dnskeys_attributes].each { |dk| verify_public_key_integrity(dk[:public_key]) }
+ domain.dnskeys_attributes = params[:dnskeys_attributes]
+ end
+
+ def verify_public_key_integrity(pub)
+ return if Dnskey.pub_key_base64?(pub)
+
+ domain.add_epp_error(2005, nil, nil, %i[dnskeys invalid])
+ end
+
+ def assign_domain_auth_codes
+ domain.transfer_code = params[:transfer_code] if params[:transfer_code].present?
+ domain.reserved_pw = params[:reserved_pw] if params[:reserved_pw].present?
+ end
+
+ def assign_domain_period
+ domain.period = params[:period]
+ domain.period_unit = params[:period_unit]
+ end
+
+ def assign_nameservers
+ return unless params[:nameservers_attributes]
+
+ domain.nameservers_attributes = params[:nameservers_attributes]
+ end
+
+ def assign_contact(contact_code, admin: true)
+ contact = Contact.find_by(code: contact_code)
+ arr = admin ? @admin_contacts : @tech_contacts
+ if contact
+ arr << { contact_id: contact.id, contact_code_cache: contact.code }
+ else
+ domain.add_epp_error('2303', 'contact', contact_code, %i[domain_contacts not_found])
+ end
+ end
+
+ def assign_domain_contacts
+ @admin_contacts = []
+ @tech_contacts = []
+ params[:admin_contacts]&.each { |c| assign_contact(c) }
+ params[:tech_contacts]&.each { |c| assign_contact(c, admin: false) }
+
+ domain.admin_domain_contacts_attributes = @admin_contacts
+ domain.tech_domain_contacts_attributes = @tech_contacts
+ check_contact_duplications
+ end
+
+ def assign_expiry_time
+ return unless domain.period
+
+ period = Integer(domain.period)
+ domain.expire_time = calculate_expiry(period)
+ end
+
+ def calculate_expiry(period)
+ plural_period_unit_name = (domain.period_unit == 'm' ? 'months' : 'years').to_sym
+ (Time.zone.now.advance(plural_period_unit_name => period) + 1.day).beginning_of_day
+ end
+
+ def action_billable?
+ unless domain_pricelist&.price
+ domain.add_epp_error(2104, nil, nil, I18n.t(:active_price_missing_for_this_operation))
+ return false
+ end
+
+ if domain.registrar.balance < domain_pricelist.price.amount
+ domain.add_epp_error(2104, nil, nil, I18n.t('billing_failure_credit_balance_low'))
+ return false
+ end
+
+ true
+ end
+
+ def debit_registrar
+ return unless action_billable?
+
+ domain.registrar.debit!(sum: domain_pricelist.price.amount, price: domain_pricelist,
+ description: "#{I18n.t('create')} #{domain.name}",
+ activity_type: AccountActivity::CREATE)
+ end
+
+ def domain_pricelist
+ @domain_pricelist ||= domain.pricelist('create', domain.period.try(:to_i), domain.period_unit)
+
+ @domain_pricelist
+ end
+
+ def maybe_attach_legal_doc
+ ::Actions::BaseAction.attach_legal_doc_to_new(domain, params[:legal_document], domain: true)
+ end
+
+ def process_auction_and_disputes
+ dn = DNS::DomainName.new(domain.name)
+ Dispute.close_by_domain(domain.name)
+ return unless Domain.release_to_auction && dn.pending_registration?
+
+ Auction.find_by(domain: domain.name,
+ status: Auction.statuses[:payment_received])&.domain_registered!
+ end
+
+ def commit
+ return false if domain.errors[:epp_errors].any? || validation_process_errored?
+
+ debit_registrar
+ return false if domain.errors.any?
+
+ process_auction_and_disputes
+ domain.save
+ end
+
+ def validation_process_errored?
+ return if domain.valid?
+
+ domain.errors.delete(:name_dirty) if domain.errors[:puny_label].any?
+ domain.errors.any?
+ end
+
+ def current_registrar
+ Registrar.find(params[:registrar])
+ end
+ end
+end
diff --git a/app/interactions/actions/domain_delete.rb b/app/interactions/actions/domain_delete.rb
new file mode 100644
index 000000000..d7fd3496a
--- /dev/null
+++ b/app/interactions/actions/domain_delete.rb
@@ -0,0 +1,57 @@
+module Actions
+ class DomainDelete
+ attr_reader :domain
+ attr_reader :params
+ attr_reader :user
+
+ def initialize(domain, params, user)
+ @domain = domain
+ @params = params
+ @user = user
+ end
+
+ def call
+ return false unless @domain.can_be_deleted?
+
+ verify_not_discarded
+ maybe_attach_legal_doc
+
+ return false if domain.errors.any?
+ return false if domain.errors[:epp_errors].any?
+
+ destroy
+ end
+
+ def maybe_attach_legal_doc
+ ::Actions::BaseAction.attach_legal_doc_to_new(domain, params[:legal_document], domain: true)
+ end
+
+ def verify_not_discarded
+ return unless domain.discarded?
+
+ domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ end
+
+ def verify?
+ return false unless Setting.request_confirmation_on_domain_deletion_enabled
+ return false if params[:delete][:verified] == true
+
+ true
+ end
+
+ def ask_delete_verification
+ domain.registrant_verification_asked!(params, user.id)
+ domain.pending_delete!
+ domain.manage_automatic_statuses
+ end
+
+ def destroy
+ if verify?
+ ask_delete_verification
+ else
+ domain.set_pending_delete!
+ end
+ true
+ end
+ end
+end
diff --git a/app/interactions/actions/domain_renew.rb b/app/interactions/actions/domain_renew.rb
new file mode 100644
index 000000000..66e29c940
--- /dev/null
+++ b/app/interactions/actions/domain_renew.rb
@@ -0,0 +1,34 @@
+module Actions
+ class DomainRenew
+ attr_reader :domain
+ attr_reader :params
+ attr_reader :user
+
+ def initialize(domain, params, user)
+ @domain = domain
+ @params = params
+ @user = user
+ end
+
+ def call
+ domain.is_renewal = true
+ if !domain.renewable? || domain.invalid?
+ domain.add_renew_epp_errors
+ false
+ else
+ domain.validate_exp_dates(params[:exp_date])
+ renew
+ end
+ end
+
+ def renew
+ return false if domain.errors[:epp_errors].any?
+
+ task = Domains::BulkRenew::SingleDomainRenew.run(domain: domain,
+ period: params[:period],
+ unit: params[:period_unit],
+ registrar: user)
+ task.valid?
+ end
+ end
+end
diff --git a/app/models/actions/domain_transfer.rb b/app/interactions/actions/domain_transfer.rb
similarity index 100%
rename from app/models/actions/domain_transfer.rb
rename to app/interactions/actions/domain_transfer.rb
diff --git a/app/interactions/actions/domain_update.rb b/app/interactions/actions/domain_update.rb
new file mode 100644
index 000000000..7da22e539
--- /dev/null
+++ b/app/interactions/actions/domain_update.rb
@@ -0,0 +1,256 @@
+module Actions
+ class DomainUpdate # rubocop:disable Metrics/ClassLength
+ attr_reader :domain, :params, :bypass_verify
+
+ def initialize(domain, params, bypass_verify)
+ @domain = domain
+ @params = params
+ @bypass_verify = bypass_verify
+ @changes_registrant = false
+ end
+
+ def call
+ validate_domain_integrity
+ assign_new_registrant if params[:registrant]
+ assign_relational_modifications
+ assign_requested_statuses
+ ::Actions::BaseAction.maybe_attach_legal_doc(domain, params[:legal_document])
+
+ commit
+ end
+
+ def assign_relational_modifications
+ assign_nameserver_modifications if params[:nameservers]
+ assign_dnssec_modifications if params[:dns_keys]
+ return unless params[:contacts]
+
+ assign_admin_contact_changes
+ assign_tech_contact_changes
+ end
+
+ def check_for_same_contacts(contacts, contact_type)
+ return unless contacts.uniq.count != contacts.count
+
+ domain.add_epp_error('2306', contact_type, nil, %i[domain_contacts invalid])
+ end
+
+ def validate_domain_integrity
+ domain.auth_info = params[:transfer_code] if params[:transfer_code]
+
+ return unless domain.discarded?
+
+ domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ end
+
+ def assign_new_registrant
+ unless params[:registrant][:code]
+ domain.add_epp_error('2306', nil, nil, %i[registrant cannot_be_missing])
+ end
+
+ regt = Registrant.find_by(code: params[:registrant][:code])
+ unless regt
+ domain.add_epp_error('2303', 'registrant', params[:registrant], %i[registrant not_found])
+ return
+ end
+
+ replace_domain_registrant(regt)
+ end
+
+ def replace_domain_registrant(new_registrant)
+ return if domain.registrant == new_registrant
+
+ @changes_registrant = true if domain.registrant.ident != new_registrant.ident
+ if @changes_registrant && domain.registrant_change_prohibited?
+ domain.add_epp_error(2304, 'status', DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED,
+ I18n.t(:object_status_prohibits_operation))
+ else
+ domain.registrant = new_registrant
+ end
+ end
+
+ def assign_nameserver_modifications
+ @nameservers = []
+ params[:nameservers].each do |ns_attr|
+ case ns_attr[:action]
+ when 'rem'
+ validate_ns_integrity(ns_attr)
+ when 'add'
+ @nameservers << ns_attr.except(:action)
+ end
+ end
+
+ domain.nameservers_attributes = @nameservers if @nameservers.present?
+ end
+
+ def validate_ns_integrity(ns_attr)
+ ns = domain.nameservers.from_hash_params(ns_attr.except(:action)).first
+ if ns
+ @nameservers << { id: ns.id, _destroy: 1 }
+ else
+ domain.add_epp_error('2303', 'hostAttr', ns_attr[:hostname], %i[nameservers not_found])
+ end
+ end
+
+ def assign_dnssec_modifications
+ @dnskeys = []
+ params[:dns_keys].each do |key|
+ case key[:action]
+ when 'add'
+ validate_dnskey_integrity(key)
+ when 'rem'
+ assign_removable_dnskey(key)
+ end
+ end
+
+ domain.dnskeys_attributes = @dnskeys.uniq
+ end
+
+ def validate_dnskey_integrity(key)
+ if key[:public_key] && !Setting.key_data_allowed
+ domain.add_epp_error('2306', nil, nil, %i[dnskeys key_data_not_allowed])
+ elsif Dnskey.pub_key_base64?(key[:public_key])
+ @dnskeys << key.except(:action)
+ else
+ domain.add_epp_error(2005, nil, nil, %i[dnskeys invalid])
+ end
+ end
+
+ def assign_removable_dnskey(key)
+ dnkey = domain.dnskeys.find_by(key.except(:action))
+ domain.add_epp_error(2303, nil, nil, %i[dnskeys not_found]) unless dnkey
+
+ @dnskeys << { id: dnkey.id, _destroy: 1 } if dnkey
+ end
+
+ def assign_admin_contact_changes
+ props = gather_domain_contacts(params[:contacts].select { |c| c[:type] == 'admin' })
+
+ if props.any? && domain.admin_change_prohibited?
+ domain.add_epp_error('2304', 'admin', DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED,
+ I18n.t(:object_status_prohibits_operation))
+ elsif props.present?
+ domain.admin_domain_contacts_attributes = props
+ check_for_same_contacts(props, 'admin')
+ end
+ end
+
+ def assign_tech_contact_changes
+ props = gather_domain_contacts(params[:contacts].select { |c| c[:type] == 'tech' },
+ admin: false)
+
+ if props.any? && domain.tech_change_prohibited?
+ domain.add_epp_error('2304', 'tech', DomainStatus::SERVER_TECH_CHANGE_PROHIBITED,
+ I18n.t(:object_status_prohibits_operation))
+ elsif props.present?
+ domain.tech_domain_contacts_attributes = props
+ check_for_same_contacts(props, 'tech')
+ end
+ end
+
+ def gather_domain_contacts(contacts, admin: true)
+ props = []
+
+ contacts.each do |c|
+ contact = contact_for_action(action: c[:action], method: admin ? 'admin' : 'tech',
+ code: c[:code])
+ entry = assign_contact(contact, add: c[:action] == 'add', admin: admin, code: c[:code])
+ props << entry if entry.is_a?(Hash)
+ end
+
+ props
+ end
+
+ def contact_for_action(action:, method:, code:)
+ contact = Epp::Contact.find_by(code: code)
+ return contact if action == 'add' || !contact
+ return domain.admin_domain_contacts.find_by(contact_id: contact.id) if method == 'admin'
+
+ domain.tech_domain_contacts.find_by(contact_id: contact.id)
+ end
+
+ def assign_contact(obj, add: false, admin: true, code:)
+ if obj.blank?
+ domain.add_epp_error('2303', 'contact', code, %i[domain_contacts not_found])
+ elsif obj.try(:org?) && admin && add
+ domain.add_epp_error('2306', 'contact', code,
+ %i[domain_contacts admin_contact_can_be_only_private_person])
+ else
+ add ? { contact_id: obj.id, contact_code_cache: obj.code } : { id: obj.id, _destroy: 1 }
+ end
+ end
+
+ def assign_requested_statuses
+ return unless params[:statuses]
+
+ @rem = []
+ @add = []
+ @failed = false
+
+ params[:statuses].each { |s| verify_status_eligiblity(s) }
+ domain.statuses = (domain.statuses - @rem + @add) unless @failed
+ end
+
+ def verify_status_eligiblity(status_entry)
+ status, action = status_entry.select_keys(:status, :action)
+ return unless permitted_status?(status, action)
+
+ action == 'add' ? @add << status : @rem << status
+ end
+
+ def permitted_status?(status, action)
+ if DomainStatus::CLIENT_STATUSES.include?(status) &&
+ (domain.statuses.include?(status) || action == 'add')
+ return true
+ end
+
+ domain.add_epp_error('2303', 'status', status, %i[statuses not_found])
+ @failed = true
+ false
+ end
+
+ def verify_registrant_change?
+ return if !@changes_registrant || params[:registrant][:verified] == true
+ return true unless domain.disputed?
+ return validate_dispute_case if params[:reserved_pw]
+
+ domain.add_epp_error('2304', nil, nil, 'Required parameter missing; reservedpw element ' \
+ 'required for dispute domains')
+
+ true
+ end
+
+ def validate_dispute_case
+ dispute = Dispute.active.find_by(domain_name: domain.name, password: params[:reserved_pw])
+ if dispute
+ Dispute.close_by_domain(domain.name)
+ false
+ else
+ domain.add_epp_error('2202', nil, nil,
+ 'Invalid authorization information; invalid reserved>pw value')
+ true
+ end
+ end
+
+ def ask_registrant_verification
+ if verify_registrant_change? && !bypass_verify &&
+ Setting.request_confirmation_on_registrant_change_enabled
+ domain.registrant_verification_asked!(params, params[:registrar])
+ end
+ end
+
+ def commit
+ return false if any_errors?
+
+ ask_registrant_verification
+ return false if any_errors?
+
+ domain.save
+ end
+
+ def any_errors?
+ return true if domain.errors[:epp_errors].any? || domain.invalid?
+
+ false
+ end
+ end
+end
diff --git a/app/interactions/domains/bulk_renew/single_domain_renew.rb b/app/interactions/domains/bulk_renew/single_domain_renew.rb
index 66c6244b3..5951ef9ec 100644
--- a/app/interactions/domains/bulk_renew/single_domain_renew.rb
+++ b/app/interactions/domains/bulk_renew/single_domain_renew.rb
@@ -9,6 +9,7 @@ module Domains
def execute
in_transaction_with_retries do
+ check_balance
success = domain.renew(domain.valid_to, period, unit)
if success
check_balance
@@ -55,6 +56,7 @@ module Domains
private
def add_error
+ domain.add_epp_error(2104, nil, nil, I18n.t(:domain_renew_error_for_domain))
errors.add(:domain, I18n.t('domain_renew_error_for_domain', domain: domain.name))
end
end
diff --git a/app/interactions/domains/check_balance/single_domain.rb b/app/interactions/domains/check_balance/single_domain.rb
index 166edc4e3..411318d66 100644
--- a/app/interactions/domains/check_balance/single_domain.rb
+++ b/app/interactions/domains/check_balance/single_domain.rb
@@ -9,15 +9,30 @@ module Domains
string :unit
def execute
- return domain_pricelist.price.amount if domain_pricelist.try(:price)
+ if domain_pricelist.try(:price)
+ price = domain_pricelist.price.amount
+ return price if balance_ok?(price)
+ else
+ domain.add_epp_error(2104, nil, nil, I18n.t(:active_price_missing_for_this_operation))
+ errors.add(:domain, I18n.t(:active_price_missing_for_operation_with_domain,
+ domain: domain.name))
+ end
- errors.add(:domain, I18n.t(:active_price_missing_for_operation_with_domain,
- domain: domain.name))
false
end
private
+ def balance_ok?(price)
+ if domain.registrar.cash_account.balance >= price
+ true
+ else
+ domain.add_epp_error(2104, nil, nil, I18n.t(:not_enough_funds))
+ errors.add(:domain, I18n.t(:billing_failure_credit_balance_low, domain: domain.name))
+ false
+ end
+ end
+
def domain_pricelist
domain.pricelist(operation, period.try(:to_i), unit)
end
diff --git a/app/interactions/domains/update_confirm/process_update_confirmed.rb b/app/interactions/domains/update_confirm/process_update_confirmed.rb
index cb69d042e..291253651 100644
--- a/app/interactions/domains/update_confirm/process_update_confirmed.rb
+++ b/app/interactions/domains/update_confirm/process_update_confirmed.rb
@@ -20,15 +20,20 @@ module Domains
WhoisRecord.find_by(domain_id: domain.id).save # need to reload model
end
- # rubocop:disable Metrics/AbcSize
def update_domain
+ frame_json = domain.pending_json['frame']
+ frame = frame_json ? frame_json.with_indifferent_access : {}
+ assign_domain_update_meta
+
+ Actions::DomainUpdate.new(domain, frame, true).call
+ end
+
+ def assign_domain_update_meta
user = ApiUser.find(domain.pending_json['current_user_id'])
- frame = Nokogiri::XML(domain.pending_json['frame'])
+
domain.upid = user.registrar.id if user.registrar
domain.up_date = Time.zone.now
- domain.update(frame, user, false)
end
- # rubocop:enable Metrics/AbcSize
end
end
end
diff --git a/app/models/billing/price.rb b/app/models/billing/price.rb
index 3b0c253d4..9cd32f55c 100644
--- a/app/models/billing/price.rb
+++ b/app/models/billing/price.rb
@@ -60,7 +60,10 @@ module Billing
def self.price_for(zone, operation_category, duration)
lists = valid.where(zone: zone, operation_category: operation_category, duration: duration)
return lists.first if lists.count == 1
+
lists.order(valid_from: :desc).first
+ rescue ActiveRecord::StatementInvalid
+ nil
end
def name
diff --git a/app/models/concerns/epp_errors.rb b/app/models/concerns/epp_errors.rb
index c1e4fa2e1..d12080158 100644
--- a/app/models/concerns/epp_errors.rb
+++ b/app/models/concerns/epp_errors.rb
@@ -86,6 +86,8 @@ module EppErrors
end
end
nil
+ rescue NameError
+ nil
end
def construct_msg_args_and_value(epp_error_args)
diff --git a/app/models/dnskey.rb b/app/models/dnskey.rb
index c0f3f7491..6c6df6e44 100644
--- a/app/models/dnskey.rb
+++ b/app/models/dnskey.rb
@@ -8,6 +8,7 @@ class Dnskey < ApplicationRecord
validate :validate_algorithm
validate :validate_protocol
validate :validate_flags
+ validate :validate_public_key
before_save lambda {
generate_digest if will_save_change_to_public_key? && !will_save_change_to_ds_digest?
@@ -115,6 +116,12 @@ class Dnskey < ApplicationRecord
self.ds_key_tag = ((c & 0xFFFF) + (c >> 16)) & 0xFFFF
end
+ def validate_public_key
+ return if Dnskey.pub_key_base64?(public_key)
+
+ errors.add(:public_key, :invalid)
+ end
+
class << self
def int_to_hex(s)
s = s.to_s(16)
@@ -128,5 +135,13 @@ class Dnskey < ApplicationRecord
def bin_to_hex(s)
s.each_byte.map { |b| format('%02X', b) }.join
end
+
+ def pub_key_base64?(pub)
+ return unless pub&.is_a?(String)
+
+ Base64.strict_encode64(Base64.strict_decode64(pub)) == pub
+ rescue ArgumentError
+ false
+ end
end
end
diff --git a/app/models/domain.rb b/app/models/domain.rb
index ac0855da3..5f32e02e2 100644
--- a/app/models/domain.rb
+++ b/app/models/domain.rb
@@ -513,7 +513,7 @@ class Domain < ApplicationRecord
# depricated not used, not valid
def update_prohibited?
- pending_update_prohibited? && pending_delete_prohibited?
+ (statuses & DomainStatus::UPDATE_PROHIBIT_STATES).present?
end
# public api
diff --git a/app/models/epp/domain.rb b/app/models/epp/domain.rb
index f3da8d533..c5ce69776 100644
--- a/app/models/epp/domain.rb
+++ b/app/models/epp/domain.rb
@@ -1,5 +1,7 @@
require 'deserializers/xml/legal_document'
-
+require 'deserializers/xml/nameserver'
+require 'deserializers/xml/domain_create'
+require 'deserializers/xml/domain_update'
class Epp::Domain < Domain
include EppErrors
@@ -27,30 +29,15 @@ class Epp::Domain < Domain
active_techs = tech_domain_contacts.select { |x| !x.marked_for_destruction? }
# validate registrant here as well
- ([Contact.find_by(code: registrant.code)] + active_admins + active_techs).each do |x|
+ ([Contact.find(registrant.id)] + active_admins + active_techs).each do |x|
unless x.valid?
- add_epp_error('2304', nil, nil, I18n.t(:contact_is_not_valid, value: x.code))
+ add_epp_error('2304', nil, nil, I18n.t(:contact_is_not_valid, value: x.try(:code)))
ok = false
end
end
ok
end
- class << self
- def new_from_epp(frame, current_user)
- domain = Epp::Domain.new
- domain.attributes = domain.attrs_from(frame, current_user)
- domain.attach_default_contacts
-
- period = domain.period.to_i
- plural_period_unit_name = (domain.period_unit == 'm' ? 'months' : 'years').to_sym
- expire_time = (Time.zone.now.advance(plural_period_unit_name => period) + 1.day).beginning_of_day
- domain.expire_time = expire_time
-
- domain
- end
- end
-
def epp_code_map
{
'2002' => [ # Command use error
@@ -123,408 +110,10 @@ class Epp::Domain < Domain
def attach_default_contacts
return if registrant.blank?
- tech_contacts << registrant if tech_domain_contacts.blank?
- admin_contacts << registrant if admin_domain_contacts.blank? && !registrant.org?
- end
+ registrant_obj = Contact.find_by(code: registrant.code)
- def attrs_from(frame, current_user, action = nil)
- at = {}.with_indifferent_access
-
- registrant_frame = frame.css('registrant').first
- code = registrant_frame.try(:text)
- if code.present?
- if action == 'chg' && registrant_change_prohibited?
- add_epp_error('2304', "status", DomainStatus::SERVER_REGISTRANT_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
- end
- regt = Registrant.find_by(code: code)
- if regt
- at[:registrant_id] = regt.id
- else
- add_epp_error('2303', 'registrant', code, [:registrant, :not_found])
- end
- else
- add_epp_error('2306', nil, nil, [:registrant, :cannot_be_missing])
- end if registrant_frame
-
-
- at[:name] = frame.css('name').text if new_record?
- at[:registrar_id] = current_user.registrar.try(:id)
-
- period = frame.css('period').text
- at[:period] = (period.to_i == 0) ? 1 : period.to_i
-
- at[:period_unit] = Epp::Domain.parse_period_unit_from_frame(frame) || 'y'
-
- at[:reserved_pw] = frame.css('reserved > pw').text
-
- # at[:statuses] = domain_statuses_attrs(frame, action)
- at[:nameservers_attributes] = nameservers_attrs(frame, action)
- at[:admin_domain_contacts_attributes] = admin_domain_contacts_attrs(frame, action)
- at[:tech_domain_contacts_attributes] = tech_domain_contacts_attrs(frame, action)
-
- check_for_same_contacts(at[:admin_domain_contacts_attributes], 'admin')
- check_for_same_contacts(at[:tech_domain_contacts_attributes], 'tech')
-
- pw = frame.css('authInfo > pw').text
- at[:transfer_code] = pw if pw.present?
-
- if new_record?
- dnskey_frame = frame.css('extension create')
- else
- dnskey_frame = frame
- end
-
- at[:dnskeys_attributes] = dnskeys_attrs(dnskey_frame, action)
-
- at
- end
-
- def check_for_same_contacts(contacts, contact_type)
- return unless contacts.uniq.count != contacts.count
-
- add_epp_error('2306', contact_type, nil, %i[domain_contacts invalid])
- end
-
- # Adding legal doc to domain and
- # if something goes wrong - raise Rollback error
- def add_legal_file_to_new frame
- legal_document_data = ::Deserializers::Xml::LegalDocument.new(frame).call
- return unless legal_document_data
- return if legal_document_data[:body].starts_with?(ENV['legal_documents_dir'])
-
- doc = LegalDocument.create(documentable_type: Domain, document_type: legal_document_data[:type],
- body: legal_document_data[:body])
- self.legal_documents = [doc]
-
- frame.css("legalDocument").first.content = doc.path if doc&.persisted?
- self.legal_document_id = doc.id
- end
-
- def nameservers_attrs(frame, action)
- ns_list = nameservers_from(frame)
-
- if action == 'rem'
- to_destroy = []
- ns_list.each do |ns_attrs|
- nameserver = nameservers.find_by_hash_params(ns_attrs).first
- if nameserver.blank?
- add_epp_error('2303', 'hostAttr', ns_attrs[:hostname], [:nameservers, :not_found])
- else
- to_destroy << {
- id: nameserver.id,
- _destroy: 1
- }
- end
- end
-
- return to_destroy
- else
- return ns_list
- end
- end
-
- def nameservers_from(frame)
- res = []
- frame.css('hostAttr').each do |x|
- host_attr = {
- hostname: x.css('hostName').first.try(:text),
- ipv4: x.css('hostAddr[ip="v4"]').map(&:text).compact,
- ipv6: x.css('hostAddr[ip="v6"]').map(&:text).compact
- }
-
- res << host_attr.delete_if { |_k, v| v.blank? }
- end
-
- res
- end
-
- def admin_domain_contacts_attrs(frame, action)
- admin_attrs = domain_contact_attrs_from(frame, action, 'admin')
-
- if admin_attrs.present? && admin_change_prohibited?
- add_epp_error('2304', 'admin', DomainStatus::SERVER_ADMIN_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
- return []
- end
-
- case action
- when 'rem'
- return destroy_attrs(admin_attrs, admin_domain_contacts)
- else
- return admin_attrs
- end
- end
-
- def tech_domain_contacts_attrs(frame, action)
- tech_attrs = domain_contact_attrs_from(frame, action, 'tech')
-
- if tech_attrs.present? && tech_change_prohibited?
- add_epp_error('2304', 'tech', DomainStatus::SERVER_TECH_CHANGE_PROHIBITED, I18n.t(:object_status_prohibits_operation))
- return []
- end
-
- case action
- when 'rem'
- return destroy_attrs(tech_attrs, tech_domain_contacts)
- else
- return tech_attrs
- end
- end
-
- def destroy_attrs(attrs, dcontacts)
- destroy_attrs = []
- attrs.each do |at|
- domain_contact_id = dcontacts.find_by(contact_id: at[:contact_id]).try(:id)
-
- unless domain_contact_id
- add_epp_error('2303', 'contact', at[:contact_code_cache], [:domain_contacts, :not_found])
- next
- end
-
- destroy_attrs << {
- id: domain_contact_id,
- _destroy: 1
- }
- end
-
- destroy_attrs
- end
-
- def domain_contact_attrs_from(frame, action, type)
- attrs = []
- frame.css('contact').each do |x|
- next if x['type'] != type
-
- c = Epp::Contact.find_by_epp_code(x.text)
- unless c
- add_epp_error('2303', 'contact', x.text, [:domain_contacts, :not_found])
- next
- end
-
- if action != 'rem'
- if x['type'] == 'admin' && c.org?
- add_epp_error('2306', 'contact', x.text, [:domain_contacts, :admin_contact_can_be_only_private_person])
- next
- end
- end
-
- attrs << {
- contact_id: c.id,
- contact_code_cache: c.code
- }
- end
-
- attrs
- end
-
- def dnskeys_attrs(frame, action)
- keys = []
- return keys if frame.blank?
- inf_data = DnsSecKeys.new(frame)
- add_epp_error('2005', nil, nil, %i[dnskeys invalid]) if not_base64?(inf_data)
-
- if action == 'rem' &&
- frame.css('rem > all').first.try(:text) == 'true'
- keys = inf_data.mark_destroy_all dnskeys
- else
- if Setting.key_data_allowed
- errors.add(:base, :ds_data_not_allowed) if inf_data.ds_data.present?
- keys = inf_data.key_data
- end
- if Setting.ds_data_allowed
- errors.add(:base, :key_data_not_allowed) if inf_data.key_data.present?
- keys = inf_data.ds_data
- end
- if action == 'rem'
- keys = inf_data.mark_destroy(dnskeys)
- add_epp_error('2303', nil, nil, [:dnskeys, :not_found]) if keys.include? nil
- end
- end
- errors.any? ? [] : keys
- end
-
- def not_base64?(inf_data)
- inf_data.key_data.any? do |key|
- value = key[:public_key]
-
- !value.is_a?(String) || Base64.strict_encode64(Base64.strict_decode64(value)) != value
- end
- rescue ArgumentError
- true
- end
-
- class DnsSecKeys
- def initialize(frame)
- @key_data = []
- @ds_data = []
- # schema validation prevents both in the same parent node
- if frame.css('dsData').present?
- ds_data_from frame
- else
- frame.css('keyData').each do |key|
- @key_data.append key_data_from(key)
- end
- end
- end
-
- attr_reader :key_data
- attr_reader :ds_data
-
- def mark_destroy_all(dns_keys)
- # if transition support required mark_destroy dns_keys when has ds/key values otherwise ...
- dns_keys.map { |inf_data| mark inf_data }
- end
-
- def mark_destroy(dns_keys)
- (ds_data.present? ? ds_filter(dns_keys) : kd_filter(dns_keys)).map do |inf_data|
- inf_data.blank? ? nil : mark(inf_data)
- end
- end
-
- private
-
- KEY_INTERFACE = {flags: 'flags', protocol: 'protocol', alg: 'alg', public_key: 'pubKey' }
- DS_INTERFACE =
- { ds_key_tag: 'keyTag',
- ds_alg: 'alg',
- ds_digest_type: 'digestType',
- ds_digest: 'digest'
- }
-
- def xm_copy(frame, map)
- result = {}
- map.each do |key, elem|
- result[key] = frame.css(elem).first.try(:text)
- end
- result
- end
-
- def key_data_from(frame)
- xm_copy frame, KEY_INTERFACE
- end
-
- def ds_data_from(frame)
- frame.css('dsData').each do |ds_data|
- key = ds_data.css('keyData')
- ds = xm_copy ds_data, DS_INTERFACE
- ds.merge(key_data_from key) if key.present?
- @ds_data << ds
- end
- end
-
- def ds_filter(dns_keys)
- @ds_data.map do |ds|
- dns_keys.find_by(ds.slice(*DS_INTERFACE.keys))
- end
- end
-
- def kd_filter(dns_keys)
- @key_data.map do |key|
- dns_keys.find_by(key)
- end
- end
-
- def mark(inf_data)
- { id: inf_data.id, _destroy: 1 }
- end
- end
-
- def domain_statuses_attrs(frame, action)
- status_list = domain_status_list_from(frame)
- if action == 'rem'
- to_destroy = []
- status_list.each do |x|
- if statuses.include?(x)
- to_destroy << x
- else
- add_epp_error('2303', 'status', x, %i[statuses not_found])
- end
- end
-
- return to_destroy
- else
- return status_list
- end
- end
-
- def domain_status_list_from(frame)
- status_list = []
-
- frame.css('status').each do |x|
- unless DomainStatus::CLIENT_STATUSES.include?(x['s'])
- add_epp_error('2303', 'status', x['s'], %i[statuses not_found])
- next
- end
-
- status_list << x['s']
- end
-
- status_list
- end
-
-
- def update(frame, current_user, verify = true)
- return super if frame.blank?
-
- if discarded? || statuses_blocks_update?
- add_epp_error('2304', nil, nil, 'Object status prohibits operation')
- return
- end
-
- at = {}.with_indifferent_access
- at.deep_merge!(attrs_from(frame.css('chg'), current_user, 'chg'))
- at.deep_merge!(attrs_from(frame.css('rem'), current_user, 'rem'))
-
- if doc = attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call)
- frame.css("legalDocument").first.content = doc.path if doc&.persisted?
- self.legal_document_id = doc.id
- end
-
- at_add = attrs_from(frame.css('add'), current_user, 'add')
- at[:nameservers_attributes] += at_add[:nameservers_attributes]
- at[:admin_domain_contacts_attributes] += at_add[:admin_domain_contacts_attributes]
- at[:tech_domain_contacts_attributes] += at_add[:tech_domain_contacts_attributes]
- at[:dnskeys_attributes] += at_add[:dnskeys_attributes]
- at[:statuses] =
- statuses - domain_statuses_attrs(frame.css('rem'), 'rem') + domain_statuses_attrs(frame.css('add'), 'add')
-
- if errors.empty? && verify
- self.upid = current_user.registrar.id if current_user.registrar
- self.up_date = Time.zone.now
- end
-
- registrant_verification_needed = false
- # registrant block may not be present, so we need this to rule out false positives
- if frame.css('registrant').text.present?
- registrant_verification_needed = verification_needed?(code: frame.css('registrant').text)
- end
-
- if registrant_verification_needed && disputed?
- disputed_pw = frame.css('reserved > pw').text
- if disputed_pw.blank?
- add_epp_error('2304', nil, nil, 'Required parameter missing; reserved' \
- 'pw element required for dispute domains')
- else
- dispute = Dispute.active.find_by(domain_name: name, password: disputed_pw)
- if dispute
- Dispute.close_by_domain(name)
- registrant_verification_needed = false # Prevent asking current registrant confirmation
- else
- add_epp_error('2202', nil, nil, 'Invalid authorization information; '\
- 'invalid reserved>pw value')
- end
- end
- end
-
- unverified_registrant_params = frame.css('registrant').present? &&
- frame.css('registrant').attr('verified').to_s.downcase != 'yes'
-
- if registrant_verification_needed && errors.empty? && verify &&
- Setting.request_confirmation_on_registrant_change_enabled &&
- unverified_registrant_params
- registrant_verification_asked!(frame.to_s, current_user.id) unless disputed?
- end
-
- errors.empty? && super(at)
+ tech_contacts << registrant_obj if tech_domain_contacts.blank?
+ admin_contacts << registrant_obj if admin_domain_contacts.blank? && !registrant.org?
end
def apply_pending_delete!
@@ -618,7 +207,7 @@ class Epp::Domain < Domain
end
def add_renew_epp_errors
- if renew_blocking_statuses.any? && !renewable?
+ if renew_blocking_statuses.any? || !renewable?
add_epp_error('2304', 'status', renew_blocking_statuses,
I18n.t('object_status_prohibits_operation'))
end
diff --git a/app/models/nameserver.rb b/app/models/nameserver.rb
index 3e4051165..85e64ce41 100644
--- a/app/models/nameserver.rb
+++ b/app/models/nameserver.rb
@@ -63,7 +63,7 @@ class Nameserver < ApplicationRecord
end
class << self
- def find_by_hash_params params
+ def from_hash_params params
params = params.with_indifferent_access
rel = all
rel = rel.where(hostname: params[:hostname])
diff --git a/app/models/tech_domain_contact.rb b/app/models/tech_domain_contact.rb
index 20f21b6ed..eff815350 100644
--- a/app/models/tech_domain_contact.rb
+++ b/app/models/tech_domain_contact.rb
@@ -6,7 +6,7 @@ class TechDomainContact < DomainContact
tech_contacts = where(contact: current_contact)
tech_contacts.each do |tech_contact|
- if tech_contact.domain.bulk_update_prohibited?
+ if irreplaceable?(tech_contact)
skipped_domains << tech_contact.domain.name
next
end
@@ -20,4 +20,9 @@ class TechDomainContact < DomainContact
end
[affected_domains.sort, skipped_domains.sort]
end
+
+ def self.irreplaceable?(tech_contact)
+ dn = tech_contact.domain
+ dn.bulk_update_prohibited? || dn.update_prohibited? || dn.tech_change_prohibited?
+ end
end
diff --git a/config/initializers/apipie.rb b/config/initializers/apipie.rb
new file mode 100644
index 000000000..6fc794b12
--- /dev/null
+++ b/config/initializers/apipie.rb
@@ -0,0 +1,10 @@
+Apipie.configure do |config|
+ config.app_name = "Estonian Internet Foundation's REST EPP"
+ config.validate = true
+ config.translate = false
+ config.api_base_url = "/api"
+ config.doc_base_url = "/apipie"
+ config.swagger_content_type_input = :json
+ # where is your API defined?
+ config.api_controllers_matcher = "#{Rails.root}/app/controllers/**/*.rb"
+end
diff --git a/config/initializers/monkey_patches.rb b/config/initializers/monkey_patches.rb
index d342fb6b0..80109ecdd 100644
--- a/config/initializers/monkey_patches.rb
+++ b/config/initializers/monkey_patches.rb
@@ -1,4 +1,5 @@
require 'core_monkey_patches/array'
+require 'core_monkey_patches/hash'
require 'gem_monkey_patches/builder'
require 'gem_monkey_patches/i18n'
require 'gem_monkey_patches/paper_trail'
diff --git a/config/locales/admin/repp_logs.en.yml b/config/locales/admin/repp_logs.en.yml
index 0a58fe7ba..2436f849a 100644
--- a/config/locales/admin/repp_logs.en.yml
+++ b/config/locales/admin/repp_logs.en.yml
@@ -6,4 +6,3 @@ en:
reset_btn: Reset
show:
title: REPP log
-
diff --git a/config/routes.rb b/config/routes.rb
index a9c38bf00..e17f5c3c6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,13 +53,22 @@ Rails.application.routes.draw do
resources :auctions, only: %i[index]
resources :retained_domains, only: %i[index]
namespace :registrar do
+ resources :notifications, only: [:index, :show, :update]
resources :nameservers do
collection do
put '/', to: 'nameservers#update'
end
end
end
- resources :domains do
+ resources :domains, constraints: { id: /.*/ } do
+ resources :nameservers, only: %i[index create destroy], constraints: { id: /.*/ }, controller: 'domains/nameservers'
+ resources :dnssec, only: %i[index create], constraints: { id: /.*/ }, controller: 'domains/dnssec'
+ resources :contacts, only: %i[index create], constraints: { id: /.*/ }, controller: 'domains/contacts'
+ resources :renew, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/renews'
+ resources :transfer, only: %i[create], constraints: { id: /.*/ }, controller: 'domains/transfers'
+ resources :statuses, only: %i[update destroy], constraints: { id: /.*/ }, controller: 'domains/statuses'
+ match "dnssec", to: "domains/dnssec#destroy", via: "delete", defaults: { id: nil }
+ match "contacts", to: "domains/contacts#destroy", via: "delete", defaults: { id: nil }
collection do
get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ }
post 'transfer', to: 'domains#transfer'
diff --git a/lib/core_monkey_patches/hash.rb b/lib/core_monkey_patches/hash.rb
new file mode 100644
index 000000000..debd1da7f
--- /dev/null
+++ b/lib/core_monkey_patches/hash.rb
@@ -0,0 +1,5 @@
+class Hash
+ def select_keys(*args)
+ select { |k, _| args.include?(k) }.map { |_k, v| v }
+ end
+end
diff --git a/lib/deserializers/xml/dnssec.rb b/lib/deserializers/xml/dnssec.rb
new file mode 100644
index 000000000..89e7934a8
--- /dev/null
+++ b/lib/deserializers/xml/dnssec.rb
@@ -0,0 +1,95 @@
+module Deserializers
+ module Xml
+ class DnssecKey
+ attr_reader :frame, :dsa
+
+ KEY_INTERFACE = { flags: 'flags', protocol: 'protocol', alg: 'alg',
+ public_key: 'pubKey' }.freeze
+ DS_INTERFACE = { ds_key_tag: 'keyTag', ds_alg: 'alg', ds_digest_type: 'digestType',
+ ds_digest: 'digest' }.freeze
+
+ def initialize(frame, dsa)
+ @frame = frame
+ @dsa = dsa
+ end
+
+ def call
+ dsa ? ds_alg_output : xm_copy(frame, KEY_INTERFACE)
+ end
+
+ def ds_alg_output
+ ds_key = xm_copy(frame, DS_INTERFACE)
+ ds_key.merge(xm_copy(frame.css('keyData'), KEY_INTERFACE)) if frame.css('keyData').present?
+ ds_key
+ end
+
+ def other_alg_output
+ xm_copy(frame, KEY_INTERFACE)
+ end
+
+ private
+
+ def xm_copy(entry, map)
+ result = {}
+ map.each do |key, elem|
+ result[key] = entry.css(elem).first.try(:text)
+ end
+ result
+ end
+ end
+
+ class DnssecKeys
+ attr_reader :frame, :key_data, :ds_data
+
+ def initialize(frame)
+ @key_data = []
+ @ds_data = []
+
+ # schema validation prevents both in the same parent node
+ if frame.css('dsData').present?
+ frame.css('dsData').each { |k| @ds_data << key_from_params(k, dsa: true) }
+ end
+
+ return if frame.css('keyData').blank?
+
+ frame.css('keyData').each { |k| @key_data << key_from_params(k, dsa: false) }
+ end
+
+ def key_from_params(obj, dsa: false)
+ Deserializers::Xml::DnssecKey.new(obj, dsa).call
+ end
+
+ def call
+ key_data + ds_data
+ end
+
+ def mark_destroy_all(dns_keys)
+ # if transition support required mark_destroy dns_keys when has ds/key values otherwise ...
+ dns_keys.map { |inf_data| mark(inf_data) }
+ end
+
+ def mark_destroy(dns_keys)
+ data = ds_data.present? ? ds_filter(dns_keys) : kd_filter(dns_keys)
+ data.each { |inf_data| inf_data.blank? ? nil : mark(inf_data) }
+ end
+
+ private
+
+ def ds_filter(dns_keys)
+ @ds_data.map do |ds|
+ dns_keys.find_by(ds.slice(*DS_INTERFACE.keys))
+ end
+ end
+
+ def kd_filter(dns_keys)
+ @key_data.map do |key|
+ dns_keys.find_by(key)
+ end
+ end
+
+ def mark(inf_data)
+ { id: inf_data.id, _destroy: 1 }
+ end
+ end
+ end
+end
diff --git a/lib/deserializers/xml/domain.rb b/lib/deserializers/xml/domain.rb
new file mode 100644
index 000000000..f06ec587a
--- /dev/null
+++ b/lib/deserializers/xml/domain.rb
@@ -0,0 +1,61 @@
+module Deserializers
+ module Xml
+ class Domain
+ attr_reader :frame, :registrar
+
+ def initialize(frame, registrar)
+ @frame = frame
+ @registrar = registrar
+ end
+
+ def call
+ attributes = {
+ name: if_present('name'),
+ registrar: registrar,
+ registrant: if_present('registrant'),
+ reserved_pw: if_present('reserved > pw'),
+ }
+
+ attributes.merge!(assign_period_attributes)
+
+ pw = frame.css('authInfo > pw').text
+ attributes[:transfer_code] = pw if pw.present?
+ attributes.compact
+ end
+
+ def assign_period_attributes
+ period = frame.css('period')
+
+ {
+ period: period.text.present? ? Integer(period.text) : 1,
+ period_unit: period.first ? period.first[:unit] : 'y',
+ exp_date: if_present('curExpDate'),
+ }
+ end
+
+ def if_present(css_path)
+ return if frame.css(css_path).blank?
+
+ frame.css(css_path).text
+ end
+
+ def statuses_to_add
+ statuses_frame = frame.css('add')
+ return if statuses_frame.blank?
+
+ statuses_frame.css('status').map do |status|
+ status['s']
+ end
+ end
+
+ def statuses_to_remove
+ statuses_frame = frame.css('rem')
+ return if statuses_frame.blank?
+
+ statuses_frame.css('status').map do |status|
+ status['s']
+ end
+ end
+ end
+ end
+end
diff --git a/lib/deserializers/xml/domain_create.rb b/lib/deserializers/xml/domain_create.rb
new file mode 100644
index 000000000..78642d892
--- /dev/null
+++ b/lib/deserializers/xml/domain_create.rb
@@ -0,0 +1,52 @@
+require 'deserializers/xml/legal_document'
+require 'deserializers/xml/domain'
+require 'deserializers/xml/nameserver'
+require 'deserializers/xml/dnssec'
+module Deserializers
+ module Xml
+ class DomainCreate
+ attr_reader :frame
+ attr_reader :registrar
+
+ def initialize(frame, registrar)
+ @frame = frame
+ @registrar = registrar
+ end
+
+ def call
+ obj = domain
+ obj[:admin_contacts] = admin_contacts
+ obj[:tech_contacts] = tech_contacts
+ obj[:nameservers_attributes] = nameservers
+ obj[:dnskeys_attributes] = dns_keys
+ obj[:legal_document] = legal_document
+
+ obj
+ end
+
+ def domain
+ @domain ||= ::Deserializers::Xml::Domain.new(frame, registrar).call
+ end
+
+ def nameservers
+ @nameservers ||= ::Deserializers::Xml::Nameservers.new(frame).call
+ end
+
+ def admin_contacts
+ frame.css('contact').select { |c| c['type'] == 'admin' }.map(&:text)
+ end
+
+ def tech_contacts
+ frame.css('contact').select { |c| c['type'] == 'tech' }.map(&:text)
+ end
+
+ def dns_keys
+ @dns_keys ||= ::Deserializers::Xml::DnssecKeys.new(frame).key_data
+ end
+
+ def legal_document
+ @legal_document ||= ::Deserializers::Xml::LegalDocument.new(frame).call
+ end
+ end
+ end
+end
diff --git a/lib/deserializers/xml/domain_delete.rb b/lib/deserializers/xml/domain_delete.rb
new file mode 100644
index 000000000..b682c9b5b
--- /dev/null
+++ b/lib/deserializers/xml/domain_delete.rb
@@ -0,0 +1,20 @@
+module Deserializers
+ module Xml
+ class DomainDelete
+ attr_reader :frame
+
+ def initialize(frame)
+ @frame = frame
+ end
+
+ def call
+ obj = {}
+ obj[:name] = frame.css('name')&.text
+ verify = frame.css('delete').children.css('delete').attr('verified').to_s.downcase == 'yes'
+ obj[:delete] = { verified: verify }
+
+ obj
+ end
+ end
+ end
+end
diff --git a/lib/deserializers/xml/domain_update.rb b/lib/deserializers/xml/domain_update.rb
new file mode 100644
index 000000000..eb1b22296
--- /dev/null
+++ b/lib/deserializers/xml/domain_update.rb
@@ -0,0 +1,89 @@
+require 'deserializers/xml/legal_document'
+require 'deserializers/xml/domain'
+require 'deserializers/xml/nameserver'
+require 'deserializers/xml/dnssec'
+module Deserializers
+ module Xml
+ class DomainUpdate
+ attr_reader :frame, :registrar
+
+ def initialize(frame, registrar)
+ @frame = frame
+ @registrar = registrar
+ @legal_document ||= ::Deserializers::Xml::LegalDocument.new(frame).call
+ end
+
+ def call
+ obj = { domain: frame.css('name')&.text, registrant: registrant, contacts: contacts,
+ transfer_code: if_present('authInfo > pw'), nameservers: nameservers,
+ registrar_id: registrar, statuses: statuses, dns_keys: dns_keys,
+ reserved_pw: if_present('reserved > pw'), legal_document: @legal_document }
+
+ obj.reject { |_key, val| val.blank? }
+ end
+
+ def registrant
+ return if frame.css('chg > registrant').blank?
+
+ { code: frame.css('chg > registrant').text,
+ verified: frame.css('chg > registrant').attr('verified').to_s.downcase == 'yes' }
+ end
+
+ def contacts
+ contacts = []
+ frame.css('add > contact').each do |c|
+ contacts << { code: c.text, type: c['type'], action: 'add' }
+ end
+
+ frame.css('rem > contact').each do |c|
+ contacts << { code: c.text, type: c['type'], action: 'rem' }
+ end
+
+ contacts.presence
+ end
+
+ def nameservers
+ @nameservers = []
+
+ frame.css('add > ns > hostAttr').each { |ns| assign_ns(ns) }
+ frame.css('rem > ns > hostAttr').each { |ns| assign_ns(ns, add: false) }
+
+ @nameservers.presence
+ end
+
+ def assign_ns(nameserver, add: true)
+ nsrv = Deserializers::Xml::Nameserver.new(nameserver).call
+ nsrv[:action] = add ? 'add' : 'rem'
+ @nameservers << nsrv
+ end
+
+ def dns_keys
+ added = ::Deserializers::Xml::DnssecKeys.new(frame.css('add')).call
+ added.each { |k| k[:action] = 'add' }
+ removed = ::Deserializers::Xml::DnssecKeys.new(frame.css('rem')).call
+ removed.each { |k| k[:action] = 'rem' }
+
+ return if (added + removed).blank?
+
+ added + removed
+ end
+
+ def statuses
+ return if frame.css('status').blank?
+
+ s = []
+
+ frame.css('add > status').each { |e| s << { status: e.attr('s'), action: 'add' } }
+ frame.css('rem > status').each { |e| s << { status: e.attr('s'), action: 'rem' } }
+
+ s
+ end
+
+ def if_present(css_path)
+ return if frame.css(css_path).blank?
+
+ frame.css(css_path).text
+ end
+ end
+ end
+end
diff --git a/lib/deserializers/xml/nameserver.rb b/lib/deserializers/xml/nameserver.rb
new file mode 100644
index 000000000..03b264718
--- /dev/null
+++ b/lib/deserializers/xml/nameserver.rb
@@ -0,0 +1,37 @@
+module Deserializers
+ module Xml
+ class Nameserver
+ attr_reader :frame
+
+ def initialize(frame)
+ @frame = frame
+ end
+
+ def call
+ {
+ hostname: frame.css('hostName').text,
+ ipv4: frame.css('hostAddr[ip="v4"]').map(&:text).compact,
+ ipv6: frame.css('hostAddr[ip="v6"]').map(&:text).compact,
+ }
+ end
+ end
+
+ class Nameservers
+ attr_reader :frame
+
+ def initialize(frame)
+ @frame = frame
+ end
+
+ def call
+ res = []
+ frame.css('hostAttr').each do |ns|
+ ns = Deserializers::Xml::Nameserver.new(ns).call
+ res << ns.delete_if { |_k, v| v.blank? }
+ end
+
+ res
+ end
+ end
+ end
+end
diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb
index 834402359..b5d03b5cd 100644
--- a/lib/serializers/repp/contact.rb
+++ b/lib/serializers/repp/contact.rb
@@ -10,7 +10,7 @@ module Serializers
def to_json(obj = contact)
json = { id: obj.code, name: obj.name, ident: ident,
- email: obj.email, phone: obj.phone, fax: obj.fax,
+ email: obj.email, phone: obj.phone,
auth_info: obj.auth_info, statuses: obj.statuses,
disclosed_attributes: obj.disclosed_attributes }
diff --git a/lib/serializers/repp/domain.rb b/lib/serializers/repp/domain.rb
new file mode 100644
index 000000000..60863373c
--- /dev/null
+++ b/lib/serializers/repp/domain.rb
@@ -0,0 +1,45 @@
+module Serializers
+ module Repp
+ class Domain
+ attr_reader :domain
+
+ def initialize(domain, sponsored: true)
+ @domain = domain
+ @sponsored = sponsored
+ end
+
+ # rubocop:disable Metrics/AbcSize
+ def to_json(obj = domain)
+ json = {
+ name: obj.name, registrant: obj.registrant.code, created_at: obj.created_at,
+ updated_at: obj.updated_at, expire_time: obj.expire_time, outzone_at: obj.outzone_at,
+ delete_date: obj.delete_date, force_delete_date: obj.force_delete_date,
+ contacts: contacts, nameservers: nameservers, dnssec_keys: dnssec_keys,
+ statuses: obj.statuses, registrar: registrar
+ }
+ json[:transfer_code] = obj.auth_info if @sponsored
+ json
+ end
+ # rubocop:enable Metrics/AbcSize
+
+ def contacts
+ domain.domain_contacts.map { |c| { code: c.contact_code_cache, type: c.type } }
+ end
+
+ def nameservers
+ domain.nameservers.map { |ns| { hostname: ns.hostname, ipv4: ns.ipv4, ipv6: ns.ipv6 } }
+ end
+
+ def dnssec_keys
+ domain.dnskeys.map do |nssec|
+ { flags: nssec.flags, protocol: nssec.protocol, alg: nssec.alg,
+ public_key: nssec.public_key }
+ end
+ end
+
+ def registrar
+ { name: domain.registrar.name, website: domain.registrar.website }
+ end
+ end
+ end
+end
diff --git a/test/integration/admin_area/domain_update_confirms_test.rb b/test/integration/admin_area/domain_update_confirms_test.rb
new file mode 100644
index 000000000..9eb79685f
--- /dev/null
+++ b/test/integration/admin_area/domain_update_confirms_test.rb
@@ -0,0 +1,80 @@
+require 'test_helper'
+require 'application_system_test_case'
+
+class AdminAreaBlockedDomainsIntegrationTest < JavaScriptApplicationSystemTestCase
+ setup do
+ WebMock.allow_net_connect!
+ sign_in users(:admin)
+ @domain = domains(:shop)
+ end
+
+ def test_t
+ new_registrant = contacts(:william)
+ assert_not_equal new_registrant, @domain.registrant
+
+ puts new_registrant.name
+ request_xml = <<-XML
+
+
+
+
+
+ #{@domain.name}
+
+ #{new_registrant.code}
+
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ post epp_update_path, params: { frame: request_xml },
+ headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ @domain.reload
+
+ puts @domain.registrant
+ end
+
+ private
+
+ def update_registrant_of_domain
+ new_registrant = contacts(:william)
+ assert_not_equal new_registrant, @domain.registrant
+
+ @domain.registrant
+ request_xml = <<-XML
+
+
+
+
+
+ #{@domain.name}
+
+ #{new_registrant.code}
+
+
+
+
+
+ #{'test' * 2000}
+
+
+
+
+ XML
+
+ post epp_update_path, params: { frame: request_xml },
+ headers: { 'HTTP_COOKIE' => 'session=api_bestnames' }
+ @domain.reload
+
+ # puts response.body
+ puts @domain.registrant
+ end
+
+end
\ No newline at end of file
diff --git a/test/integration/repp/v1/contacts/tech_replace_test.rb b/test/integration/repp/v1/contacts/tech_replace_test.rb
new file mode 100644
index 000000000..ed8c9d0cc
--- /dev/null
+++ b/test/integration/repp/v1/contacts/tech_replace_test.rb
@@ -0,0 +1,68 @@
+require 'test_helper'
+
+class ReppV1ContactsTechReplaceTest < 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_contacts
+ old_contact = contacts(:john)
+ new_contact = contacts(:william)
+
+ assert DomainContact.where(contact: old_contact, type: 'TechDomainContact').any?
+
+ payload = { current_contact_id: old_contact.code, new_contact_id: new_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_empty ["hospital.test", "library.test"] - json[:data][:affected_domains]
+
+ assert DomainContact.where(contact: old_contact, type: 'TechDomainContact').blank?
+ end
+
+ def test_validates_contact_codes
+ payload = { current_contact_id: 'aaa', new_contact_id: 'bbb'}
+ 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
+
+ def test_new_contact_must_be_different
+ old_contact = contacts(:john)
+
+ payload = { current_contact_id: old_contact.code, new_contact_id: old_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_domain_has_status_tech_change_prohibited
+ domain_shop = domains(:shop)
+ domain_shop_tech_contact_id = domain_shop.tech_domain_contacts[0][:contact_id]
+ old_contact = Contact.find_by(id: domain_shop_tech_contact_id)
+ new_contact = contacts(:john)
+
+ domain_shop.update(statuses: [DomainStatus::SERVER_TECH_CHANGE_PROHIBITED])
+ domain_shop.reload
+ assert domain_shop.statuses.include? DomainStatus::SERVER_TECH_CHANGE_PROHIBITED
+
+ payload = { current_contact_id: old_contact.code, new_contact_id: new_contact.code }
+ patch "/repp/v1/domains/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_equal json[:data][:skipped_domains], ["shop.test"]
+ assert domain_shop.contacts.find_by(id: domain_shop_tech_contact_id).present?
+ end
+end
diff --git a/test/integration/repp/v1/domains/bulk_renew_test.rb b/test/integration/repp/v1/domains/bulk_renew_test.rb
index 15ebe892b..19a3e34fc 100644
--- a/test/integration/repp/v1/domains/bulk_renew_test.rb
+++ b/test/integration/repp/v1/domains/bulk_renew_test.rb
@@ -106,7 +106,7 @@ class ReppV1DomainsBulkRenewTest < ActionDispatch::IntegrationTest
assert_response :bad_request
assert_equal 2002, json[:code]
- assert_equal 'Not enough funds for renew domains', json[:message]
+ assert_equal 'Domain Billing failure - credit balance low', json[:message]
end
end
diff --git a/test/integration/repp/v1/domains/contacts_test.rb b/test/integration/repp/v1/domains/contacts_test.rb
new file mode 100644
index 000000000..b9b26a745
--- /dev/null
+++ b/test/integration/repp/v1/domains/contacts_test.rb
@@ -0,0 +1,100 @@
+require 'test_helper'
+
+class ReppV1DomainsContactsTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_shows_existing_domain_contacts
+ get "/repp/v1/domains/#{@domain.name}/contacts", 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 @domain.admin_contacts.length, json[:data][:admin_contacts].length
+ assert_equal @domain.tech_contacts.length, json[:data][:tech_contacts].length
+ end
+
+ def test_can_add_new_admin_contacts
+ new_contact = contacts(:john)
+ refute @domain.admin_contacts.find_by(code: new_contact.code).present?
+
+ payload = { contacts: [ { code: new_contact.code, type: 'admin' } ] }
+ post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+
+ assert @domain.admin_contacts.find_by(code: new_contact.code).present?
+ end
+
+ def test_can_add_new_tech_contacts
+ new_contact = contacts(:john)
+ refute @domain.tech_contacts.find_by(code: new_contact.code).present?
+
+ payload = { contacts: [ { code: new_contact.code, type: 'tech' } ] }
+ post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ @domain.reload
+
+ assert @domain.tech_contacts.find_by(code: new_contact.code).present?
+ end
+
+ def test_can_remove_admin_contacts
+ contact = contacts(:john)
+ payload = { contacts: [ { code: contact.code, type: 'admin' } ] }
+ post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ assert @domain.admin_contacts.find_by(code: contact.code).present?
+
+ # Actually delete the contact
+ delete "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+
+ refute @domain.admin_contacts.find_by(code: contact.code).present?
+ end
+
+ def test_can_remove_tech_contacts
+ contact = contacts(:john)
+ payload = { contacts: [ { code: contact.code, type: 'tech' } ] }
+ post "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ assert @domain.tech_contacts.find_by(code: contact.code).present?
+
+ # Actually delete the contact
+ delete "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+
+ refute @domain.tech_contacts.find_by(code: contact.code).present?
+ end
+
+ def test_can_not_remove_one_and_only_contact
+ contact = @domain.admin_contacts.last
+
+ payload = { contacts: [ { code: contact.code, type: 'admin' } ] }
+ delete "/repp/v1/domains/#{@domain.name}/contacts", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ @domain.reload
+ assert_response :bad_request
+ assert_equal 2004, json[:code]
+
+ assert @domain.admin_contacts.any?
+ end
+
+end
diff --git a/test/integration/repp/v1/domains/create_test.rb b/test/integration/repp/v1/domains/create_test.rb
new file mode 100644
index 000000000..7907e709e
--- /dev/null
+++ b/test/integration/repp/v1/domains/create_test.rb
@@ -0,0 +1,137 @@
+require 'test_helper'
+
+class ReppV1DomainsCreateTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_creates_new_domain_successfully
+ @auth_headers['Content-Type'] = 'application/json'
+ contact = contacts(:john)
+
+ payload = {
+ domain: {
+ name: 'domeener.test',
+ registrant: contact.code,
+ period: 1,
+ period_unit: 'y'
+ }
+ }
+
+ post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @user.registrar.domains.find_by(name: 'domeener.test').present?
+ end
+
+ def test_validates_price_on_domain_create
+ @auth_headers['Content-Type'] = 'application/json'
+ contact = contacts(:john)
+
+ payload = {
+ domain: {
+ name: 'domeener.test',
+ registrant: contact.code,
+ period: 3,
+ period_unit: 'y'
+ }
+ }
+
+ post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :bad_request
+ assert_equal 2104, json[:code]
+ assert_equal 'Active price missing for this operation!', json[:message]
+
+ refute @user.registrar.domains.find_by(name: 'domeener.test').present?
+ end
+
+ def test_creates_domain_with_predefined_nameservers
+ @auth_headers['Content-Type'] = 'application/json'
+ contact = contacts(:john)
+
+ payload = {
+ domain: {
+ name: 'domeener.test',
+ registrant: contact.code,
+ period: 1,
+ period_unit: 'y',
+ nameservers_attributes: [
+ { hostname: 'ns1.domeener.ee' },
+ { hostname: 'ns2.domeener.ee' }
+ ]
+ }
+ }
+
+ post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ domain = @user.registrar.domains.find_by(name: 'domeener.test')
+ assert domain.present?
+ assert_empty ['ns1.domeener.ee', 'ns2.domeener.ee'] - domain.nameservers.collect(&:hostname)
+ end
+
+ def test_creates_domain_with_custom_contacts
+ @auth_headers['Content-Type'] = 'application/json'
+ contact = contacts(:john)
+ admin_contact = contacts(:william)
+ tech_contact = contacts(:jane)
+
+ payload = {
+ domain: {
+ name: 'domeener.test',
+ registrant: contact.code,
+ period: 1,
+ period_unit: 'y',
+ admin_contacts: [ admin_contact.code ],
+ tech_contacts: [ tech_contact.code ],
+ }
+ }
+
+ post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ domain = @user.registrar.domains.find_by(name: 'domeener.test')
+ assert domain.present?
+ assert_equal tech_contact, domain.tech_domain_contacts.first.contact
+ assert_equal admin_contact, domain.admin_domain_contacts.first.contact
+ end
+
+ def test_creates_new_domain_with_desired_transfer_code
+ @auth_headers['Content-Type'] = 'application/json'
+ contact = contacts(:john)
+
+ payload = {
+ domain: {
+ name: 'domeener.test',
+ registrant: contact.code,
+ transfer_code: 'ABADIATS',
+ period: 1,
+ period_unit: 'y'
+ }
+ }
+
+ post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @user.registrar.domains.find_by(name: 'domeener.test').present?
+ assert_equal 'ABADIATS', @user.registrar.domains.find_by(name: 'domeener.test').transfer_code
+ end
+end
diff --git a/test/integration/repp/v1/domains/delete_test.rb b/test/integration/repp/v1/domains/delete_test.rb
new file mode 100644
index 000000000..08b73e832
--- /dev/null
+++ b/test/integration/repp/v1/domains/delete_test.rb
@@ -0,0 +1,54 @@
+require 'test_helper'
+
+class ReppV1DomainsDeleteTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_domain_pending_delete_confirmation
+ Setting.request_confirmation_on_domain_deletion_enabled = true
+ @auth_headers['Content-Type'] = 'application/json'
+
+ payload = {
+ delete: {
+ verified: false
+ }
+ }
+
+ delete "/repp/v1/domains/#{@domain.name}", headers: @auth_headers, params: payload.to_json
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION
+ assert_not @domain.statuses.include? DomainStatus::PENDING_DELETE
+ end
+
+ def test_domain_pending_delete_on_verified_delete
+ Setting.request_confirmation_on_domain_deletion_enabled = true
+ @auth_headers['Content-Type'] = 'application/json'
+
+ payload = {
+ delete: {
+ verified: true
+ }
+ }
+
+ delete "/repp/v1/domains/#{@domain.name}", headers: @auth_headers, params: payload.to_json
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ refute @domain.statuses.include? DomainStatus::PENDING_DELETE_CONFIRMATION
+ assert @domain.statuses.include? DomainStatus::PENDING_DELETE
+ end
+end
diff --git a/test/integration/repp/v1/domains/dnssec_test.rb b/test/integration/repp/v1/domains/dnssec_test.rb
new file mode 100644
index 000000000..cced7e646
--- /dev/null
+++ b/test/integration/repp/v1/domains/dnssec_test.rb
@@ -0,0 +1,96 @@
+require 'test_helper'
+
+class ReppV1DomainsDnssecTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_shows_dnssec_keys_associated_with_domain
+ get "/repp/v1/domains/#{@domain.name}/dnssec", 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_empty json[:data][:dns_keys]
+
+ payload = {
+ dns_keys: [
+ { flags: '256',
+ alg: '14',
+ protocol: '3',
+ public_key: 'dGVzdA=='
+ }
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ get "/repp/v1/domains/#{@domain.name}/dnssec", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_equal 1, json[:data][:dns_keys].length
+ end
+
+ def test_creates_dnssec_key_successfully
+ assert @domain.dnskeys.empty?
+ payload = {
+ dns_keys: [
+ { flags: '256',
+ alg: '14',
+ protocol: '3',
+ public_key: 'dGVzdA=='
+ }
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+ @domain.reload
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @domain.dnskeys.present?
+ dnssec_key = @domain.dnskeys.last
+ assert_equal payload[:dns_keys][0][:flags].to_i, dnssec_key.flags
+ assert_equal payload[:dns_keys][0][:alg].to_i, dnssec_key.alg
+ assert_equal payload[:dns_keys][0][:protocol].to_i, dnssec_key.protocol
+ assert_equal payload[:dns_keys][0][:public_key], dnssec_key.public_key
+ end
+
+ def test_removes_existing_dnssec_key_successfully
+ payload = {
+ dns_keys: [
+ { flags: '256',
+ alg: '14',
+ protocol: '3',
+ public_key: 'dGVzdA=='
+ }
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert @domain.dnskeys.any?
+
+ # Real delete here
+ delete "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+ @domain.reload
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @domain.dnskeys.empty?
+ end
+end
diff --git a/test/integration/repp/v1/domains/list_test.rb b/test/integration/repp/v1/domains/list_test.rb
index bee390e5c..366ac4d26 100644
--- a/test/integration/repp/v1/domains/list_test.rb
+++ b/test/integration/repp/v1/domains/list_test.rb
@@ -51,4 +51,17 @@ class ReppV1DomainsListTest < ActionDispatch::IntegrationTest
assert_equal (@user.registrar.domains.count - offset), json[:data][:domains].length
end
+
+ def test_returns_specific_domain_details_by_name
+ domain = domains(:shop)
+ get "/repp/v1/domains/#{domain.name}", 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]
+
+ serialized_domain = Serializers::Repp::Domain.new(domain).to_json
+ assert_equal serialized_domain.as_json, json[:data][:domain].as_json
+ end
end
diff --git a/test/integration/repp/v1/domains/nameservers_test.rb b/test/integration/repp/v1/domains/nameservers_test.rb
new file mode 100644
index 000000000..780e889c1
--- /dev/null
+++ b/test/integration/repp/v1/domains/nameservers_test.rb
@@ -0,0 +1,94 @@
+require 'test_helper'
+
+class ReppV1DomainsNameserversTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_can_add_new_nameserver
+ payload = {
+ nameservers: [
+ { hostname: "ns1.domeener.ee",
+ ipv4: ["192.168.1.1"],
+ ipv6: ["FE80::AEDE:48FF:FE00:1122"]}
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/nameservers", params: payload, 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 payload[:nameservers][0][:hostname], @domain.nameservers.last.hostname
+ assert_equal payload[:nameservers][0][:ipv4], @domain.nameservers.last.ipv4
+ assert_equal payload[:nameservers][0][:ipv6], @domain.nameservers.last.ipv6
+ end
+
+ def test_can_remove_existing_nameserver
+ payload = {
+ nameservers: [
+ { hostname: "ns1.domeener.ee",
+ ipv4: ["192.168.1.1"],
+ ipv6: ["FE80::AEDE:48FF:FE00:1122"]}
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/nameservers", params: payload, headers: @auth_headers
+ assert_response :ok
+
+ @domain.reload
+ assert @domain.nameservers.where(hostname: payload[:nameservers][0][:hostname]).any?
+
+ delete "/repp/v1/domains/#{@domain.name}/nameservers/#{payload[:nameservers][0][:hostname]}",
+ params: payload, 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]
+
+ @domain.reload
+ refute @domain.nameservers.where(hostname: payload[:nameservers][0][:hostname]).any?
+ end
+
+ def test_can_not_add_duplicate_nameserver
+ payload = {
+ nameservers: [
+ { hostname: @domain.nameservers.last.hostname,
+ ipv4: @domain.nameservers.last.ipv4,
+ ipv6: @domain.nameservers.last.ipv6 }
+ ]
+ }
+
+ post "/repp/v1/domains/#{@domain.name}/nameservers", params: payload, headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :bad_request
+ assert_equal 2302, json[:code]
+ assert_equal 'Nameserver already exists on this domain [hostname]', json[:message]
+ end
+
+ def test_returns_errors_when_removing_unknown_nameserver
+ delete "/repp/v1/domains/#{@domain.name}/nameservers/ns.nonexistant.test", 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_returns_error_when_ns_count_too_low
+ delete "/repp/v1/domains/#{@domain.name}/nameservers/#{@domain.nameservers.last.hostname}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2308, json[:code]
+ assert_equal 'Data management policy violation; Nameserver count must be between 2-11 for active ' \
+ 'domains [nameservers]', json[:message]
+ end
+end
diff --git a/test/integration/repp/v1/domains/renews_test.rb b/test/integration/repp/v1/domains/renews_test.rb
new file mode 100644
index 000000000..c3e73669a
--- /dev/null
+++ b/test/integration/repp/v1/domains/renews_test.rb
@@ -0,0 +1,66 @@
+require 'test_helper'
+
+class ReppV1DomainsRenewsTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_domain_can_be_renewed
+ original_valid_to = @domain.valid_to
+ travel_to Time.zone.parse('2010-07-05')
+
+ @auth_headers['Content-Type'] = 'application/json'
+ payload = { renew: { period: 1, period_unit: 'y', exp_date: original_valid_to } }
+ post "/repp/v1/domains/#{@domain.name}/renew", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @domain.valid_to, original_valid_to + 1.year
+ end
+
+ def test_domain_renew_pricing_error
+ original_valid_to = @domain.valid_to
+ travel_to Time.zone.parse('2010-07-05')
+
+ @auth_headers['Content-Type'] = 'application/json'
+ payload = { renew: { period: 100, period_unit: 'y', exp_date: original_valid_to } }
+ post "/repp/v1/domains/#{@domain.name}/renew", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2104, json[:code]
+ assert_equal 'Active price missing for this operation!', json[:message]
+
+ assert @domain.valid_to, original_valid_to
+ end
+
+ def test_some_test
+ days_to_renew_domain_before_expire = setting_entries(:days_to_renew_domain_before_expire)
+ days_to_renew_domain_before_expire.update(value: '1')
+ days_to_renew_domain_before_expire.reload
+
+ original_valid_to = @domain.valid_to
+ travel_to @domain.valid_to - 3.days
+
+ one_year = billing_prices(:renew_one_year)
+ one_year.update(valid_from: @domain.valid_to - 5.days)
+ one_year.reload
+
+ @auth_headers['Content-Type'] = 'application/json'
+ payload = { renew: { period: 1, period_unit: 'y', exp_date: original_valid_to } }
+ post "/repp/v1/domains/#{@domain.name}/renew", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 2304, json[:code]
+ assert_equal 'Object status prohibits operation', json[:message]
+ end
+end
diff --git a/test/integration/repp/v1/domains/statuses_test.rb b/test/integration/repp/v1/domains/statuses_test.rb
new file mode 100644
index 000000000..271752ae3
--- /dev/null
+++ b/test/integration/repp/v1/domains/statuses_test.rb
@@ -0,0 +1,82 @@
+require 'test_helper'
+
+class ReppV1DomainsStatusesTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_client_hold_can_be_added
+ refute @domain.statuses.include?(DomainStatus::CLIENT_HOLD)
+ put repp_v1_domain_status_path(domain_id: @domain.name, id: DomainStatus::CLIENT_HOLD), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ @domain.reload
+
+ assert @domain.statuses.include?(DomainStatus::CLIENT_HOLD)
+ end
+
+ def test_client_hold_can_be_removed
+ statuses = @domain.statuses << DomainStatus::CLIENT_HOLD
+ @domain.update(statuses: statuses)
+ delete repp_v1_domain_status_path(domain_id: @domain.name, id: DomainStatus::CLIENT_HOLD), headers: @auth_headers
+
+ assert_response :ok
+ @domain.reload
+ refute @domain.statuses.include?(DomainStatus::CLIENT_HOLD)
+ end
+
+ def test_can_not_remove_disallowed_statuses
+ statuses = @domain.statuses << DomainStatus::FORCE_DELETE
+ @domain.update(statuses: statuses)
+
+ delete repp_v1_domain_status_path(domain_id: @domain.name, id: DomainStatus::FORCE_DELETE), headers: @auth_headers
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 'Parameter value policy error. Client-side object status management not supported: status serverForceDelete', json[:message]
+
+ assert @domain.statuses.include?(DomainStatus::FORCE_DELETE)
+ end
+
+ def test_can_not_add_disallowed_statuses
+ put repp_v1_domain_status_path(domain_id: @domain.name, id: DomainStatus::DELETE_CANDIDATE), headers: @auth_headers
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 'Parameter value policy error. Client-side object status management not supported: status deleteCandidate', json[:message]
+
+ refute @domain.statuses.include?(DomainStatus::DELETE_CANDIDATE)
+ end
+
+ def test_can_not_remove_unexistant_status
+ refute @domain.statuses.include?(DomainStatus::CLIENT_HOLD)
+ delete repp_v1_domain_status_path(domain_id: @domain.name, id: DomainStatus::CLIENT_HOLD), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert_equal 'Parameter value policy error. Client-side object status management not supported: status clientHold', json[:message]
+ end
+
+ def test_returns_normal_error_when_action_fails
+ @invalid_domain = domains(:invalid)
+
+ put repp_v1_domain_status_path(domain_id: @invalid_domain.name, id: DomainStatus::CLIENT_HOLD), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :bad_request
+ assert_equal 2304, json[:code]
+
+ delete repp_v1_domain_status_path(domain_id: @invalid_domain.name, id: DomainStatus::FORCE_DELETE), headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :bad_request
+ assert_equal 2306, json[:code]
+ end
+
+end
diff --git a/test/integration/repp/v1/domains/transfer_info_test.rb b/test/integration/repp/v1/domains/transfer_info_test.rb
index df6bc4f12..a3b8fe874 100644
--- a/test/integration/repp/v1/domains/transfer_info_test.rb
+++ b/test/integration/repp/v1/domains/transfer_info_test.rb
@@ -28,6 +28,7 @@ class ReppV1DomainsTransferInfoTest < ActionDispatch::IntegrationTest
def test_respects_domain_authorization_code
headers = @auth_headers
headers['Auth-Code'] = 'jhfgifhdg'
+ @domain.update!(registrar: registrars(:goodnames))
get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers
json = JSON.parse(response.body, symbolize_names: true)
diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb
index 46d5c30c4..a86395083 100644
--- a/test/integration/repp/v1/domains/transfer_test.rb
+++ b/test/integration/repp/v1/domains/transfer_test.rb
@@ -10,6 +10,34 @@ class ReppV1DomainsTransferTest < ActionDispatch::IntegrationTest
@auth_headers = { 'Authorization' => token }
end
+ def test_transfers_scoped_domain
+ refute @domain.registrar == @user.registrar
+ payload = { transfer: { transfer_code: @domain.transfer_code } }
+ post "/repp/v1/domains/#{@domain.name}/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+ @domain.reload
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert_equal @domain.registrar, @user.registrar
+ end
+
+ def test_does_not_transfer_scoped_domain_with_invalid_transfer_code
+ refute @domain.registrar == @user.registrar
+ payload = { transfer: { transfer_code: 'invalid' } }
+ post "/repp/v1/domains/#{@domain.name}/transfer", headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+ @domain.reload
+
+ assert_response :bad_request
+ assert_equal 2202, json[:code]
+ assert_equal 'Invalid authorization information', json[:message]
+
+ refute @domain.registrar == @user.registrar
+ end
+
def test_transfers_domain
payload = {
"data": {
diff --git a/test/integration/repp/v1/domains/update_test.rb b/test/integration/repp/v1/domains/update_test.rb
new file mode 100644
index 000000000..d924fe7a3
--- /dev/null
+++ b/test/integration/repp/v1/domains/update_test.rb
@@ -0,0 +1,85 @@
+require 'test_helper'
+
+class ReppV1DomainsUpdateTest < ActionDispatch::IntegrationTest
+ def setup
+ @user = users(:api_bestnames)
+ @domain = domains(:shop)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ token = "Basic #{token}"
+
+ @auth_headers = { 'Authorization' => token }
+ end
+
+ def test_updates_transfer_code_for_domain
+ @auth_headers['Content-Type'] = 'application/json'
+ new_auth_code = 'aisdcbkabcsdnc'
+
+ payload = {
+ domain: {
+ auth_code: new_auth_code
+ }
+ }
+
+ put "/repp/v1/domains/#{@domain.name}", headers: @auth_headers, params: payload.to_json
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert new_auth_code, @domain.auth_info
+ end
+
+ def test_domain_pending_update_on_registrant_change
+ Setting.request_confirmation_on_registrant_change_enabled = true
+
+ @auth_headers['Content-Type'] = 'application/json'
+ new_registrant = contacts(:william)
+ refute @domain.registrant == new_registrant
+
+ payload = {
+ domain: {
+ registrant: {
+ code: new_registrant.code
+ }
+ }
+ }
+
+ put "/repp/v1/domains/#{@domain.name}", headers: @auth_headers, params: payload.to_json
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ refute @domain.registrant.code == new_registrant.code
+ assert @domain.statuses.include? DomainStatus::PENDING_UPDATE
+ end
+
+ def test_replaces_registrant_when_verified
+ Setting.request_confirmation_on_registrant_change_enabled = true
+
+ @auth_headers['Content-Type'] = 'application/json'
+ new_registrant = contacts(:william)
+ refute @domain.registrant == new_registrant
+
+ payload = {
+ domain: {
+ registrant: {
+ code: new_registrant.code,
+ verified: true
+ }
+ }
+ }
+
+ put "/repp/v1/domains/#{@domain.name}", headers: @auth_headers, params: payload.to_json
+ @domain.reload
+ json = JSON.parse(response.body, symbolize_names: true)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+
+ assert @domain.registrant.code == new_registrant.code
+ refute @domain.statuses.include? DomainStatus::PENDING_UPDATE
+ end
+end
diff --git a/test/integration/repp/v1/registrar/nameservers_test.rb b/test/integration/repp/v1/registrar/nameservers_test.rb
index e2c6f890b..01f30c813 100644
--- a/test/integration/repp/v1/registrar/nameservers_test.rb
+++ b/test/integration/repp/v1/registrar/nameservers_test.rb
@@ -101,4 +101,79 @@ class ReppV1RegistrarNameserversTest < ActionDispatch::IntegrationTest
assert_equal 2005, json[:code]
assert_equal 'IPv6 is invalid [ipv6]', json[:message]
end
+
+ def test_ipv4_isnt_array
+ nameserver = nameservers(:shop_ns1)
+ payload = {
+ "data": {
+ "id": nameserver.hostname,
+ "type": "nameserver",
+ "domains": ["shop.test", "airport.test", "library.test", "metro.test"],
+ "attributes": {
+ "hostname": nameserver.hostname,
+ "ipv4": "1.1.1.1",
+ "ipv6": ["2620:119:352::36"]
+ }
+ }
+ }
+
+ 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 json[:message].include? 'Must be an array of String'
+ end
+
+ def test_ipv6_isnt_array
+ nameserver = nameservers(:shop_ns1)
+ payload = {
+ "data": {
+ "id": nameserver.hostname,
+ "type": "nameserver",
+ "domains": ["shop.test", "airport.test", "library.test", "metro.test"],
+ "attributes": {
+ "hostname": nameserver.hostname,
+ "ipv4": ["1.1.1.1"],
+ "ipv6": "2620:119:352::36"
+ }
+ }
+ }
+
+ 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 json[:message].include? 'Must be an array of String'
+ end
+
+ def test_bulk_nameservers_change_in_array_of_domains
+ domain_shop = domains(:shop)
+ domain_airport = domains(:airport)
+
+ payload = {
+ "data": {
+ "type": "nameserver",
+ "id": "ns1.bestnames.test",
+ "domains": ["shop.test", "airport.test"],
+ "attributes": {
+ "hostname": "ns4.bestnames.test",
+ "ipv4": ["192.168.1.1"],
+ "ipv6": ["2620:119:35::36"]
+ }
+ }
+ }
+
+ put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload
+ json = JSON.parse(response.body, symbolize_names: true)
+ domain_airport.reload
+ domain_shop.reload
+
+ refute domain_shop.nameservers.find_by(hostname: 'ns1.bestnames.test').present?
+ assert domain_shop.nameservers.find_by(hostname: 'ns4.bestnames.test').present?
+ assert_equal({ hostname: "ns4.bestnames.test", ipv4: ["192.168.1.1"], ipv6: ["2620:119:35::36"] }, json[:data][:attributes])
+ assert json[:data][:affected_domains].include? 'airport.test'
+ assert json[:data][:affected_domains].include? 'shop.test'
+ end
end
diff --git a/test/integration/repp/v1/registrar/notifications_test.rb b/test/integration/repp/v1/registrar/notifications_test.rb
new file mode 100644
index 000000000..9fafca443
--- /dev/null
+++ b/test/integration/repp/v1/registrar/notifications_test.rb
@@ -0,0 +1,50 @@
+require 'test_helper'
+
+class ReppV1RegistrarNotificationsTest < 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_gets_latest_unread_poll_message
+ notification = @user.registrar.notifications.where(read: false).order(created_at: :desc).first
+ get "/repp/v1/registrar/notifications", 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 notification.text, json[:data][:text]
+ end
+
+ def test_can_read_specific_notification_by_id
+ notification = @user.registrar.notifications.order(created_at: :desc).second
+
+ get "/repp/v1/registrar/notifications/#{notification.id}", 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 notification.text, json[:data][:text]
+ end
+
+ def test_can_mark_notification_as_read
+ @auth_headers['Content-Type'] = 'application/json'
+ notification = @user.registrar.notifications.where(read: false).order(created_at: :desc).first
+
+ payload = { notification: { read: true} }
+ put "/repp/v1/registrar/notifications/#{notification.id}", headers: @auth_headers, params: payload.to_json
+ json = JSON.parse(response.body, symbolize_names: true)
+ notification.reload
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ assert_equal 'Command completed successfully', json[:message]
+ assert_equal notification.id, json[:data][:notification_id]
+ assert_equal notification.read, json[:data][:read]
+ end
+end
diff --git a/test/integration/repp/v1/retained_domains_test.rb b/test/integration/repp/v1/retained_domains_test.rb
index 824c826ef..814eabfc1 100644
--- a/test/integration/repp/v1/retained_domains_test.rb
+++ b/test/integration/repp/v1/retained_domains_test.rb
@@ -77,7 +77,7 @@ class ReppV1RetainedDomainsTest < ActionDispatch::IntegrationTest
status: 'disputed',
punycode_name: 'reserved.test' }]
- assert_empty response_json[:domains] - expected_objects
+ assert_equal response_json[:domains].to_set, expected_objects.to_set
end
def test_etags_cache
diff --git a/test/jobs/domain_update_confirm_job_test.rb b/test/jobs/domain_update_confirm_job_test.rb
index d2d3a3252..aa686a9f8 100644
--- a/test/jobs/domain_update_confirm_job_test.rb
+++ b/test/jobs/domain_update_confirm_job_test.rb
@@ -1,4 +1,6 @@
require "test_helper"
+require 'deserializers/xml/domain_update'
+
class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
def setup
super
@@ -55,7 +57,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
@domain.reload
@@ -71,7 +75,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
DomainUpdateConfirmJob.perform_now(@domain.id, RegistrantVerification::REJECTED)
@@ -86,7 +92,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
@domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED])
@@ -104,7 +112,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
@domain.update(statuses: [DomainStatus::DELETE_CANDIDATE, DomainStatus::DISPUTED])
@@ -122,7 +132,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
@domain.update(statuses: [DomainStatus::PENDING_UPDATE])
@domain.nameservers.destroy_all
@@ -142,7 +154,9 @@ class DomainUpdateConfirmJobTest < ActiveSupport::TestCase
" \n #{@new_registrant.code}\n \n \n \n \n \n" \
" \n #{@legal_doc_path}\n \n" \
" \n 20alla-1594199756\n \n\n"
- @domain.pending_json['frame'] = epp_xml
+ parsed_frame = Deserializers::Xml::DomainUpdate.new(Nokogiri::XML(epp_xml), @domain.registrar.id).call
+
+ @domain.pending_json['frame'] = parsed_frame
@domain.update(pending_json: @domain.pending_json)
@domain.update(statuses: [DomainStatus::OK, DomainStatus::PENDING_UPDATE])