From 52802e3cc997ad3181d6cf262c8fa87ab2d620b8 Mon Sep 17 00:00:00 2001 From: Charles Ng Date: Tue, 6 Jan 2026 14:59:49 -0500 Subject: [PATCH 1/3] Fix test failures due to example.com DNS hosting change to Cloudflare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example.com has migrated from IANA nameservers to Cloudflare. This change updates tests to reflect the new Cloudflare nameservers (hera.ns.cloudflare.com and elliott.ns.cloudflare.com). Additionally, root servers now return authoritative SOA answers for example.com instead of NS referrals. Updated the DNS query logic to handle this by fetching NS records directly when we receive an authoritative SOA answer. Also added the csv gem as a runtime dependency for Ruby 3.4+ compatibility, as it was removed from the standard library. Changes: - Updated test expectations in ZoneTest and CLI::InfoTest for Cloudflare nameservers - Modified fetch_authority to handle SOA answer sections from authoritative servers - Added fetch_ns_records and fetch_ns_records_for_domain methods - Added csv gem to runtime dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- lib/record_store/zone.rb | 29 ++++++++++++++++++++++++++++- record_store.gemspec | 1 + test/cli/info_test.rb | 11 ++++------- test/zone_test.rb | 11 ++++++----- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/lib/record_store/zone.rb b/lib/record_store/zone.rb index 850a8513..6c328d91 100644 --- a/lib/record_store/zone.rb +++ b/lib/record_store/zone.rb @@ -143,6 +143,12 @@ def write(**write_options) def fetch_authority(nameserver = ROOT_SERVERS.sample) authority = fetch_soa(nameserver) do |reply, _name| + # If we get a valid SOA answer, the nameserver is authoritative, so fetch NS records directly + if reply.answer.any? && reply.answer.first[0].to_s == unrooted_name + break fetch_ns_records(nameserver) + end + + # For NXDOMAIN or referral responses, continue with authority section break if reply.answer.any? raise "No authority found (#{name})" if reply.authority.none? @@ -151,7 +157,14 @@ def fetch_authority(nameserver = ROOT_SERVERS.sample) end # candidate DNS name is returned instead when NXDomain or other error - return if unrooted_name.casecmp?(Array(authority).first.to_s) + # In this case, query the parent domain's NS records + if authority.is_a?(Array) && authority.first.is_a?(Resolv::DNS::Name) && unrooted_name.casecmp?(authority.first.to_s) + # Extract parent domain from unrooted_name (e.g., "sub.example.com" -> "example.com") + parts = unrooted_name.split('.') + return nil if parts.length <= 1 # No parent domain available (TLD) + parent_domain = parts[1..-1].join('.') + '.' + return fetch_ns_records_for_domain(parent_domain, nameserver) + end authority end @@ -164,6 +177,20 @@ def fetch_soa(nameserver, &block) end end + def fetch_ns_records(nameserver) + fetch_ns_records_for_domain(name, nameserver) + end + + def fetch_ns_records_for_domain(domain, nameserver) + Resolv::DNS.open(nameserver: nameserver) do |resolv| + resources = resolv.getresources(domain, Resolv::DNS::Resource::IN::NS) + return nil if resources.empty? + resources.map.with_index do |ns, index| + Record::NS.new(ttl: ns.ttl, fqdn: domain, nsdname: ns.name.to_s, record_id: index) + end + end + end + def resolve_authority(authority) nameservers = authority.map { |a| a.last.name.to_s } diff --git a/record_store.gemspec b/record_store.gemspec index 48d78eb4..b478df3a 100644 --- a/record_store.gemspec +++ b/record_store.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'activemodel', '>= 4.2' spec.add_runtime_dependency 'activesupport', '>= 4.2' + spec.add_runtime_dependency 'csv' spec.add_runtime_dependency 'ejson' spec.add_runtime_dependency 'thor', '>= 1.4.0' diff --git a/test/cli/info_test.rb b/test/cli/info_test.rb index 151acc48..4290b240 100644 --- a/test/cli/info_test.rb +++ b/test/cli/info_test.rb @@ -35,13 +35,10 @@ def test_lists_providers def test_lists_authoritative_nameservers RecordStore::CLI.start(%w(info)) - authority = <<~AUTHORITY - Authoritative nameservers: - - [NSRecord] example.com. 172800 IN NS a.iana-servers.net. - - [NSRecord] example.com. 172800 IN NS b.iana-servers.net. - AUTHORITY - - assert_includes($stdout.string, authority) + output = $stdout.string + assert_includes(output, "Authoritative nameservers:") + assert_includes(output, "hera.ns.cloudflare.com.") + assert_includes(output, "elliott.ns.cloudflare.com.") end end end diff --git a/test/zone_test.rb b/test/zone_test.rb index 3068eadb..1b52ada5 100644 --- a/test/zone_test.rb +++ b/test/zone_test.rb @@ -678,11 +678,12 @@ def implicit_record_injection_does_not_occur_if_no_implicit_records_templates_ar def test_fetch_authority zone = Zone.new(name: 'example.com') nameservers = zone.fetch_authority - expected = [ - Record::NS.new(fqdn: 'example.com', ttl: 172_800, nsdname: 'a.iana-servers.net.'), - Record::NS.new(fqdn: 'example.com', ttl: 172_800, nsdname: 'b.iana-servers.net.'), - ] - assert_equal(expected, nameservers) + + assert_equal 2, nameservers.count + nsdnames = nameservers.map(&:nsdname).sort + assert_equal ['elliott.ns.cloudflare.com.', 'hera.ns.cloudflare.com.'], nsdnames + assert_equal 'example.com.', nameservers.first.fqdn + assert nameservers.first.ttl > 0 end def test_fetch_authority_handles_unreachable_host From fff2561ad0c2a37053868077b1a5145ebd1d4380 Mon Sep 17 00:00:00 2001 From: Charles Ng Date: Tue, 6 Jan 2026 15:04:01 -0500 Subject: [PATCH 2/3] Fix rubocop style violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed line length violation by splitting long conditional - Added empty lines after guard clauses - Changed 'return nil' to 'return' - Removed extra spacing - Added parentheses to test method calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- lib/record_store/zone.rb | 7 +++++-- test/zone_test.rb | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/record_store/zone.rb b/lib/record_store/zone.rb index 6c328d91..a55bf91b 100644 --- a/lib/record_store/zone.rb +++ b/lib/record_store/zone.rb @@ -158,10 +158,12 @@ def fetch_authority(nameserver = ROOT_SERVERS.sample) # candidate DNS name is returned instead when NXDomain or other error # In this case, query the parent domain's NS records - if authority.is_a?(Array) && authority.first.is_a?(Resolv::DNS::Name) && unrooted_name.casecmp?(authority.first.to_s) + if authority.is_a?(Array) && authority.first.is_a?(Resolv::DNS::Name) && + unrooted_name.casecmp?(authority.first.to_s) # Extract parent domain from unrooted_name (e.g., "sub.example.com" -> "example.com") parts = unrooted_name.split('.') - return nil if parts.length <= 1 # No parent domain available (TLD) + return if parts.length <= 1 # No parent domain available (TLD) + parent_domain = parts[1..-1].join('.') + '.' return fetch_ns_records_for_domain(parent_domain, nameserver) end @@ -185,6 +187,7 @@ def fetch_ns_records_for_domain(domain, nameserver) Resolv::DNS.open(nameserver: nameserver) do |resolv| resources = resolv.getresources(domain, Resolv::DNS::Resource::IN::NS) return nil if resources.empty? + resources.map.with_index do |ns, index| Record::NS.new(ttl: ns.ttl, fqdn: domain, nsdname: ns.name.to_s, record_id: index) end diff --git a/test/zone_test.rb b/test/zone_test.rb index 1b52ada5..437d3ccb 100644 --- a/test/zone_test.rb +++ b/test/zone_test.rb @@ -679,11 +679,11 @@ def test_fetch_authority zone = Zone.new(name: 'example.com') nameservers = zone.fetch_authority - assert_equal 2, nameservers.count + assert_equal(2, nameservers.count) nsdnames = nameservers.map(&:nsdname).sort - assert_equal ['elliott.ns.cloudflare.com.', 'hera.ns.cloudflare.com.'], nsdnames - assert_equal 'example.com.', nameservers.first.fqdn - assert nameservers.first.ttl > 0 + assert_equal(['elliott.ns.cloudflare.com.', 'hera.ns.cloudflare.com.'], nsdnames) + assert_equal('example.com.', nameservers.first.fqdn) + assert(nameservers.first.ttl > 0) end def test_fetch_authority_handles_unreachable_host From 61768375edbc274947f599c136be1ac51d20514b Mon Sep 17 00:00:00 2001 From: Charles Ng Date: Tue, 6 Jan 2026 16:24:30 -0500 Subject: [PATCH 3/3] Update CHANGELOG and set version to 8.0.11 --- CHANGELOG.md | 3 +++ lib/record_store/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 899e5b3d..5a526f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG +## 8.0.11 +- Modified Zone#fetch_authority to handle SOA answer sections from authoritative servers + ## 8.0.10 - Fix Cloudflare provider to skip empty changesets - Add TXT record denormalization for DNSimple provider diff --git a/lib/record_store/version.rb b/lib/record_store/version.rb index a25d6029..bee5e28e 100644 --- a/lib/record_store/version.rb +++ b/lib/record_store/version.rb @@ -1,3 +1,3 @@ module RecordStore - VERSION = '8.0.10'.freeze + VERSION = '8.0.11'.freeze end