package rr import ( "strings" "testing" ) const zone = "example.com." func TestParseCanonical(t *testing.T) { cases := []struct { owner, typ, data, want string }{ {"www.example.com.", "a", "192.0.2.1", "192.0.2.1"}, {"www.example.com.", "AAAA", "2001:DB8:0:0::1", "2001:db8::1"}, {"www.example.com.", "CNAME", "web", "web.example.com."}, {"www.example.com.", "CNAME", "Web.Example.Org.", "web.example.org."}, {"www.example.com.", "CNAME", "host.example.com", "host.example.com."}, {"example.com.", "MX", "10 mail", "10 mail.example.com."}, {"example.com.", "MX", "0 .", "0 ."}, {"example.com.", "TXT", "v=spf1 mx -all", `"v=spf1 mx -all"`}, {"example.com.", "TXT", `"a b" "c"`, `"a b" "c"`}, {"example.com.", "TXT", `say "hi" \o/`, `"say \"hi\" \\o/"`}, {"example.com.", "TXT", "semi; colon", `"semi; colon"`}, {"example.com.", "CAA", "0 issue letsencrypt.org", `0 issue "letsencrypt.org"`}, {"_sip._tcp.example.com.", "SRV", "0 5 5060 sip", "0 5 5060 sip.example.com."}, {"svc.example.com.", "HTTPS", `1 . alpn="h2,h3"`, `1 . alpn="h2,h3"`}, {"x.example.com.", "type65280", `\# 4 deadbeef`, `\# 4 deadbeef`}, {"10.2.0.192.in-addr.arpa.", "PTR", "host.example.com.", "host.example.com."}, {"_dmarc.example.com.", "TXT", "v=DMARC1; p=none", `"v=DMARC1; p=none"`}, {"*.example.com.", "A", "192.0.2.9", "192.0.2.9"}, } for _, c := range cases { p, err := Parse(zone, c.owner, c.typ, c.data) if err != nil { t.Errorf("Parse(%s %s %q): %v", c.owner, c.typ, c.data, err) continue } if p.Data != c.want { t.Errorf("Parse(%s %s %q) = %q, want %q", c.owner, c.typ, c.data, p.Data, c.want) } } } func TestParseWarnings(t *testing.T) { p, err := Parse(zone, "www.example.com.", "CNAME", "host.example.com") if err != nil || len(p.Warnings) != 1 || !strings.Contains(p.Warnings[0], "absolute name") { t.Errorf("double origin warning missing: %+v %v", p, err) } p, err = Parse(zone, "sip.example.com.", "SRV", "0 5 5060 sip") if err != nil || len(p.Warnings) != 1 { t.Errorf("SRV owner warning missing: %+v %v", p, err) } } func TestParseErrors(t *testing.T) { cases := []struct { owner, typ, data, want string }{ {"www.example.com.", "A", "01.2.3.4", "invalid A data"}, {"www.example.com.", "A", "", "empty"}, {"www.example.com.", "SOA", "x", "managed by Jarl"}, {"www.example.com.", "RRSIG", "x", "maintained by named"}, {"www.example.com.", "BOGUS", "x", "unknown record type"}, {"foo_bar.example.com.", "A", "1.2.3.4", "valid host name"}, {"example.com.", "MX", "10 1.2.3.4", "not an IP address"}, {"example.com.", "MX", "10 bad_host.example.org.", "not a valid host name"}, {"example.com.", "MX", "5 .", "null MX"}, {"www.example.com.", "A", "1.2.3.4\n$INCLUDE /etc/passwd", "line breaks"}, {"www.example.com.", "A", "1.2.3.4 ; comment", "comment"}, {"www.example.com.", "A", "1.2.3.4 5.6.7.8", "invalid A data"}, {"www.example.com.", "DS", "12345 13 2 abcd", "needs 32 bytes"}, {"www.example.com.", "NS", "1.2.3.4.", "not an IP address"}, {"www.example.com.", "CNAME", ".", "root"}, {"www.example.com.", "A", "$ORIGIN x", "'$'"}, } for _, c := range cases { _, err := Parse(zone, c.owner, c.typ, c.data) if err == nil || !strings.Contains(err.Error(), c.want) { t.Errorf("Parse(%s %s %q) error = %v, want %q", c.owner, c.typ, c.data, err, c.want) } } } func TestQuoteText(t *testing.T) { long := strings.Repeat("a", 600) q := QuoteText(long) if strings.Count(q, `"`) != 6 { t.Errorf("600 bytes should be 3 strings: %q", q) } p, err := Parse(zone, "k._domainkey.example.com.", "TXT", "v=DKIM1; k=rsa; p="+long) if err != nil { t.Fatal(err) } v, _ := TextValue(p.RR) if v != "v=DKIM1; k=rsa; p="+long { t.Errorf("TextValue mismatch") } // Multi-byte characters are not split across strings. s := strings.Repeat("é", 200) // 400 bytes for _, part := range strings.Split(QuoteText(s), `" "`) { if strings.ContainsRune(strings.Trim(part, `"`), '\uFFFD') { t.Error("split inside a UTF-8 sequence") } } p, err = Parse(zone, "example.com.", "TXT", `say "hi" \o/`) if err != nil { t.Fatal(err) } if v, _ := TextValue(p.RR); v != `say "hi" \o/` { t.Errorf("TextValue = %q", v) } } func TestTypes(t *testing.T) { types := Types() if types[0].Type != "A" || !types[0].Common { t.Errorf("first type = %+v", types[0]) } seen := map[string]bool{} for _, ti := range types { if seen[ti.Type] { t.Errorf("duplicate %s", ti.Type) } seen[ti.Type] = true if _, _, err := NormalizeType(ti.Type); err != nil { t.Errorf("listed type %s not accepted: %v", ti.Type, err) } if ti.Example != "" { owner := "x.example.com." switch ti.Type { case "SRV": owner = "_sip._tcp.example.com." case "TLSA": owner = "_443._tcp.example.com." } if _, err := Parse(zone, owner, ti.Type, ti.Example); err != nil { t.Errorf("example for %s fails: %v", ti.Type, err) } } } for _, bad := range []string{"SOA", "RRSIG", "OPT", "DNSKEY"} { if seen[bad] { t.Errorf("forbidden type %s listed", bad) } } } func TestParseErrorText(t *testing.T) { for _, tc := range []struct{ typ, data, want string }{ {"A", "192.168.1.300", `invalid A data: bad value "192.168.1.300"`}, {"MX", "x mail", `invalid MX data: bad Pref "x"`}, } { _, err := Parse("example.com.", "www.example.com.", tc.typ, tc.data) if err == nil || err.Error() != tc.want { t.Errorf("Parse(%s %q) error = %v, want %q", tc.typ, tc.data, err, tc.want) } } } func TestPTRTargetsInReverseZones(t *testing.T) { zone := "0.10.10.in-addr.arpa." p, err := Parse(zone, "20."+zone, "PTR", "nas.adbraun.lol") if err != nil || p.Data != "nas.adbraun.lol." || len(p.Warnings) != 1 { t.Errorf("unqualified PTR target: %v %+v", err, p) } if p, err := Parse(zone, "20."+zone, "PTR", "nas.adbraun.lol."); err != nil || p.Data != "nas.adbraun.lol." || len(p.Warnings) != 0 { t.Errorf("qualified PTR target: %v %+v", err, p) } if _, err := Parse(zone, "20."+zone, "PTR", "nas"); err == nil { t.Error("a single-label PTR target inside the reverse zone was accepted") } // Forward zones keep zone-file semantics. if p, err := Parse("adbraun.lol.", "x.adbraun.lol.", "CNAME", "nas"); err != nil || p.Data != "nas.adbraun.lol." { t.Errorf("forward CNAME: %v %+v", err, p) } }