From 48a7e29159019190bbc5cbf633803f49a0522d42 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 5 Sep 2025 12:38:07 +0300 Subject: [PATCH 1/6] feat: Add CSV support for bulk domain operations in REPP API - Add CSV file upload support for domain transfers (/repp/v1/domains/transfer) - Add CSV file upload support for bulk nameserver changes (/repp/v1/domains/nameservers/bulk) - Implement parse_transfer_csv() and parse_nameserver_csv() methods - Add proper strong parameters handling for file uploads - Add bulk nameserver update route and controller action - Fix nameserver replacement logic to maintain minimum count requirement - Add comprehensive test coverage with valid/invalid CSV fixtures - Support both JSON and CSV input formats for bulk operations CSV formats: - Domain transfers: Domain,Transfer code - Nameserver changes: Domain (+ required new_hostname parameter) --- .../repp/v1/domains/nameservers_controller.rb | 149 ++++++++++++++++++ app/controllers/repp/v1/domains_controller.rb | 54 ++++++- config/routes.rb | 1 + .../files/domain_transfer_invalid.csv | 3 + test/fixtures/files/domain_transfer_valid.csv | 2 + .../files/nameserver_change_invalid.csv | 3 + .../files/nameserver_change_valid.csv | 2 + .../repp/v1/domains/nameservers_test.rb | 28 ++++ .../repp/v1/domains/transfer_test.rb | 26 +++ 9 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/files/domain_transfer_invalid.csv create mode 100644 test/fixtures/files/domain_transfer_valid.csv create mode 100644 test/fixtures/files/nameserver_change_invalid.csv create mode 100644 test/fixtures/files/nameserver_change_valid.csv diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index 8ee1cba357..59abdd7391 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 @@ -49,6 +50,39 @@ 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! :update, Epp::Domain + @errors ||= [] + @successful = [] + + # Get permitted parameters + permitted_params = bulk_params + + if permitted_params[:csv_file].present? + # Handle CSV file upload + nameserver_changes = parse_nameserver_csv(permitted_params[:csv_file]) + else + # Handle JSON data + nameserver_changes = permitted_params[:nameserver_changes] + end + + nameserver_changes.each do |change| + process_nameserver_change(change) + end + + render_success(data: { success: @successful, failed: @errors }) + end + private def set_nameserver @@ -58,6 +92,121 @@ def set_nameserver def nameserver_params params.permit(:domain_id, nameservers: [[:hostname, :action, { ipv4: [], ipv6: [] }]]) end + + def bulk_params + if params[:csv_file].present? + # Allow csv_file and new_hostname parameters for CSV upload + params.permit(:csv_file, :new_hostname, ipv4: [], ipv6: []) + else + # Allow JSON data parameters + params.require(:data).require(:nameserver_changes) + params.require(:data).permit(nameserver_changes: [%i[domain_name new_hostname], { ipv4: [], ipv6: [] }]) + end + end + + # Parse CSV file for nameserver changes + # Expected CSV format: Domain + def parse_nameserver_csv(csv_file) + nameserver_changes = [] + permitted_params = bulk_params + + begin + CSV.foreach(csv_file.path, headers: true) do |row| + next if row['Domain'].blank? + + nameserver_changes << { + domain_name: row['Domain'].strip, + new_hostname: permitted_params[:new_hostname] || '', + ipv4: permitted_params[:ipv4] || [], + ipv6: permitted_params[:ipv6] || [] + } + 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 + + # Validate CSV headers and required params + if nameserver_changes.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required header: Domain' } + elsif permitted_params[:new_hostname].blank? + @errors << { type: 'csv_error', message: 'new_hostname parameter is required when using CSV' } + end + + nameserver_changes + end + + # Process individual nameserver change + def process_nameserver_change(change) + begin + domain = Epp::Domain.find_by!('name = ? OR name_puny = ?', + change[:domain_name], change[:domain_name]) + + # Check if user has permission for this domain + unless domain.registrar == current_user.registrar + @errors << { + type: 'nameserver_change', + domain_name: change[:domain_name], + errors: { code: 2201, msg: 'Authorization error' } + } + return + end + + # Replace first nameserver or add new one if hostname doesn't exist + existing_hostnames = domain.nameservers.map(&:hostname) + + if existing_hostnames.include?(change[:new_hostname]) + # Hostname already exists, no changes needed + @successful << { type: 'nameserver_change', domain_name: domain.name } + return + end + + nameserver_actions = [] + + if domain.nameservers.count > 0 + # Replace first nameserver with new one + first_ns = domain.nameservers.first + nameserver_actions << { hostname: first_ns.hostname, action: 'rem' } + end + + # Add new nameserver + 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, current_user) + + if action.call + @successful << { type: 'nameserver_change', domain_name: domain.name } + else + @errors << { + type: 'nameserver_change', + domain_name: domain.name, + errors: domain.errors.where(:epp_errors).first&.options || domain.errors.full_messages + } + end + rescue ActiveRecord::RecordNotFound + @errors << { + type: 'nameserver_change', + domain_name: change[:domain_name], + errors: { code: 2303, msg: 'Domain not found' } + } + rescue StandardError => e + @errors << { + type: 'nameserver_change', + domain_name: change[:domain_name], + errors: { message: e.message } + } + end + end end end end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index ed86638e43..42dca0aafd 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,12 +125,27 @@ 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| + + if params[:csv_file].present? + # Handle CSV file upload + domain_transfers = parse_transfer_csv(params[:csv_file]) + else + # Handle JSON data + domain_transfers = transfer_params[:domain_transfers] + end + + domain_transfers.each do |transfer| initiate_transfer(transfer) end @@ -179,6 +195,10 @@ def initiate_transfer(transfer) end def transfer_params + # Allow csv_file parameter + params.permit(:csv_file) + return {} if params[:csv_file].present? + params.require(:data).require(:domain_transfers) params.require(:data).permit(domain_transfers: [%i[domain_name transfer_code]]) end @@ -282,6 +302,36 @@ def domain_params dnskeys_attributes: [%i[flags alg protocol public_key]], delete: [:verified]) end + + # Parse CSV file for domain transfers + # Expected CSV format: Domain, Transfer code + 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 + + # Validate CSV headers + if domain_transfers.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, Transfer code' } + end + + domain_transfers + end end end end diff --git a/config/routes.rb b/config/routes.rb index 50e72511b6..923d7e1296 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,6 +167,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/repp/v1/domains/nameservers_test.rb b/test/integration/repp/v1/domains/nameservers_test.rb index 3ff85260ee..2f7291d104 100644 --- a/test/integration/repp/v1/domains/nameservers_test.rb +++ b/test/integration/repp/v1/domains/nameservers_test.rb @@ -109,4 +109,32 @@ 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 :ok + assert_equal 1000, json[:code] + 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..164e9ce66b 100644 --- a/test/integration/repp/v1/domains/transfer_test.rb +++ b/test/integration/repp/v1/domains/transfer_test.rb @@ -171,4 +171,30 @@ 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) + + 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 + 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 :ok + assert_equal 1000, json[:code] + 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 From 63e4bd7ec6d562d000bf0ae8ca21f5c3d2952a20 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 5 Sep 2025 13:53:13 +0300 Subject: [PATCH 2/6] refactor --- .../repp/v1/domains/nameservers_controller.rb | 37 +++++-------------- app/controllers/repp/v1/domains_controller.rb | 15 ++------ 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index 59abdd7391..b8e62d44a7 100644 --- a/app/controllers/repp/v1/domains/nameservers_controller.rb +++ b/app/controllers/repp/v1/domains/nameservers_controller.rb @@ -61,24 +61,17 @@ def destroy end end def bulk_update - authorize! :update, Epp::Domain + authorize! :manage, :repp @errors ||= [] @successful = [] - # Get permitted parameters - permitted_params = bulk_params - - if permitted_params[:csv_file].present? - # Handle CSV file upload - nameserver_changes = parse_nameserver_csv(permitted_params[:csv_file]) + nameserver_changes = if bulk_params[:csv_file].present? + parse_nameserver_csv(bulk_params[:csv_file]) else - # Handle JSON data - nameserver_changes = permitted_params[:nameserver_changes] + bulk_params[:nameserver_changes] end - nameserver_changes.each do |change| - process_nameserver_change(change) - end + nameserver_changes.each { |change| process_nameserver_change(change) } render_success(data: { success: @successful, failed: @errors }) end @@ -95,20 +88,15 @@ def nameserver_params def bulk_params if params[:csv_file].present? - # Allow csv_file and new_hostname parameters for CSV upload params.permit(:csv_file, :new_hostname, ipv4: [], ipv6: []) else - # Allow JSON data parameters params.require(:data).require(:nameserver_changes) params.require(:data).permit(nameserver_changes: [%i[domain_name new_hostname], { ipv4: [], ipv6: [] }]) end end - # Parse CSV file for nameserver changes - # Expected CSV format: Domain def parse_nameserver_csv(csv_file) nameserver_changes = [] - permitted_params = bulk_params begin CSV.foreach(csv_file.path, headers: true) do |row| @@ -116,9 +104,9 @@ def parse_nameserver_csv(csv_file) nameserver_changes << { domain_name: row['Domain'].strip, - new_hostname: permitted_params[:new_hostname] || '', - ipv4: permitted_params[:ipv4] || [], - ipv6: permitted_params[:ipv6] || [] + new_hostname: bulk_params[:new_hostname] || '', + ipv4: bulk_params[:ipv4] || [], + ipv6: bulk_params[:ipv6] || [] } end rescue CSV::MalformedCSVError => e @@ -129,23 +117,20 @@ def parse_nameserver_csv(csv_file) return [] end - # Validate CSV headers and required params if nameserver_changes.empty? @errors << { type: 'csv_error', message: 'CSV file is empty or missing required header: Domain' } - elsif permitted_params[:new_hostname].blank? + elsif bulk_params[:new_hostname].blank? @errors << { type: 'csv_error', message: 'new_hostname parameter is required when using CSV' } end nameserver_changes end - # Process individual nameserver change def process_nameserver_change(change) begin domain = Epp::Domain.find_by!('name = ? OR name_puny = ?', change[:domain_name], change[:domain_name]) - # Check if user has permission for this domain unless domain.registrar == current_user.registrar @errors << { type: 'nameserver_change', @@ -155,11 +140,9 @@ def process_nameserver_change(change) return end - # Replace first nameserver or add new one if hostname doesn't exist existing_hostnames = domain.nameservers.map(&:hostname) if existing_hostnames.include?(change[:new_hostname]) - # Hostname already exists, no changes needed @successful << { type: 'nameserver_change', domain_name: domain.name } return end @@ -167,12 +150,10 @@ def process_nameserver_change(change) nameserver_actions = [] if domain.nameservers.count > 0 - # Replace first nameserver with new one first_ns = domain.nameservers.first nameserver_actions << { hostname: first_ns.hostname, action: 'rem' } end - # Add new nameserver nameserver_actions << { hostname: change[:new_hostname], action: 'add', diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index 42dca0aafd..bafd9491ec 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -137,17 +137,13 @@ def transfer @errors ||= [] @successful = [] - if params[:csv_file].present? - # Handle CSV file upload - domain_transfers = parse_transfer_csv(params[:csv_file]) + domain_transfers = if params[:csv_file].present? + parse_transfer_csv(params[:csv_file]) else - # Handle JSON data - domain_transfers = transfer_params[:domain_transfers] + transfer_params[:domain_transfers] end - domain_transfers.each do |transfer| - initiate_transfer(transfer) - end + domain_transfers.each { |transfer| initiate_transfer(transfer) } render_success(data: { success: @successful, failed: @errors }) end @@ -303,8 +299,6 @@ def domain_params delete: [:verified]) end - # Parse CSV file for domain transfers - # Expected CSV format: Domain, Transfer code def parse_transfer_csv(csv_file) domain_transfers = [] @@ -325,7 +319,6 @@ def parse_transfer_csv(csv_file) return [] end - # Validate CSV headers if domain_transfers.empty? @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, Transfer code' } end From 15a0a0e2ff5daf0f170876f58ab0d02ecd28c2c5 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 12 Sep 2025 14:27:08 +0300 Subject: [PATCH 3/6] fixed render issues --- .../repp/v1/domains/nameservers_controller.rb | 237 +++++++++++++++-- app/controllers/repp/v1/domains_controller.rb | 244 ++++++++++++++++-- 2 files changed, 443 insertions(+), 38 deletions(-) diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index b8e62d44a7..f260b9958a 100644 --- a/app/controllers/repp/v1/domains/nameservers_controller.rb +++ b/app/controllers/repp/v1/domains/nameservers_controller.rb @@ -61,19 +61,101 @@ def destroy end end def bulk_update - authorize! :manage, :repp - @errors ||= [] - @successful = [] + Rails.logger.info "[REPP Nameservers] Starting bulk nameserver update" + Rails.logger.info "[REPP Nameservers] Request params: #{params.inspect}" + Rails.logger.info "[REPP Nameservers] Content-Type: #{request.content_type}" + + begin + authorize! :manage, :repp + Rails.logger.info "[REPP Nameservers] Authorization successful" + + @errors ||= [] + @successful = [] - nameserver_changes = if bulk_params[:csv_file].present? - parse_nameserver_csv(bulk_params[:csv_file]) - else - bulk_params[:nameserver_changes] - end + nameserver_changes = if is_csv_request? + Rails.logger.info "[REPP Nameservers] Processing CSV data from raw body" + parse_nameserver_csv_from_body(request.raw_post) + elsif bulk_params[:csv_file].present? + Rails.logger.info "[REPP Nameservers] Processing CSV file upload" + parse_nameserver_csv(bulk_params[:csv_file]) + else + Rails.logger.info "[REPP Nameservers] Processing JSON data" + bulk_params[:nameserver_changes] + end + + Rails.logger.info "[REPP Nameservers] Nameserver changes to process: #{nameserver_changes.inspect}" - nameserver_changes.each { |change| process_nameserver_change(change) } + nameserver_changes.each { |change| process_nameserver_change(change) } - render_success(data: { success: @successful, failed: @errors }) + Rails.logger.info "[REPP Nameservers] Processing complete. Successful: #{@successful.count}, Failed: #{@errors.count}" + + # Применяем ту же логику ответов что и в transfer + if @errors.any? && @successful.empty? + # Все изменения провалились + Rails.logger.error "[REPP Nameservers] All nameserver changes failed" + + error_summary = analyze_nameserver_errors(@errors) + message = build_nameserver_error_message(error_summary, nameserver_changes.count) + + @response = { + code: 2304, + message: message, + data: { + success: @successful, + failed: @errors, + summary: { + total: nameserver_changes.count, + successful: @successful.count, + failed: @errors.count, + error_breakdown: error_summary + } + } + } + render(json: @response, status: :bad_request) + elsif @errors.any? && @successful.any? + # Частичный успех + Rails.logger.warn "[REPP Nameservers] Partial success: #{@successful.count} succeeded, #{@errors.count} failed" + + 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 = { + code: 2400, + message: message, + data: { + success: @successful, + failed: @errors, + summary: { + total: nameserver_changes.count, + successful: @successful.count, + failed: @errors.count, + error_breakdown: error_summary + } + } + } + render(json: @response, status: :multi_status) + else + # Все успешно + Rails.logger.info "[REPP Nameservers] All nameserver changes successful" + render_success(data: { + success: @successful, + failed: @errors, + summary: { + total: nameserver_changes.count, + successful: @successful.count, + failed: @errors.count + } + }) + end + + rescue StandardError => e + Rails.logger.error "[REPP Nameservers] Exception occurred: #{e.class} - #{e.message}" + Rails.logger.error "[REPP Nameservers] Backtrace: #{e.backtrace.join("\n")}" + + @response = { code: 2304, message: "Nameserver bulk update failed: #{e.message}", data: {} } + render(json: @response, status: :bad_request) + end end private @@ -127,22 +209,31 @@ def parse_nameserver_csv(csv_file) end def process_nameserver_change(change) + Rails.logger.info "[REPP Nameservers] Processing domain: #{change[:domain_name]}" + begin domain = Epp::Domain.find_by!('name = ? OR name_puny = ?', change[:domain_name], change[:domain_name]) + Rails.logger.info "[REPP Nameservers] Domain found: #{domain.name}" unless domain.registrar == current_user.registrar - @errors << { - type: 'nameserver_change', + Rails.logger.warn "[REPP Nameservers] Authorization failed for #{domain.name}" + error_info = { + type: 'nameserver_change', domain_name: change[:domain_name], - errors: { code: 2201, msg: 'Authorization error' } + error_code: '2201', + error_message: 'Authorization error', + details: { code: '2201', msg: 'Authorization error' } } + @errors << error_info return end existing_hostnames = domain.nameservers.map(&:hostname) + Rails.logger.info "[REPP Nameservers] Existing nameservers: #{existing_hostnames}" if existing_hostnames.include?(change[:new_hostname]) + Rails.logger.info "[REPP Nameservers] Nameserver already exists, marking as successful" @successful << { type: 'nameserver_change', domain_name: domain.name } return end @@ -152,6 +243,7 @@ def process_nameserver_change(change) if domain.nameservers.count > 0 first_ns = domain.nameservers.first nameserver_actions << { hostname: first_ns.hostname, action: 'rem' } + Rails.logger.info "[REPP Nameservers] Removing old nameserver: #{first_ns.hostname}" end nameserver_actions << { @@ -160,33 +252,132 @@ def process_nameserver_change(change) ipv4: change[:ipv4] || [], ipv6: change[:ipv6] || [] } + Rails.logger.info "[REPP Nameservers] Adding new nameserver: #{change[:new_hostname]}" nameserver_params = { nameservers: nameserver_actions } - action = Actions::DomainUpdate.new(domain, nameserver_params, current_user) + action = Actions::DomainUpdate.new(domain, nameserver_params, false) if action.call + Rails.logger.info "[REPP Nameservers] Nameserver change successful for #{domain.name}" @successful << { type: 'nameserver_change', domain_name: domain.name } else - @errors << { - type: 'nameserver_change', + Rails.logger.info "[REPP Nameservers] Nameserver change failed for #{domain.name}" + Rails.logger.info "[REPP Nameservers] Domain errors: #{domain.errors.inspect}" + + 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, - errors: domain.errors.where(:epp_errors).first&.options || domain.errors.full_messages + error_code: error_details[:code] || 'UNKNOWN', + error_message: error_details[:msg] || error_details[:message] || 'Unknown error', + details: error_details } + + @errors << error_info end rescue ActiveRecord::RecordNotFound - @errors << { - type: 'nameserver_change', + Rails.logger.warn "[REPP Nameservers] Domain not found: #{change[:domain_name]}" + error_info = { + type: 'nameserver_change', domain_name: change[:domain_name], - errors: { code: 2303, msg: 'Domain not found' } + error_code: '2303', + error_message: 'Domain not found', + details: { code: '2303', msg: 'Domain not found' } } + @errors << error_info rescue StandardError => e - @errors << { - type: 'nameserver_change', + Rails.logger.error "[REPP Nameservers] Unexpected error for #{change[:domain_name]}: #{e.message}" + error_info = { + type: 'nameserver_change', domain_name: change[:domain_name], - errors: { message: e.message } + error_code: 'UNKNOWN', + error_message: e.message, + details: { message: e.message } + } + @errors << error_info + end + end + + def is_csv_request? + request.content_type&.include?('text/csv') || request.content_type&.include?('application/csv') + end + + def parse_nameserver_csv_from_body(csv_data) + nameserver_changes = [] + + begin + 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 + 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 nameserver_changes.empty? + @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, New_Nameserver' } + end + + Rails.logger.info "[REPP Nameservers] Parsed #{nameserver_changes.count} nameserver changes from CSV" + nameserver_changes + 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 '2303' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} not found" + when '2201' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} unauthorized" + when '2304' + messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} prohibited from changes" + when '2306' + 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 diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index bafd9491ec..579c084963 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -133,19 +133,101 @@ def transfer_info end end def transfer - authorize! :transfer, Epp::Domain - @errors ||= [] - @successful = [] - - domain_transfers = if params[:csv_file].present? - parse_transfer_csv(params[:csv_file]) - else - transfer_params[:domain_transfers] - end + 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 + Rails.logger.info "[REPP Transfer] Authorization successful" + + @errors ||= [] + @successful = [] + + domain_transfers = if is_csv_request? + Rails.logger.info "[REPP Transfer] Processing CSV data from raw body" + parse_transfer_csv_from_body(request.raw_post) + else + Rails.logger.info "[REPP Transfer] Processing JSON data" + Rails.logger.info "[REPP Transfer] transfer_params call starting" + transfer_params[:domain_transfers] + end + + Rails.logger.info "[REPP Transfer] Domain transfers to process: #{domain_transfers.inspect}" - domain_transfers.each { |transfer| initiate_transfer(transfer) } + domain_transfers.each { |transfer| initiate_transfer(transfer) } - render_success(data: { success: @successful, failed: @errors }) + Rails.logger.info "[REPP Transfer] Processing complete. Successful: #{@successful.count}, Failed: #{@errors.count}" + + # Определяем статус ответа на основе результатов + if @errors.any? && @successful.empty? + # Все трансферы провалились - полная ошибка + Rails.logger.error "[REPP Transfer] All transfers failed" + + # Анализируем типы ошибок для более информативного сообщения + 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? + # Частичный успех - некоторые прошли, некоторые нет + Rails.logger.warn "[REPP Transfer] Partial success: #{@successful.count} succeeded, #{@errors.count} failed" + + 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 + # Все успешно + Rails.logger.info "[REPP Transfer] All transfers successful" + render_success(data: { + success: @successful, + failed: @errors, + summary: { + total: domain_transfers.count, + successful: @successful.count, + failed: @errors.count + } + }) + end + + rescue StandardError => e + Rails.logger.error "[REPP Transfer] Exception occurred: #{e.class} - #{e.message}" + Rails.logger.error "[REPP Transfer] Backtrace: #{e.backtrace.join("\n")}" + + @response = { code: 2304, message: "Transfer failed: #{e.message}", data: {} } + render(json: @response, status: :bad_request) + end end api :DELETE, '/repp/v1/domains/:domain_name' @@ -178,25 +260,78 @@ def serialized_domains(domains) end def initiate_transfer(transfer) + Rails.logger.info "[REPP Transfer] Processing domain: #{transfer[:domain_name]}" + domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name]) + Rails.logger.info "[REPP Transfer] Domain persisted?: #{domain.persisted?}" + action = Actions::DomainTransfer.new(domain, transfer[:transfer_code], current_user.registrar) if action.call + Rails.logger.info "[REPP Transfer] Transfer successful for #{domain.name}" @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 } + Rails.logger.info "[REPP Transfer] Transfer failed for #{domain.name}" + Rails.logger.info "[REPP Transfer] Domain errors: #{domain.errors.inspect}" + Rails.logger.info "[REPP Transfer] EPP errors: #{domain.errors.where(:epp_errors).inspect}" + + 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 + Rails.logger.info "[REPP Transfer] transfer_params called" + Rails.logger.info "[REPP Transfer] Checking for csv_file param: #{params[:csv_file].present?}" + # Allow csv_file parameter params.permit(:csv_file) return {} if params[:csv_file].present? - params.require(:data).require(:domain_transfers) - params.require(:data).permit(domain_transfers: [%i[domain_name transfer_code]]) + Rails.logger.info "[REPP Transfer] Requiring data param" + Rails.logger.info "[REPP Transfer] params[:data] present: #{params[:data].present?}" + Rails.logger.info "[REPP Transfer] params[:data] content: #{params[:data].inspect}" + + begin + # Проверяем наличие data и domain_transfers + data_params = params.require(:data) + + unless data_params.key?(:domain_transfers) + Rails.logger.error "[REPP Transfer] domain_transfers key missing" + raise ActionController::ParameterMissing.new(:domain_transfers) + end + + domain_transfers_array = data_params[:domain_transfers] + Rails.logger.info "[REPP Transfer] domain_transfers array: #{domain_transfers_array.inspect}" + + if domain_transfers_array.blank? || !domain_transfers_array.is_a?(Array) + Rails.logger.error "[REPP Transfer] domain_transfers is empty or not an 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 @@ -299,6 +434,38 @@ def domain_params 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 = [] @@ -325,6 +492,53 @@ def parse_transfer_csv(csv_file) 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 From 7f8f964500155e104219660f286611126f50b7cb Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 12 Sep 2025 15:04:19 +0300 Subject: [PATCH 4/6] fixed tests --- app/controllers/repp/v1/domains_controller.rb | 3 ++ test/integration/api/domain_transfers_test.rb | 22 +++++----- .../repp/v1/domains/nameservers_test.rb | 5 ++- .../repp/v1/domains/transfer_test.rb | 40 ++++++++++--------- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index 579c084963..7e5df0f258 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -148,6 +148,9 @@ def transfer domain_transfers = if is_csv_request? Rails.logger.info "[REPP Transfer] Processing CSV data from raw body" parse_transfer_csv_from_body(request.raw_post) + elsif params[:csv_file].present? + Rails.logger.info "[REPP Transfer] Processing CSV file upload" + parse_transfer_csv(params[:csv_file]) else Rails.logger.info "[REPP Transfer] Processing JSON data" Rails.logger.info "[REPP Transfer] transfer_params call starting" 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 2f7291d104..d41ffdaf77 100644 --- a/test/integration/repp/v1/domains/nameservers_test.rb +++ b/test/integration/repp/v1/domains/nameservers_test.rb @@ -131,8 +131,9 @@ def test_returns_error_with_invalid_csv_headers_bulk 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_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' diff --git a/test/integration/repp/v1/domains/transfer_test.rb b/test/integration/repp/v1/domains/transfer_test.rb index 164e9ce66b..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 @@ -178,11 +178,12 @@ def test_transfers_domains_with_valid_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 @domain.name, json[:data][:success][0][:domain_name] + assert_equal 'hospital.test', json[:data][:success][0][:domain_name] end def test_returns_error_with_invalid_csv_headers @@ -191,8 +192,9 @@ def test_returns_error_with_invalid_csv_headers post "/repp/v1/domains/transfer", headers: @auth_headers, params: { csv_file: csv_file } json = JSON.parse(response.body, symbolize_names: true) - assert_response :ok - assert_equal 1000, json[:code] + 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' From f5a8b686a6ea34f5a7eb7a02ecc36da0c1146130 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 12 Sep 2025 15:14:20 +0300 Subject: [PATCH 5/6] cleanup from logs and comments --- .../repp/v1/domains/nameservers_controller.rb | 37 +------------------ app/controllers/repp/v1/domains_controller.rb | 37 ------------------- 2 files changed, 1 insertion(+), 73 deletions(-) diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index f260b9958a..baadc2d20d 100644 --- a/app/controllers/repp/v1/domains/nameservers_controller.rb +++ b/app/controllers/repp/v1/domains/nameservers_controller.rb @@ -61,39 +61,23 @@ def destroy end end def bulk_update - Rails.logger.info "[REPP Nameservers] Starting bulk nameserver update" - Rails.logger.info "[REPP Nameservers] Request params: #{params.inspect}" - Rails.logger.info "[REPP Nameservers] Content-Type: #{request.content_type}" - begin authorize! :manage, :repp - Rails.logger.info "[REPP Nameservers] Authorization successful" @errors ||= [] @successful = [] nameserver_changes = if is_csv_request? - Rails.logger.info "[REPP Nameservers] Processing CSV data from raw body" parse_nameserver_csv_from_body(request.raw_post) elsif bulk_params[:csv_file].present? - Rails.logger.info "[REPP Nameservers] Processing CSV file upload" parse_nameserver_csv(bulk_params[:csv_file]) else - Rails.logger.info "[REPP Nameservers] Processing JSON data" bulk_params[:nameserver_changes] end - Rails.logger.info "[REPP Nameservers] Nameserver changes to process: #{nameserver_changes.inspect}" - nameserver_changes.each { |change| process_nameserver_change(change) } - Rails.logger.info "[REPP Nameservers] Processing complete. Successful: #{@successful.count}, Failed: #{@errors.count}" - - # Применяем ту же логику ответов что и в transfer if @errors.any? && @successful.empty? - # Все изменения провалились - Rails.logger.error "[REPP Nameservers] All nameserver changes failed" - error_summary = analyze_nameserver_errors(@errors) message = build_nameserver_error_message(error_summary, nameserver_changes.count) @@ -113,9 +97,6 @@ def bulk_update } render(json: @response, status: :bad_request) elsif @errors.any? && @successful.any? - # Частичный успех - Rails.logger.warn "[REPP Nameservers] Partial success: #{@successful.count} succeeded, #{@errors.count} failed" - 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) @@ -136,8 +117,6 @@ def bulk_update } render(json: @response, status: :multi_status) else - # Все успешно - Rails.logger.info "[REPP Nameservers] All nameserver changes successful" render_success(data: { success: @successful, failed: @errors, @@ -209,15 +188,12 @@ def parse_nameserver_csv(csv_file) end def process_nameserver_change(change) - Rails.logger.info "[REPP Nameservers] Processing domain: #{change[:domain_name]}" begin domain = Epp::Domain.find_by!('name = ? OR name_puny = ?', change[:domain_name], change[:domain_name]) - Rails.logger.info "[REPP Nameservers] Domain found: #{domain.name}" unless domain.registrar == current_user.registrar - Rails.logger.warn "[REPP Nameservers] Authorization failed for #{domain.name}" error_info = { type: 'nameserver_change', domain_name: change[:domain_name], @@ -230,10 +206,8 @@ def process_nameserver_change(change) end existing_hostnames = domain.nameservers.map(&:hostname) - Rails.logger.info "[REPP Nameservers] Existing nameservers: #{existing_hostnames}" if existing_hostnames.include?(change[:new_hostname]) - Rails.logger.info "[REPP Nameservers] Nameserver already exists, marking as successful" @successful << { type: 'nameserver_change', domain_name: domain.name } return end @@ -243,7 +217,6 @@ def process_nameserver_change(change) if domain.nameservers.count > 0 first_ns = domain.nameservers.first nameserver_actions << { hostname: first_ns.hostname, action: 'rem' } - Rails.logger.info "[REPP Nameservers] Removing old nameserver: #{first_ns.hostname}" end nameserver_actions << { @@ -252,19 +225,14 @@ def process_nameserver_change(change) ipv4: change[:ipv4] || [], ipv6: change[:ipv6] || [] } - Rails.logger.info "[REPP Nameservers] Adding new nameserver: #{change[:new_hostname]}" nameserver_params = { nameservers: nameserver_actions } action = Actions::DomainUpdate.new(domain, nameserver_params, false) if action.call - Rails.logger.info "[REPP Nameservers] Nameserver change successful for #{domain.name}" @successful << { type: 'nameserver_change', domain_name: domain.name } - else - Rails.logger.info "[REPP Nameservers] Nameserver change failed for #{domain.name}" - Rails.logger.info "[REPP Nameservers] Domain errors: #{domain.errors.inspect}" - + else epp_error = domain.errors.where(:epp_errors).first error_details = epp_error&.options || { message: domain.errors.full_messages.join(', ') } @@ -279,7 +247,6 @@ def process_nameserver_change(change) @errors << error_info end rescue ActiveRecord::RecordNotFound - Rails.logger.warn "[REPP Nameservers] Domain not found: #{change[:domain_name]}" error_info = { type: 'nameserver_change', domain_name: change[:domain_name], @@ -289,7 +256,6 @@ def process_nameserver_change(change) } @errors << error_info rescue StandardError => e - Rails.logger.error "[REPP Nameservers] Unexpected error for #{change[:domain_name]}: #{e.message}" error_info = { type: 'nameserver_change', domain_name: change[:domain_name], @@ -331,7 +297,6 @@ def parse_nameserver_csv_from_body(csv_data) @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, New_Nameserver' } end - Rails.logger.info "[REPP Nameservers] Parsed #{nameserver_changes.count} nameserver changes from CSV" nameserver_changes end diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index 7e5df0f258..9267ecd3f4 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -140,35 +140,23 @@ def transfer begin authorize! :transfer, Epp::Domain - Rails.logger.info "[REPP Transfer] Authorization successful" @errors ||= [] @successful = [] domain_transfers = if is_csv_request? - Rails.logger.info "[REPP Transfer] Processing CSV data from raw body" parse_transfer_csv_from_body(request.raw_post) elsif params[:csv_file].present? - Rails.logger.info "[REPP Transfer] Processing CSV file upload" parse_transfer_csv(params[:csv_file]) else - Rails.logger.info "[REPP Transfer] Processing JSON data" - Rails.logger.info "[REPP Transfer] transfer_params call starting" transfer_params[:domain_transfers] end - Rails.logger.info "[REPP Transfer] Domain transfers to process: #{domain_transfers.inspect}" domain_transfers.each { |transfer| initiate_transfer(transfer) } - Rails.logger.info "[REPP Transfer] Processing complete. Successful: #{@successful.count}, Failed: #{@errors.count}" - - # Определяем статус ответа на основе результатов if @errors.any? && @successful.empty? - # Все трансферы провалились - полная ошибка - Rails.logger.error "[REPP Transfer] All transfers failed" - # Анализируем типы ошибок для более информативного сообщения error_summary = analyze_transfer_errors(@errors) message = build_error_message(error_summary, domain_transfers.count) @@ -188,8 +176,6 @@ def transfer } render(json: @response, status: :bad_request) elsif @errors.any? && @successful.any? - # Частичный успех - некоторые прошли, некоторые нет - Rails.logger.warn "[REPP Transfer] Partial success: #{@successful.count} succeeded, #{@errors.count} failed" error_summary = analyze_transfer_errors(@errors) message = "#{@successful.count} domains transferred successfully, #{@errors.count} failed. " + @@ -211,8 +197,6 @@ def transfer } render(json: @response, status: :multi_status) # 207 Multi-Status else - # Все успешно - Rails.logger.info "[REPP Transfer] All transfers successful" render_success(data: { success: @successful, failed: @errors, @@ -225,8 +209,6 @@ def transfer end rescue StandardError => e - Rails.logger.error "[REPP Transfer] Exception occurred: #{e.class} - #{e.message}" - Rails.logger.error "[REPP Transfer] Backtrace: #{e.backtrace.join("\n")}" @response = { code: 2304, message: "Transfer failed: #{e.message}", data: {} } render(json: @response, status: :bad_request) @@ -263,26 +245,19 @@ def serialized_domains(domains) end def initiate_transfer(transfer) - Rails.logger.info "[REPP Transfer] Processing domain: #{transfer[:domain_name]}" domain = Epp::Domain.find_or_initialize_by(name: transfer[:domain_name]) - Rails.logger.info "[REPP Transfer] Domain persisted?: #{domain.persisted?}" action = Actions::DomainTransfer.new(domain, transfer[:transfer_code], current_user.registrar) if action.call - Rails.logger.info "[REPP Transfer] Transfer successful for #{domain.name}" @successful << { type: 'domain_transfer', domain_name: domain.name } else - Rails.logger.info "[REPP Transfer] Transfer failed for #{domain.name}" - Rails.logger.info "[REPP Transfer] Domain errors: #{domain.errors.inspect}" - Rails.logger.info "[REPP Transfer] EPP errors: #{domain.errors.where(:epp_errors).inspect}" 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, @@ -296,37 +271,25 @@ def initiate_transfer(transfer) end def transfer_params - Rails.logger.info "[REPP Transfer] transfer_params called" - Rails.logger.info "[REPP Transfer] Checking for csv_file param: #{params[:csv_file].present?}" - # Allow csv_file parameter params.permit(:csv_file) return {} if params[:csv_file].present? - Rails.logger.info "[REPP Transfer] Requiring data param" - Rails.logger.info "[REPP Transfer] params[:data] present: #{params[:data].present?}" - Rails.logger.info "[REPP Transfer] params[:data] content: #{params[:data].inspect}" begin - # Проверяем наличие data и domain_transfers data_params = params.require(:data) unless data_params.key?(:domain_transfers) - Rails.logger.error "[REPP Transfer] domain_transfers key missing" raise ActionController::ParameterMissing.new(:domain_transfers) end domain_transfers_array = data_params[:domain_transfers] - Rails.logger.info "[REPP Transfer] domain_transfers array: #{domain_transfers_array.inspect}" if domain_transfers_array.blank? || !domain_transfers_array.is_a?(Array) - Rails.logger.error "[REPP Transfer] domain_transfers is empty or not an 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}" From c290587d7ee25db6e9f6f09d26ac739605950d39 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Fri, 12 Sep 2025 16:27:14 +0300 Subject: [PATCH 6/6] refactor --- .../repp/v1/domains/nameservers_controller.rb | 296 +++++++++--------- app/controllers/repp/v1/domains_controller.rb | 2 + 2 files changed, 151 insertions(+), 147 deletions(-) diff --git a/app/controllers/repp/v1/domains/nameservers_controller.rb b/app/controllers/repp/v1/domains/nameservers_controller.rb index baadc2d20d..ac21ebb220 100644 --- a/app/controllers/repp/v1/domains/nameservers_controller.rb +++ b/app/controllers/repp/v1/domains/nameservers_controller.rb @@ -9,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 @@ -61,105 +68,88 @@ def destroy end end def bulk_update - begin - authorize! :manage, :repp - - @errors ||= [] - @successful = [] + 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) } + 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? - error_summary = analyze_nameserver_errors(@errors) - message = build_nameserver_error_message(error_summary, nameserver_changes.count) - - @response = { - code: 2304, - message: message, - data: { - success: @successful, - failed: @errors, - summary: { - total: nameserver_changes.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_nameserver_errors(@errors) - message = "#{@successful.count} nameserver changes successful, #{@errors.count} failed. " + - build_nameserver_error_message(error_summary, @errors.count, partial: true) - - @response = { - code: 2400, - message: message, - data: { - success: @successful, - failed: @errors, - summary: { - total: nameserver_changes.count, - successful: @successful.count, - failed: @errors.count, - error_breakdown: error_summary - } - } + 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 } - render(json: @response, status: :multi_status) - else - render_success(data: { - success: @successful, - failed: @errors, - summary: { - total: nameserver_changes.count, - successful: @successful.count, - failed: @errors.count - } - }) - end - - rescue StandardError => e - Rails.logger.error "[REPP Nameservers] Exception occurred: #{e.class} - #{e.message}" - Rails.logger.error "[REPP Nameservers] Backtrace: #{e.backtrace.join("\n")}" - - @response = { code: 2304, message: "Nameserver bulk update failed: #{e.message}", data: {} } - render(json: @response, status: :bad_request) + }) end end private - def set_nameserver - @nameserver = @domain.nameservers.find_by!(hostname: params[:id]) + 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 nameserver_params - params.permit(:domain_id, nameservers: [[:hostname, :action, { ipv4: [], ipv6: [] }]]) + 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 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 + 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 = [] - begin + csv_parse_wrapper(csv_file) do CSV.foreach(csv_file.path, headers: true) do |row| next if row['Domain'].blank? @@ -170,12 +160,6 @@ def parse_nameserver_csv(csv_file) ipv6: bulk_params[:ipv6] || [] } 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 nameserver_changes.empty? @@ -187,21 +171,84 @@ def parse_nameserver_csv(csv_file) nameserver_changes end - def process_nameserver_change(change) + def parse_nameserver_csv_from_body(csv_data) + nameserver_changes = [] - begin + 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 + + 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 - error_info = { - type: 'nameserver_change', - domain_name: change[:domain_name], - error_code: '2201', - error_message: 'Authorization error', - details: { code: '2201', msg: 'Authorization error' } - } - @errors << error_info + @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 @@ -246,24 +293,6 @@ def process_nameserver_change(change) @errors << error_info end - rescue ActiveRecord::RecordNotFound - error_info = { - type: 'nameserver_change', - domain_name: change[:domain_name], - error_code: '2303', - error_message: 'Domain not found', - details: { code: '2303', msg: 'Domain not found' } - } - @errors << error_info - rescue StandardError => e - error_info = { - type: 'nameserver_change', - domain_name: change[:domain_name], - error_code: 'UNKNOWN', - error_message: e.message, - details: { message: e.message } - } - @errors << error_info end end @@ -271,34 +300,7 @@ def is_csv_request? request.content_type&.include?('text/csv') || request.content_type&.include?('application/csv') end - def parse_nameserver_csv_from_body(csv_data) - nameserver_changes = [] - - begin - 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 - 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 nameserver_changes.empty? - @errors << { type: 'csv_error', message: 'CSV file is empty or missing required headers: Domain, New_Nameserver' } - end - - nameserver_changes - end def analyze_nameserver_errors(errors) error_counts = {} @@ -328,13 +330,13 @@ def build_nameserver_error_message(error_summary, total_count, partial: false) error_summary.each do |error_info| case error_info[:code] - when '2303' + when OBJECT_DOES_NOT_EXIST_EPP_CODE.to_s messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} not found" - when '2201' + when AUTHORIZATION_ERROR_EPP_CODE.to_s messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} unauthorized" - when '2304' + when PROHIBIT_EPP_CODE.to_s messages << "#{error_info[:count]} domain#{'s' if error_info[:count] > 1} prohibited from changes" - when '2306' + 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]})" diff --git a/app/controllers/repp/v1/domains_controller.rb b/app/controllers/repp/v1/domains_controller.rb index 9267ecd3f4..567211186c 100644 --- a/app/controllers/repp/v1/domains_controller.rb +++ b/app/controllers/repp/v1/domains_controller.rb @@ -289,7 +289,9 @@ def transfer_params 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}"