package normalize import ( "fmt" "reflect" "testing" "github.com/StackExchange/dnscontrol/v3/models" "github.com/StackExchange/dnscontrol/v3/providers" ) func TestSoaLabelAndTarget(t *testing.T) { var tests = []struct { isError bool label string target string }{ {false, "@", "ns1.foo.com."}, // Invalid target {true, "@", "ns1.foo.com"}, // Invalid label, only '@' is allowed for SOA records {true, "foo.com", "ns1.foo.com."}, } for _, test := range tests { experiment := fmt.Sprintf("%s %s", test.label, test.target) rc := makeRC(test.label, "foo.com", test.target, models.RecordConfig{Type: "SOA", SoaExpire: 1, SoaMinttl: 1, SoaRefresh: 1, SoaRetry: 1, SoaSerial: 1, SoaMbox: "bar.foo.com"}) err := checkTargets(rc, "foo.com") if err != nil && !test.isError { t.Errorf("%v: Error (%v)\n", experiment, err) } if err == nil && test.isError { t.Errorf("%v: Expected error but got none \n", experiment) } } } func TestCheckSoa(t *testing.T) { var tests = []struct { isError bool expire uint32 minttl uint32 refresh uint32 retry uint32 serial uint32 mbox string }{ // Expire {false, 123, 123, 123, 123, 123, "foo.bar.com."}, {true, 0, 123, 123, 123, 123, "foo.bar.com."}, // MinTTL {false, 123, 123, 123, 123, 123, "foo.bar.com."}, {true, 123, 0, 123, 123, 123, "foo.bar.com."}, // Refresh {false, 123, 123, 123, 123, 123, "foo.bar.com."}, {true, 123, 123, 0, 123, 123, "foo.bar.com."}, // Retry {false, 123, 123, 123, 123, 123, "foo.bar.com."}, {true, 123, 123, 123, 0, 123, "foo.bar.com."}, // Serial {false, 123, 123, 123, 123, 123, "foo.bar.com."}, {false, 123, 123, 123, 123, 0, "foo.bar.com."}, // MBox {true, 123, 123, 123, 123, 123, ""}, {true, 123, 123, 123, 123, 123, "foo@bar.com."}, {false, 123, 123, 123, 123, 123, "foo.bar.com."}, } for _, test := range tests { experiment := fmt.Sprintf("%d %d %d %d %d %s", test.expire, test.minttl, test.refresh, test.retry, test.serial, test.mbox) t.Run(experiment, func(t *testing.T) { err := checkSoa(test.expire, test.minttl, test.refresh, test.retry, test.serial, test.mbox) checkError(t, err, test.isError, experiment) }) } } func TestCheckLabel(t *testing.T) { var tests = []struct { label string rType string target string isError bool hasSkipMeta bool }{ {"@", "A", "zap", false, false}, {"foo.bar", "A", "zap", false, false}, {"_foo", "A", "zap", false, false}, {"_foo", "SRV", "zap", false, false}, {"_foo", "TLSA", "zap", false, false}, {"_foo", "TXT", "zap", false, false}, {"_y2", "CNAME", "foo", false, false}, {"s1._domainkey", "CNAME", "foo", false, false}, {"_y3", "CNAME", "asfljds.acm-validations.aws.", false, false}, {"test.foo.tld", "A", "zap", true, false}, {"test.foo.tld", "A", "zap", false, true}, } for i, test := range tests { t.Run(fmt.Sprintf("%s %s", test.label, test.rType), func(t *testing.T) { meta := map[string]string{} if test.hasSkipMeta { meta["skip_fqdn_check"] = "true" } err := checkLabel(test.label, test.rType, test.target, "foo.tld", meta) if err != nil && !test.isError { t.Errorf("%02d: Expected no error but got %s", i, err) } if err == nil && test.isError { t.Errorf("%02d: Expected error but got none", i) } }) } } func checkError(t *testing.T, err error, shouldError bool, experiment string) { if err != nil && !shouldError { t.Errorf("%v: Error (%v)\n", experiment, err) } if err == nil && shouldError { t.Errorf("%v: Expected error but got none \n", experiment) } } func Test_assert_valid_ipv4(t *testing.T) { var tests = []struct { experiment string isError bool }{ {"1.2.3.4", false}, {"1.2.3.4/10", true}, {"1.2.3", true}, {"foo", true}, } for _, test := range tests { err := checkIPv4(test.experiment) checkError(t, err, test.isError, test.experiment) } } func Test_assert_valid_target(t *testing.T) { var tests = []struct { experiment string isError bool }{ {"@", false}, {"foo", false}, {"foo.bar.", false}, {"foo.", false}, {"foo.bar", true}, {"foo&bar", true}, {"foo bar", true}, {"elb21.freshdesk.com/", true}, {"elb21.freshdesk.com/.", true}, } for _, test := range tests { err := checkTarget(test.experiment) checkError(t, err, test.isError, test.experiment) } } func Test_transform_cname(t *testing.T) { var tests = []struct { experiment string expected string }{ {"@", "old.com.new.com."}, {"foo", "foo.old.com.new.com."}, {"foo.bar", "foo.bar.old.com.new.com."}, {"foo.bar.", "foo.bar.new.com."}, {"chat.stackexchange.com.", "chat.stackexchange.com.new.com."}, } for _, test := range tests { actual := transformCNAME(test.experiment, "old.com", "new.com") if test.expected != actual { t.Errorf("%v: expected (%v) got (%v)\n", test.experiment, test.expected, actual) } } } func TestNSAtRoot(t *testing.T) { // do not allow ns records for @ rec := &models.RecordConfig{Type: "NS"} rec.SetLabel("test", "foo.com") rec.SetTarget("ns1.name.com.") errs := checkTargets(rec, "foo.com") if len(errs) > 0 { t.Error("Expect no error with ns record on subdomain") } rec.SetLabel("@", "foo.com") errs = checkTargets(rec, "foo.com") if len(errs) != 1 { t.Error("Expect error with ns record on @") } } func TestTransforms(t *testing.T) { var tests = []struct { givenIP string expectedRecords []string }{ {"0.0.5.5", []string{"2.0.5.5"}}, {"3.0.5.5", []string{"5.5.5.5"}}, {"7.0.5.5", []string{"9.9.9.9", "10.10.10.10"}}, } const transform = "0.0.0.0~1.0.0.0~2.0.0.0~; 3.0.0.0~4.0.0.0~~5.5.5.5; 7.0.0.0~8.0.0.0~~9.9.9.9,10.10.10.10" for i, test := range tests { dc := &models.DomainConfig{ Records: []*models.RecordConfig{ makeRC("f", "example.tld", test.givenIP, models.RecordConfig{Type: "A", Metadata: map[string]string{"transform": transform}}), }, } err := applyRecordTransforms(dc) if err != nil { t.Errorf("error on test %d: %s", i, err) continue } if len(dc.Records) != len(test.expectedRecords) { t.Errorf("test %d: expect %d records but found %d", i, len(test.expectedRecords), len(dc.Records)) continue } for r, rec := range dc.Records { if rec.GetTargetField() != test.expectedRecords[r] { t.Errorf("test %d at index %d: records don't match. Expect %s but found %s.", i, r, test.expectedRecords[r], rec.GetTargetField()) continue } } } } func TestCNAMEMutex(t *testing.T) { var recA = &models.RecordConfig{Type: "CNAME"} recA.SetLabel("foo", "foo.example.com") recA.SetTarget("example.com.") tests := []struct { rType string name string fail bool }{ {"A", "foo", true}, {"A", "foo2", false}, {"CNAME", "foo", true}, {"CNAME", "foo2", false}, } for _, tst := range tests { t.Run(fmt.Sprintf("%s %s", tst.rType, tst.name), func(t *testing.T) { var recB = &models.RecordConfig{Type: tst.rType} recB.SetLabel(tst.name, "example.com") recB.SetTarget("example2.com.") dc := &models.DomainConfig{ Name: "example.com", Records: []*models.RecordConfig{recA, recB}, } errs := checkCNAMEs(dc) if errs != nil && !tst.fail { t.Error("Got error but expected none") } if errs == nil && tst.fail { t.Error("Expected error but got none") } }) } } func TestCAAValidation(t *testing.T) { config := &models.DNSConfig{ Domains: []*models.DomainConfig{ { Name: "example.com", RegistrarName: "BIND", Records: []*models.RecordConfig{ makeRC("@", "example.com", "example.com", models.RecordConfig{Type: "CAA", CaaTag: "invalid"}), }, }, }, } errs := ValidateAndNormalizeConfig(config) if len(errs) != 1 { t.Error("Expect error on invalid CAA but got none") } } func TestCheckDuplicates(t *testing.T) { records := []*models.RecordConfig{ // The only difference is the target: makeRC("www", "example.com", "4.4.4.4", models.RecordConfig{Type: "A"}), makeRC("www", "example.com", "5.5.5.5", models.RecordConfig{Type: "A"}), // The only difference is the rType: makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "NS"}), makeRC("aaa", "example.com", "uniquestring.com.", models.RecordConfig{Type: "PTR"}), // The only difference is the TTL. makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}), makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 222}), // Three records each with a different target. makeRC("@", "example.com", "ns1.foo.com.", models.RecordConfig{Type: "NS"}), makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}), makeRC("@", "example.com", "ns3.foo.com.", models.RecordConfig{Type: "NS"}), } errs := checkDuplicates(records) if len(errs) != 0 { t.Errorf("Expect duplicate NOT found but found %q", errs) } } func TestCheckDuplicates_dup_a(t *testing.T) { records := []*models.RecordConfig{ // A records that are exact dupliates. makeRC("@", "example.com", "1.1.1.1", models.RecordConfig{Type: "A"}), makeRC("@", "example.com", "1.1.1.1", models.RecordConfig{Type: "A"}), } errs := checkDuplicates(records) if len(errs) == 0 { t.Error("Expect duplicate found but found none") } } func TestCheckDuplicates_dup_ns(t *testing.T) { records := []*models.RecordConfig{ // Three records, the last 2 are duplicates. // NB: This is a common issue. makeRC("@", "example.com", "ns1.foo.com.", models.RecordConfig{Type: "NS"}), makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}), makeRC("@", "example.com", "ns2.foo.com.", models.RecordConfig{Type: "NS"}), } errs := checkDuplicates(records) if len(errs) == 0 { t.Error("Expect duplicate found but found none") } } func TestUniq(t *testing.T) { a := []uint32{1, 2, 2, 3, 4, 5, 5, 6} expected := []uint32{1, 2, 3, 4, 5, 6} r := uniq(a) if !reflect.DeepEqual(r, expected) { t.Error("Deduplicated slice is different than expected") } } func TestCheckLabelHasMultipleTTLs(t *testing.T) { records := []*models.RecordConfig{ // different ttl per record makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}), makeRC("zzz", "example.com", "4.4.4.5", models.RecordConfig{Type: "A", TTL: 222}), } errs := checkLabelHasMultipleTTLs(records) if len(errs) == 0 { t.Error("Expected error on multiple TTLs under the same label, but got none") } } func TestCheckLabelHasNoMultipleTTLs(t *testing.T) { records := []*models.RecordConfig{ // different ttl per record makeRC("zzz", "example.com", "4.4.4.4", models.RecordConfig{Type: "A", TTL: 111}), makeRC("zzz", "example.com", "4.4.4.5", models.RecordConfig{Type: "A", TTL: 111}), } errs := checkLabelHasMultipleTTLs(records) if len(errs) != 0 { t.Errorf("Expected 0 errors on records having the same TTL under the same label, but got %d", len(errs)) } } func TestTLSAValidation(t *testing.T) { config := &models.DNSConfig{ Domains: []*models.DomainConfig{ { Name: "_443._tcp.example.com", RegistrarName: "BIND", Records: []*models.RecordConfig{ makeRC("_443._tcp", "_443._tcp.example.com", "abcdef0", models.RecordConfig{ Type: "TLSA", TlsaUsage: 4, TlsaSelector: 1, TlsaMatchingType: 1}), }, }, }, } errs := ValidateAndNormalizeConfig(config) if len(errs) != 1 { t.Error("Expect error on invalid TLSA but got none") } } const ( ProviderNoDS = "NO_DS_SUPPORT" ProviderFullDS = "FULL_DS_SUPPORT" ProviderChildDSOnly = "CHILD_DS_SUPPORT" ProviderBothDSCaps = "BOTH_DS_CAPABILITIES" ) func init() { providers.RegisterDomainServiceProviderType(ProviderNoDS, providers.DspFuncs{}, providers.DocumentationNotes{}) providers.RegisterDomainServiceProviderType(ProviderFullDS, providers.DspFuncs{}, providers.DocumentationNotes{ providers.CanUseDS: providers.Can(), }) providers.RegisterDomainServiceProviderType(ProviderChildDSOnly, providers.DspFuncs{}, providers.DocumentationNotes{ providers.CanUseDSForChildren: providers.Can(), }) providers.RegisterDomainServiceProviderType(ProviderBothDSCaps, providers.DspFuncs{}, providers.DocumentationNotes{ providers.CanUseDS: providers.Can(), providers.CanUseDSForChildren: providers.Can(), }) } func Test_DSChecks(t *testing.T) { t.Run("no DS support", func(t *testing.T) { err := checkProviderDS(ProviderNoDS, nil) if err == nil { t.Errorf("Provider %s implements no DS capabilities, so should have failed the check", ProviderNoDS) } }) t.Run("full DS support", func(t *testing.T) { apexDS := models.RecordConfig{Type: "DS"} apexDS.SetLabel("@", "example.com") childDS := models.RecordConfig{Type: "DS"} childDS.SetLabel("child", "example.com") records := models.Records{&apexDS, &childDS} // check permutations of ProviderCanDS and having both DS caps for _, pType := range []string{ProviderFullDS, ProviderBothDSCaps} { err := checkProviderDS(pType, records) if err != nil { t.Errorf("Provider %s implements full DS capabilities and should process the provided records", ProviderFullDS) } } }) t.Run("child DS support only", func(t *testing.T) { apexDS := models.RecordConfig{Type: "DS"} apexDS.SetLabel("@", "example.com") childDS := models.RecordConfig{Type: "DS"} childDS.SetLabel("child", "example.com") // this record is included at the apex to check the Type of the // recordset is verified to only inspect records with type == DS apexA := models.RecordConfig{Type: "A"} apexA.SetLabel("@", "example.com") t.Run("accepts when child DS records only", func(t *testing.T) { records := models.Records{&childDS, &apexA} err := checkProviderDS(ProviderChildDSOnly, records) if err != nil { t.Errorf("Provider %s implements child DS support so the provided records should be accepted", ProviderChildDSOnly, ) } }) t.Run("fails with apex and child DS records", func(t *testing.T) { records := models.Records{&apexDS, &childDS, &apexA} err := checkProviderDS(ProviderChildDSOnly, records) if err == nil { t.Errorf("Provider %s does not implement DS support at the zone apex, so should reject provided records", ProviderChildDSOnly, ) } }) }) } func Test_errorRepeat(t *testing.T) { type args struct { label string domain string } tests := []struct { name string args args want string }{ { name: "1", args: args{label: "foo.bar.com", domain: "bar.com"}, want: `The name "foo.bar.com.bar.com." is an error (repeats the domain).` + ` Maybe instead of "foo.bar.com" you intended "foo"?` + ` If not add DISABLE_REPEATED_DOMAIN_CHECK to this record to permit this as-is.`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := errorRepeat(tt.args.label, tt.args.domain); got != tt.want { t.Errorf("errorRepeat() = \n'%s', want\n'%s'", got, tt.want) } }) } }