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])