ServeDNS now answers authoritatively for the configured zone(s): - Apex SOA → synthetic SOA (serial = store generation counter) - Apex NS → synthetic NS pointing at p.Nameserver - In-store lookups for any qtype - NODATA vs NXDOMAIN correctly distinguished (SOA in authority section) - UPDATE opcode → REFUSED (Phase 1.4 implements properly) - Queries outside our zones pass through to Next Added: - store.go: recordStore with sync.RWMutex + atomic generation counter. Operations: Add (de-dupes), RemoveRRset, RemoveRR, RemoveName, Lookup (returns a copy so callers can't corrupt internal state), NameExists. All keyed on canonical lowercase + trailing-dot names. - plugin.go: ServeDNS dispatch, findZone (longest-suffix match), syntheticSOA, syntheticNS. New Nameserver field. - setup.go: nameserver directive. Default Nameserver = first zone apex. Store initialised at parse time. - store_test.go: 12 unit tests covering add/dedupe/remove/lookup/ generation/case-insensitivity/copy-safety. - plugin_test.go: 10 dispatch tests covering pass-through, apex synthetics, in-store lookups, NXDOMAIN/NODATA semantics, UPDATE refusal, findZone longest-suffix-wins and case behavior. - setup_test.go: 3 new cases for the nameserver directive + store init. Total: 38 tests passing. Module: git.supported.systems/rsp2k/coredns-rfc2136
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
package rfc2136
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
func mustRR(t *testing.T, s string) dns.RR {
|
|
t.Helper()
|
|
rr, err := dns.NewRR(s)
|
|
if err != nil {
|
|
t.Fatalf("failed to parse RR %q: %v", s, err)
|
|
}
|
|
return rr
|
|
}
|
|
|
|
func TestStore_AddLookup(t *testing.T) {
|
|
s := newStore()
|
|
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-1"`))
|
|
|
|
got := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
if len(got) != 1 {
|
|
t.Fatalf("Lookup TXT: got %d records, want 1", len(got))
|
|
}
|
|
if got[0].String() != `foo.example.com. 60 IN TXT "token-1"` {
|
|
t.Errorf("unexpected RR: %s", got[0].String())
|
|
}
|
|
}
|
|
|
|
func TestStore_AddMultipleRRsetEntries(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-1"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "token-2"`))
|
|
|
|
got := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
if len(got) != 2 {
|
|
t.Errorf("RRset size = %d, want 2 (both TXT values)", len(got))
|
|
}
|
|
}
|
|
|
|
func TestStore_AddDedupes(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "same"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "same"`))
|
|
|
|
got := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
if len(got) != 1 {
|
|
t.Errorf("RRset size = %d, want 1 (duplicate ignored)", len(got))
|
|
}
|
|
}
|
|
|
|
func TestStore_LookupCaseInsensitive(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `FOO.example.com. 60 IN TXT "token"`))
|
|
|
|
if got := s.Lookup("foo.EXAMPLE.com.", dns.TypeTXT); len(got) != 1 {
|
|
t.Errorf("case-insensitive lookup failed: got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestStore_LookupMissingNameReturnsNil(t *testing.T) {
|
|
s := newStore()
|
|
if got := s.Lookup("nope.example.com.", dns.TypeTXT); got != nil {
|
|
t.Errorf("expected nil for missing name, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestStore_LookupNameExistsWrongTypeReturnsNil(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`))
|
|
|
|
if got := s.Lookup("foo.example.com.", dns.TypeTXT); got != nil {
|
|
t.Errorf("expected nil for wrong type (A exists but not TXT), got %v", got)
|
|
}
|
|
if !s.NameExists("foo.example.com.") {
|
|
t.Errorf("NameExists should return true (A record exists)")
|
|
}
|
|
}
|
|
|
|
func TestStore_RemoveRRset(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "b"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`))
|
|
|
|
s.RemoveRRset("foo.example.com.", dns.TypeTXT)
|
|
|
|
if got := s.Lookup("foo.example.com.", dns.TypeTXT); got != nil {
|
|
t.Errorf("TXT RRset should be gone, got %v", got)
|
|
}
|
|
if got := s.Lookup("foo.example.com.", dns.TypeA); len(got) != 1 {
|
|
t.Errorf("A record should survive RRset deletion, got %v", got)
|
|
}
|
|
if !s.NameExists("foo.example.com.") {
|
|
t.Errorf("name should still exist (A remains)")
|
|
}
|
|
}
|
|
|
|
func TestStore_RemoveRRsetReapsEmptyName(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`))
|
|
s.RemoveRRset("foo.example.com.", dns.TypeTXT)
|
|
|
|
if s.NameExists("foo.example.com.") {
|
|
t.Errorf("name should have been reaped after last RRset removed")
|
|
}
|
|
}
|
|
|
|
func TestStore_RemoveRR(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "keep"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "drop"`))
|
|
|
|
s.RemoveRR(mustRR(t, `foo.example.com. 60 IN TXT "drop"`))
|
|
|
|
got := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
if len(got) != 1 {
|
|
t.Fatalf("RRset size after RemoveRR = %d, want 1", len(got))
|
|
}
|
|
if got[0].(*dns.TXT).Txt[0] != "keep" {
|
|
t.Errorf("wrong RR remained: %v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestStore_RemoveName(t *testing.T) {
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`))
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN A 192.0.2.1`))
|
|
|
|
s.RemoveName("foo.example.com.")
|
|
|
|
if s.NameExists("foo.example.com.") {
|
|
t.Errorf("name should be gone after RemoveName")
|
|
}
|
|
}
|
|
|
|
func TestStore_GenerationBumpsOnMutation(t *testing.T) {
|
|
s := newStore()
|
|
start := s.generation()
|
|
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`))
|
|
if s.generation() != start+1 {
|
|
t.Errorf("generation after Add: %d, want %d", s.generation(), start+1)
|
|
}
|
|
|
|
// Re-adding the same RR is a no-op → generation must NOT bump.
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "a"`))
|
|
if s.generation() != start+1 {
|
|
t.Errorf("generation after duplicate Add: %d, want %d (no bump)", s.generation(), start+1)
|
|
}
|
|
|
|
s.RemoveRRset("foo.example.com.", dns.TypeTXT)
|
|
if s.generation() != start+2 {
|
|
t.Errorf("generation after RemoveRRset: %d, want %d", s.generation(), start+2)
|
|
}
|
|
|
|
// Removing again is a no-op → no bump.
|
|
s.RemoveRRset("foo.example.com.", dns.TypeTXT)
|
|
if s.generation() != start+2 {
|
|
t.Errorf("generation after no-op RemoveRRset: %d, want %d", s.generation(), start+2)
|
|
}
|
|
}
|
|
|
|
func TestStore_LookupReturnsCopy(t *testing.T) {
|
|
// The returned slice must be a copy so external mutations don't
|
|
// affect store state.
|
|
s := newStore()
|
|
s.Add(mustRR(t, `foo.example.com. 60 IN TXT "original"`))
|
|
|
|
got := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
got[0] = mustRR(t, `foo.example.com. 60 IN TXT "tampered"`)
|
|
|
|
// Re-lookup should still see the original.
|
|
again := s.Lookup("foo.example.com.", dns.TypeTXT)
|
|
if again[0].(*dns.TXT).Txt[0] != "original" {
|
|
t.Errorf("Lookup returned shared slice — store corrupted by external mutation: %v", again[0])
|
|
}
|
|
}
|