358 lines
11 KiB
Go
358 lines
11 KiB
Go
package validate
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormaliseFQDN(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{"example.com", "example.com.", false},
|
|
{"example.com.", "example.com.", false},
|
|
{"EXAMPLE.COM", "example.com.", false},
|
|
{" example.com ", "example.com.", false},
|
|
{"a.b.c.example.com", "a.b.c.example.com.", false},
|
|
{"_dmarc.example.com", "_dmarc.example.com.", false}, // underscores are needed by SRV/DKIM
|
|
{".", ".", false},
|
|
{"", "", true},
|
|
{"example..com", "", true},
|
|
{"-bad.example.com", "", true},
|
|
{"bad-.example.com", "", true},
|
|
{"exa mple.com", "", true},
|
|
{strings.Repeat("a", 64) + ".example.com", "", true}, // label too long
|
|
{strings.Repeat("a.", 130) + "example.com", "", true}, // name too long
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
got, err := NormaliseFQDN(tc.in)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("expected an error, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormaliseRecordName(t *testing.T) {
|
|
const zone = "example.com."
|
|
tests := []struct {
|
|
in string
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{"", "@", false},
|
|
{"@", "@", false},
|
|
{"www", "www", false},
|
|
{"WWW", "www", false},
|
|
{"www.example.com.", "www", false}, // absolute inside the zone
|
|
{"example.com.", "@", false}, // the apex itself
|
|
{"*", "*", false}, // wildcard
|
|
{"*.sub", "*.sub", false},
|
|
{"a.b.c", "a.b.c", false},
|
|
{"www.example.org.", "", true}, // outside the zone
|
|
{"sub.*", "", true}, // wildcard must be leftmost
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
got, err := NormaliseRecordName(tc.in, zone)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("expected an error, got %q", got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestReverseZone covers the feature that spares the operator from reversing
|
|
// octets by hand.
|
|
func TestReverseZone(t *testing.T) {
|
|
tests := []struct {
|
|
cidr string
|
|
want string
|
|
wantNote bool
|
|
wantErr bool
|
|
}{
|
|
{"192.168.1.0/24", "1.168.192.in-addr.arpa.", false, false},
|
|
{"10.0.0.0/8", "10.in-addr.arpa.", false, false},
|
|
{"172.16.0.0/16", "16.172.in-addr.arpa.", false, false},
|
|
{"192.0.2.10", "10.2.0.192.in-addr.arpa.", false, false}, // bare host
|
|
// Non-octet boundaries round down to the enclosing zone with a note.
|
|
{"192.168.1.0/25", "1.168.192.in-addr.arpa.", true, false},
|
|
{"10.1.2.3/30", "2.1.10.in-addr.arpa.", true, false},
|
|
|
|
{"2001:db8::/32", "8.b.d.0.1.0.0.2.ip6.arpa.", false, false},
|
|
{"2001:db8:1::/48", "1.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.", false, false},
|
|
{"2001:db8::/33", "8.b.d.0.1.0.0.2.ip6.arpa.", true, false},
|
|
|
|
{"not-a-cidr", "", false, true},
|
|
{"192.168.1.0/4", "", false, true}, // finer than a /8 leaves no IPv4 zone
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.cidr, func(t *testing.T) {
|
|
zone, note, err := ReverseZone(tc.cidr)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Errorf("expected an error, got %q", zone)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if zone != tc.want {
|
|
t.Errorf("zone = %q, want %q", zone, tc.want)
|
|
}
|
|
if tc.wantNote && note == "" {
|
|
t.Error("expected a note explaining the rounding")
|
|
}
|
|
if !tc.wantNote && note != "" {
|
|
t.Errorf("unexpected note: %s", note)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPTRName(t *testing.T) {
|
|
tests := []struct{ ip, want string }{
|
|
{"192.0.2.10", "10.2.0.192.in-addr.arpa."},
|
|
{"10.1.2.3", "3.2.1.10.in-addr.arpa."},
|
|
}
|
|
for _, tc := range tests {
|
|
got, err := PTRName(tc.ip)
|
|
if err != nil {
|
|
t.Fatalf("PTRName(%q): %v", tc.ip, err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("PTRName(%q) = %q, want %q", tc.ip, got, tc.want)
|
|
}
|
|
}
|
|
if _, err := PTRName("not-an-ip"); err == nil {
|
|
t.Error("expected an error for an invalid address")
|
|
}
|
|
}
|
|
|
|
func TestBuildRRValidation(t *testing.T) {
|
|
const zone = "example.com."
|
|
tests := []struct {
|
|
name string
|
|
rtype string
|
|
data string
|
|
wantErr bool
|
|
}{
|
|
{"valid A", "A", "192.0.2.1", false},
|
|
{"A with IPv6", "A", "2001:db8::1", true},
|
|
{"A with garbage", "A", "not-an-ip", true},
|
|
{"valid AAAA", "AAAA", "2001:db8::1", false},
|
|
{"AAAA with IPv4", "AAAA", "192.0.2.1", true},
|
|
{"valid CNAME", "CNAME", "target.example.com.", false},
|
|
{"CNAME with two names", "CNAME", "a.example.com. b.example.com.", true},
|
|
{"valid MX", "MX", "10 mail.example.com.", false},
|
|
{"MX without preference", "MX", "mail.example.com.", true},
|
|
{"MX with bad preference", "MX", "abc mail.example.com.", true},
|
|
{"valid TXT", "TXT", `"some text"`, false},
|
|
{"unquoted TXT", "TXT", "some text", true},
|
|
{"valid SRV", "SRV", "10 20 5060 sip.example.com.", false},
|
|
{"SRV missing fields", "SRV", "10 20 sip.example.com.", true},
|
|
{"valid CAA", "CAA", `0 issue "letsencrypt.org"`, false},
|
|
{"valid TLSA", "TLSA", "3 1 1 abcdef0123456789", false},
|
|
{"valid SSHFP", "SSHFP", "4 2 abcdef0123456789", false},
|
|
{"valid DS", "DS", "12345 13 2 abcdef0123456789", false},
|
|
{"valid HTTPS", "HTTPS", "1 . alpn=h2,h3", false},
|
|
{"valid NS", "NS", "ns1.example.com.", false},
|
|
{"valid PTR", "PTR", "host.example.com.", false},
|
|
{"unknown type", "NOTATYPE", "whatever", true},
|
|
{"meta type rejected", "ANY", "whatever", true},
|
|
{"RFC3597 unknown type", "TYPE65280", `\# 4 0A0B0C0D`, false},
|
|
{"empty data", "A", "", true},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := BuildRR(zone, "test", tc.rtype, tc.data, 3600)
|
|
if tc.wantErr && err == nil {
|
|
t.Error("expected an error, got none")
|
|
}
|
|
if !tc.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCNAMEConflict covers the RFC 1034 rule that trips people up most often.
|
|
func TestCNAMEConflict(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
newType string
|
|
existing []string
|
|
wantErr bool
|
|
}{
|
|
{"CNAME on an empty name", "CNAME", nil, false},
|
|
{"second CNAME", "CNAME", []string{"CNAME"}, true},
|
|
{"CNAME alongside an A", "CNAME", []string{"A"}, true},
|
|
{"A alongside a CNAME", "A", []string{"CNAME"}, true},
|
|
{"MX alongside a CNAME", "MX", []string{"CNAME"}, true},
|
|
{"A alongside another A", "A", []string{"A"}, false},
|
|
{"A alongside AAAA", "A", []string{"AAAA"}, false},
|
|
// DNSSEC types are the standard exception.
|
|
{"RRSIG alongside a CNAME", "RRSIG", []string{"CNAME"}, false},
|
|
{"CNAME alongside RRSIG only", "CNAME", []string{"RRSIG", "NSEC"}, false},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := CNAMEConflict(tc.newType, tc.existing)
|
|
if tc.wantErr && err == nil {
|
|
t.Error("expected a conflict, got none")
|
|
}
|
|
if !tc.wantErr && err != nil {
|
|
t.Errorf("unexpected conflict: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApexRestrictions(t *testing.T) {
|
|
if err := ApexRestricted("CNAME"); err == nil {
|
|
t.Error("a CNAME at the apex must be rejected")
|
|
}
|
|
if err := ApexRestricted("DNAME"); err == nil {
|
|
t.Error("a DNAME at the apex must be rejected")
|
|
}
|
|
for _, ok := range []string{"A", "AAAA", "MX", "TXT", "NS", "HTTPS"} {
|
|
if err := ApexRestricted(ok); err != nil {
|
|
t.Errorf("%s should be allowed at the apex: %v", ok, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQuoteTXT(t *testing.T) {
|
|
tests := []struct{ in, want string }{
|
|
{"hello", `"hello"`},
|
|
{"", `""`},
|
|
{`already "quoted"`, `"already \"quoted\""`},
|
|
{`"pre-quoted"`, `"pre-quoted"`},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := QuoteTXT(tc.in); got != tc.want {
|
|
t.Errorf("QuoteTXT(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// A string past the 255-byte character-string limit must be split into
|
|
// several quoted chunks, not truncated.
|
|
long := strings.Repeat("a", 600)
|
|
got := QuoteTXT(long)
|
|
if strings.Count(got, `"`) != 6 {
|
|
t.Errorf("a 600 character string produced %d quotes, want 6 (three chunks)", strings.Count(got, `"`))
|
|
}
|
|
if _, err := BuildRR("example.com.", "long", "TXT", got, 300); err != nil {
|
|
t.Errorf("chunked TXT did not parse: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAssembleAndSplitRData(t *testing.T) {
|
|
tests := []struct {
|
|
rtype string
|
|
fields map[string]string
|
|
want string
|
|
}{
|
|
{"A", map[string]string{"address": "192.0.2.1"}, "192.0.2.1"},
|
|
{"MX", map[string]string{"preference": "10", "exchange": "mail.example.com."}, "10 mail.example.com."},
|
|
{"SRV", map[string]string{"priority": "10", "weight": "20", "port": "5060", "target": "sip.example.com."},
|
|
"10 20 5060 sip.example.com."},
|
|
{"TXT", map[string]string{"text": "v=spf1 -all"}, `"v=spf1 -all"`},
|
|
{"CAA", map[string]string{"flags": "0", "tag": "issue", "value": "letsencrypt.org"},
|
|
`0 issue "letsencrypt.org"`},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.rtype, func(t *testing.T) {
|
|
got, err := AssembleRData(tc.rtype, tc.fields)
|
|
if err != nil {
|
|
t.Fatalf("assemble: %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("assembled = %q, want %q", got, tc.want)
|
|
}
|
|
if _, err := BuildRR("example.com.", "test", tc.rtype, got, 3600); err != nil {
|
|
t.Errorf("assembled rdata does not parse: %v", err)
|
|
}
|
|
|
|
// Round trip: splitting must return the values we started with.
|
|
split := SplitRData(tc.rtype, got)
|
|
for k, v := range tc.fields {
|
|
if split[k] != v {
|
|
t.Errorf("round trip field %q = %q, want %q", k, split[k], v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssembleRequiresMandatoryFields(t *testing.T) {
|
|
if _, err := AssembleRData("MX", map[string]string{"exchange": "mail.example.com."}); err == nil {
|
|
t.Error("expected an error when a required field is missing")
|
|
}
|
|
}
|
|
|
|
func TestTypeCatalogue(t *testing.T) {
|
|
// Every type the brief asks for must have a dedicated editor.
|
|
required := []string{
|
|
"A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "PTR", "CAA", "SOA",
|
|
"NAPTR", "TLSA", "SSHFP", "SVCB", "HTTPS", "DS", "DNSKEY",
|
|
}
|
|
for _, want := range required {
|
|
info, ok := TypeInfoFor(want)
|
|
if !ok {
|
|
t.Errorf("no editor is defined for %s", want)
|
|
continue
|
|
}
|
|
if len(info.Fields) == 0 {
|
|
t.Errorf("%s has an editor with no fields", want)
|
|
}
|
|
}
|
|
if _, ok := TypeInfoFor("RAW"); !ok {
|
|
t.Error("the advanced raw editor is missing")
|
|
}
|
|
}
|
|
|
|
func TestIsSubdomain(t *testing.T) {
|
|
tests := []struct {
|
|
child, parent string
|
|
want bool
|
|
}{
|
|
{"www.example.com.", "example.com.", true},
|
|
{"example.com.", "example.com.", true},
|
|
{"a.b.example.com.", "example.com.", true},
|
|
{"notexample.com.", "example.com.", false},
|
|
{"example.org.", "example.com.", false},
|
|
{"anything.", ".", true},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := IsSubdomain(tc.child, tc.parent); got != tc.want {
|
|
t.Errorf("IsSubdomain(%q, %q) = %v, want %v", tc.child, tc.parent, got, tc.want)
|
|
}
|
|
}
|
|
}
|