Files
jarl/internal/rr/rr_test.go
T
abrown38andClaude Opus 5.5 bf7f99874b Web UI: zone naming and reverse zones, ACL suggestions, record form
- New zone dialog: choose Domain or Reverse zone. Reverse zones are
  created from a network and their name is generated; both kinds show the
  exact zone name and flag likely mistakes (addresses or URLs as names,
  host-like names, duplicates in the view, parent/child zones, networks
  off the octet/nibble boundaries) before anything is submitted.
- Record form: clears every field but the type after an add; shows the
  record exactly as it will be stored and warns about unqualified names.
  In reverse zones it takes an address, defaults to PTR, suggests host
  names of A/AAAA records with that address and warns about a second PTR;
  the record list shows addresses.
- Address match lists (options, view match lists, ACL elements) offer the
  existing ACLs, TSIG keys and built-ins as one-click suggestions and
  flag unknown names.
- Server: in reverse zones a record can be named by its address, and a
  PTR target like nas.example.com without the final dot is read as the
  full host name (a bare label is refused). Warnings noticed while parsing
  record data reach the result again.
- Fixes: brand link hover underline, the dashboard's "New zone" link
  opening the dialog twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:49:43 +00:00

178 lines
6.2 KiB
Go

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)
}
}