Phase 2b: refactor to file-backed storage; UPDATE writes zones/*.zone
Major architectural pivot per the user's "RFC 2136 mechanism for the existing zonefiles, not a new in-memory thing" framing. The plugin no longer maintains its own in-memory state OR serves any queries -- both of those are now the auto plugin's job, reading the same zone files. The plugin's sole responsibility is now: receive TSIG-authed UPDATE messages, edit the matching zones/<zone>.zone file, bump the SOA serial in CalVer (YYYYMMDDNN) form, and optionally auto-commit to git. What changed: - DELETED: store.go (in-memory recordStore), store_test.go (12 tests), plugin_test.go (10 ServeDNS query tests), old update_test.go. - NEW: zonefile.go -- file-backed authority for one zone. loadRRs via miekg/dns zone parser; mutation helpers (lookupIn/nameExistsIn/ removeRRsetFrom/removeRRFrom/removeNameFrom/addRRTo) on []dns.RR slices; bumpSerial with CalVer semantics + NN exhaustion handling; writeAtomic via temp-file rename; commit shells to `git add && git commit` with configurable author. - NEW: zonefile_test.go -- 17 tests covering load/lookup/mutate/bump/ write paths. - REWRITTEN: plugin.go -- ServeDNS is now thin: UPDATE → TSIG → handler; everything else → Next. No synthetic SOA/NS, no query serving. - REWRITTEN: update.go -- handleUpdate now opens the zoneFile, loads, applies (with prereq checks against the loaded RRs), bumps serial, writes, commits. Detects no-op updates to avoid spurious file writes. - REWRITTEN: setup.go -- new directives: `zones-dir` (required), `auto-commit` (default true), `git-author <name> <email>`. Dropped `nameserver` and `persist`. Validates each declared zone has a file on disk via os.Stat before CoreDNS finishes starting. - REWRITTEN: setup_test.go -- 17 cases for the new grammar. - REWRITTEN: update_test.go -- 11 cases using real temp zone files via t.TempDir(). Total: 30 tests passing, 0 failures. Next: Phase 2c (custom CoreDNS image, deploy, smoke test with nsupdate).
This commit is contained in:
parent
1d2d919728
commit
0f28127284
10 changed files with 1223 additions and 1109 deletions
362
update_test.go
362
update_test.go
|
|
@ -1,15 +1,52 @@
|
|||
package rfc2136
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// newUpdate builds a minimal UPDATE message for one zone with zero
|
||||
// prereqs and the given update RRs. Caddy's caddy-dns/rfc2136 module
|
||||
// produces messages in this same shape.
|
||||
// captureWriter implements dns.ResponseWriter and stashes the message
|
||||
// passed to WriteMsg so tests can inspect it after handleUpdate returns.
|
||||
type captureWriter struct {
|
||||
msg *dns.Msg
|
||||
}
|
||||
|
||||
func (cw *captureWriter) WriteMsg(m *dns.Msg) error { cw.msg = m; return nil }
|
||||
func (cw *captureWriter) Write([]byte) (int, error) { return 0, nil }
|
||||
func (cw *captureWriter) Close() error { return nil }
|
||||
func (cw *captureWriter) TsigStatus() error { return nil }
|
||||
func (cw *captureWriter) TsigTimersOnly(bool) {}
|
||||
func (cw *captureWriter) Hijack() {}
|
||||
func (cw *captureWriter) LocalAddr() net.Addr { return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)} }
|
||||
func (cw *captureWriter) RemoteAddr() net.Addr { return &net.IPAddr{IP: net.IPv4(127, 0, 0, 1)} }
|
||||
func (cw *captureWriter) Network() string { return "udp" }
|
||||
|
||||
// newTestPluginWithZone builds an RFC2136 backed by a temp zones-dir.
|
||||
// AutoCommit is disabled by default (tests don't need git side-effects).
|
||||
func newTestPluginWithZone(t *testing.T, zone string) *RFC2136 {
|
||||
t.Helper()
|
||||
dir := withTempZonesDir(t, zone)
|
||||
zone = dns.Fqdn(zone)
|
||||
|
||||
p := &RFC2136{
|
||||
Zones: []string{zone},
|
||||
TTL: 60,
|
||||
ZonesDir: dir,
|
||||
zones: map[string]*zoneFile{
|
||||
zone: openZoneFile(filepath.Join(dir, strings.TrimSuffix(zone, ".")+".zone"), zone),
|
||||
},
|
||||
}
|
||||
p.zones[zone].AutoCommit = false
|
||||
return p
|
||||
}
|
||||
|
||||
// newUpdate builds an UPDATE message for `zone` with zero prereqs and
|
||||
// the given update RRs. Matches what caddy-dns/rfc2136 sends.
|
||||
func newUpdate(zone string, updates ...dns.RR) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate(dns.Fqdn(zone))
|
||||
|
|
@ -17,220 +54,229 @@ func newUpdate(zone string, updates ...dns.RR) *dns.Msg {
|
|||
return m
|
||||
}
|
||||
|
||||
// applyUpdateNoAuth bypasses the TSIG gate so we can test handler
|
||||
// logic directly without setting up dns.Server-level integration in
|
||||
// unit tests. End-to-end TSIG verification happens in Phase 2 with
|
||||
// nsupdate against the live custom CoreDNS binary.
|
||||
func applyUpdateNoAuth(t *testing.T, p *RFC2136, msg *dns.Msg) (rcode int, response *dns.Msg) {
|
||||
// runUpdate sends msg through the handler with TSIG auth bypassed
|
||||
// (calling handleUpdate directly instead of ServeDNS).
|
||||
func runUpdate(t *testing.T, p *RFC2136, msg *dns.Msg) (rcode int) {
|
||||
t.Helper()
|
||||
w := &captureWriter{}
|
||||
rcode, _ = p.handleUpdate(w, msg)
|
||||
return rcode, w.msg
|
||||
return rcode
|
||||
}
|
||||
|
||||
func TestUpdate_AddSingleTXT(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
// readZoneRecords loads the zone file and returns the RRs for inspection.
|
||||
func readZoneRecords(t *testing.T, p *RFC2136, zone string) []dns.RR {
|
||||
t.Helper()
|
||||
zf := p.zones[dns.Fqdn(zone)]
|
||||
rrs, err := zf.loadRRs()
|
||||
if err != nil {
|
||||
t.Fatalf("loadRRs: %v", err)
|
||||
}
|
||||
return rrs
|
||||
}
|
||||
|
||||
upd := newUpdate("auth.example.com.",
|
||||
func TestUpdate_AddSingleTXT_PersistsToFile(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
upd := newUpdate("auth.example.com",
|
||||
mustRR(t, `token-1.auth.example.com. 60 IN TXT "validation-1"`),
|
||||
)
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, upd)
|
||||
if rcode != dns.RcodeSuccess {
|
||||
if rcode := runUpdate(t, p, upd); rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("rcode = %d, want NOERROR", rcode)
|
||||
}
|
||||
|
||||
// Verify via a query through ServeDNS.
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("token-1.auth.example.com.", dns.TypeTXT)
|
||||
w := &captureWriter{}
|
||||
p.ServeDNS(context.Background(), w, req)
|
||||
|
||||
if len(w.msg.Answer) != 1 {
|
||||
t.Fatalf("Answer len = %d, want 1", len(w.msg.Answer))
|
||||
// Verify by re-reading the file.
|
||||
rrs := readZoneRecords(t, p, "auth.example.com")
|
||||
for _, rr := range rrs {
|
||||
if txt, ok := rr.(*dns.TXT); ok && txt.Hdr.Name == "token-1.auth.example.com." {
|
||||
if txt.Txt[0] == "validation-1" {
|
||||
return // success
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.msg.Answer[0].(*dns.TXT).Txt[0] != "validation-1" {
|
||||
t.Errorf("TXT mismatch: %v", w.msg.Answer[0])
|
||||
t.Errorf("added TXT not found in re-read zone file")
|
||||
}
|
||||
|
||||
func TestUpdate_AddBumpsSerial(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
before := readZoneRecords(t, p, "auth.example.com")
|
||||
var beforeSerial uint32
|
||||
for _, rr := range before {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
beforeSerial = soa.Serial
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
upd := newUpdate("auth.example.com",
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "x"`),
|
||||
)
|
||||
runUpdate(t, p, upd)
|
||||
|
||||
after := readZoneRecords(t, p, "auth.example.com")
|
||||
var afterSerial uint32
|
||||
for _, rr := range after {
|
||||
if soa, ok := rr.(*dns.SOA); ok {
|
||||
afterSerial = soa.Serial
|
||||
break
|
||||
}
|
||||
}
|
||||
if afterSerial <= beforeSerial {
|
||||
t.Errorf("Serial did not advance: before=%d after=%d", beforeSerial, afterSerial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_DeleteRRset(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
p.store.Add(mustRR(t, `token-1.auth.example.com. 60 IN TXT "old-token"`))
|
||||
func TestUpdate_DeleteRRset_RemovesAllOfType(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// CLASS=ANY, RDLEN=0 → delete this specific RRset.
|
||||
del := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "token-1.auth.example.com.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassANY,
|
||||
Ttl: 0,
|
||||
}}
|
||||
upd := newUpdate("auth.example.com.", del)
|
||||
// First add two TXTs at the same name.
|
||||
runUpdate(t, p, newUpdate("auth.example.com",
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "a"`),
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "b"`),
|
||||
))
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, upd)
|
||||
if rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("rcode = %d, want NOERROR", rcode)
|
||||
}
|
||||
|
||||
if got := p.store.Lookup("token-1.auth.example.com.", dns.TypeTXT); got != nil {
|
||||
t.Errorf("RRset should be gone, still got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_DeleteAllRRsetsAtName(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
p.store.Add(mustRR(t, `foo.auth.example.com. 60 IN TXT "t"`))
|
||||
p.store.Add(mustRR(t, `foo.auth.example.com. 60 IN A 192.0.2.1`))
|
||||
|
||||
// CLASS=ANY, TYPE=ANY, RDLEN=0 → wipe the name.
|
||||
// Now delete the whole TXT RRset.
|
||||
del := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "foo.auth.example.com.",
|
||||
Rrtype: dns.TypeANY,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassANY,
|
||||
}}
|
||||
rcode, _ := applyUpdateNoAuth(t, p, newUpdate("auth.example.com.", del))
|
||||
if rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("rcode = %d", rcode)
|
||||
if rcode := runUpdate(t, p, newUpdate("auth.example.com", del)); rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("delete rcode = %d", rcode)
|
||||
}
|
||||
if p.store.NameExists("foo.auth.example.com.") {
|
||||
t.Errorf("name should be wiped")
|
||||
|
||||
for _, rr := range readZoneRecords(t, p, "auth.example.com") {
|
||||
if rr.Header().Rrtype == dns.TypeTXT && rr.Header().Name == "foo.auth.example.com." {
|
||||
t.Errorf("TXT %s should be gone, still present: %s", rr.Header().Name, rr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_OutsideZone_NOTZONE(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
func TestUpdate_OutOfZone_Refused(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// Update tries to write into a different zone.
|
||||
upd := newUpdate("auth.example.com.",
|
||||
mustRR(t, `evil.different.tld. 60 IN TXT "nope"`),
|
||||
upd := newUpdate("auth.example.com",
|
||||
mustRR(t, `evil.other.tld. 60 IN TXT "nope"`),
|
||||
)
|
||||
rcode, _ := applyUpdateNoAuth(t, p, upd)
|
||||
if rcode != dns.RcodeNotZone {
|
||||
if rcode := runUpdate(t, p, upd); rcode != dns.RcodeNotZone {
|
||||
t.Errorf("rcode = %d, want NOTZONE (%d)", rcode, dns.RcodeNotZone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_ZoneSectionNotSOA_FORMERR(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
func TestUpdate_UnknownZone_NOTAUTH(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// Hand-built broken UPDATE: zone section type is TXT, not SOA.
|
||||
// Build UPDATE for a zone we don't manage.
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate("auth.example.com.")
|
||||
m.Question[0].Qtype = dns.TypeTXT // <-- wrong; must be SOA per RFC 2136
|
||||
m.SetUpdate("other.zone.")
|
||||
m.Ns = []dns.RR{mustRR(t, `x.other.zone. 60 IN TXT "y"`)}
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, m)
|
||||
if rcode != dns.RcodeFormatError {
|
||||
t.Errorf("rcode = %d, want FORMERR (%d)", rcode, dns.RcodeFormatError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_UnauthorisedZone_NOTAUTH(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate("not-our-zone.com.") // we're not authoritative for this
|
||||
m.Ns = []dns.RR{mustRR(t, `x.not-our-zone.com. 60 IN TXT "hi"`)}
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, m)
|
||||
if rcode != dns.RcodeNotAuth {
|
||||
if rcode := runUpdate(t, p, m); rcode != dns.RcodeNotAuth {
|
||||
t.Errorf("rcode = %d, want NOTAUTH (%d)", rcode, dns.RcodeNotAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_PrereqNameExists_NXDOMAIN(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
func TestUpdate_ApexSOADeletion_Refused(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// Prereq: name must exist (CLASS=ANY, TYPE=ANY). It doesn't.
|
||||
prereq := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "ghost.auth.example.com.",
|
||||
// CLASS=ANY type=SOA at apex → would wipe the SOA.
|
||||
del := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "auth.example.com.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassANY,
|
||||
}}
|
||||
if rcode := runUpdate(t, p, newUpdate("auth.example.com", del)); rcode != dns.RcodeRefused {
|
||||
t.Errorf("rcode = %d, want REFUSED", rcode)
|
||||
}
|
||||
|
||||
// SOA must still be in the file.
|
||||
hasSOA := false
|
||||
for _, rr := range readZoneRecords(t, p, "auth.example.com") {
|
||||
if _, ok := rr.(*dns.SOA); ok {
|
||||
hasSOA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSOA {
|
||||
t.Errorf("SOA was deleted despite REFUSED rcode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_ApexWipe_Refused(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
del := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "auth.example.com.",
|
||||
Rrtype: dns.TypeANY,
|
||||
Class: dns.ClassANY,
|
||||
}}
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate("auth.example.com.")
|
||||
m.Answer = []dns.RR{prereq}
|
||||
m.Ns = []dns.RR{mustRR(t, `x.auth.example.com. 60 IN TXT "y"`)}
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, m)
|
||||
if rcode != dns.RcodeNameError {
|
||||
t.Errorf("rcode = %d, want NXDOMAIN (%d)", rcode, dns.RcodeNameError)
|
||||
if rcode := runUpdate(t, p, newUpdate("auth.example.com", del)); rcode != dns.RcodeRefused {
|
||||
t.Errorf("rcode = %d, want REFUSED", rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_PrereqRRsetMustNotExist_YXRRSET(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
p.store.Add(mustRR(t, `existing.auth.example.com. 60 IN TXT "present"`))
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// Seed a TXT then send an UPDATE whose prereq says "no TXT here".
|
||||
runUpdate(t, p, newUpdate("auth.example.com",
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "present"`),
|
||||
))
|
||||
|
||||
// Prereq: TXT RRset must NOT exist at this name (CLASS=NONE, TYPE=TXT).
|
||||
prereq := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "existing.auth.example.com.",
|
||||
Name: "foo.auth.example.com.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassNONE,
|
||||
}}
|
||||
m := new(dns.Msg)
|
||||
m.SetUpdate("auth.example.com.")
|
||||
m.Answer = []dns.RR{prereq}
|
||||
m.Ns = []dns.RR{mustRR(t, `foo.auth.example.com. 60 IN TXT "should-not-be-added"`)}
|
||||
|
||||
rcode, _ := applyUpdateNoAuth(t, p, m)
|
||||
if rcode != dns.RcodeYXRrset {
|
||||
t.Errorf("rcode = %d, want YXRRSET (%d)", rcode, dns.RcodeYXRrset)
|
||||
if rcode := runUpdate(t, p, m); rcode != dns.RcodeYXRrset {
|
||||
t.Errorf("rcode = %d, want YXRRSET", rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_ApexSOA_RefusedForAdd(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
func TestUpdate_NoOpAdd_DoesntRewriteFile(t *testing.T) {
|
||||
p := newTestPluginWithZone(t, "auth.example.com")
|
||||
|
||||
// Attempting to add an SOA at the apex must be refused — we serve
|
||||
// SOA synthetically.
|
||||
soa := mustRR(t, `auth.example.com. 60 IN SOA ns.example.com. admin.auth.example.com. 1 3600 600 604800 60`)
|
||||
rcode, _ := applyUpdateNoAuth(t, p, newUpdate("auth.example.com.", soa))
|
||||
if rcode != dns.RcodeRefused {
|
||||
t.Errorf("rcode = %d, want REFUSED (%d) for SOA-at-apex add", rcode, dns.RcodeRefused)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_ApexDeletion_Refused(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
|
||||
// CLASS=ANY, TYPE=ANY at the apex → would wipe the zone. Refuse.
|
||||
del := &dns.ANY{Hdr: dns.RR_Header{
|
||||
Name: "auth.example.com.",
|
||||
Rrtype: dns.TypeANY,
|
||||
Class: dns.ClassANY,
|
||||
}}
|
||||
rcode, _ := applyUpdateNoAuth(t, p, newUpdate("auth.example.com.", del))
|
||||
if rcode != dns.RcodeRefused {
|
||||
t.Errorf("rcode = %d, want REFUSED for apex wipe", rcode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_DefaultTTL_Applied(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
p.TTL = 120 // configure non-default
|
||||
|
||||
// Build an UPDATE add with TTL=0 → plugin should fill in p.TTL.
|
||||
rr := mustRR(t, `foo.auth.example.com. 0 IN TXT "x"`)
|
||||
rcode, _ := applyUpdateNoAuth(t, p, newUpdate("auth.example.com.", rr))
|
||||
if rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("rcode = %d", rcode)
|
||||
}
|
||||
|
||||
got := p.store.Lookup("foo.auth.example.com.", dns.TypeTXT)
|
||||
if got[0].Header().Ttl != 120 {
|
||||
t.Errorf("TTL = %d, want default 120", got[0].Header().Ttl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate_GenerationBumps(t *testing.T) {
|
||||
p := newTestPlugin("auth.example.com.", "ns.example.com.", nil)
|
||||
start := p.store.generation()
|
||||
|
||||
upd := newUpdate("auth.example.com.",
|
||||
// Add a record once.
|
||||
runUpdate(t, p, newUpdate("auth.example.com",
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "x"`),
|
||||
)
|
||||
applyUpdateNoAuth(t, p, upd)
|
||||
))
|
||||
|
||||
if p.store.generation() <= start {
|
||||
t.Errorf("generation did not bump: was %d, still %d", start, p.store.generation())
|
||||
path := p.zones["auth.example.com."].Path
|
||||
beforeStat, _ := os.Stat(path)
|
||||
|
||||
// Same UPDATE again — should be a no-op (dedupe).
|
||||
runUpdate(t, p, newUpdate("auth.example.com",
|
||||
mustRR(t, `foo.auth.example.com. 60 IN TXT "x"`),
|
||||
))
|
||||
|
||||
afterStat, _ := os.Stat(path)
|
||||
if afterStat.ModTime().After(beforeStat.ModTime()) {
|
||||
t.Errorf("file was rewritten despite no-op update (mtime advanced)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindZone_LongestSuffixWins(t *testing.T) {
|
||||
p := &RFC2136{Zones: []string{"example.com.", "auth.example.com."}}
|
||||
if got := p.findZone("foo.auth.example.com."); got != "auth.example.com." {
|
||||
t.Errorf("findZone returned %q, expected longest match", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindZone_OutsideAllZones(t *testing.T) {
|
||||
p := &RFC2136{Zones: []string{"auth.example.com."}}
|
||||
if got := p.findZone("other.tld."); got != "" {
|
||||
t.Errorf("findZone returned %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindZone_CaseInsensitive(t *testing.T) {
|
||||
p := &RFC2136{Zones: []string{"auth.example.com."}}
|
||||
if got := p.findZone("Foo.AUTH.example.COM."); got != "auth.example.com." {
|
||||
t.Errorf("case-insensitive findZone returned %q", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue