more code
This commit is contained in:
1 parent
f24fd63e9a
commit
ee72257c64
29 files changed
+5636
-440
No files matched your search
@@ -0,0 +1,806 @@
|
||||
package zonefile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func mustParse(t *testing.T, src string, opt Options) *Zone {
|
||||
t.Helper()
|
||||
z, err := Parse(strings.NewReader(src), opt)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: unexpected error: %v\nsource:\n%s", err, src)
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
func parseErr(t *testing.T, src string, opt Options) error {
|
||||
t.Helper()
|
||||
z, err := Parse(strings.NewReader(src), opt)
|
||||
if err == nil {
|
||||
t.Fatalf("Parse: expected an error, got %d records: %v", len(z.Records), z.Records)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// summary renders every record as "line name ttl class type rdata" so a single
|
||||
// comparison covers folding, owner/TTL/class inheritance and origin resolution.
|
||||
func summary(t *testing.T, z *Zone) []string {
|
||||
t.Helper()
|
||||
out := make([]string, 0, len(z.Records))
|
||||
for _, rec := range z.Records {
|
||||
parts := strings.SplitN(rec.RR.String(), "\t", 5)
|
||||
if len(parts) != 5 {
|
||||
t.Fatalf("record line %d does not render as 5 tab-separated fields: %q", rec.Line, rec.Text())
|
||||
}
|
||||
out = append(out, fmt.Sprintf("%d %s %d %s %s %s",
|
||||
rec.Line, parts[0], rec.TTL, parts[2], parts[3], parts[4]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func checkSummary(t *testing.T, z *Zone, want []string) {
|
||||
t.Helper()
|
||||
got := summary(t, z)
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("records mismatch\n got: %q\nwant: %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTTL(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want uint32
|
||||
wantErr bool
|
||||
}{
|
||||
{in: "", wantErr: true},
|
||||
{in: " ", wantErr: true},
|
||||
{in: "300", want: 300},
|
||||
{in: " 60 ", want: 60},
|
||||
{in: "0", want: 0},
|
||||
{in: "300s", want: 300},
|
||||
{in: "300sec", want: 300},
|
||||
{in: "300seconds", want: 300},
|
||||
{in: "5m", want: 300},
|
||||
{in: "5min", want: 300},
|
||||
{in: "5minutes", want: 300},
|
||||
{in: "1h", want: 3600},
|
||||
{in: "1hour", want: 3600},
|
||||
{in: "1hours", want: 3600},
|
||||
{in: "1d", want: 86400},
|
||||
{in: "1days", want: 86400},
|
||||
{in: "1w", want: 604800},
|
||||
{in: "1weeks", want: 604800},
|
||||
{in: "1h30m", want: 5400},
|
||||
{in: "1w2d3h4m5s", want: 788645},
|
||||
{in: "1h30", want: 3630},
|
||||
{in: "4294967295", want: 4294967295},
|
||||
{in: "4294967296", wantErr: true},
|
||||
{in: "-1", wantErr: true},
|
||||
{in: "-5m", wantErr: true},
|
||||
{in: "abc", wantErr: true},
|
||||
{in: "m", wantErr: true},
|
||||
{in: "1x", wantErr: true},
|
||||
{in: "1.5h", wantErr: true},
|
||||
{in: "1h-", wantErr: true},
|
||||
{in: "0x10", wantErr: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := ParseTTL(tc.in)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("ParseTTL(%q) = %d, want an error", tc.in, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("ParseTTL(%q) error: %v", tc.in, err)
|
||||
continue
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("ParseTTL(%q) = %d, want %d", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatTTL(t *testing.T) {
|
||||
tests := []struct {
|
||||
in uint32
|
||||
want string
|
||||
}{
|
||||
{0, "0"},
|
||||
{1, "1"},
|
||||
{59, "59"},
|
||||
{60, "1m"},
|
||||
{300, "5m"},
|
||||
{3600, "1h"},
|
||||
{86400, "1d"},
|
||||
{604800, "1w"},
|
||||
{3601, "3601"},
|
||||
{1209600, "2w"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := FormatTTL(tc.in); got != tc.want {
|
||||
t.Errorf("FormatTTL(%d) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatTTLRoundTrip(t *testing.T) {
|
||||
for _, ttl := range []uint32{0, 1, 59, 60, 300, 3600, 3601, 86400, 604800, 1209600, 4294967295} {
|
||||
s := FormatTTL(ttl)
|
||||
got, err := ParseTTL(s)
|
||||
if err != nil {
|
||||
t.Errorf("ParseTTL(FormatTTL(%d)=%q) error: %v", ttl, s, err)
|
||||
continue
|
||||
}
|
||||
if got != ttl {
|
||||
t.Errorf("round trip of %d via %q = %d", ttl, s, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerialNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current uint32
|
||||
spec string
|
||||
want uint32
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "increment", current: 1, spec: "+1", want: 2},
|
||||
{name: "increment large", current: 2024010101, spec: "+100", want: 2024010201},
|
||||
{name: "increment from zero", current: 0, spec: "+7", want: 7},
|
||||
{name: "decrement", current: 10, spec: "-1", want: 9},
|
||||
{name: "decrement below zero wraps", current: 0, spec: "-1", want: 4294967295},
|
||||
{name: "absolute", current: 5, spec: "42", want: 42},
|
||||
{name: "absolute max", current: 5, spec: "4294967295", want: 4294967295},
|
||||
{name: "surrounding space", current: 1, spec: " +1 ", want: 2},
|
||||
{name: "empty", current: 1, spec: "", wantErr: true},
|
||||
{name: "only sign", current: 1, spec: "+", wantErr: true},
|
||||
{name: "not a number", current: 1, spec: "+x", wantErr: true},
|
||||
{name: "word", current: 1, spec: "next", wantErr: true},
|
||||
{name: "bare minus", current: 1, spec: "-", wantErr: true},
|
||||
{name: "overflow", current: 1, spec: "+4294967296", wantErr: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := SerialNext(tc.current, tc.spec)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("SerialNext(%d, %q) = %d, want an error", tc.current, tc.spec, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("SerialNext(%d, %q): %v", tc.current, tc.spec, err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Errorf("SerialNext(%d, %q) = %d, want %d", tc.current, tc.spec, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// addSerial is plain modular (RFC 1982 §2.1) arithmetic: 0xFFFFFFFF + 1 is 0,
|
||||
// not 1. The high-half special case only keeps two large operands from being
|
||||
// mistaken for a wrap.
|
||||
func TestAddSerial(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b, want uint32
|
||||
}{
|
||||
{1, 1, 2},
|
||||
{0, 0, 0},
|
||||
{4294967295, 1, 0},
|
||||
{4294967295, 2, 1},
|
||||
{4294967294, 3, 1},
|
||||
{0, 4294967295, 4294967295},
|
||||
{0x7FFFFFFF, 1, 0x80000000},
|
||||
{0x80000000, 0x80000000, 0},
|
||||
{0x80000000, 0x7FFFFFFF, 0xFFFFFFFF},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := addSerial(tc.a, tc.b); got != tc.want {
|
||||
t.Errorf("addSerial(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fullZone = `$ORIGIN example.com.
|
||||
$TTL 1h
|
||||
@ IN SOA ns1.example.com. hostmaster.example.com. (
|
||||
2024010101 ; serial
|
||||
7200 ; refresh
|
||||
3600 ; retry
|
||||
1209600 ; expire
|
||||
300 ) ; minimum
|
||||
@ IN NS ns1.example.com.
|
||||
www IN A 192.0.2.1
|
||||
www 600 IN A 192.0.2.2
|
||||
AAAA 2001:db8::1
|
||||
alias IN CNAME www.example.com.
|
||||
mail IN MX 10 mx1.example.com.
|
||||
note IN TXT "hello ; world @ example"
|
||||
*.wild IN A 192.0.2.3
|
||||
198.51.100.5. IN A 198.51.100.5
|
||||
low in a 192.0.2.7
|
||||
mixed 600 IN A 192.0.2.8
|
||||
cls in 3600 A 192.0.2.9
|
||||
at IN TXT "@"
|
||||
|
||||
; a whole-line comment
|
||||
; an indented comment
|
||||
|
||||
`
|
||||
|
||||
func TestParseFullZone(t *testing.T) {
|
||||
z := mustParse(t, fullZone, Options{File: "full.zone", Origin: "ignored.example."})
|
||||
if z.Origin != "example.com." {
|
||||
t.Errorf("Origin = %q, want %q", z.Origin, "example.com.")
|
||||
}
|
||||
if z.File != "full.zone" {
|
||||
t.Errorf("File = %q, want %q", z.File, "full.zone")
|
||||
}
|
||||
checkSummary(t, z, []string{
|
||||
`3 example.com. 3600 IN SOA ns1.example.com. hostmaster.example.com. 2024010101 7200 3600 1209600 300`,
|
||||
`9 example.com. 3600 IN NS ns1.example.com.`,
|
||||
`10 www.example.com. 3600 IN A 192.0.2.1`,
|
||||
`11 www.example.com. 600 IN A 192.0.2.2`,
|
||||
`12 www.example.com. 3600 IN AAAA 2001:db8::1`,
|
||||
`13 alias.example.com. 3600 IN CNAME www.example.com.`,
|
||||
`14 mail.example.com. 3600 IN MX 10 mx1.example.com.`,
|
||||
`15 note.example.com. 3600 IN TXT "hello ; world @ example"`,
|
||||
`16 *.wild.example.com. 3600 IN A 192.0.2.3`,
|
||||
`17 198.51.100.5. 3600 IN A 198.51.100.5`,
|
||||
`18 low.example.com. 3600 IN A 192.0.2.7`,
|
||||
`19 mixed.example.com. 600 IN A 192.0.2.8`,
|
||||
`20 cls.example.com. 3600 IN A 192.0.2.9`,
|
||||
`21 at.example.com. 3600 IN TXT "@"`,
|
||||
})
|
||||
if z.SOA == nil {
|
||||
t.Fatal("SOA = nil, want the parsed start of authority")
|
||||
}
|
||||
if z.SOA.Serial != 2024010101 {
|
||||
t.Errorf("SOA.Serial = %d, want 2024010101", z.SOA.Serial)
|
||||
}
|
||||
if z.DefaultTTL != 3600 {
|
||||
t.Errorf("DefaultTTL = %d, want 3600 ($TTL 1h)", z.DefaultTTL)
|
||||
}
|
||||
if z.MinTTL != 600 {
|
||||
t.Errorf("MinTTL = %d, want 600 (the smallest explicit TTL)", z.MinTTL)
|
||||
}
|
||||
for _, rec := range z.Records {
|
||||
if rec.RR.Header().Class != dns.ClassINET {
|
||||
t.Errorf("record %q has class %s, want IN", rec.Text(), dns.ClassToString[rec.RR.Header().Class])
|
||||
}
|
||||
if rec.Name != strings.ToLower(rec.Name) {
|
||||
t.Errorf("record name %q is not lowercased", rec.Name)
|
||||
}
|
||||
if !strings.HasSuffix(rec.Name, ".") {
|
||||
t.Errorf("record name %q is not fully qualified", rec.Name)
|
||||
}
|
||||
if rec.File != "full.zone" {
|
||||
t.Errorf("record %q has File %q, want full.zone", rec.Text(), rec.File)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTTLInheritance(t *testing.T) {
|
||||
z := mustParse(t, `$ORIGIN example.com.
|
||||
$TTL 1h
|
||||
one IN A 192.0.2.1
|
||||
two 300 IN A 192.0.2.2
|
||||
three IN A 192.0.2.3
|
||||
$TTL 2h
|
||||
four IN A 192.0.2.4
|
||||
`, Options{})
|
||||
checkSummary(t, z, []string{
|
||||
`2 one.example.com. 3600 IN A 192.0.2.1`,
|
||||
`3 two.example.com. 300 IN A 192.0.2.2`,
|
||||
`4 three.example.com. 3600 IN A 192.0.2.3`,
|
||||
`6 four.example.com. 7200 IN A 192.0.2.4`,
|
||||
})
|
||||
if z.DefaultTTL != 7200 {
|
||||
t.Errorf("DefaultTTL = %d, want 7200", z.DefaultTTL)
|
||||
}
|
||||
if z.MinTTL != 300 {
|
||||
t.Errorf("MinTTL = %d, want 300", z.MinTTL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDefaultTTLSources(t *testing.T) {
|
||||
t.Run("option default ttl", func(t *testing.T) {
|
||||
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "example.com.", DefaultTTL: 42})
|
||||
if len(z.Records) != 1 || z.Records[0].TTL != 42 {
|
||||
t.Fatalf("records = %v, want one record with TTL 42", summary(t, z))
|
||||
}
|
||||
if z.DefaultTTL != 42 {
|
||||
t.Errorf("DefaultTTL = %d, want 42", z.DefaultTTL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no ttl source leaves zero", func(t *testing.T) {
|
||||
z := mustParse(t, "host IN A 192.0.2.1\n", Options{Origin: "example.com."})
|
||||
if len(z.Records) != 1 || z.Records[0].TTL != 0 {
|
||||
t.Fatalf("records = %v, want one record with TTL 0", summary(t, z))
|
||||
}
|
||||
if z.MinTTL != 0 {
|
||||
t.Errorf("MinTTL = %d, want 0", z.MinTTL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("soa ttl is the default", func(t *testing.T) {
|
||||
z := mustParse(t, "@ 300 IN SOA ns1.example.com. hostmaster.example.com. 1 2 3 4 5\nhost IN A 192.0.2.1\n",
|
||||
Options{Origin: "example.com."})
|
||||
if z.DefaultTTL != 300 {
|
||||
t.Errorf("DefaultTTL = %d, want 300 from the SOA", z.DefaultTTL)
|
||||
}
|
||||
if z.Records[1].TTL != 300 {
|
||||
t.Errorf("records = %v, want 300 inherited from the SOA", summary(t, z))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("soa minttl is the last resort", func(t *testing.T) {
|
||||
z := mustParse(t, "@ IN SOA ns1.example.com. hostmaster.example.com. 1 2 3 4 7\nhost IN A 192.0.2.1\n",
|
||||
Options{Origin: "example.com."})
|
||||
if z.Records[1].TTL != 7 {
|
||||
t.Errorf("records = %v, want the SOA minimum 7", summary(t, z))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseOwnerForms(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "at sign is the origin",
|
||||
src: "@ IN A 192.0.2.1\n@ IN A 192.0.2.2\n",
|
||||
want: []string{"origin.example. 192.0.2.1", "origin.example. 192.0.2.2"},
|
||||
},
|
||||
{
|
||||
name: "at sign follows $ORIGIN",
|
||||
src: "$ORIGIN sub.example.com.\n@ IN A 192.0.2.1\n",
|
||||
want: []string{"sub.example.com. 192.0.2.1"},
|
||||
},
|
||||
{
|
||||
name: "omitted owner repeats the previous one",
|
||||
src: "one IN A 192.0.2.1\n IN A 192.0.2.2\n IN AAAA 2001:db8::1\n A 192.0.2.3\n",
|
||||
want: []string{
|
||||
"one.origin.example. 192.0.2.1",
|
||||
"one.origin.example. 192.0.2.2",
|
||||
"one.origin.example. 2001:db8::1",
|
||||
"one.origin.example. 192.0.2.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "omitted owner after a parenthesised record",
|
||||
src: "one IN TXT ( \"one\" ; comment\n \"two\" )\n IN A 192.0.2.1\n",
|
||||
want: []string{"one.origin.example. \"one\" \"two\"", "one.origin.example. 192.0.2.1"},
|
||||
},
|
||||
{
|
||||
name: "wildcard owner is expanded but keeps the star",
|
||||
src: "*.sub IN A 192.0.2.1\n",
|
||||
want: []string{"*.sub.origin.example. 192.0.2.1"},
|
||||
},
|
||||
{
|
||||
name: "uppercase owner is lowercased",
|
||||
src: "WWW IN A 192.0.2.1\n",
|
||||
want: []string{"www.origin.example. 192.0.2.1"},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
z := mustParse(t, tc.src, Options{Origin: "origin.example."})
|
||||
got := make([]string, 0, len(z.Records))
|
||||
for _, rec := range z.Records {
|
||||
parts := strings.SplitN(rec.RR.String(), "\t", 5)
|
||||
got = append(got, parts[0]+" "+parts[4])
|
||||
}
|
||||
if !slices.Equal(got, tc.want) {
|
||||
t.Errorf("got %q, want %q", got, tc.want)
|
||||
}
|
||||
// Record.Name is the resolved, lowercased, fully qualified owner:
|
||||
// "@" always resolves to the origin, never to a literal "@" label.
|
||||
for _, rec := range z.Records {
|
||||
if strings.ContainsAny(rec.Name, "@") {
|
||||
t.Errorf("Record.Name = %q, want an expanded owner", rec.Name)
|
||||
}
|
||||
if !strings.HasSuffix(rec.Name, ".") {
|
||||
t.Errorf("Record.Name = %q, want a fully qualified name", rec.Name)
|
||||
}
|
||||
if rec.Name != strings.ToLower(rec.Name) {
|
||||
t.Errorf("Record.Name = %q, want lowercase", rec.Name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseParenthesised(t *testing.T) {
|
||||
t.Run("multi line rdata", func(t *testing.T) {
|
||||
z := mustParse(t, `@ SOA ns1.example.com. hostmaster.example.com. (
|
||||
2024010101
|
||||
7200
|
||||
3600
|
||||
1209600
|
||||
300 )
|
||||
`, Options{Origin: "example.com."})
|
||||
if z.SOA == nil {
|
||||
t.Fatal("SOA = nil")
|
||||
}
|
||||
if z.SOA.Minttl != 300 {
|
||||
t.Errorf("SOA.Minttl = %d, want 300", z.SOA.Minttl)
|
||||
}
|
||||
if z.Records[0].Line != 1 {
|
||||
t.Errorf("Line = %d, want 1 (the start of the folded record)", z.Records[0].Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("comment inside parentheses", func(t *testing.T) {
|
||||
z := mustParse(t, "host TXT ( \"one\" ; first\n \"two\" ; second\n )\n", Options{Origin: "example.com."})
|
||||
if len(z.Records) != 1 {
|
||||
t.Fatalf("got %d records, want 1", len(z.Records))
|
||||
}
|
||||
if got := summary(t, z)[0]; !strings.HasSuffix(got, `IN TXT "one" "two"`) {
|
||||
t.Errorf("record = %q, want a two-string TXT", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rdata on one line with parens", func(t *testing.T) {
|
||||
z := mustParse(t, "host MX ( 10 mx1.example.com. 20 mx2.example.com. )\n", Options{Origin: "example.com."})
|
||||
if len(z.Records) != 1 {
|
||||
t.Fatalf("got %d records, want 1", len(z.Records))
|
||||
}
|
||||
if got := summary(t, z)[0]; !strings.HasSuffix(got, "IN MX 10 mx1.example.com. 20 mx2.example.com.") {
|
||||
t.Errorf("record = %q, want two MX rdata fields", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseOriginDirective(t *testing.T) {
|
||||
t.Run("relative names follow $ORIGIN", func(t *testing.T) {
|
||||
z := mustParse(t, "$ORIGIN sub.example.com.\nwww IN A 192.0.2.1\n", Options{Origin: "example.com."})
|
||||
if z.Origin != "sub.example.com." {
|
||||
t.Errorf("Origin = %q, want sub.example.com.", z.Origin)
|
||||
}
|
||||
if z.Records[0].Name != "www.sub.example.com." {
|
||||
t.Errorf("name = %q, want www.sub.example.com.", z.Records[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("absolute names are left alone", func(t *testing.T) {
|
||||
z := mustParse(t, "$ORIGIN sub.example.com.\nwww.other.example. IN A 192.0.2.1\n", Options{})
|
||||
if z.Records[0].Name != "www.other.example." {
|
||||
t.Errorf("name = %q, want www.other.example.", z.Records[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no origin at all is the root", func(t *testing.T) {
|
||||
z := mustParse(t, "host IN A 192.0.2.1\n", Options{})
|
||||
if z.Origin != "." {
|
||||
t.Errorf("Origin = %q, want .", z.Origin)
|
||||
}
|
||||
if z.Records[0].Name != "host." {
|
||||
t.Errorf("name = %q, want host.", z.Records[0].Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("$ORIGIN without a name is an error", func(t *testing.T) {
|
||||
// BIND resets to the zone origin when $ORIGIN carries no argument;
|
||||
// this parser rejects it instead. Documented as current behaviour.
|
||||
err := parseErr(t, "$ORIGIN example.com.\n$ORIGIN\n", Options{})
|
||||
if !strings.Contains(err.Error(), "$ORIGIN needs a name") {
|
||||
t.Errorf("error = %q, want it to mention \"$ORIGIN needs a name\"", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("origin option applies when the file has no $ORIGIN", func(t *testing.T) {
|
||||
z := mustParse(t, "www IN A 192.0.2.1\n", Options{Origin: "example.com"})
|
||||
if z.Origin != "example.com." || z.Records[0].Name != "www.example.com." {
|
||||
t.Errorf("origin = %q, name = %q", z.Origin, z.Records[0].Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseClassHandling(t *testing.T) {
|
||||
z := mustParse(t, `one IN A 192.0.2.1
|
||||
two in A 192.0.2.2
|
||||
three A 192.0.2.3
|
||||
four 300 in A 192.0.2.4
|
||||
five ch A 192.0.2.5
|
||||
six hs A 192.0.2.6
|
||||
seven 300 A 192.0.2.7
|
||||
`, Options{Origin: "example.com."})
|
||||
if len(z.Records) != 7 {
|
||||
t.Fatalf("got %d records, want 7", len(z.Records))
|
||||
}
|
||||
want := []uint16{
|
||||
dns.ClassINET, dns.ClassINET, dns.ClassINET, dns.ClassINET,
|
||||
dns.ClassCHAOS, dns.ClassHESIOD, dns.ClassINET,
|
||||
}
|
||||
for i, rec := range z.Records {
|
||||
if rec.RR.Header().Class != want[i] {
|
||||
t.Errorf("%q has class %s, want %s", rec.Text(),
|
||||
dns.ClassToString[rec.RR.Header().Class], dns.ClassToString[want[i]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommentsAndBlankLines(t *testing.T) {
|
||||
z := mustParse(t, `; leading comment
|
||||
$TTL 1h
|
||||
|
||||
; indented comment
|
||||
|
||||
one IN A 192.0.2.1 ; trailing comment
|
||||
|
||||
|
||||
two IN A 192.0.2.2 ;comment without space
|
||||
three IN TXT "semi ; colon" ; real comment
|
||||
`, Options{Origin: "example.com."})
|
||||
if len(z.Records) != 3 {
|
||||
t.Fatalf("got %d records, want 3: %v", len(z.Records), summary(t, z))
|
||||
}
|
||||
if !strings.Contains(summary(t, z)[2], `"semi ; colon"`) {
|
||||
t.Errorf("record = %q, want the quoted semicolon preserved", summary(t, z)[2])
|
||||
}
|
||||
for _, rec := range z.Records {
|
||||
if rec.RR.Header().Ttl != 3600 {
|
||||
t.Errorf("%q TTL = %d, want 3600", rec.Text(), rec.RR.Header().Ttl)
|
||||
}
|
||||
}
|
||||
checkSummary(t, z, []string{
|
||||
`6 one.example.com. 3600 IN A 192.0.2.1`,
|
||||
`9 two.example.com. 3600 IN A 192.0.2.2`,
|
||||
`10 three.example.com. 3600 IN TXT "semi ; colon"`,
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
opt Options
|
||||
wantSub string
|
||||
}{
|
||||
{name: "no type", src: "host 3600\n", wantSub: "has no type"},
|
||||
{name: "bad token before type", src: "host BOGUS 1.2.3.4\n", wantSub: `unexpected token "BOGUS"`},
|
||||
{name: "bad rdata", src: "host 3600 IN A not-an-ip\n", wantSub: ""},
|
||||
{name: "unterminated parenthesis", src: "host 3600 IN A ( 192.0.2.1\n", wantSub: "unterminated parenthesis"},
|
||||
{name: "unterminated parenthesis with a blank line", src: "host 3600 IN A (\n\n 192.0.2.1\n", wantSub: "unterminated parenthesis"},
|
||||
{name: "unterminated quote", src: "host 3600 IN TXT \"unterminated\n", wantSub: ""},
|
||||
{name: "no owner and no previous owner", src: "A 192.0.2.1\n", wantSub: "no owner name"},
|
||||
{name: "bad ttl directive", src: "$TTL forever\n", wantSub: "bad TTL"},
|
||||
{name: "ttl directive without a value", src: "$TTL\n", wantSub: "$TTL needs a value"},
|
||||
{name: "origin directive without a name", src: "$ORIGIN\n", wantSub: "$ORIGIN needs a name"},
|
||||
{name: "include disabled", src: "$INCLUDE other.zone\n", wantSub: "disabled"},
|
||||
{name: "include unsupported", src: "$INCLUDE other.zone\n", opt: Options{MaxIncludeDepth: 3}, wantSub: "not supported"},
|
||||
{name: "include without a name", src: "$INCLUDE\n", wantSub: "$INCLUDE needs a file name"},
|
||||
{name: "error carries file and line", src: "host 3600 IN A 192.0.2.1\nother 3600 IN A nope\n", opt: Options{File: "bad.zone"}, wantSub: "bad.zone:2:"},
|
||||
{name: "directive error carries the line", src: "$TTL 1h\n$TTL\n", opt: Options{File: "bad.zone"}, wantSub: "bad.zone:2:"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := parseErr(t, tc.src, tc.opt)
|
||||
if tc.wantSub != "" && !strings.Contains(err.Error(), tc.wantSub) {
|
||||
t.Errorf("error = %q, want it to contain %q", err, tc.wantSub)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmptyFile(t *testing.T) {
|
||||
for _, src := range []string{"", "\n\n", " \n\t\n", "; only a comment\n"} {
|
||||
z := mustParse(t, src, Options{Origin: "example.com."})
|
||||
if len(z.Records) != 0 {
|
||||
t.Errorf("Parse(%q) returned %d records, want 0", src, len(z.Records))
|
||||
}
|
||||
if z.SOA != nil {
|
||||
t.Errorf("Parse(%q) returned an SOA, want nil", src)
|
||||
}
|
||||
if z.Origin != "example.com." {
|
||||
t.Errorf("Parse(%q) origin = %q, want example.com.", src, z.Origin)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFileAndParseFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
a := filepath.Join(dir, "a.zone")
|
||||
b := filepath.Join(dir, "b.zone")
|
||||
if err := os.WriteFile(a, []byte("$TTL 1h\nwww IN A 192.0.2.1\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(b, []byte("$TTL 2h\nmail IN A 192.0.2.2\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
z, err := ParseFile(a, "example.com.")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFile: %v", err)
|
||||
}
|
||||
if z.File != a || z.Origin != "example.com." || len(z.Records) != 1 {
|
||||
t.Fatalf("ParseFile = %+v", z)
|
||||
}
|
||||
if z.Records[0].Name != "www.example.com." {
|
||||
t.Errorf("name = %q, want www.example.com.", z.Records[0].Name)
|
||||
}
|
||||
if _, err := ParseFile(filepath.Join(dir, "missing.zone"), "example.com."); err == nil {
|
||||
t.Error("ParseFile on a missing file: want an error")
|
||||
}
|
||||
|
||||
merged, err := ParseFiles([]string{a, b}, OriginFromPath)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseFiles: %v", err)
|
||||
}
|
||||
if len(merged.Records) != 2 {
|
||||
t.Fatalf("ParseFiles returned %d records, want 2", len(merged.Records))
|
||||
}
|
||||
if merged.DefaultTTL != 7200 {
|
||||
t.Errorf("merged DefaultTTL = %d, want 7200 (the last file wins)", merged.DefaultTTL)
|
||||
}
|
||||
if _, err := ParseFiles([]string{a, filepath.Join(dir, "missing.zone")}, OriginFromPath); err == nil {
|
||||
t.Error("ParseFiles with a missing file: want an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOriginFromPath(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"ads.zone", "ads."},
|
||||
{"rpz/ads.zone", "ads."},
|
||||
{"example.com.zone", "example.com."},
|
||||
{"/var/lib/fancy-dns/example.com.zone", "example.com."},
|
||||
{"plain", "plain."},
|
||||
{".hidden", ".hidden."},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := OriginFromPath(tc.in); got != tc.want {
|
||||
t.Errorf("OriginFromPath(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDot(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"", ""},
|
||||
{".", "."},
|
||||
{"example.com", "example.com."},
|
||||
{"example.com.", "example.com."},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := ensureDot(tc.in); got != tc.want {
|
||||
t.Errorf("ensureDot(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelative(t *testing.T) {
|
||||
tests := []struct{ name, origin, want string }{
|
||||
{"@", "example.com.", "example.com."},
|
||||
{"www", "example.com.", "www.example.com."},
|
||||
{"www.other.", "example.com.", "www.other."},
|
||||
{"www", ".", "www."},
|
||||
{"*", "example.com.", "*.example.com."},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := relative(tc.name, tc.origin); got != tc.want {
|
||||
t.Errorf("relative(%q, %q) = %q, want %q", tc.name, tc.origin, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripComment(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"a ; comment", "a "},
|
||||
{"a;comment", "a"},
|
||||
{`a "has ; semicolon" b`, `a "has ; semicolon" b`},
|
||||
{`a \; escaped`, `a \`},
|
||||
{"no comment here", "no comment here"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := stripComment(tc.in); got != tc.want {
|
||||
t.Errorf("stripComment(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"a b c", []string{"a", "b", "c"}},
|
||||
{`a "b c" d`, []string{"a", `"b c"`, "d"}},
|
||||
{" ", nil},
|
||||
{`a\ b c`, []string{`a\ b`, "c"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := splitFields(tc.in); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("splitFields(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeparen(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"a ( b", "a b"},
|
||||
{"a ) b", "a b"},
|
||||
{"a b", "a b"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := deparen(tc.in); got != tc.want {
|
||||
t.Errorf("deparen(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenClassification(t *testing.T) {
|
||||
for _, s := range []string{"0", "300", "1h30m", "300s"} {
|
||||
if !isTTLToken(s) {
|
||||
t.Errorf("isTTLToken(%q) = false, want true", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"", "-1", "IN", "in", "CH", "HS", "1x", "forever"} {
|
||||
if isTTLToken(s) {
|
||||
t.Errorf("isTTLToken(%q) = true, want false", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"IN", "in", "In", "CH", "ch", "HS", "hs"} {
|
||||
if !isClassToken(s) {
|
||||
t.Errorf("isClassToken(%q) = false, want true", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"", "300", "ANY", "cs", "in4"} {
|
||||
if isClassToken(s) {
|
||||
t.Errorf("isClassToken(%q) = true, want false", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"A", "a", "AAAA", "ns", "CNAME", "SOA", "ANY", "SVCB", "HTTPS", "nsec3"} {
|
||||
if !isRRTYPE(s) {
|
||||
t.Errorf("isRRTYPE(%q) = false, want true", s)
|
||||
}
|
||||
}
|
||||
for _, s := range []string{"", "www", "IN", "300", "BOGUS", "www.example.com."} {
|
||||
if isRRTYPE(s) {
|
||||
t.Errorf("isRRTYPE(%q) = true, want false", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneSOA(t *testing.T) {
|
||||
if cloneSOA(nil) != nil {
|
||||
t.Error("cloneSOA(nil) != nil")
|
||||
}
|
||||
in := &dns.SOA{Hdr: dns.RR_Header{Name: "example.com."}, Serial: 7, Minttl: 60}
|
||||
out := cloneSOA(in)
|
||||
if out == in {
|
||||
t.Fatal("cloneSOA returned the same pointer")
|
||||
}
|
||||
if out.Serial != 7 || out.Minttl != 60 {
|
||||
t.Errorf("cloneSOA copied %+v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereAndDirOf(t *testing.T) {
|
||||
if got := where(""); got != "zone" {
|
||||
t.Errorf("where(\"\") = %q, want zone", got)
|
||||
}
|
||||
if got := where("a.zone"); got != "a.zone" {
|
||||
t.Errorf("where(a.zone) = %q", got)
|
||||
}
|
||||
if got := dirOf("a.zone"); got != "." {
|
||||
t.Errorf("dirOf(a.zone) = %q, want .", got)
|
||||
}
|
||||
if got := dirOf("/x/y/a.zone"); got != "/x/y" {
|
||||
t.Errorf("dirOf(/x/y/a.zone) = %q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user