diff --git a/Gemfile b/Gemfile index d35238fc0..7ae8d4ef6 100644 --- a/Gemfile +++ b/Gemfile @@ -36,8 +36,6 @@ gem 'select2-rails', '3.5.9.3' # for autocomplete gem 'cancancan' gem 'devise', '~> 4.7' -gem 'grape' - # registry specfic gem 'data_migrate', '~> 6.1' gem 'isikukood' # for EE-id validation diff --git a/Gemfile.lock b/Gemfile.lock index 14970c2c9..72c716aae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -198,28 +198,6 @@ GEM docile (1.3.2) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dry-configurable (0.11.6) - concurrent-ruby (~> 1.0) - dry-core (~> 0.4, >= 0.4.7) - dry-equalizer (~> 0.2) - dry-container (0.7.2) - concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.9) - concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-inflector (0.2.0) - dry-logic (1.0.7) - concurrent-ruby (~> 1.0) - dry-core (~> 0.2) - dry-equalizer (~> 0.2) - dry-types (1.4.0) - concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.0, >= 1.0.2) erubi (1.9.0) erubis (2.7.0) execjs (2.7.0) @@ -228,13 +206,6 @@ GEM thor (~> 0.14) globalid (0.4.2) activesupport (>= 4.2.0) - grape (1.4.0) - activesupport - builder - dry-types (>= 1.1) - mustermann-grape (~> 1.0.0) - rack (>= 1.3.0) - rack-accept gyoku (1.3.1) builder (>= 2.1.2) haml (5.1.2) @@ -314,8 +285,6 @@ GEM multi_json (1.15.0) mustermann (1.1.1) ruby2_keywords (~> 0.0.1) - mustermann-grape (1.0.1) - mustermann (>= 1.0.0) netrc (0.11.0) nio4r (2.5.4) nokogiri (1.10.10) @@ -359,8 +328,6 @@ GEM que (~> 0.8) sinatra rack (2.2.3) - rack-accept (0.4.5) - rack (>= 0.4) rack-oauth2 (1.16.0) activesupport attr_required @@ -545,7 +512,6 @@ DEPENDENCIES epp! epp-xml (= 1.1.0)! figaro (= 1.1.1) - grape haml (~> 5.0) isikukood iso8601 (= 0.12.1) diff --git a/app/api/repp/account_v1.rb b/app/api/repp/account_v1.rb deleted file mode 100644 index fe3acd387..000000000 --- a/app/api/repp/account_v1.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Repp - class AccountV1 < Grape::API - version 'v1', using: :path - - resource :accounts do - desc 'Return current cash account balance' - - get 'balance' do - @response = { - balance: current_user.registrar.cash_account.balance, - currency: current_user.registrar.cash_account.currency - } - end - end - end -end diff --git a/app/api/repp/api.rb b/app/api/repp/api.rb deleted file mode 100644 index af6864cfa..000000000 --- a/app/api/repp/api.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Repp - class API < Grape::API - format :json - prefix :repp - - http_basic do |username, password| - @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) - if @current_user - true - else - error! I18n.t('api_user_not_found'), 401 - end - end - - before do - webclient_request = ENV['webclient_ips'].split(',').map(&:strip).include?(request.ip) - unless webclient_request - error! I18n.t('api.authorization.ip_not_allowed', ip: request.ip), 401 unless @current_user.registrar.api_ip_white?(request.ip) - end - - if @current_user.cannot?(:view, :repp) - error! I18n.t('no_permission'), 401 unless @current_user.registrar.api_ip_white?(request.ip) - end - - next if Rails.env.test? || Rails.env.development? - message = 'Certificate mismatch! Cert common name should be:' - request_name = env['HTTP_SSL_CLIENT_S_DN_CN'] - - if webclient_request - webclient_cert_name = ENV['webclient_cert_common_name'] || 'webclient' - error! "Webclient #{message} #{webclient_cert_name}", 401 if webclient_cert_name != request_name - else - unless @current_user.pki_ok?(request.env['HTTP_SSL_CLIENT_CERT'], - request.env['HTTP_SSL_CLIENT_S_DN_CN']) - error! "#{message} #{@current_user.username}", 401 - end - end - end - - helpers do - attr_reader :current_user - end - - after do - ApiLog::ReppLog.create({ - request_path: request.path, - request_method: request.request_method, - request_params: request.params.except('route_info').to_json, - response: @response.to_json, - response_code: status, - api_user_name: current_user.try(:username), - api_user_registrar: current_user.try(:registrar).try(:to_s), - ip: request.ip, - uuid: request.try(:uuid) - }) - end - - mount Repp::DomainV1 - mount Repp::ContactV1 - mount Repp::AccountV1 - mount Repp::DomainTransfersV1 - mount Repp::NameserversV1 - mount Repp::DomainContactsV1 - end -end diff --git a/app/api/repp/contact_v1.rb b/app/api/repp/contact_v1.rb deleted file mode 100644 index 810829ef7..000000000 --- a/app/api/repp/contact_v1.rb +++ /dev/null @@ -1,35 +0,0 @@ -module Repp - class ContactV1 < Grape::API - version 'v1', using: :path - - resource :contacts do - desc 'Return list of contact' - params do - optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many contacts to show' - optional :offset, type: Integer, desc: 'Contact number to start at' - optional :details, type: String, values: %w(true false), desc: 'Whether to include details' - end - - get '/' do - limit = params[:limit] || 200 - offset = params[:offset] || 0 - - if params[:details] == 'true' - contacts = current_user.registrar.contacts.limit(limit).offset(offset) - - unless Contact.address_processing? - attributes = Contact.attribute_names - Contact.address_attribute_names - contacts = contacts.select(attributes) - end - else - contacts = current_user.registrar.contacts.limit(limit).offset(offset).pluck(:code) - end - - @response = { - contacts: contacts, - total_number_of_records: current_user.registrar.contacts.count - } - end - end - end -end diff --git a/app/api/repp/domain_contacts_v1.rb b/app/api/repp/domain_contacts_v1.rb deleted file mode 100644 index 7f3e323ac..000000000 --- a/app/api/repp/domain_contacts_v1.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Repp - class DomainContactsV1 < Grape::API - version 'v1', using: :path - - resource :domains do - resource :contacts do - patch '/' do - current_contact = current_user.registrar.contacts - .find_by(code: params[:current_contact_id]) - new_contact = current_user.registrar.contacts.find_by(code: params[:new_contact_id]) - - unless current_contact - error!({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: "No such contact: #{params[:current_contact_id]}"} }, - :bad_request) - end - - unless new_contact - error!({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: "No such contact: #{params[:new_contact_id]}" } }, - :bad_request) - end - - if new_contact.invalid? - error!({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'New contact must be valid' } }, - :bad_request) - end - - if current_contact == new_contact - error!({ error: { type: 'invalid_request_error', - message: 'New contact ID must be different from current' \ - ' contact ID' } }, - :bad_request) - end - - affected_domains, skipped_domains = TechDomainContact - .replace(current_contact, new_contact) - @response = { affected_domains: affected_domains, skipped_domains: skipped_domains } - end - end - end - end -end diff --git a/app/api/repp/domain_transfers_v1.rb b/app/api/repp/domain_transfers_v1.rb deleted file mode 100644 index c6a48df6d..000000000 --- a/app/api/repp/domain_transfers_v1.rb +++ /dev/null @@ -1,48 +0,0 @@ -module Repp - class DomainTransfersV1 < Grape::API - version 'v1', using: :path - - resource :domain_transfers do - post '/' do - params do - requires :data, type: Hash do - requires :domainTransfers, type: Array do - requires :domainName, type: String, allow_blank: false - requires :transferCode, type: String, allow_blank: false - end - end - end - - new_registrar = current_user.registrar - domain_transfers = params['data']['domainTransfers'] - successful_domain_transfers = [] - errors = [] - - domain_transfers.each do |domain_transfer| - domain_name = domain_transfer['domainName'] - transfer_code = domain_transfer['transferCode'] - domain = Domain.find_by(name: domain_name) - - if domain - if domain.transfer_code == transfer_code - DomainTransfer.request(domain, new_registrar) - successful_domain_transfers << { type: 'domain_transfer', attributes: { domain_name: domain.name } } - else - errors << { title: "#{domain_name} transfer code is wrong" } - end - else - errors << { title: "#{domain_name} does not exist" } - end - end - - if errors.none? - status 200 - @response = { data: successful_domain_transfers } - else - status 400 - @response = { errors: errors } - end - end - end - end -end diff --git a/app/api/repp/domain_v1.rb b/app/api/repp/domain_v1.rb deleted file mode 100644 index cf45bfc6f..000000000 --- a/app/api/repp/domain_v1.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Repp - class DomainV1 < Grape::API - version 'v1', using: :path - - resource :domains do - desc 'Return list of domains' - params do - optional :limit, type: Integer, values: (1..200).to_a, desc: 'How many domains to show' - optional :offset, type: Integer, desc: 'Domain number to start at' - optional :details, type: String, values: %w(true false), desc: 'Whether to include details' - end - - get '/' do - limit = params[:limit] || 200 - offset = params[:offset] || 0 - - if params[:details] == 'true' - domains = current_user.registrar.domains.limit(limit).offset(offset) - else - domains = current_user.registrar.domains.limit(limit).offset(offset).pluck(:name) - end - - @response = { - domains: domains, - total_number_of_records: current_user.registrar.domains.count - } - end - - # example: curl -u registrar1:password localhost:3000/repp/v1/domains/1/transfer_info -H "Auth-Code: authinfopw1" - get '/:id/transfer_info', requirements: { id: /.*/ } do - ident = params[:id] - domain = ident.match?(/\A[0-9]+\z/) ? Domain.find_by(id: ident) : Domain.find_by_idn(ident) - - error! I18n.t('errors.messages.epp_domain_not_found'), 404 unless domain - error! I18n.t('errors.messages.epp_authorization_error'), 401 unless domain.transfer_code.eql? request.headers['Auth-Code'] - - contact_repp_json = proc{|contact| - contact.as_json.slice("code", "name", "ident", "ident_type", "ident_country_code", "phone", "email", "street", "city", "zip","country_code", "statuses") - } - - @response = { - domain: domain.name, - registrant: contact_repp_json.call(domain.registrant), - admin_contacts: domain.admin_contacts.map{|e| contact_repp_json.call(e)}, - tech_contacts: domain.tech_contacts.map{|e| contact_repp_json.call(e)} - } - end - end - end -end diff --git a/app/api/repp/nameservers_v1.rb b/app/api/repp/nameservers_v1.rb deleted file mode 100644 index 56a893880..000000000 --- a/app/api/repp/nameservers_v1.rb +++ /dev/null @@ -1,49 +0,0 @@ -module Repp - class NameserversV1 < Grape::API - version 'v1', using: :path - - resource 'registrar/nameservers' do - put '/' do - params do - requires :data, type: Hash, allow_blank: false do - requires :type, type: String, allow_blank: false - requires :id, type: String, allow_blank: false - optional :domains, type: Array - requires :attributes, type: Hash, allow_blank: false do - requires :hostname, type: String, allow_blank: false - requires :ipv4, type: Array - requires :ipv6, type: Array - end - end - end - - hostname = params[:data][:id] - - unless current_user.registrar.nameservers.exists?(hostname: hostname) - error!({ errors: [{ title: "Hostname #{hostname} does not exist" }] }, 404) - end - - new_attributes = { - hostname: params[:data][:attributes][:hostname], - ipv4: params[:data][:attributes][:ipv4], - ipv6: params[:data][:attributes][:ipv6], - } - - domains = params[:data][:domains] || [] - - begin - affected_domains = current_user.registrar.replace_nameservers(hostname, new_attributes, - domains: domains) - rescue ActiveRecord::RecordInvalid => e - error!({ errors: e.record.errors.full_messages.map { |error| { title: error } } }, 400) - end - - status 200 - @response = { data: { type: 'nameserver', - id: params[:data][:attributes][:hostname], - attributes: params[:data][:attributes] }, - affected_domains: affected_domains } - end - end - end -end diff --git a/app/controllers/epp/contacts_controller.rb b/app/controllers/epp/contacts_controller.rb index df9755af6..85305213b 100644 --- a/app/controllers/epp/contacts_controller.rb +++ b/app/controllers/epp/contacts_controller.rb @@ -1,10 +1,9 @@ require 'deserializers/xml/contact_update' - +require 'deserializers/xml/contact_create' module Epp class ContactsController < BaseController before_action :find_contact, only: [:info, :update, :delete] before_action :find_password, only: [:info, :update, :delete] - helper_method :address_processing? def info authorize! :info, @contact, @password @@ -21,25 +20,13 @@ module Epp def create authorize! :create, Epp::Contact - frame = params[:parsed_frame] - @contact = Epp::Contact.new(frame, current_user.registrar) - @contact.add_legal_file_to_new(frame) - @contact.generate_code + @contact = Epp::Contact.new(params[:parsed_frame], current_user.registrar) + collected_data = ::Deserializers::Xml::ContactCreate.new(params[:parsed_frame]) + action = Actions::ContactCreate.new(@contact, collected_data.legal_document, + collected_data.ident) - if @contact.save - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') - else - @response_code = 1000 - @response_description = t('epp.contacts.completed') - end - - render_epp_response '/epp/contacts/save' - else - handle_errors(@contact) - end + action_call_response(action: action) end def update @@ -52,29 +39,18 @@ module Epp collected_data.ident, current_user) - if action.call - if !address_processing? && address_given? - @response_code = 1100 - @response_description = t('epp.contacts.completed_without_address') - else - @response_code = 1000 - @response_description = t('epp.contacts.completed') - end - - render_epp_response 'epp/contacts/save' - else - handle_errors(@contact) - end + action_call_response(action: action) end def delete authorize! :delete, @contact, @password - - if @contact.destroy_and_clean(params[:parsed_frame]) - render_epp_response '/epp/contacts/delete' - else + action = Actions::ContactDelete.new(@contact, params[:legal_document]) + unless action.call handle_errors(@contact) + return end + + render_epp_response '/epp/contacts/delete' end def renew @@ -91,6 +67,26 @@ module Epp private + def opt_addr? + !Contact.address_processing? && address_given? + end + + def action_call_response(action:) + # rubocop:disable Style/AndOr + (handle_errors(@contact) and return) unless action.call + # rubocop:enable Style/AndOr + + if opt_addr? + @response_code = 1100 + @response_description = t('epp.contacts.completed_without_address') + else + @response_code = 1000 + @response_description = t('epp.contacts.completed') + end + + render_epp_response('epp/contacts/save') + end + def find_password @password = params[:parsed_frame].css('authInfo pw').text end @@ -129,8 +125,7 @@ module Epp 'postalInfo > addr > cc', ] - required_attributes.concat(address_attributes) if address_processing? - + required_attributes.concat(address_attributes) if Contact.address_processing? requires(*required_attributes) ident = params[:parsed_frame].css('ident') @@ -206,9 +201,5 @@ module Epp def address_given? params[:parsed_frame].css('postalInfo addr').size != 0 end - - def address_processing? - Contact.address_processing? - end end end diff --git a/app/controllers/registrar/domain_transfers_controller.rb b/app/controllers/registrar/domain_transfers_controller.rb index acacc3ef4..ca08c73cf 100644 --- a/app/controllers/registrar/domain_transfers_controller.rb +++ b/app/controllers/registrar/domain_transfers_controller.rb @@ -15,12 +15,12 @@ class Registrar csv.each do |row| domain_name = row['Domain'] transfer_code = row['Transfer code'] - domain_transfers << { 'domainName' => domain_name, 'transferCode' => transfer_code } + domain_transfers << { 'domain_name' => domain_name, 'transfer_code' => transfer_code } end - uri = URI.parse("#{ENV['repp_url']}domain_transfers") + uri = URI.parse("#{ENV['repp_url']}domains/transfer") request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json') - request.body = { data: { domainTransfers: domain_transfers } }.to_json + request.body = { data: { domain_transfers: domain_transfers } }.to_json request.basic_auth(current_registrar_user.username, current_registrar_user.plain_text_password) diff --git a/app/controllers/repp/v1/accounts_controller.rb b/app/controllers/repp/v1/accounts_controller.rb new file mode 100644 index 000000000..89c14808f --- /dev/null +++ b/app/controllers/repp/v1/accounts_controller.rb @@ -0,0 +1,11 @@ +module Repp + module V1 + class AccountsController < BaseController + def balance + resp = { balance: current_user.registrar.cash_account.balance, + currency: current_user.registrar.cash_account.currency } + render_success(data: resp) + end + end + end +end diff --git a/app/controllers/repp/v1/auctions_controller.rb b/app/controllers/repp/v1/auctions_controller.rb index 4a5265d13..676dac266 100644 --- a/app/controllers/repp/v1/auctions_controller.rb +++ b/app/controllers/repp/v1/auctions_controller.rb @@ -3,9 +3,9 @@ module Repp class AuctionsController < ActionController::API def index auctions = Auction.started + @response = { count: auctions.count, auctions: auctions_to_json(auctions) } - render json: { count: auctions.count, - auctions: auctions_to_json(auctions) } + render json: @response end private diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb new file mode 100644 index 000000000..ca9ef6fb5 --- /dev/null +++ b/app/controllers/repp/v1/base_controller.rb @@ -0,0 +1,111 @@ +module Repp + module V1 + class BaseController < ActionController::API + rescue_from ActiveRecord::RecordNotFound, with: :not_found_error + before_action :authenticate_user + before_action :check_ip_restriction + attr_reader :current_user + + before_action :set_paper_trail_whodunnit + + rescue_from ActionController::ParameterMissing do |exception| + render json: { code: 2003, message: exception }, status: :bad_request + end + + after_action do + ApiLog::ReppLog.create( + request_path: request.path, request_method: request.request_method, + request_params: request.params.except('route_info').to_json, uuid: request.try(:uuid), + response: @response.to_json, response_code: status, ip: request.ip, + api_user_name: current_user.try(:username), + api_user_registrar: current_user.try(:registrar).try(:to_s) + ) + end + + private + + def set_paper_trail_whodunnit + ::PaperTrail.request.whodunnit = current_user + end + + def render_success(code: nil, message: nil, data: nil) + @response = { code: code || 1000, message: message || 'Command completed successfully', + data: data || {} } + + render(json: @response, status: :ok) + end + + def epp_errors + @epp_errors ||= [] + end + + def handle_errors(obj = nil, update: false) + @epp_errors ||= [] + + obj&.construct_epp_errors + @epp_errors += obj.errors[:epp_errors] if obj + + format_epp_errors if update + @epp_errors.uniq! + + render_epp_error + end + + def format_epp_errors + @epp_errors.each_with_index do |error, index| + blocked_by_delete_prohibited?(error, index) + end + end + + def blocked_by_delete_prohibited?(error, index) + if error[:code] == 2304 && error[:value][:val] == DomainStatus::SERVER_DELETE_PROHIBITED && + error[:value][:obj] == 'status' + + @epp_errors[index][:value][:val] = DomainStatus::PENDING_UPDATE + end + end + + def render_epp_error(status = :bad_request, data = {}) + @epp_errors ||= [] + @epp_errors << { code: 2304, msg: 'Command failed' } if data != {} + + @response = { code: @epp_errors[0][:code].to_i, message: @epp_errors[0][:msg], data: data } + render(json: @response, status: status) + end + + def basic_token + pattern = /^Basic / + header = request.headers['Authorization'] + header = header.gsub(pattern, '') if header&.match(pattern) + header.strip + end + + def authenticate_user + username, password = Base64.urlsafe_decode64(basic_token).split(':') + @current_user ||= ApiUser.find_by(username: username, plain_text_password: password) + + return if @current_user + + raise(ArgumentError) + rescue NoMethodError, ArgumentError + @response = { code: 2202, message: 'Invalid authorization information' } + render(json: @response, status: :unauthorized) + end + + def check_ip_restriction + allowed = @current_user.registrar.api_ip_white?(request.ip) + + return if allowed + + @response = { code: 2202, + message: I18n.t('registrar.authorization.ip_not_allowed', ip: request.ip) } + render(json: @response, status: :unauthorized) + end + + def not_found_error + @response = { code: 2303, message: 'Object does not exist' } + render(json: @response, status: :not_found) + end + end + end +end diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb new file mode 100644 index 000000000..acf275c47 --- /dev/null +++ b/app/controllers/repp/v1/contacts_controller.rb @@ -0,0 +1,137 @@ +require 'serializers/repp/contact' +module Repp + module V1 + class ContactsController < BaseController + before_action :find_contact, only: %i[show update destroy] + + ## GET /repp/v1/contacts + def index + record_count = current_user.registrar.contacts.count + contacts = showable_contacts(params[:details], params[:limit] || 200, + params[:offset] || 0) + @response = { contacts: contacts, total_number_of_records: record_count } + render(json: @response, status: :ok) + end + + ## GET /repp/v1/contacts/1 + def show + serializer = ::Serializers::Repp::Contact.new(@contact, + show_address: Contact.address_processing?) + render_success(data: serializer.to_json) + end + + ## GET /repp/v1/contacts/check/1 + def check + contact = Epp::Contact.find_by(code: params[:id]) + data = { contact: { id: params[:id], available: contact.nil? } } + + render_success(data: data) + end + + ## POST /repp/v1/contacts + def create + @contact = Epp::Contact.new(contact_params_with_address, current_user.registrar, epp: false) + action = Actions::ContactCreate.new(@contact, params[:legal_document], + contact_ident_params) + + unless action.call + handle_errors(@contact) + return + end + + render_success(create_update_success_body) + end + + ## PUT /repp/v1/contacts/1 + def update + action = Actions::ContactUpdate.new(@contact, contact_params_with_address(required: false), + params[:legal_document], + contact_ident_params(required: false), current_user) + + unless action.call + handle_errors(@contact) + return + end + + render_success(create_update_success_body) + end + + def destroy + action = Actions::ContactDelete.new(@contact, params[:legal_document]) + unless action.call + handle_errors(@contact) + return + end + + render_success + end + + def contact_addr_present? + return false unless contact_addr_params.key?(:addr) + + contact_addr_params[:addr].keys.any? + end + + def create_update_success_body + { code: opt_addr? ? 1100 : nil, data: { contact: { id: @contact.code } }, + message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : nil } + end + + def showable_contacts(details, limit, offset) + contacts = current_user.registrar.contacts.limit(limit).offset(offset) + + return contacts.pluck(:code) unless details + + contacts = contacts.map do |contact| + serializer = ::Serializers::Repp::Contact.new(contact, + show_address: Contact.address_processing?) + serializer.to_json + end + + contacts + end + + def opt_addr? + !Contact.address_processing? && contact_addr_present? + end + + def find_contact + code = params[:id] + @contact = Epp::Contact.find_by!(code: code, registrar: current_user.registrar) + end + + def contact_params_with_address(required: true) + return contact_create_params(required: required) unless contact_addr_params.key?(:addr) + + addr = {} + contact_addr_params[:addr].each_key { |k| addr[k] = contact_addr_params[:addr][k] } + contact_create_params(required: required).merge(addr) + end + + def contact_create_params(required: true) + params.require(:contact).require(%i[name email phone]) if required + params.require(:contact).permit(:name, :email, :phone, :id) + end + + def contact_ident_params(required: true) + if required + params.require(:contact).require(:ident).require(%i[ident ident_type ident_country_code]) + params.require(:contact).require(:ident).permit(:ident, :ident_type, :ident_country_code) + else + params.permit(contact: { ident: %i[ident ident_type ident_country_code] }) + end + + params[:contact][:ident] + end + + def contact_addr_params + if Contact.address_processing? + params.require(:contact).require(:addr).require(%i[country_code city street zip]) + params.require(:contact).require(:addr).permit(:country_code, :city, :street, :zip) + else + params.require(:contact).permit(addr: %i[country_code city street zip]) + end + end + end + end +end diff --git a/app/controllers/repp/v1/domains/contacts_controller.rb b/app/controllers/repp/v1/domains/contacts_controller.rb new file mode 100644 index 000000000..75404e0c6 --- /dev/null +++ b/app/controllers/repp/v1/domains/contacts_controller.rb @@ -0,0 +1,42 @@ +module Repp + module V1 + module Domains + class ContactsController < BaseController + before_action :set_current_contact, only: [:update] + before_action :set_new_contact, only: [:update] + + def set_current_contact + @current_contact = current_user.registrar.contacts.find_by!( + code: contact_params[:current_contact_id] + ) + end + + def set_new_contact + @new_contact = current_user.registrar.contacts.find_by!(code: params[:new_contact_id]) + end + + def update + @epp_errors ||= [] + @epp_errors << { code: 2304, msg: 'New contact must be valid' } if @new_contact.invalid? + + if @new_contact == @current_contact + @epp_errors << { code: 2304, msg: 'New contact must be different from current' } + end + + return handle_errors if @epp_errors.any? + + affected, skipped = TechDomainContact.replace(@current_contact, @new_contact) + @response = { affected_domains: affected, skipped_domains: skipped } + render_success(data: @response) + end + + private + + def contact_params + params.require(%i[current_contact_id new_contact_id]) + params.permit(:current_contact_id, :new_contact_id) + end + end + end + end +end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb new file mode 100644 index 000000000..ba90f23f2 --- /dev/null +++ b/app/controllers/repp/v1/domains_controller.rb @@ -0,0 +1,94 @@ +module Repp + module V1 + class DomainsController < BaseController + before_action :set_authorized_domain, only: [:transfer_info] + + def index + records = current_user.registrar.domains + domains = records.limit(limit).offset(offset) + domains = domains.pluck(:name) unless index_params[:details] == 'true' + + render_success(data: { domains: domains, total_number_of_records: records.count }) + end + + def transfer_info + contact_fields = %i[code name ident ident_type ident_country_code phone email street city + zip country_code statuses] + + data = { + domain: @domain.name, + registrant: @domain.registrant.as_json(only: contact_fields), + admin_contacts: @domain.admin_contacts.map { |c| c.as_json(only: contact_fields) }, + tech_contacts: @domain.tech_contacts.map { |c| c.as_json(only: contact_fields) }, + } + + render_success(data: data) + end + + def transfer + @errors ||= [] + @successful = [] + + transfer_params[:domain_transfers].each do |transfer| + initiate_transfer(transfer) + end + + render_success(data: { success: @successful, failed: @errors }) + end + + def initiate_transfer(transfer) + domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name]) + action = Actions::DomainTransfer.new(domain, transfer[:transfer_code], + current_user.registrar) + + if action.call + @successful << { type: 'domain_transfer', domain_name: domain.name } + else + @errors << { type: 'domain_transfer', domain_name: domain.name, + errors: domain.errors[:epp_errors] } + end + end + + private + + def transfer_params + params.require(:data).require(:domain_transfers).each do |t| + t.require(:domain_name) + t.permit(:domain_name) + t.require(:transfer_code) + t.permit(:transfer_code) + end + params.require(:data).permit(domain_transfers: %i[domain_name transfer_code]) + end + + def transfer_info_params + params.require(:id) + params.permit(:id) + end + + def set_authorized_domain + @epp_errors ||= [] + h = {} + h[transfer_info_params[:id].match?(/\A[0-9]+\z/) ? :id : :name] = transfer_info_params[:id] + @domain = Domain.find_by!(h) + + return if @domain.transfer_code.eql?(request.headers['Auth-Code']) + + @epp_errors << { code: 2202, msg: I18n.t('errors.messages.epp_authorization_error') } + handle_errors + end + + def limit + index_params[:limit] || 200 + end + + def offset + index_params[:offset] || 0 + end + + def index_params + params.permit(:limit, :offset, :details) + end + end + end +end diff --git a/app/controllers/repp/v1/registrar/nameservers_controller.rb b/app/controllers/repp/v1/registrar/nameservers_controller.rb new file mode 100644 index 000000000..47004d97b --- /dev/null +++ b/app/controllers/repp/v1/registrar/nameservers_controller.rb @@ -0,0 +1,51 @@ +module Repp + module V1 + module Registrar + class NameserversController < BaseController + before_action :verify_nameserver_existance, only: %i[update] + + def update + domains = params[:data][:domains] || [] + affected = current_user.registrar + .replace_nameservers(hostname, + hostname_params[:data][:attributes], + domains: domains) + + render_success(data: data_format_for_success(affected)) + rescue ActiveRecord::RecordInvalid => e + handle_errors(e.record) + end + + private + + def data_format_for_success(affected_domains) + { + type: 'nameserver', + id: params[:data][:attributes][:hostname], + attributes: params[:data][:attributes], + affected_domains: affected_domains, + } + end + + def hostname_params + params.require(:data).require(%i[type id]) + params.require(:data).require(:attributes).require([:hostname]) + + params.permit(data: [ + :type, :id, + { domains: [], + attributes: [:hostname, { ipv4: [], ipv6: [] }] } + ]) + end + + def hostname + hostname_params[:data][:id] + end + + def verify_nameserver_existance + current_user.registrar.nameservers.find_by!(hostname: hostname) + end + end + end + end +end diff --git a/app/controllers/repp/v1/retained_domains_controller.rb b/app/controllers/repp/v1/retained_domains_controller.rb index c1bb458e9..8edc32f5b 100644 --- a/app/controllers/repp/v1/retained_domains_controller.rb +++ b/app/controllers/repp/v1/retained_domains_controller.rb @@ -3,8 +3,9 @@ module Repp class RetainedDomainsController < ActionController::API def index domains = RetainedDomains.new(query_params) + @response = { count: domains.count, domains: domains.to_jsonable } - render json: { count: domains.count, domains: domains.to_jsonable } + render json: @response end def query_params diff --git a/app/helpers/object_versions_helper.rb b/app/helpers/object_versions_helper.rb index d8e00abbe..be8ef1217 100644 --- a/app/helpers/object_versions_helper.rb +++ b/app/helpers/object_versions_helper.rb @@ -3,7 +3,7 @@ module ObjectVersionsHelper version.object_changes.to_h.each do |key, value| method_name = "#{key}=".to_sym if new_object.respond_to?(method_name) - new_object.public_send(method_name, value.last) + new_object.public_send(method_name, event_value(version, value)) end end end @@ -12,4 +12,10 @@ module ObjectVersionsHelper field_names = model.column_names version.object.to_h.select { |key, _value| field_names.include?(key) } end + + private + + def event_value(version, val) + version.event == 'destroy' ? val.first : val.last + end end diff --git a/app/models/actions/contact_create.rb b/app/models/actions/contact_create.rb new file mode 100644 index 000000000..22fabc7b9 --- /dev/null +++ b/app/models/actions/contact_create.rb @@ -0,0 +1,81 @@ +module Actions + class ContactCreate + attr_reader :contact, :legal_document, :ident + + def initialize(contact, legal_document, ident) + @contact = contact + @legal_document = legal_document + @ident = ident + end + + def call + maybe_remove_address + maybe_attach_legal_doc + validate_ident + commit + end + + def maybe_remove_address + return if Contact.address_processing? + + contact.city = nil + contact.zip = nil + contact.street = nil + contact.state = nil + contact.country_code = nil + end + + def validate_ident + validate_ident_integrity + validate_ident_birthday + + identifier = ::Contact::Ident.new(code: ident[:ident], type: ident[:ident_type], + country_code: ident[:ident_country_code]) + + identifier.validate + contact.identifier = identifier + end + + def validate_ident_integrity + return if ident.blank? + + if ident[:ident_type].blank? + contact.add_epp_error('2003', nil, 'ident_type', + I18n.t('errors.messages.required_ident_attribute_missing')) + @error = true + elsif !%w[priv org birthday].include?(ident[:ident_type]) + contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type') + @error = true + end + end + + def validate_ident_birthday + return if ident.blank? + return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank? + + contact.add_epp_error('2003', nil, 'ident_country_code', + I18n.t('errors.messages.required_ident_attribute_missing')) + @error = true + end + + def maybe_attach_legal_doc + return unless legal_document + + doc = LegalDocument.create( + documentable_type: Contact, + document_type: legal_document[:type], body: legal_document[:body] + ) + + contact.legal_documents = [doc] + contact.legal_document_id = doc.id + end + + def commit + contact.id = nil # new record + return false if @error + + contact.generate_code + contact.save + end + end +end diff --git a/app/models/actions/contact_delete.rb b/app/models/actions/contact_delete.rb new file mode 100644 index 000000000..59032d566 --- /dev/null +++ b/app/models/actions/contact_delete.rb @@ -0,0 +1,41 @@ +module Actions + class ContactDelete + attr_reader :contact + attr_reader :new_attributes + attr_reader :legal_document + attr_reader :ident + attr_reader :user + + def initialize(contact, legal_document = nil) + @legal_document = legal_document + @contact = contact + end + + def call + maybe_attach_legal_doc + + if contact.linked? + contact.errors.add(:domains, :exist) + return + end + + commit + end + + def maybe_attach_legal_doc + return unless legal_document + + document = contact.legal_documents.create( + document_type: legal_document[:type], + body: legal_document[:body] + ) + + contact.legal_document_id = document.id + contact.save + end + + def commit + contact.destroy + end + end +end diff --git a/app/models/actions/contact_update.rb b/app/models/actions/contact_update.rb index f8b39ecb4..7ca7b6b04 100644 --- a/app/models/actions/contact_update.rb +++ b/app/models/actions/contact_update.rb @@ -17,7 +17,7 @@ module Actions def call maybe_remove_address maybe_update_statuses - maybe_update_ident + maybe_update_ident if ident.present? maybe_attach_legal_doc commit end @@ -53,7 +53,11 @@ module Actions end def maybe_update_ident - return unless ident[:ident] + unless ident.is_a?(Hash) + contact.add_epp_error('2308', nil, nil, I18n.t('epp.contacts.errors.valid_ident')) + @error = true + return + end if contact.identifier.valid? submitted_ident = ::Contact::Ident.new(code: ident[:ident], diff --git a/app/models/actions/domain_transfer.rb b/app/models/actions/domain_transfer.rb new file mode 100644 index 000000000..1ff9aafe9 --- /dev/null +++ b/app/models/actions/domain_transfer.rb @@ -0,0 +1,74 @@ +module Actions + class DomainTransfer + attr_reader :domain + attr_reader :transfer_code + attr_reader :legal_document + attr_reader :ident + attr_reader :user + + def initialize(domain, transfer_code, user) + @domain = domain + @transfer_code = transfer_code + @user = user + end + + def call + return unless domain_exists? + return unless valid_transfer_code? + + run_validations + + # return domain.pending_transfer if domain.pending_transfer + # attach_legal_document(::Deserializers::Xml::LegalDocument.new(frame).call) + + return if domain.errors[:epp_errors].any? + + commit + end + + def domain_exists? + return true if domain.persisted? + + domain.add_epp_error('2303', nil, nil, 'Object does not exist') + + false + end + + def run_validations + validate_registrar + validate_eligilibty + validate_not_discarded + end + + def valid_transfer_code? + return true if transfer_code == domain.transfer_code + + domain.add_epp_error('2202', nil, nil, 'Invalid authorization information') + false + end + + def validate_registrar + return unless user == domain.registrar + + domain.add_epp_error('2002', nil, nil, + I18n.t(:domain_already_belongs_to_the_querying_registrar)) + end + + def validate_eligilibty + return unless domain.non_transferable? + + domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation') + end + + def validate_not_discarded + return unless domain.discarded? + + domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer') + end + + def commit + bare_domain = Domain.find(domain.id) + ::DomainTransfer.request(bare_domain, user) + end + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index 9dc1e34a2..8a154c50c 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -333,31 +333,6 @@ class Contact < ApplicationRecord Country.new(country_code) end - # TODO: refactor, it should not allow to destroy with normal destroy, - # no need separate method - # should use only in transaction - def destroy_and_clean frame - if linked? - errors.add(:domains, :exist) - return false - end - - legal_document_data = ::Deserializers::Xml::LegalDocument.new(frame).call - - if legal_document_data - - doc = LegalDocument.create( - documentable_type: Contact, - document_type: legal_document_data[:type], - body: legal_document_data[:body] - ) - self.legal_documents = [doc] - self.legal_document_id = doc.id - self.save - end - destroy - end - def to_upcase_country_code self.ident_country_code = ident_country_code.upcase if ident_country_code self.country_code = country_code.upcase if country_code diff --git a/app/models/epp/contact.rb b/app/models/epp/contact.rb index 6867b037d..50ebac065 100644 --- a/app/models/epp/contact.rb +++ b/app/models/epp/contact.rb @@ -30,12 +30,13 @@ class Epp::Contact < Contact at end - def new(frame, registrar) + def new(frame, registrar, epp: true) return super if frame.blank? + attrs = epp ? attrs_from(frame, new_record: true) : frame super( - attrs_from(frame, new_record: true).merge( - code: frame.css('id').text, + attrs.merge( + code: epp ? frame.css('id').text : frame[:id], registrar: registrar ) ) diff --git a/app/views/epp/contacts/info.xml.builder b/app/views/epp/contacts/info.xml.builder index 4874080e8..6ce0a17f4 100644 --- a/app/views/epp/contacts/info.xml.builder +++ b/app/views/epp/contacts/info.xml.builder @@ -22,7 +22,7 @@ xml.epp_head do if can? :view_full_info, @contact, @password xml.tag!('contact:org', @contact.org_name) if @contact.org_name.present? - if address_processing? + if Contact.address_processing? xml.tag!('contact:addr') do xml.tag!('contact:street', @contact.street) xml.tag!('contact:city', @contact.city) @@ -35,7 +35,7 @@ xml.epp_head do else xml.tag!('contact:org', 'No access') - if address_processing? + if Contact.address_processing? xml.tag!('contact:addr') do xml.tag!('contact:street', 'No access') xml.tag!('contact:city', 'No access') diff --git a/app/views/registrar/contacts/_form.haml b/app/views/registrar/contacts/_form.haml index cf8217e13..953c502e5 100644 --- a/app/views/registrar/contacts/_form.haml +++ b/app/views/registrar/contacts/_form.haml @@ -5,7 +5,7 @@ .col-md-8 = render 'registrar/contacts/form/general', f: f -- if address_processing? +- if Contact.address_processing? .row .col-md-8 = render 'registrar/contacts/form/address', f: f diff --git a/config/routes.rb b/config/routes.rb index 440c9c05e..0af3d95c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -37,12 +37,35 @@ Rails.application.routes.draw do get 'error/:command', to: 'errors#error' end - mount Repp::API => '/' - namespace :repp do namespace :v1 do + resources :contacts do + collection do + get 'check/:id', to: 'contacts#check' + end + end + + resources :accounts do + collection do + get 'balance' + end + end resources :auctions, only: %i[index] resources :retained_domains, only: %i[index] + namespace :registrar do + resources :nameservers do + collection do + put '/', to: 'nameservers#update' + end + end + end + resources :domains do + collection do + get ':id/transfer_info', to: 'domains#transfer_info', constraints: { id: /.*/ } + post 'transfer', to: 'domains#transfer' + patch 'contacts', to: 'domains/contacts#update' + end + end end end diff --git a/doc/repp/v1/account.md b/doc/repp/v1/account.md index 511c51382..80afabcdb 100644 --- a/doc/repp/v1/account.md +++ b/doc/repp/v1/account.md @@ -19,7 +19,11 @@ Content-Length: 37 Content-Type: application/json { - "balance": "324.45", - "currency": "EUR" + "code": 1000, + "message": "Command completed successfully", + "data": { + "balance": "356.0", + "currency": "EUR" + } } ``` diff --git a/doc/repp/v1/contact.md b/doc/repp/v1/contact.md index 41f45551f..aa6904b72 100644 --- a/doc/repp/v1/contact.md +++ b/doc/repp/v1/contact.md @@ -88,3 +88,141 @@ Content-Type: application/json "total_number_of_records": 2 } ``` + +## POST /repp/v1/contacts +Creates new contact + + +#### Request +``` +POST /repp/v1/contacts HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json + +{ + "contact": { + "name": "John Doe", + "email": "john@doe.com", + "phone": "+371.1234567", + "ident": { + "ident": "12345678901", + "ident_type": "priv", + "ident_country_code": "EE" + } + } +} +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": { + "contact": { + "id": "ATSAA:20DCDCA1" + } + } +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2005, + "message": "Ident code does not conform to national identification number format of Estonia", + "data": {} +} +``` + +## PUT /repp/v1/contacts/**contact id** +Updates existing contact + + +#### Request +``` +PUT /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json + +{ + "contact": { + "phone": "+372.123123123" + } +} +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": { + "contact": { + "id": "ATSAA:20DCDCA1" + } + } +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2005, + "message": "Phone nr is invalid [phone]", + "data": {} +} +``` + +## DELETE /repp/v1/contacts/**contact id** +Deletes existing contact + + +#### Request +``` +DELETE /repp/v1/contacts/ATSAA:9CD5F321 HTTP/1.1 +Authorization: Basic dGVzdDp0ZXN0MTIz +Content-Type: application/json +``` + +#### Response +``` +HTTP/1.1 200 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 1000, + "message": "Command completed successfully", + "data": {} +} +``` + +#### Failed response +``` +HTTP/1.1 400 +Cache-Control: max-age=0, private, must-revalidate +Content-Type: application/json + +{ + "code": 2305, + "message": "Object association prohibits operation [domains]", + "data": {} +} +``` diff --git a/doc/repp/v1/domain.md b/doc/repp/v1/domain.md index c6734cbe2..20607f3b9 100644 --- a/doc/repp/v1/domain.md +++ b/doc/repp/v1/domain.md @@ -25,44 +25,52 @@ Content-Type: application/json ``` HTTP/1.1 200 Cache-Control: max-age=0, private, must-revalidate -Content-Length: 808 Content-Type: application/json { - "domains": [ - { - "id": 1, - "name": "domain0.ee", - "registrar_id": 2, - "registered_at": "2015-09-09T09:11:14.861Z", - "status": null, - "valid_from": "2015-09-09T09:11:14.861Z", - "valid_to": "2016-09-09T09:11:14.861Z", - "registrant_id": 1, - "transfer_code": "98oiewslkfkd", - "created_at": "2015-09-09T09:11:14.861Z", - "updated_at": "2015-09-09T09:11:14.860Z", - "name_dirty": "domain0.ee", - "name_puny": "domain0.ee", - "period": 1, - "period_unit": "y", - "creator_str": null, - "updator_str": null, - "outzone_at": "2016-09-24T09:11:14.861Z", - "delete_date": "2016-10-24", - "registrant_verification_asked_at": null, - "registrant_verification_token": null, - "pending_json": { - }, - "force_delete_date": null, - "statuses": [ - "ok" - ], - "status_notes": { + "code": 1000, + "message": "Command completed successfully", + "data": { + "domains": [ + { + "id": 7, + "name": "private.ee", + "registrar_id": 2, + "valid_to": "2022-09-23T00:00:00.000+03:00", + "registrant_id": 11, + "created_at": "2020-09-22T14:16:47.420+03:00", + "updated_at": "2020-10-21T13:31:43.733+03:00", + "name_dirty": "private.ee", + "name_puny": "private.ee", + "period": 1, + "period_unit": "y", + "creator_str": "2-ApiUser: test", + "updator_str": null, + "outzone_at": null, + "delete_date": null, + "registrant_verification_asked_at": null, + "registrant_verification_token": null, + "pending_json": {}, + "force_delete_date": null, + "statuses": [ + "serverRenewProhibited" + ], + "status_notes": { + "ok": "", + "serverRenewProhibited": "" + }, + "upid": null, + "up_date": null, + "uuid": "6b6affa7-1449-4bd8-acf5-8b4752406705", + "locked_by_registrant_at": null, + "force_delete_start": null, + "force_delete_data": null, + "auth_info": "367b1e6d1f0d9aa190971ad8f571cd4d", + "valid_from": "2020-09-22T14:16:47.420+03:00" } - } - ], - "total_number_of_records": 2 + ], + "total_number_of_records": 10 + } } ``` @@ -83,14 +91,17 @@ Content-Type: application/json ``` HTTP/1.1 200 Cache-Control: max-age=0, private, must-revalidate -Content-Length: 54 Content-Type: application/json { - "domains": [ - "domain1.ee" - ], - "total_number_of_records": 2 + "code": 1000, + "message": "Command completed successfully", + "data": { + "domains": [ + "private.ee", + ], + "total_number_of_records": 1 + } } ``` @@ -117,65 +128,68 @@ Please note that domain transfer/authorisation code must be placed in header - * ``` HTTP/1.1 200 OK Cache-Control: max-age=0, private, must-revalidate -Content-Length: 784 Content-Type: application/json - { - "domain":"ee-test.ee", - "registrant":{ - "code":"EE:R1", - "name":"Registrant", - "ident":"17612535", - "ident_type":"org", - "ident_country_code":"EE", - "phone":"+372.1234567", - "email":"registrant@cache.ee", - "street":"Businesstreet 1", - "city":"Tallinn", - "zip":"10101", - "country_code":"EE", - "statuses":[ - "ok", - "linked" - ] - }, - "admin_contacts":[ - { - "code":"EE:A1", - "name":"Admin Contact", - "ident":"17612535376", - "ident_type":"priv", - "ident_country_code":"EE", - "phone":"+372.7654321", - "email":"admin@cache.ee", - "street":"Adminstreet 2", - "city":"Tallinn", - "zip":"12345", - "country_code":"EE", - "statuses":[ - "ok", - "linked" + "code": 1000, + "message": "Command completed successfully", + "data": { + "domain":"ee-test.ee", + "registrant":{ + "code":"EE:R1", + "name":"Registrant", + "ident":"17612535", + "ident_type":"org", + "ident_country_code":"EE", + "phone":"+372.1234567", + "email":"registrant@cache.ee", + "street":"Businesstreet 1", + "city":"Tallinn", + "zip":"10101", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + }, + "admin_contacts":[ + { + "code":"EE:A1", + "name":"Admin Contact", + "ident":"17612535376", + "ident_type":"priv", + "ident_country_code":"EE", + "phone":"+372.7654321", + "email":"admin@cache.ee", + "street":"Adminstreet 2", + "city":"Tallinn", + "zip":"12345", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + } + ], + "tech_contacts":[ + { + "code":"EE:T1", + "name":"Tech Contact", + "ident":"17612536", + "ident_type":"org", + "ident_country_code":"EE", + "phone":"+372.7654321", + "email":"tech@cache.ee", + "street":"Techstreet 1", + "city":"Tallinn", + "zip":"12345", + "country_code":"EE", + "statuses":[ + "ok", + "linked" + ] + } ] } - ], - "tech_contacts":[ - { - "code":"EE:T1", - "name":"Tech Contact", - "ident":"17612536", - "ident_type":"org", - "ident_country_code":"EE", - "phone":"+372.7654321", - "email":"tech@cache.ee", - "street":"Techstreet 1", - "city":"Tallinn", - "zip":"12345", - "country_code":"EE", - "statuses":[ - "ok", - "linked" - ] - } - ] + } } ``` diff --git a/doc/repp/v1/domain_contacts.md b/doc/repp/v1/domain_contacts.md index b412e9f73..2e542bf81 100644 --- a/doc/repp/v1/domain_contacts.md +++ b/doc/repp/v1/domain_contacts.md @@ -5,15 +5,26 @@ Replaces all domain contacts of the current registrar. ### Example request ``` -$ curl https://repp.internet.ee/v1/domains/contacts \ - -X PATCH \ - -u username:password \ - -d current_contact_id=foo \ - -d new_contact_id=bar +PATCH /repp/v1/domains/contacts HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic dGVzdDp0ZXN0dGVzdA== + +{ + "current_contact_id": "ATSAA:749AA80F", + "new_contact_id": "ATSAA:E36957D7" +} ``` ### Example response ``` { - "affected_domains": ["example.com", "example.org"] + "code": 1000, + "message": "Command completed successfully", + "data": { + "affected_domains": [ + "private.ee", + ], + "skipped_domains": [] + } } ``` diff --git a/doc/repp/v1/domain_transfers.md b/doc/repp/v1/domain_transfers.md index a6eb4683c..a11c0a852 100644 --- a/doc/repp/v1/domain_transfers.md +++ b/doc/repp/v1/domain_transfers.md @@ -1,28 +1,28 @@ # Domain transfers -## POST /repp/v1/domain_transfers +## POST /repp/v1/domains/transfer Transfers domains. #### Request ``` -POST /repp/v1/domain_transfers +POST /repp/v1/domains/transfer Accept: application/json Content-Type: application/json Authorization: Basic dGVzdDp0ZXN0dGVzdA== { - "data":{ - "domainTransfers":[ - { - "domainName":"example.com", - "transferCode":"63e7" - }, - { - "domainName":"example.org", - "transferCode":"15f9" - } - ] - } + "data": { + "domain_transfers": [ + { + "domain_name":"example.com", + "transferCode":"63e7" + }, + { + "domain_name":"example.org", + "transferCode":"15f9" + } + ] + } } ``` @@ -31,14 +31,21 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA== HTTP/1.1 200 Content-Type: application/json { - "data":[ + "code": 1000, + "message": "Command completed successfully", + "data": { + "success": [ { - "type":"domain_transfer" + "type": "domain_transfer", + "domain_name": "example.com" }, { - "type":"domain_transfer" + "type": "domain_transfer", + "domain_name": "example.org" } - ] + ], + "failed": [] + } } ``` @@ -48,13 +55,32 @@ Content-Type: application/json HTTP/1.1 400 Content-Type: application/json { - "errors":[ + "code": 1000, + "message": "Command completed successfully", + "data": { + "success": [], + "failed": [ { - "title":"example.com transfer code is wrong" + "type": "domain_transfer", + "domain_name": "example.com", + "errors": [ + { + "code": "2202", + "msg": "Invalid authorization information" + } + ] }, { - "title":"example.org does not exist" + "type": "domain_transfer", + "domain_name": "example.org", + "errors": [ + { + "code": "2304", + "msg": "Object status prohibits operation" + } + ] } - ] + ] + } } ``` diff --git a/doc/repp/v1/nameservers.md b/doc/repp/v1/nameservers.md index 8190530d7..ab53c72df 100644 --- a/doc/repp/v1/nameservers.md +++ b/doc/repp/v1/nameservers.md @@ -10,15 +10,15 @@ Accept: application/json Content-Type: application/json Authorization: Basic dGVzdDp0ZXN0dGVzdA== { - "data":{ - "type": "nameserver", - "id": "ns1.example.com", - "attributes": { - "hostname": "new-ns1.example.com", - "ipv4": ["192.0.2.1", "192.0.2.2"], - "ipv6": ["2001:db8::1", "2001:db8::2"] - }, + "data": { + "type": "nameserver", + "id": "ns1.example.com", + "attributes": { + "hostname": "new-ns1.example.com", + "ipv4": ["192.0.2.1", "192.0.2.2"], + "ipv6": ["2001:db8::1", "2001:db8::2"] } + } } ``` @@ -27,16 +27,26 @@ Authorization: Basic dGVzdDp0ZXN0dGVzdA== HTTP/1.1 200 Content-Type: application/json { - "data":{ + "code": 1000, + "message": "Command completed successfully", + "data": { "type": "nameserver", "id": "new-ns1.example.com", "attributes": { "hostname": "new-ns1.example.com", - "ipv4": ["192.0.2.1", "192.0.2.2"], - "ipv6": ["2001:db8::1", "2001:db8::2"] - } - }, - "affected_domains": ["example.com", "example.org"] + "ipv4": [ + "192.0.2.1", + "192.0.2.2" + ], + "ipv6": [ + "2001:db8::1", + "2001:db8::2" + ] + }, + "affected_domains": [ + "private.ee" + ] + } } ``` @@ -44,14 +54,10 @@ Content-Type: application/json ``` HTTP/1.1 400 Content-Type: application/json + { - "errors":[ - { - "title":"ns1.example.com does not exist" - }, - { - "title":"192.0.2.1 is not a valid IPv4 address" - } - ] + "code": 2005, + "message": "IPv4 is invalid [ipv4]", + "data": {} } ``` diff --git a/lib/deserializers/xml/contact_create.rb b/lib/deserializers/xml/contact_create.rb new file mode 100644 index 000000000..5dfa32ef7 --- /dev/null +++ b/lib/deserializers/xml/contact_create.rb @@ -0,0 +1,10 @@ +require 'deserializers/xml/legal_document' +require 'deserializers/xml/ident' +require 'deserializers/xml/contact' + +module Deserializers + module Xml + class ContactCreate < ContactUpdate + end + end +end diff --git a/lib/serializers/registrant_api/.DS_Store b/lib/serializers/registrant_api/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/lib/serializers/registrant_api/.DS_Store differ diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb new file mode 100644 index 000000000..834402359 --- /dev/null +++ b/lib/serializers/repp/contact.rb @@ -0,0 +1,36 @@ +module Serializers + module Repp + class Contact + attr_reader :contact + + def initialize(contact, show_address:) + @contact = contact + @show_address = show_address + end + + def to_json(obj = contact) + json = { id: obj.code, name: obj.name, ident: ident, + email: obj.email, phone: obj.phone, fax: obj.fax, + auth_info: obj.auth_info, statuses: obj.statuses, + disclosed_attributes: obj.disclosed_attributes } + + json[:address] = address if @show_address + + json + end + + def ident + { + code: contact.ident, + type: contact.ident_type, + country_code: contact.ident_country_code, + } + end + + def address + { street: contact.street, zip: contact.zip, city: contact.city, + state: contact.state, country_code: contact.country_code } + end + end + end +end diff --git a/test/integration/api/domain_contacts_test.rb b/test/integration/api/domain_contacts_test.rb index 5336cc10a..6704739d1 100644 --- a/test/integration/api/domain_contacts_test.rb +++ b/test/integration/api/domain_contacts_test.rb @@ -27,8 +27,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :ok - assert_equal ({ affected_domains: %w[airport.test shop.test], - skipped_domains: [] }), + assert_equal ({ code: 1000, message: 'Command completed successfully', data: { affected_domains: %w[airport.test shop.test], + skipped_domains: [] }}), JSON.parse(response.body, symbolize_names: true) end @@ -42,7 +42,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest assert_response :ok assert_equal %w[airport.test shop.test], JSON.parse(response.body, - symbolize_names: true)[:skipped_domains] + symbolize_names: true)[:data][:skipped_domains] end def test_keep_other_tech_contacts_intact @@ -66,10 +66,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'william-002' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: 'No such contact: jack-001' } }), + assert_response :not_found + assert_equal ({ code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -77,10 +75,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest patch '/repp/v1/domains/contacts', params: { current_contact_id: 'non-existent', new_contact_id: 'john-001' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'current_contact_id', - message: 'No such contact: non-existent' } }), + assert_response :not_found + assert_equal ({ code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -88,10 +84,8 @@ class APIDomainContactsTest < ApplicationIntegrationTest patch '/repp/v1/domains/contacts', params: { current_contact_id: 'william-001', new_contact_id: 'non-existent' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'No such contact: non-existent' } }), + assert_response :not_found + assert_equal ({code: 2303, message: 'Object does not exist'}), JSON.parse(response.body, symbolize_names: true) end @@ -100,9 +94,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'invalid' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - param: 'new_contact_id', - message: 'New contact must be valid' } }), + assert_equal ({ code: 2304, message: 'New contact must be valid', data: {} }), JSON.parse(response.body, symbolize_names: true) end @@ -111,8 +103,7 @@ class APIDomainContactsTest < ApplicationIntegrationTest new_contact_id: 'william-001' }, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response :bad_request - assert_equal ({ error: { type: 'invalid_request_error', - message: 'New contact ID must be different from current contact ID' } }), + assert_equal ({ code: 2304, message: 'New contact must be different from current', data: {} }), JSON.parse(response.body, symbolize_names: true) end diff --git a/test/integration/api/domain_transfers_test.rb b/test/integration/api/domain_transfers_test.rb index aabaeb728..3e9c10100 100644 --- a/test/integration/api/domain_transfers_test.rb +++ b/test/integration/api/domain_transfers_test.rb @@ -12,34 +12,21 @@ class APIDomainTransfersTest < ApplicationIntegrationTest Setting.transfer_wait_time = @original_transfer_wait_time end - def test_returns_domain_transfers - post '/repp/v1/domain_transfers', params: request_params, as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 200 - assert_equal ({ data: [{ - type: 'domain_transfer', - attributes: { - domain_name: 'shop.test' - }, - }] }), - JSON.parse(response.body, symbolize_names: true) - end - def test_creates_new_domain_transfer assert_difference -> { @domain.transfers.size } do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_approves_automatically_if_auto_approval_is_enabled - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert @domain.transfers.last.approved? end def test_assigns_new_registrar - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } @domain.reload assert_equal @new_registrar, @domain.registrar @@ -48,7 +35,7 @@ class APIDomainTransfersTest < ApplicationIntegrationTest def test_regenerates_transfer_code @old_transfer_code = @domain.transfer_code - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } @domain.reload refute_equal @domain.transfer_code, @old_transfer_code @@ -58,51 +45,28 @@ class APIDomainTransfersTest < ApplicationIntegrationTest @old_registrar = @domain.registrar assert_difference -> { @old_registrar.notifications.count } do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_duplicates_registrant_admin_and_tech_contacts assert_difference -> { @new_registrar.contacts.size }, 3 do - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } end end def test_reuses_identical_contact - post '/repp/v1/domain_transfers', params: request_params, as: :json, + post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_equal 1, @new_registrar.contacts.where(name: 'William').size end - def test_fails_if_domain_does_not_exist - post '/repp/v1/domain_transfers', - params: { data: { domainTransfers: [{ domainName: 'non-existent.test', - transferCode: 'any' }] } }, - as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 400 - assert_equal ({ errors: [{ title: 'non-existent.test does not exist' }] }), - JSON.parse(response.body, symbolize_names: true) - end - - def test_fails_if_transfer_code_is_wrong - post '/repp/v1/domain_transfers', - params: { data: { domainTransfers: [{ domainName: 'shop.test', - transferCode: 'wrong' }] } }, - as: :json, - headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response 400 - refute_equal @new_registrar, @domain.registrar - assert_equal ({ errors: [{ title: 'shop.test transfer code is wrong' }] }), - JSON.parse(response.body, symbolize_names: true) - end - private def request_params - { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } } + { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } } end def http_auth_key diff --git a/test/integration/api/nameservers/put_test.rb b/test/integration/api/nameservers/put_test.rb index 853a20549..3ab4f4dd4 100644 --- a/test/integration/api/nameservers/put_test.rb +++ b/test/integration/api/nameservers/put_test.rb @@ -60,12 +60,14 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 200 - assert_equal ({ data: { type: 'nameserver', + assert_equal ({ code: 1000, + message: 'Command completed successfully', + data: { type: 'nameserver', id: 'ns55.bestnames.test', attributes: { hostname: 'ns55.bestnames.test', ipv4: ['192.0.2.55'], - ipv6: ['2001:db8::55'] } }, - affected_domains: ["airport.test", "shop.test"] }), + ipv6: ['2001:db8::55'] }, + affected_domains: ["airport.test", "shop.test"] }}), JSON.parse(response.body, symbolize_names: true) end @@ -85,7 +87,7 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 404 - assert_equal ({ errors: [{ title: 'Hostname non-existent.test does not exist' }] }), + assert_equal ({code: 2303, message: 'Object does not exist' }), JSON.parse(response.body, symbolize_names: true) end @@ -96,7 +98,8 @@ class APINameserversPutTest < ApplicationIntegrationTest headers: { 'HTTP_AUTHORIZATION' => http_auth_key } assert_response 400 - assert_equal ({ errors: [{ title: 'Hostname is missing' }] }), + assert_equal ({ code: 2003, + message: 'param is missing or the value is empty: hostname' }), JSON.parse(response.body, symbolize_names: true) end diff --git a/test/integration/repp/v1/accounts/balance_test.rb b/test/integration/repp/v1/accounts/balance_test.rb new file mode 100644 index 000000000..785e0aee8 --- /dev/null +++ b/test/integration/repp/v1/accounts/balance_test.rb @@ -0,0 +1,22 @@ +require 'test_helper' + +class ReppV1BalanceTest < ActionDispatch::IntegrationTest + def setup + @registrar = users(:api_bestnames) + token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_can_query_balance + get '/repp/v1/accounts/balance', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal @registrar.registrar.cash_account.balance.to_s, json[:data][:balance] + assert_equal @registrar.registrar.cash_account.currency, json[:data][:currency] + end +end diff --git a/test/integration/repp/auctions_test.rb b/test/integration/repp/v1/auctions_test.rb similarity index 100% rename from test/integration/repp/auctions_test.rb rename to test/integration/repp/v1/auctions_test.rb diff --git a/test/integration/repp/v1/base_test.rb b/test/integration/repp/v1/base_test.rb new file mode 100644 index 000000000..d0baed30e --- /dev/null +++ b/test/integration/repp/v1/base_test.rb @@ -0,0 +1,63 @@ +require 'test_helper' + +class ReppV1BaseTest < ActionDispatch::IntegrationTest + def setup + @registrar = users(:api_bestnames) + token = Base64.encode64("#{@registrar.username}:#{@registrar.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_unauthorized_user_has_no_access + get repp_v1_contacts_path + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + + invalid_token = Base64.encode64("nonexistant:user") + headers = { 'Authorization' => "Basic #{invalid_token}" } + + get repp_v1_contacts_path, headers: headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + end + + def test_authenticates_valid_user + get repp_v1_contacts_path, headers: @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + end + + def test_processes_invalid_base64_token_format_properly + token = '??as8d9sf kjsdjh klsdfjjf' + headers = { 'Authorization' => "Basic #{token}"} + get repp_v1_contacts_path, headers: headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 'Invalid authorization information', response_json[:message] + end + + def test_takes_ip_whitelist_into_account + Setting.api_ip_whitelist_enabled = true + Setting.registrar_ip_whitelist_enabled = true + + whiteip = white_ips(:one) + whiteip.update(ipv4: '1.1.1.1') + + get repp_v1_contacts_path, headers: @auth_headers + response_json = JSON.parse(response.body, symbolize_names: true) + + assert_response :unauthorized + assert_equal 2202, response_json[:code] + assert response_json[:message].include? 'Access denied from IP' + + Setting.api_ip_whitelist_enabled = false + Setting.registrar_ip_whitelist_enabled = false + end +end diff --git a/test/integration/repp/v1/contacts/check_test.rb b/test/integration/repp/v1/contacts/check_test.rb new file mode 100644 index 000000000..be0d979b1 --- /dev/null +++ b/test/integration/repp/v1/contacts/check_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class ReppV1ContactsCheckTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_code_based_check_returns_true_for_available_contact + get '/repp/v1/contacts/check/nonexistant:code', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 'nonexistant:code', json[:data][:contact][:id] + assert_equal true, json[:data][:contact][:available] + end + + def test_code_based_check_returns_true_for_available_contact + contact = contacts(:jack) + get "/repp/v1/contacts/check/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal contact.code, json[:data][:contact][:id] + assert_equal false, json[:data][:contact][:available] + end +end diff --git a/test/integration/repp/v1/contacts/create_test.rb b/test/integration/repp/v1/contacts/create_test.rb new file mode 100644 index 000000000..f30bc368f --- /dev/null +++ b/test/integration/repp/v1/contacts/create_test.rb @@ -0,0 +1,156 @@ +require 'test_helper' + +class ReppV1ContactsCreateTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_creates_new_contact + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_equal(request_body[:contact][:name], contact.name) + assert_equal(request_body[:contact][:phone], contact.phone) + assert_equal(request_body[:contact][:email], contact.email) + assert_equal(request_body[:contact][:ident][:ident_type], contact.ident_type) + assert_equal(request_body[:contact][:ident][:ident_country_code], contact.ident_country_code) + assert_equal(request_body[:contact][:ident][:ident], contact.ident) + end + + def test_removes_postal_info_when_contact_created + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111111", + "email": "donald@trump.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + }, + "addr": { + "city": "Tallinn", + "street": "Wismari 13", + "zip": "12345", + "country_code": "EE" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1100, json[:code] + assert_equal 'Command completed successfully; Postal address data discarded', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_nil contact.city + assert_nil contact.street + assert_nil contact.zip + assert_nil contact.country_code + end + + def test_requires_contact_address_when_processing_enabled + Setting.address_processing = true + + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2003, json[:code] + assert json[:message].include? 'param is missing or the value is empty' + + Setting.address_processing = false + end + + def test_validates_ident_code + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "123123123" + } + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2005, json[:code] + assert json[:message].include? 'Ident code does not conform to national identification number format' + end + + def test_attaches_legaldoc_if_present + request_body = { + "contact": { + "name": "Donald Trump", + "phone": "+372.51111112", + "email": "donald@trumptower.com", + "ident": { + "ident_type": "priv", + "ident_country_code": "EE", + "ident": "39708290069" + }, + }, + "legal_document": { + "type": "pdf", + "body": "#{'test' * 2000}" + } + } + + post '/repp/v1/contacts', headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.legal_documents.any? + end +end diff --git a/test/integration/repp/v1/contacts/delete_test.rb b/test/integration/repp/v1/contacts/delete_test.rb new file mode 100644 index 000000000..07438d8af --- /dev/null +++ b/test/integration/repp/v1/contacts/delete_test.rb @@ -0,0 +1,47 @@ +require 'test_helper' + +class ReppV1ContactsDeleteTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_deletes_unassociated_contact + contact = contacts(:invalid_email) + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + end + + def test_can_not_delete_associated_contact + contact = contacts(:john) + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2305, json[:code] + assert_equal 'Object association prohibits operation [domains]', json[:message] + end + + def test_handles_unknown_contact + delete "/repp/v1/contacts/definitely:unexistant", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + end + + def test_can_not_destroy_other_registrar_contact + contact = contacts(:jack) + + delete "/repp/v1/contacts/#{contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + end +end diff --git a/test/integration/repp/v1/contacts/list_test.rb b/test/integration/repp/v1/contacts/list_test.rb new file mode 100644 index 000000000..31c4baaf9 --- /dev/null +++ b/test/integration/repp/v1/contacts/list_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' + +class ReppV1ContactsListTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_registrar_contacts + get repp_v1_contacts_path, headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.contacts.count, json[:total_number_of_records] + assert_equal @user.registrar.contacts.count, json[:contacts].length + + assert json[:contacts][0].is_a? String + end + + + def test_returns_detailed_registrar_contacts + get repp_v1_contacts_path(details: true), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.contacts.count, json[:total_number_of_records] + assert_equal @user.registrar.contacts.count, json[:contacts].length + + assert json[:contacts][0].is_a? Hash + end + + def test_respects_limit + get repp_v1_contacts_path(details: true, limit: 2), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal 2, json[:contacts].length + end + + def test_respects_offset + offset = 1 + get repp_v1_contacts_path(details: true, offset: offset), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal (@user.registrar.contacts.count - offset), json[:contacts].length + end +end diff --git a/test/integration/repp/v1/contacts/show_test.rb b/test/integration/repp/v1/contacts/show_test.rb new file mode 100644 index 000000000..4a6f5b615 --- /dev/null +++ b/test/integration/repp/v1/contacts/show_test.rb @@ -0,0 +1,45 @@ +require 'test_helper' + +class ReppV1ContactsShowTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_error_when_not_found + get repp_v1_contact_path(id: 'definitelynotexistant'), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_shows_existing_contact + contact = @user.registrar.contacts.first + + get repp_v1_contact_path(id: contact.code), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal contact.code, json[:data][:id] + end + + def test_can_not_access_out_of_scope_contacts + # Contact of registrar goodnames, we're using bestnames API credentials + contact = contacts(:jack) + + get repp_v1_contact_path(id: contact.code), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end +end diff --git a/test/integration/repp/v1/contacts/update_test.rb b/test/integration/repp/v1/contacts/update_test.rb new file mode 100644 index 000000000..cf27f98da --- /dev/null +++ b/test/integration/repp/v1/contacts/update_test.rb @@ -0,0 +1,119 @@ +require 'test_helper' + +class ReppV1ContactsUpdateTest < ActionDispatch::IntegrationTest + def setup + @contact = contacts(:john) + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_updates_contact + request_body = { + "contact": { + "email": "donaldtrump@yandex.ru" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_equal(request_body[:contact][:email], contact.email) + end + + def test_removes_postal_info_when_updated + request_body = { + "contact": { + "addr": { + "city": "Tallinn", + "street": "Wismari 13", + "zip": "12345", + "country_code": "EE" + } + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1100, json[:code] + assert_equal 'Command completed successfully; Postal address data discarded', json[:message] + + contact = Contact.find_by(code: json[:data][:contact][:id]) + assert contact.present? + + assert_nil contact.city + assert_nil contact.street + assert_nil contact.zip + assert_nil contact.country_code + end + + def test_can_not_change_ident_code + request_body = { + "contact": { + "name": "Donald Trumpster", + "ident": { + "ident_type": "priv", + "ident_country_code": "US", + "ident": "12345" + } + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + @contact.reload + assert_not @contact.ident == 12345 + assert_response :bad_request + assert_equal 2308, json[:code] + assert json[:message].include? 'Ident update is not allowed. Consider creating new contact object' + end + + def test_attaches_legaldoc_if_present + request_body = { + "contact": { + "email": "donaldtrump@yandex.ru" + }, + "legal_document": { + "type": "pdf", + "body": "#{'test' * 2000}" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + @contact.reload + assert @contact.legal_documents.any? + end + + def test_returns_error_if_ident_wrong_format + request_body = { + "contact": { + "ident": "123" + } + } + + put "/repp/v1/contacts/#{@contact.code}", headers: @auth_headers, params: request_body + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2308, json[:code] + assert_equal 'Ident update is not allowed. Consider creating new contact object', json[:message] + end +end diff --git a/test/integration/repp/v1/domains/contact_replacement_test.rb b/test/integration/repp/v1/domains/contact_replacement_test.rb new file mode 100644 index 000000000..3cbd9eb8e --- /dev/null +++ b/test/integration/repp/v1/domains/contact_replacement_test.rb @@ -0,0 +1,65 @@ +require 'test_helper' + +class ReppV1DomainsContactReplacementTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_replaces_tech_contact_with_new_one + replaceable_contact = contacts(:william) + replacing_contact = contacts(:jane) + + payload = { + "current_contact_id": replaceable_contact.code, + "new_contact_id": replacing_contact.code + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert json[:data][:affected_domains].include? 'airport.test' + assert json[:data][:affected_domains].include? 'shop.test' + + assert_empty json[:data][:skipped_domains] + end + + def test_tech_contact_id_must_differ + replaceable_contact = contacts(:william) + replacing_contact = contacts(:william) + + payload = { + "current_contact_id": replaceable_contact.code, + "new_contact_id": replacing_contact.code + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'New contact must be different from current', json[:message] + end + + def test_contact_codes_must_be_valid + payload = { + "current_contact_id": 'dfgsdfg', + "new_contact_id": 'vvv' + } + + patch '/repp/v1/domains/contacts', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + +end diff --git a/test/integration/repp/v1/domains/list_test.rb b/test/integration/repp/v1/domains/list_test.rb new file mode 100644 index 000000000..bee390e5c --- /dev/null +++ b/test/integration/repp/v1/domains/list_test.rb @@ -0,0 +1,54 @@ +require 'test_helper' + +class ReppV1DomainsListTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_returns_registrar_domains + get repp_v1_domains_path, headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records] + assert_equal @user.registrar.domains.count, json[:data][:domains].length + + assert json[:data][:domains][0].is_a? String + end + + def test_returns_detailed_registrar_domains + get repp_v1_domains_path(details: true), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal @user.registrar.domains.count, json[:data][:total_number_of_records] + assert_equal @user.registrar.domains.count, json[:data][:domains].length + + assert json[:data][:domains][0].is_a? Hash + end + + def test_respects_limit + get repp_v1_domains_path(details: true, limit: 2), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal 2, json[:data][:domains].length + end + + def test_respects_offset + offset = 1 + get repp_v1_domains_path(details: true, offset: offset), headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + + assert_equal (@user.registrar.domains.count - offset), json[:data][:domains].length + end +end diff --git a/test/integration/repp/v1/domains/transfer_info_test.rb b/test/integration/repp/v1/domains/transfer_info_test.rb new file mode 100644 index 000000000..f57675b06 --- /dev/null +++ b/test/integration/repp/v1/domains/transfer_info_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class ReppV1DomainsTransferInfoTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + @domain = domains(:shop) + @auth_headers = { 'Authorization' => token } + end + + def test_can_query_domain_info + headers = @auth_headers + headers['Auth-Code'] = @domain.transfer_code + + get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal @domain.name, json[:data][:domain] + assert json[:data][:registrant].present? + assert json[:data][:admin_contacts].present? + assert json[:data][:tech_contacts].present? + end + + def test_respects_domain_authorization_code + headers = @auth_headers + headers['Auth-Code'] = 'jhfgifhdg' + + get "/repp/v1/domains/#{@domain.name}/transfer_info", headers: headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2202, json[:code] + assert_equal 'Authorization error', json[:message] + assert_empty json[:data] + end +end diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb new file mode 100644 index 000000000..46d5c30c4 --- /dev/null +++ b/test/integration/repp/v1/domains/transfer_test.rb @@ -0,0 +1,127 @@ +require 'test_helper' + +class ReppV1DomainsTransferTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + @domain = domains(:hospital) + + @auth_headers = { 'Authorization' => token } + end + + def test_transfers_domain + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal @domain.name, json[:data][:success][0][:domain_name] + + @domain.reload + + assert @domain.registrar = @user.registrar + end + + def test_does_not_transfer_domain_if_not_transferable + @domain.schedule_force_delete(type: :fast_track) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Object status prohibits operation', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert_not @domain.registrar == @user.registrar + end + + def test_does_not_transfer_domain_with_invalid_auth_code + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": "sdfgsdfg" } + ] + } + } + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal "Invalid authorization information", json[:data][:failed][0][:errors][0][:msg] + end + + def test_does_not_transfer_domain_to_same_registrar + @domain.update!(registrar: @user.registrar) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Domain already belongs to the querying registrar', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert @domain.registrar == @user.registrar + end + + def test_does_not_transfer_domain_if_discarded + @domain.update!(statuses: [DomainStatus::DELETE_CANDIDATE]) + + payload = { + "data": { + "domain_transfers": [ + { "domain_name": @domain.name, "transfer_code": @domain.transfer_code } + ] + } + } + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + + assert_equal 'Object is not eligible for transfer', json[:data][:failed][0][:errors][0][:msg] + + @domain.reload + + assert_not @domain.registrar == @user.registrar + end +end diff --git a/test/integration/repp/v1/registrar/nameservers_test.rb b/test/integration/repp/v1/registrar/nameservers_test.rb new file mode 100644 index 000000000..f01769dfb --- /dev/null +++ b/test/integration/repp/v1/registrar/nameservers_test.rb @@ -0,0 +1,77 @@ +require 'test_helper' + +class ReppV1RegistrarNameserversTest < ActionDispatch::IntegrationTest + def setup + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + end + + def test_updates_nameserver_values + nameserver = nameservers(:shop_ns1) + payload = { + "data": { + "id": nameserver.hostname, + "type": "nameserver", + "attributes": { + "hostname": "#{nameserver.hostname}.test", + "ipv4": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes]) + assert_equal({ hostname: "#{nameserver.hostname}.test", ipv4: ["1.1.1.1"] }, json[:data][:attributes]) + assert json[:data][:affected_domains].include? 'airport.test' + assert json[:data][:affected_domains].include? 'shop.test' + end + + def test_nameserver_with_hostname_must_exist + payload = { + "data": { + "id": 'ns.nonexistant.test', + "type": "nameserver", + "attributes": { + "hostname": "ns1.dn.test", + "ipv4": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_ip_must_be_in_correct_format + nameserver = nameservers(:shop_ns1) + payload = { + "data": { + "id": nameserver.hostname, + "type": "nameserver", + "attributes": { + "hostname": "#{nameserver.hostname}.test", + "ipv6": ["1.1.1.1"] + } + } + } + + put '/repp/v1/registrar/nameservers', headers: @auth_headers, params: payload + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2005, json[:code] + assert_equal 'IPv6 is invalid [ipv6]', json[:message] + end +end diff --git a/test/integration/repp/retained_domains_test.rb b/test/integration/repp/v1/retained_domains_test.rb similarity index 100% rename from test/integration/repp/retained_domains_test.rb rename to test/integration/repp/v1/retained_domains_test.rb diff --git a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb index 69b755499..a531ff4dc 100644 --- a/test/system/registrar_area/bulk_change/bulk_transfer_test.rb +++ b/test/system/registrar_area/bulk_change/bulk_transfer_test.rb @@ -6,9 +6,9 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase end def test_transfer_multiple_domains_in_bulk - request_body = { data: { domainTransfers: [{ domainName: 'shop.test', transferCode: '65078d5' }] } } + request_body = { data: { domain_transfers: [{ domain_name: 'shop.test', transfer_code: '65078d5' }] } } headers = { 'Content-type' => Mime[:json] } - request_stub = stub_request(:post, /domain_transfers/).with(body: request_body, + request_stub = stub_request(:post, /domains\/transfer/).with(body: request_body, headers: headers, basic_auth: ['test_goodnames', 'testtest']) .to_return(body: { data: [{ @@ -29,7 +29,7 @@ class RegistrarAreaBulkTransferTest < ApplicationSystemTestCase def test_fail_gracefully body = { errors: [{ title: 'epic fail' }] }.to_json headers = { 'Content-type' => Mime[:json] } - stub_request(:post, /domain_transfers/).to_return(status: 400, body: body, headers: headers) + stub_request(:post, /domains\/transfer/).to_return(status: 400, body: body, headers: headers) visit registrar_domains_url click_link 'Bulk change'