initial commit
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user