diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index 8ee1cba357..ac21ebb220 100644 --- a/app/controllers/repp/v1/domains/nameservers_controller.rb +++ b/app/controllers/repp/v1/domains/nameservers_controller.rb @@ -1,3 +1,4 @@ +require 'csv' module Repp module V1 module Domains @@ -8,6 +9,13 @@ class NameserversController < BaseController THROTTLED_ACTIONS = %i[index create destroy].freeze include Shunter::Integration::Throttle + COMMAND_FAILED_EPP_CODE = 2400 + PROHIBIT_EPP_CODE = 2304 + OBJECT_DOES_NOT_EXIST_EPP_CODE = 2303 + AUTHORIZATION_ERROR_EPP_CODE = 2201 + PARAMETER_VALUE_POLICY_ERROR_EPP_CODE = 2306 + UNKNOWN_EPP_CODE = 2000 + api :GET, '/repp/v1/domains/:domain_name/nameservers' desc "Get domain's nameservers" def index @@ -49,8 +57,143 @@ def destroy render_success(data: { domain: { name: @domain.name } }) end + api :POST, '/repp/v1/domains/nameservers/bulk' + desc 'Bulk update nameservers for multiple domains (supports JSON data or CSV file upload)' + param :data, Hash, required: false, desc: 'JSON data for nameserver changes' do + param :nameserver_changes, Array, required: true, desc: 'Array of nameserver changes' do + param :domain_name, String, required: true, desc: 'Domain name' + param :new_hostname, String, required: true, desc: 'New nameserver hostname' + param :ipv4, Array, required: false, desc: 'Array of IPv4 addresses' + param :ipv6, Array, required: false, desc: 'Array of IPv6 addresses' + end + end + def bulk_update + authorize! :manage, :repp + + @errors ||= [] + @successful = [] + + nameserver_changes = if is_csv_request? + parse_nameserver_csv_from_body(request.raw_post) + elsif bulk_params[:csv_file].present? + parse_nameserver_csv(bulk_params[:csv_file]) + else + bulk_params[:nameserver_changes] + end + + nameserver_changes.each { |change| process_nameserver_change(change) } + + if @errors.any? && @successful.empty? + render_empty_success_objects_with_errors(nameserver_changes_count: nameserver_changes.count) + elsif @errors.any? && @successful.any? + render_success_objects_and_objects_with_errors(nameserver_changes_count: nameserver_changes.count) + else + render_success(data: { + success: @successful, + failed: @errors, + summary: { + total: nameserver_changes.count, + successful: @successful.count, + failed: @errors.count + } + }) + end + end + private + def render_success_objects_and_objects_with_errors(nameserver_changes_count:) + error_summary = analyze_nameserver_errors(@errors) + message = "#{successful.count} nameserver changes successful, #{errors.count} failed. " + + build_nameserver_error_message(error_summary, errors.count, partial: true) + + response = build_nameserver_response_for_bulk_operation(code: COMMAND_FAILED_EPP_CODE, message: message, successful: @successful, errors: @errors, nameserver_changes_count: nameserver_changes_count, error_summary: error_summary) + render(json: response, status: :multi_status) + end + + def render_empty_success_objects_with_errors(nameserver_changes_count:) + error_summary = analyze_nameserver_errors(@errors) + message = build_nameserver_error_message(error_summary, nameserver_changes_count) + + @response = build_nameserver_response_for_bulk_operation(code: PROHIBIT_EPP_CODE, message: message, successful: @successful, errors: @errors, nameserver_changes_count: nameserver_changes_count, error_summary: error_summary) + render(json: @response, status: :bad_request) + end + + def build_nameserver_response_for_bulk_operation(code:, message:, successful:, errors:, nameserver_changes_count:, error_summary:) + { + code: code, + message: message, + data: { + success: successful, + failed: errors, + summary: { + total: nameserver_changes_count, + successful: successful.count, + failed: errors.count, + error_breakdown: error_summary + } + } + } + end + + def csv_parse_wrapper(csv_data) + yield + rescue CSV::MalformedCSVError => e + @errors << { type: 'csv_error', message: "Invalid CSV format: #{e.message}" } + return [] + rescue StandardError => e + @errors << { type: 'csv_error', message: "Error processing CSV: #{e.message}" } + return [] + end + + def parse_nameserver_csv(csv_file) + nameserver_changes = [] + + csv_parse_wrapper(csv_file) do + CSV.foreach(csv_file.path, headers: true) do |row| + next if row['Domain'].blank? + + nameserver_changes << { + domain_name: row['Domain'].strip, + new_hostname: bulk_params[:new_hostname] || '', + ipv4: bulk_params[:ipv4] || [], + ipv6: bulk_params[:ipv6] || [] + } + end + end + + if nameserver_changes.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required header: Domain' } + elsif bulk_params[:new_hostname].blank? + @errors << { type: 'csv_error', message: 'new_hostname parameter is required when using CSV' } + end + + nameserver_changes + end + + def parse_nameserver_csv_from_body(csv_data) + nameserver_changes = [] + + csv_parse_wrapper(csv_data) do + CSV.parse(csv_data, headers: true) do |row| + next if row['Domain'].blank? || row['New_Nameserver'].blank? + + nameserver_changes << { + domain_name: row['Domain'].strip, + new_hostname: row['New_Nameserver'].strip, + ipv4: row['IPv4']&.split(',')&.map(&:strip) || [], + ipv6: row['IPv6']&.split(',')&.map(&:strip) || [] + } + end + end + + if nameserver_changes.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, New_Nameserver' } + end + + nameserver_changes + end + def set_nameserver @nameserver = @domain.nameservers.find_by!(hostname: params[:id]) end @@ -58,6 +201,151 @@ def set_nameserver def nameserver_params params.permit(:domain_id, nameservers: [[:hostname, :action, { ipv4: [], ipv6: [] }]]) end + + def bulk_params + if params[:csv_file].present? + params.permit(:csv_file, :new_hostname, ipv4: [], ipv6: []) + else + params.require(:data).require(:nameserver_changes) + params.require(:data).permit(nameserver_changes: [%i[domain_name new_hostname], { ipv4: [], ipv6: [] }]) + end + end + + def build_error_info(change:, error_code:, error_message:, details:) + { + type: 'nameserver_change', + domain_name: change[:domain_name], + error_code: error_code.to_s, + error_message: error_message, + details: details + } + end + + def process_nameserver_change_wrapper(change) + yield + rescue ActiveRecord::RecordNotFound => e + @errors << build_error_info( + change: change, error_code: OBJECT_DOES_NOT_EXIST_EPP_CODE, + error_message: 'Domain not found', + details: { code: OBJECT_DOES_NOT_EXIST_EPP_CODE.to_s, msg: 'Domain not found' } + ) + rescue StandardError => e + @errors << build_error_info( + change: change, + error_code: UNKNOWN_EPP_CODE, + error_message: e.message, + details: { message: e.message } + ) + end + + def process_nameserver_change(change) + process_nameserver_change_wrapper(change) do + domain = Epp::Domain.find_by!('name = ? OR name_puny = ?', + change[:domain_name], change[:domain_name]) + + unless domain.registrar == current_user.registrar + @errors << build_error_info( + change: change, + error_code: AUTHORIZATION_ERROR_EPP_CODE, + error_message: 'Authorization error', + details: { code: AUTHORIZATION_ERROR_EPP_CODE.to_s, msg: 'Authorization error' }) + return + end + + existing_hostnames = domain.nameservers.map(&:hostname) + + if existing_hostnames.include?(change[:new_hostname]) + @successful << { type: 'nameserver_change', domain_name: domain.name } + return + end + + nameserver_actions = [] + + if domain.nameservers.count > 0 + first_ns = domain.nameservers.first + nameserver_actions << { hostname: first_ns.hostname, action: 'rem' } + end + + nameserver_actions << { + hostname: change[:new_hostname], + action: 'add', + ipv4: change[:ipv4] || [], + ipv6: change[:ipv6] || [] + } + + nameserver_params = { nameservers: nameserver_actions } + + action = Actions::DomainUpdate.new(domain, nameserver_params, false) + + if action.call + @successful << { type: 'nameserver_change', domain_name: domain.name } + else + epp_error = domain.errors.where(:epp_errors).first + error_details = epp_error&.options || { message: domain.errors.full_messages.join(', ') } + + error_info = { + type: 'nameserver_change', + domain_name: domain.name, + error_code: error_details[:code] || 'UNKNOWN', + error_message: error_details[:msg] || error_details[:message] || 'Unknown error', + details: error_details + } + + @errors << error_info + end + end + end + + def is_csv_request? + request.content_type&.include?('text/csv') || request.content_type&.include?('application/csv') + end + + + + def analyze_nameserver_errors(errors) + error_counts = {} + + errors.each do |error| + error_code = error[:error_code] || 'UNKNOWN' + error_message = error[:error_message] || 'Unknown error' + + key = "#{error_code}:#{error_message}" + error_counts[key] ||= { + code: error_code, + message: error_message, + count: 0, + domains: [] + } + error_counts[key][:count] += 1 + error_counts[key][:domains] << error[:domain_name] + end + + error_counts.values + end + + def build_nameserver_error_message(error_summary, total_count, partial: false) + return "All #{total_count} nameserver changes failed" if error_summary.empty? + + messages = [] + + error_summary.each do |error_info| + case error_info[:code] + when OBJECT_DOES_NOT_EXIST_EPP_CODE.to_s + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} not found" + when AUTHORIZATION_ERROR_EPP_CODE.to_s + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} unauthorized" + when PROHIBIT_EPP_CODE.to_s + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} prohibited from changes" + when PARAMETER_VALUE_POLICY_ERROR_EPP_CODE.to_s + messages << "#{error_info[:count]} nameserver#{'s' if error_info[:count] > 1} invalid" + else + messages << "#{error_info[:count]} change#{'s' if error_info[:count] > 1} failed (#{error_info[:message]})" + end + end + + prefix = partial ? "Failures: " : "All #{total_count} changes failed: " + prefix + messages.join(', ') + end end end end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index ed86638e43..567211186c 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -1,4 +1,5 @@ require 'serializers/repp/domain' +require 'csv' module Repp module V1 class DomainsController < BaseController # rubocop:disable Metrics/ClassLength @@ -124,16 +125,94 @@ def transfer_info end api :POST, '/repp/v1/domains/transfer' - desc 'Transfer multiple domains' + desc 'Transfer multiple domains (supports JSON data or CSV file upload)' + param :data, Hash, required: false, desc: 'JSON data for domain transfers' do + param :domain_transfers, Array, required: true, desc: 'Array of domain transfers' do + param :domain_name, String, required: true, desc: 'Domain name' + param :transfer_code, String, required: true, desc: 'Transfer authorization code' + end + end def transfer - authorize! :transfer, Epp::Domain - @errors ||= [] - @successful = [] - transfer_params[:domain_transfers].each do |transfer| - initiate_transfer(transfer) + Rails.logger.info "[REPP Transfer] Starting transfer request" + Rails.logger.info "[REPP Transfer] Request params: #{params.inspect}" + Rails.logger.info "[REPP Transfer] Content-Type: #{request.content_type}" + Rails.logger.info "[REPP Transfer] Raw body: #{request.raw_post}" + + begin + authorize! :transfer, Epp::Domain + + @errors ||= [] + @successful = [] + + domain_transfers = if is_csv_request? + parse_transfer_csv_from_body(request.raw_post) + elsif params[:csv_file].present? + parse_transfer_csv(params[:csv_file]) + else + transfer_params[:domain_transfers] + end + + + domain_transfers.each { |transfer| initiate_transfer(transfer) } + + if @errors.any? && @successful.empty? + + error_summary = analyze_transfer_errors(@errors) + message = build_error_message(error_summary, domain_transfers.count) + + @response = { + code: 2304, + message: message, + data: { + success: @successful, + failed: @errors, + summary: { + total: domain_transfers.count, + successful: @successful.count, + failed: @errors.count, + error_breakdown: error_summary + } + } + } + render(json: @response, status: :bad_request) + elsif @errors.any? && @successful.any? + + error_summary = analyze_transfer_errors(@errors) + message = "#{@successful.count} domains transferred successfully, #{@errors.count} failed. " + + build_error_message(error_summary, @errors.count, partial: true) + + @response = { + code: 2400, + message: message, + data: { + success: @successful, + failed: @errors, + summary: { + total: domain_transfers.count, + successful: @successful.count, + failed: @errors.count, + error_breakdown: error_summary + } + } + } + render(json: @response, status: :multi_status) # 207 Multi-Status + else + render_success(data: { + success: @successful, + failed: @errors, + summary: { + total: domain_transfers.count, + successful: @successful.count, + failed: @errors.count + } + }) + end + + rescue StandardError => e + + @response = { code: 2304, message: "Transfer failed: #{e.message}", data: {} } + render(json: @response, status: :bad_request) end - - render_success(data: { success: @successful, failed: @errors }) end api :DELETE, '/repp/v1/domains/:domain_name' @@ -166,21 +245,61 @@ def serialized_domains(domains) 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.where(:epp_errors).first.options } + + epp_error = domain.errors.where(:epp_errors).first + error_details = epp_error&.options || { message: 'Unknown error' } + + error_info = { + type: 'domain_transfer', + domain_name: domain.name, + error_code: error_details[:code] || 'UNKNOWN', + error_message: error_details[:msg] || error_details[:message] || 'Unknown error', + details: error_details + } + + @errors << error_info end end def transfer_params - params.require(:data).require(:domain_transfers) - params.require(:data).permit(domain_transfers: [%i[domain_name transfer_code]]) + + params.permit(:csv_file) + return {} if params[:csv_file].present? + + + begin + data_params = params.require(:data) + + unless data_params.key?(:domain_transfers) + raise ActionController::ParameterMissing.new(:domain_transfers) + end + + domain_transfers_array = data_params[:domain_transfers] + + if domain_transfers_array.blank? || !domain_transfers_array.is_a?(Array) + raise ActionController::ParameterMissing.new(:domain_transfers, "domain_transfers cannot be empty") + end + + Rails.logger.info "[REPP Transfer] Required params validation passed" + result = data_params.permit(domain_transfers: [%i[domain_name transfer_code]]) + Rails.logger.info "[REPP Transfer] Permitted params result: #{result.inspect}" + result + rescue ActionController::ParameterMissing => e + Rails.logger.error "[REPP Transfer] Parameter missing error: #{e.message}" + raise e + rescue StandardError => e + Rails.logger.error "[REPP Transfer] transfer_params error: #{e.class} - #{e.message}" + raise e + end end def transfer_info_params @@ -282,6 +401,112 @@ def domain_params dnskeys_attributes: [%i[flags alg protocol public_key]], delete: [:verified]) end + + def is_csv_request? + request.content_type&.include?('text/csv') || request.content_type&.include?('application/csv') + end + + def parse_transfer_csv_from_body(csv_data) + domain_transfers = [] + + begin + CSV.parse(csv_data, headers: true) do |row| + next if row['Domain'].blank? || row['Transfer code'].blank? + + domain_transfers << { + domain_name: row['Domain'].strip, + transfer_code: row['Transfer code'].strip + } + end + rescue CSV::MalformedCSVError => e + @errors << { type: 'csv_error', message: "Invalid CSV format: #{e.message}" } + return [] + rescue StandardError => e + @errors << { type: 'csv_error', message: "Error processing CSV: #{e.message}" } + return [] + end + + if domain_transfers.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, Transfer code' } + end + + Rails.logger.info "[REPP Transfer] Parsed #{domain_transfers.count} domains from CSV" + domain_transfers + end + + def parse_transfer_csv(csv_file) + domain_transfers = [] + + begin + CSV.foreach(csv_file.path, headers: true) do |row| + next if row['Domain'].blank? || row['Transfer code'].blank? + + domain_transfers << { + domain_name: row['Domain'].strip, + transfer_code: row['Transfer code'].strip + } + end + rescue CSV::MalformedCSVError => e + @errors << { type: 'csv_error', message: "Invalid CSV format: #{e.message}" } + return [] + rescue StandardError => e + @errors << { type: 'csv_error', message: "Error processing CSV: #{e.message}" } + return [] + end + + if domain_transfers.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, Transfer code' } + end + + domain_transfers + end + + def analyze_transfer_errors(errors) + error_counts = {} + + errors.each do |error| + error_code = error[:error_code] || 'UNKNOWN' + error_message = error[:error_message] || 'Unknown error' + + key = "#{error_code}:#{error_message}" + error_counts[key] ||= { + code: error_code, + message: error_message, + count: 0, + domains: [] + } + error_counts[key][:count] += 1 + error_counts[key][:domains] << error[:domain_name] + end + + error_counts.values + end + + def build_error_message(error_summary, total_count, partial: false) + return "All #{total_count} domain transfers failed" if error_summary.empty? + + messages = [] + + error_summary.each do |error_info| + case error_info[:code] + when '2303' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} not found" + when '2202' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} with invalid transfer code" + when '2002' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} already belong to your registrar" + when '2304' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} prohibited from transfer" + when '2106' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} not eligible for transfer" + else + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} failed (#{error_info[:message]})" + end + end + + prefix = partial ? "Failures: " : "All #{total_count} transfers failed: " + prefix + messages.join(', ') + end end end end diff --git a/config/routes.rb b/config/routes.rb index f9d5f60845..769cd996dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,6 +168,7 @@ patch 'contacts', to: 'domains/contacts#update' patch 'admin_contacts', to: 'domains/admin_contacts#update' post 'renew/bulk', to: 'domains/renews#bulk_renew' + post 'nameservers/bulk', to: 'domains/nameservers#bulk_update' end end end diff --git a/test/fixtures/files/domain_transfer_invalid.csv b/test/fixtures/files/domain_transfer_invalid.csv new file mode 100644 index 0000000000..97680dd48e --- /dev/null +++ b/test/fixtures/files/domain_transfer_invalid.csv @@ -0,0 +1,3 @@ +WrongHeader,AnotherWrongHeader +hospital.test,61445d2e-3edc-4d48-9a3a-fcb4d9b5d331 +shop.test,a39f33a5c6a5aa3ac44e625c9eaa7476 diff --git a/test/fixtures/files/domain_transfer_valid.csv b/test/fixtures/files/domain_transfer_valid.csv new file mode 100644 index 0000000000..910be07975 --- /dev/null +++ b/test/fixtures/files/domain_transfer_valid.csv @@ -0,0 +1,2 @@ +Domain,Transfer code +hospital.test,23118v2 diff --git a/test/fixtures/files/nameserver_change_invalid.csv b/test/fixtures/files/nameserver_change_invalid.csv new file mode 100644 index 0000000000..c4e2b85153 --- /dev/null +++ b/test/fixtures/files/nameserver_change_invalid.csv @@ -0,0 +1,3 @@ +WrongHeader +shop.test +hospital.test diff --git a/test/fixtures/files/nameserver_change_valid.csv b/test/fixtures/files/nameserver_change_valid.csv new file mode 100644 index 0000000000..3ff8ebfa21 --- /dev/null +++ b/test/fixtures/files/nameserver_change_valid.csv @@ -0,0 +1,2 @@ +Domain +shop.test diff --git a/test/integration/api/domain_transfers_test.rb b/test/integration/api/domain_transfers_test.rb index c56417f4dc..4a036b4cb4 100644 --- a/test/integration/api/domain_transfers_test.rb +++ b/test/integration/api/domain_transfers_test.rb @@ -69,15 +69,19 @@ def test_bulk_transfer_if_domain_has_update_prohibited_status post '/repp/v1/domains/transfer', params: request_params, as: :json, headers: { 'HTTP_AUTHORIZATION' => http_auth_key } - assert_response :ok - assert_equal ({ code: 1000, - message: 'Command completed successfully', - data: { success: [], - failed: [{ type: "domain_transfer", - domain_name: "shop.test", - errors: {:code=>"2304", :msg=>"Object status prohibits operation"} }], - }}), - JSON.parse(response.body, symbolize_names: true) + assert_response :bad_request + json = JSON.parse(response.body, symbolize_names: true) + + assert_equal 2304, json[:code] + assert_equal 'All 1 transfers failed: 1 domain prohibited from transfer', json[:message] + assert_equal [], json[:data][:success] + assert_equal 1, json[:data][:failed].size + + failed_transfer = json[:data][:failed][0] + assert_equal 'domain_transfer', failed_transfer[:type] + assert_equal 'shop.test', failed_transfer[:domain_name] + assert_equal '2304', failed_transfer[:error_code] + assert_equal 'Object status prohibits operation', failed_transfer[:error_message] end private diff --git a/test/integration/repp/v1/domains/nameservers_test.rb b/test/integration/repp/v1/domains/nameservers_test.rb index 3ff85260ee..d41ffdaf77 100644 --- a/test/integration/repp/v1/domains/nameservers_test.rb +++ b/test/integration/repp/v1/domains/nameservers_test.rb @@ -109,4 +109,33 @@ def test_returns_error_when_ns_count_too_low assert_equal 'Data management policy violation; Nameserver count must be between 2-11 for active ' \ 'domains [nameservers]', json[:message] end + + def test_bulk_update_nameservers_with_valid_csv + csv_file = fixture_file_upload('files/nameserver_change_valid.csv', 'text/csv') + + post "/repp/v1/domains/nameservers/bulk", headers: @auth_headers, + params: { csv_file: csv_file, new_hostname: 'ns1.newserver.ee' } + 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 1, json[:data][:success].length + assert_equal @domain.name, json[:data][:success][0][:domain_name] + end + + def test_returns_error_with_invalid_csv_headers_bulk + csv_file = fixture_file_upload('files/nameserver_change_invalid.csv', 'text/csv') + + post "/repp/v1/domains/nameservers/bulk", headers: @auth_headers, + params: { csv_file: csv_file, new_hostname: 'ns1.newserver.ee' } + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2304, json[:code] + assert_includes json[:message], 'changes failed' + assert_equal 1, json[:data][:failed].length + assert_equal 'csv_error', json[:data][:failed][0][:type] + assert_includes json[:data][:failed][0][:message], 'CSV file is empty or missing required header' + end end diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb index fdcbe41d77..f74840643f 100644 --- a/test/integration/repp/v1/domains/transfer_test.rb +++ b/test/integration/repp/v1/domains/transfer_test.rb @@ -77,11 +77,11 @@ def test_does_not_transfer_domain_if_not_transferable 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_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'All 1 transfers failed: 1 domain prohibited from transfer', json[:message] - assert_equal 'Object status prohibits operation', json[:data][:failed][0][:errors][:msg] + assert_equal 'Object status prohibits operation', json[:data][:failed][0][:error_message] @domain.reload @@ -99,11 +99,11 @@ def test_does_not_transfer_domain_with_invalid_auth_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_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'All 1 transfers failed: 1 domain with invalid transfer code', json[:message] - assert_equal "Invalid authorization information", json[:data][:failed][0][:errors][:msg] + assert_equal "Invalid authorization information", json[:data][:failed][0][:error_message] end def test_does_not_transfer_domain_to_same_registrar @@ -120,11 +120,11 @@ def test_does_not_transfer_domain_to_same_registrar 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_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'All 1 transfers failed: 1 domain already belong to your registrar', json[:message] - assert_equal 'Domain already belongs to the querying registrar', json[:data][:failed][0][:errors][:msg] + assert_equal 'Domain already belongs to the querying registrar', json[:data][:failed][0][:error_message] @domain.reload @@ -145,11 +145,11 @@ def test_does_not_transfer_domain_if_discarded 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_response :bad_request + assert_equal 2304, json[:code] + assert_equal 'All 1 transfers failed: 1 domain not eligible for transfer', json[:message] - assert_equal 'Object is not eligible for transfer', json[:data][:failed][0][:errors][:msg] + assert_equal 'Object is not eligible for transfer', json[:data][:failed][0][:error_message] @domain.reload @@ -171,4 +171,32 @@ def test_returns_error_response_if_throttled ENV["shunter_default_threshold"] = '10000' ENV["shunter_enabled"] = 'false' end + + def test_transfers_domains_with_valid_csv + csv_file = fixture_file_upload('files/domain_transfer_valid.csv', 'text/csv') + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: { csv_file: csv_file } + json = JSON.parse(response.body, symbolize_names: true) + + # Поскольку домен hospital.test принадлежит другому регистратору, трансфер должен быть успешным + assert_response :ok + assert_equal 1000, json[:code] + assert_equal 'Command completed successfully', json[:message] + assert_equal 1, json[:data][:success].length + assert_equal 'hospital.test', json[:data][:success][0][:domain_name] + end + + def test_returns_error_with_invalid_csv_headers + csv_file = fixture_file_upload('files/domain_transfer_invalid.csv', 'text/csv') + + post "/repp/v1/domains/transfer", headers: @auth_headers, params: { csv_file: csv_file } + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 2304, json[:code] + assert_includes json[:message], 'transfers failed' + assert_equal 1, json[:data][:failed].length + assert_equal 'csv_error', json[:data][:failed][0][:type] + assert_includes json[:data][:failed][0][:message], 'CSV file is empty or missing required headers' + end end