added tests

This commit is contained in:
oleghasjanov 2025-08-07 15:07:28 +03:00
parent aa6e2e99ec
commit 2620933543
9 changed files with 375 additions and 40 deletions

View file

@ -18,6 +18,8 @@ module Actions
# domain.attach_default_contacts # domain.attach_default_contacts
assign_expiry_time assign_expiry_time
maybe_attach_legal_doc maybe_attach_legal_doc
validate_ns_records
validate_dns_records
commit commit
end end
@ -181,6 +183,26 @@ module Actions
::Actions::BaseAction.attach_legal_doc_to_new(domain, params[:legal_document], domain: true) ::Actions::BaseAction.attach_legal_doc_to_new(domain, params[:legal_document], domain: true)
end end
def validate_ns_records
return unless domain.nameservers.any?
return unless dns_validation_enabled?
result = DNSValidator.validate(domain: domain, name: domain.name, record_type: 'NS')
return if result[:errors].blank?
assign_dns_validation_error(result[:errors])
end
def validate_dns_records
return unless domain.dnskeys.any?
return unless dns_validation_enabled?
result = DNSValidator.validate(domain: domain, name: domain.name, record_type: 'DNSKEY')
return if result[:errors].blank?
assign_dns_validation_error(result[:errors])
end
def process_auction_and_disputes def process_auction_and_disputes
dn = DNS::DomainName.new(domain.name) dn = DNS::DomainName.new(domain.name)
Dispute.close_by_domain(domain.name) Dispute.close_by_domain(domain.name)
@ -200,6 +222,19 @@ module Actions
domain.save domain.save
end end
def assign_dns_validation_error(errors)
errors.each do |error|
domain.add_epp_error('2306', nil, nil, error)
end
end
def dns_validation_enabled?
# Enable DNS validation in production or when explicitly enabled
# Disabled in test environment by default unless explicitly enabled
return ENV['DNS_VALIDATION_ENABLED'] == 'true' if Rails.env.test?
Rails.env.production? || ENV['DNS_VALIDATION_ENABLED'] == 'true'
end
def validation_process_errored? def validation_process_errored?
return if domain.valid? return if domain.valid?

View file

@ -13,6 +13,8 @@ module Actions
validate_domain_integrity validate_domain_integrity
assign_new_registrant if params[:registrant] assign_new_registrant if params[:registrant]
assign_relational_modifications assign_relational_modifications
validate_ns_records
validate_dns_records
assign_requested_statuses assign_requested_statuses
::Actions::BaseAction.maybe_attach_legal_doc(domain, params[:legal_document]) ::Actions::BaseAction.maybe_attach_legal_doc(domain, params[:legal_document])
@ -97,6 +99,39 @@ module Actions
end end
end end
def validate_ns_records
return unless domain.nameservers.any?
return unless dns_validation_enabled?
result = DNSValidator.validate(domain: domain, name: domain.name, record_type: 'NS')
return if result[:errors].blank?
assign_dns_validation_error(result[:errors])
end
def validate_dns_records
return unless domain.dnskeys.any?
return unless dns_validation_enabled?
result = DNSValidator.validate(domain: domain, name: domain.name, record_type: 'DNSKEY')
return if result[:errors].blank?
assign_dns_validation_error(result[:errors])
end
def assign_dns_validation_error(errors)
errors.each do |error|
domain.add_epp_error('2306', nil, nil, error)
end
end
def dns_validation_enabled?
# Enable DNS validation in production or when explicitly enabled
# Disabled in test environment by default unless explicitly enabled
return ENV['DNS_VALIDATION_ENABLED'] == 'true' if Rails.env.test?
Rails.env.production? || ENV['DNS_VALIDATION_ENABLED'] == 'true'
end
def assign_dnssec_modifications def assign_dnssec_modifications
@dnskeys = [] @dnskeys = []
params[:dns_keys].each do |key| params[:dns_keys].each do |key|

View file

@ -3,6 +3,6 @@ class DNSValidationJob < ApplicationJob
def perform(domain_id) def perform(domain_id)
domain = Domain.find(domain_id) domain = Domain.find(domain_id)
DNSValidator.validate(domain: domain, name: domain.name) DNSValidator.validate(domain: domain, name: domain.name, record_type: 'all')
end end
end end

View file

@ -4,11 +4,12 @@ require 'dnsruby'
class DNSValidator class DNSValidator
include Dnsruby include Dnsruby
attr_reader :domain, :results attr_reader :domain, :results, :record_type
def initialize(domain:, name:) def initialize(domain:, name:, record_type:)
@domain = domain.present? ? domain : Domain.find_by_name(name) @domain = domain.present? ? domain : Domain.find_by_name(name)
raise "Domain not found" if @domain.blank? raise "Domain not found" if @domain.blank?
@record_type = record_type
@results = { @results = {
nameservers: {}, nameservers: {},
@ -20,17 +21,31 @@ class DNSValidator
} }
end end
def self.validate(domain:, name:) def self.validate(domain:, name:, record_type:)
new(domain: domain, name: name).validate new(domain: domain, name: name, record_type: record_type).validate
end end
def validate def validate
Rails.logger.info "Starting DNS validation for domain: #{domain.name}" Rails.logger.info "Starting DNS validation for domain: #{domain.name}"
validate_nameservers case record_type
validate_dns_records when 'NS'
check_dnssec_sync_records validate_nameservers
check_csync_records when 'A', 'AAAA'
validate_dns_records
when 'DNSKEY'
check_dnssec_sync_records
when 'CSYNC'
check_csync_records
when 'all'
validate_nameservers
validate_dns_records
check_dnssec_sync_records
check_csync_records
else
raise "Invalid record type: #{record_type}"
end
apply_enforcement_actions apply_enforcement_actions
Rails.logger.info "DNS validation completed for domain: #{domain.name}" Rails.logger.info "DNS validation completed for domain: #{domain.name}"
@ -638,8 +653,8 @@ class DNSValidator
# Class methods for easy usage # Class methods for easy usage
class << self class << self
def validate_domain(domain) def validate_domain(domain, record_type: 'all')
validator = new(domain) validator = new(domain: domain, name: domain.name, record_type: record_type)
validator.validate validator.validate
end end
end end

View file

@ -134,4 +134,148 @@ class ReppV1DomainsCreateTest < ActionDispatch::IntegrationTest
assert @user.registrar.domains.find_by(name: 'domeener.test').present? assert @user.registrar.domains.find_by(name: 'domeener.test').present?
assert_equal 'ABADIATS', @user.registrar.domains.find_by(name: 'domeener.test').transfer_code assert_equal 'ABADIATS', @user.registrar.domains.find_by(name: 'domeener.test').transfer_code
end end
def test_creates_domain_with_nameservers_validates_dns
ENV['DNS_VALIDATION_ENABLED'] = 'true'
@auth_headers['Content-Type'] = 'application/json'
contact = contacts(:john)
# Mock successful DNS validation
DNSValidator.stub :validate, { errors: [] } do
payload = {
domain: {
name: 'domeener.test',
registrant: contact.code,
period: 1,
period_unit: 'y',
nameservers_attributes: [
{ hostname: 'ns1.example.com' },
{ hostname: 'ns2.example.com' }
]
}
}
post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
domain = @user.registrar.domains.find_by(name: 'domeener.test')
assert domain.present?
assert_equal 2, domain.nameservers.count
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
def test_fails_to_create_domain_with_invalid_nameservers
ENV['DNS_VALIDATION_ENABLED'] = 'true'
@auth_headers['Content-Type'] = 'application/json'
contact = contacts(:john)
# Mock DNS validation failure
DNSValidator.stub :validate, { errors: ['Nameserver ns1.example.com is not authoritative for domain'] } do
payload = {
domain: {
name: 'domeener.test',
registrant: contact.code,
period: 1,
period_unit: 'y',
nameservers_attributes: [
{ hostname: 'ns1.example.com' },
{ hostname: 'ns2.example.com' }
]
}
}
post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 2306, json[:code]
assert json[:message].include?('Nameserver ns1.example.com is not authoritative')
refute @user.registrar.domains.find_by(name: 'domeener.test').present?
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
def test_creates_domain_with_dnssec_validates_dnskey
ENV['DNS_VALIDATION_ENABLED'] = 'true'
@auth_headers['Content-Type'] = 'application/json'
contact = contacts(:john)
# Mock successful DNSSEC validation
DNSValidator.stub :validate, { errors: [] } do
payload = {
domain: {
name: 'domeener.test',
registrant: contact.code,
period: 1,
period_unit: 'y',
dnskeys_attributes: [
{
flags: '257',
protocol: '3',
alg: '8',
public_key: 'AwEAAddt2AkLfYGKgiEZB5SmIF8EvrjxNMH6HtxWEA4RJ9Ao6LCWheg8'
}
]
}
}
post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
domain = @user.registrar.domains.find_by(name: 'domeener.test')
assert domain.present?
assert_equal 1, domain.dnskeys.count
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
def test_fails_to_create_domain_with_invalid_dnskey
ENV['DNS_VALIDATION_ENABLED'] = 'true'
@auth_headers['Content-Type'] = 'application/json'
contact = contacts(:john)
# Mock DNSSEC validation failure
DNSValidator.stub :validate, { errors: ['DNSKEY record not found in DNS'] } do
payload = {
domain: {
name: 'domeener.test',
registrant: contact.code,
period: 1,
period_unit: 'y',
dnskeys_attributes: [
{
flags: '257',
protocol: '3',
alg: '8',
public_key: 'AwEAAddt2AkLfYGKgiEZB5SmIF8EvrjxNMH6HtxWEA4RJ9Ao6LCWheg8'
}
]
}
}
post "/repp/v1/domains", headers: @auth_headers, params: payload.to_json
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 2306, json[:code]
assert json[:message].include?('DNSKEY record not found')
refute @user.registrar.domains.find_by(name: 'domeener.test').present?
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
end end

View file

@ -138,4 +138,63 @@ class ReppV1DomainsDnssecTest < ActionDispatch::IntegrationTest
ENV["shunter_default_threshold"] = '10000' ENV["shunter_default_threshold"] = '10000'
ENV["shunter_enabled"] = 'false' ENV["shunter_enabled"] = 'false'
end end
def test_validates_dns_when_adding_dnskey
ENV['DNS_VALIDATION_ENABLED'] = 'true'
# Mock successful DNS validation
DNSValidator.stub :validate, { errors: [] } do
payload = {
dns_keys: [
{ flags: '257',
alg: '8',
protocol: '3',
public_key: 'AwEAAddt2AkLfYGKgiEZB5SmIF8EvrjxNMH6HtxWEA4RJ9Ao6LCWheg8' },
],
}
post "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
@domain.reload
assert @domain.dnskeys.present?
dnssec_key = @domain.dnskeys.last
assert_equal 257, dnssec_key.flags
assert_equal 8, dnssec_key.alg
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
def test_fails_to_add_dnskey_with_invalid_dns
ENV['DNS_VALIDATION_ENABLED'] = 'true'
# Mock DNS validation failure
DNSValidator.stub :validate, { errors: ['DNSKEY record not found in DNS'] } do
payload = {
dns_keys: [
{ flags: '257',
alg: '8',
protocol: '3',
public_key: 'AwEAAddt2AkLfYGKgiEZB5SmIF8EvrjxNMH6HtxWEA4RJ9Ao6LCWheg8' },
],
}
post "/repp/v1/domains/#{@domain.name}/dnssec", params: payload, headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 2306, json[:code]
assert json[:message].include?('DNSKEY record not found')
@domain.reload
assert @domain.dnskeys.empty?
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
end end

View file

@ -109,4 +109,58 @@ class ReppV1DomainsNameserversTest < ActionDispatch::IntegrationTest
assert_equal 'Data management policy violation; Nameserver count must be between 2-11 for active ' \ assert_equal 'Data management policy violation; Nameserver count must be between 2-11 for active ' \
'domains [nameservers]', json[:message] 'domains [nameservers]', json[:message]
end end
def test_validates_dns_when_adding_nameserver
ENV['DNS_VALIDATION_ENABLED'] = 'true'
# Mock successful DNS validation
DNSValidator.stub :validate, { errors: [] } do
payload = {
nameservers: [
{ hostname: "ns3.example.com",
ipv4: ["192.168.1.1"],
ipv6: ["FE80::AEDE:48FF:FE00:1122"]}
]
}
post "/repp/v1/domains/#{@domain.name}/nameservers", params: payload, headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :ok
assert_equal 1000, json[:code]
assert_equal 'Command completed successfully', json[:message]
@domain.reload
assert @domain.nameservers.where(hostname: 'ns3.example.com').any?
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
def test_fails_to_add_nameserver_with_invalid_dns
ENV['DNS_VALIDATION_ENABLED'] = 'true'
# Mock DNS validation failure
DNSValidator.stub :validate, { errors: ['Nameserver ns3.example.com is not authoritative for domain'] } do
payload = {
nameservers: [
{ hostname: "ns3.example.com",
ipv4: ["192.168.1.1"],
ipv6: ["FE80::AEDE:48FF:FE00:1122"]}
]
}
post "/repp/v1/domains/#{@domain.name}/nameservers", params: payload, headers: @auth_headers
json = JSON.parse(response.body, symbolize_names: true)
assert_response :bad_request
assert_equal 2306, json[:code]
assert json[:message].include?('Nameserver ns3.example.com is not authoritative')
@domain.reload
refute @domain.nameservers.where(hostname: 'ns3.example.com').any?
end
ensure
ENV.delete('DNS_VALIDATION_ENABLED')
end
end end

View file

@ -121,4 +121,5 @@ class ReppV1DomainsUpdateTest < ActionDispatch::IntegrationTest
assert_equal 2202, json[:code] assert_equal 2202, json[:code]
assert_equal 'Invalid authorization information; invalid reserved>pw value', json[:message] assert_equal 'Invalid authorization information; invalid reserved>pw value', json[:message]
end end
end end

View file

@ -19,7 +19,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Basic functionality tests # Basic functionality tests
test 'initializes with correct structure' do test 'initializes with correct structure' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
assert_equal @domain, validator.domain assert_equal @domain, validator.domain
assert_instance_of Hash, validator.results assert_instance_of Hash, validator.results
@ -31,27 +31,19 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'class method validate creates instance' do test 'class method validate creates instance' do
validator = DNSValidator.new(@domain) result = { test: 'result' }
# Mock validate method to avoid DNS calls # Mock the class method directly
validator.define_singleton_method(:validate) { { test: 'result' } } DNSValidator.stub :validate, result do
actual_result = DNSValidator.validate(domain: @domain, name: @domain.name, record_type: 'NS')
# Temporarily override new method assert_equal result, actual_result
original_new = DNSValidator.method(:new) end
DNSValidator.define_singleton_method(:new) { |domain| validator }
result = DNSValidator.validate(@domain)
assert_equal({ test: 'result' }, result)
ensure
# Restore original new method
DNSValidator.define_singleton_method(:new, original_new)
end end
# Story 1: Nameserver Validation Tests # Story 1: Nameserver Validation Tests
test 'validates nameservers successfully' do test 'validates nameservers successfully' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
# Mock successful nameserver validation # Mock successful nameserver validation
validator.define_singleton_method(:validate_single_nameserver) do |ns| validator.define_singleton_method(:validate_single_nameserver) do |ns|
@ -70,7 +62,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'handles failed nameserver validation' do test 'handles failed nameserver validation' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
# Mock failed nameserver validation # Mock failed nameserver validation
validator.define_singleton_method(:validate_single_nameserver) do |ns| validator.define_singleton_method(:validate_single_nameserver) do |ns|
@ -89,7 +81,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'validates single nameserver with mocked DNS' do test 'validates single nameserver with mocked DNS' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
# Create mock resolver # Create mock resolver
resolver = create_mock_resolver resolver = create_mock_resolver
@ -123,7 +115,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'detects nameserver not in NS records' do test 'detects nameserver not in NS records' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
resolver = create_mock_resolver resolver = create_mock_resolver
# Pre-create responses # Pre-create responses
@ -150,7 +142,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'detects CNAME at apex' do test 'detects CNAME at apex' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
resolver = create_mock_resolver resolver = create_mock_resolver
# Pre-create CNAME response # Pre-create CNAME response
@ -173,7 +165,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Story 2: DNS Records Validation Tests # Story 2: DNS Records Validation Tests
test 'validates DNS records from valid nameservers' do test 'validates DNS records from valid nameservers' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'A')
# Set up nameserver validation results # Set up nameserver validation results
validator.instance_variable_set(:@results, { validator.instance_variable_set(:@results, {
@ -216,7 +208,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'skips DNS validation when no valid nameservers' do test 'skips DNS validation when no valid nameservers' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'A')
# Set up scenario with no valid nameservers # Set up scenario with no valid nameservers
validator.instance_variable_set(:@results, { validator.instance_variable_set(:@results, {
@ -235,7 +227,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Story 4: DNSSEC Tests # Story 4: DNSSEC Tests
test 'processes CDS records for DNSSEC sync' do test 'processes CDS records for DNSSEC sync' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'DNSKEY')
resolver = create_mock_resolver resolver = create_mock_resolver
# Pre-create responses # Pre-create responses
@ -273,7 +265,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Ensure domain has enough nameservers # Ensure domain has enough nameservers
@domain.nameservers.create!(hostname: 'backup.ns.com') @domain.nameservers.create!(hostname: 'backup.ns.com')
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
# Initialize complete results structure # Initialize complete results structure
validator.instance_variable_set(:@results, { validator.instance_variable_set(:@results, {
@ -300,7 +292,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
@nameserver1.update!(validation_counter: 3, failed_validation_reason: 'Failed validation') @nameserver1.update!(validation_counter: 3, failed_validation_reason: 'Failed validation')
@nameserver2.destroy # Only one nameserver left @nameserver2.destroy # Only one nameserver left
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
validator.instance_variable_set(:@results, { validator.instance_variable_set(:@results, {
nameservers: {}, nameservers: {},
@ -323,7 +315,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Integration Tests # Integration Tests
test 'full validation workflow' do test 'full validation workflow' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'all')
# Mock all validation methods to avoid DNS calls # Mock all validation methods to avoid DNS calls
validator.define_singleton_method(:validate_nameservers) { nil } validator.define_singleton_method(:validate_nameservers) { nil }
@ -342,7 +334,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
end end
test 'handles validation exceptions gracefully' do test 'handles validation exceptions gracefully' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
validator.define_singleton_method(:validate_nameservers) do validator.define_singleton_method(:validate_nameservers) do
raise StandardError.new('DNS timeout') raise StandardError.new('DNS timeout')
@ -356,7 +348,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
# Helper method tests # Helper method tests
test 'helper methods work correctly' do test 'helper methods work correctly' do
validator = DNSValidator.new(@domain) validator = DNSValidator.new(domain: @domain, name: @domain.name, record_type: 'NS')
# Test parse_type_bitmap # Test parse_type_bitmap
assert_equal ['NS', 'A'], validator.send(:parse_type_bitmap, ['NS', 'A']) assert_equal ['NS', 'A'], validator.send(:parse_type_bitmap, ['NS', 'A'])
@ -378,7 +370,7 @@ class DNSValidatorTest < ActiveSupport::TestCase
registrant: @domain.registrant registrant: @domain.registrant
) )
validator = DNSValidator.new(domain_without_ns) validator = DNSValidator.new(domain: domain_without_ns, name: domain_without_ns.name, record_type: 'NS')
assert_nothing_raised do assert_nothing_raised do
validator.send(:validate_nameservers) validator.send(:validate_nameservers)