diff --git a/plugin/auto/auto_test.go b/plugin/auto/auto_test.go new file mode 100644 index 000000000..243c7ebc8 --- /dev/null +++ b/plugin/auto/auto_test.go @@ -0,0 +1,259 @@ +package auto + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/file" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +func TestAutoName(t *testing.T) { + t.Parallel() + a := Auto{} + if a.Name() != "auto" { + t.Errorf("Expected 'auto', got %s", a.Name()) + } +} + +func TestAutoServeDNS(t *testing.T) { + t.Parallel() + tests := []struct { + name string + qname string + qtype uint16 + zones []string + expectedCode int + shouldMatch bool + }{ + { + name: "valid A query", + qname: "test.example.org.", + qtype: dns.TypeA, + zones: []string{"example.org."}, + expectedCode: dns.RcodeServerFailure, // Zone exists but no data + shouldMatch: true, + }, + { + name: "AXFR query refused", + qname: "test.example.org.", + qtype: dns.TypeAXFR, + zones: []string{"example.org."}, + expectedCode: dns.RcodeRefused, + shouldMatch: true, + }, + { + name: "IXFR query refused", + qname: "test.example.org.", + qtype: dns.TypeIXFR, + zones: []string{"example.org."}, + expectedCode: dns.RcodeRefused, + shouldMatch: true, + }, + { + name: "no matching zone", + qname: "test.notfound.org.", + qtype: dns.TypeA, + zones: []string{"example.org."}, + shouldMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + a := createTestAuto(tt.zones) + + m := new(dns.Msg) + m.SetQuestion(tt.qname, tt.qtype) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if !tt.shouldMatch { + if err == nil { + t.Errorf("Expected error for non-matching zone, got nil") + } + return + } + + if err != nil { + t.Errorf("ServeDNS returned error: %v", err) + } + + if tt.qtype == dns.TypeAXFR || tt.qtype == dns.TypeIXFR { + if code != dns.RcodeRefused { + t.Errorf("Expected RcodeRefused for %s, got %d", dns.TypeToString[tt.qtype], code) + } + return + } + + if code != tt.expectedCode { + t.Errorf("Expected code %d, got %d", tt.expectedCode, code) + } + }) + } +} + +func TestAutoServeDNSZoneMatching(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + origins []string + names []string + qname string + hasZone bool + }{ + { + name: "exact zone match", + origins: []string{"example.org."}, + names: []string{"example.org."}, + qname: "test.example.org.", + hasZone: true, + }, + { + name: "subdomain zone match", + origins: []string{"example.org."}, + names: []string{"example.org."}, + qname: "sub.test.example.org.", + hasZone: true, + }, + { + name: "no origin match", + origins: []string{"other.org."}, + names: []string{"example.org."}, + qname: "test.example.org.", + hasZone: false, + }, + { + name: "origin match but no name match", + origins: []string{"example.org."}, + names: []string{"other.org."}, + qname: "test.example.org.", + hasZone: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: tt.origins, + names: tt.names, + }, + Next: nil, + } + + for _, name := range tt.names { + a.Z[name] = &file.Zone{} + } + + m := new(dns.Msg) + m.SetQuestion(tt.qname, dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + _, err := a.ServeDNS(ctx, rec, m) + + if tt.hasZone { + if err != nil { + t.Errorf("Expected no error for zone match, got: %v", err) + } + } else { + if err == nil { + t.Errorf("Expected error for no zone match, got nil") + } + } + }) + } +} + +func TestAutoServeDNSNilZone(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: []string{"example.org."}, + names: []string{"example.org."}, + }, + Next: nil, + } + + a.Z["example.org."] = nil + + m := new(dns.Msg) + m.SetQuestion("test.example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if code != dns.RcodeServerFailure { + t.Errorf("Expected RcodeServerFailure for nil zone, got %d", code) + } + if err != nil { + t.Errorf("Expected no error for nil zone, got: %v", err) + } +} + +func TestAutoServeDNSMissingZone(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: []string{"example.org."}, + names: []string{"example.org."}, + }, + Next: nil, + } + + // Don't add the zone to the map to test the missing zone case + + m := new(dns.Msg) + m.SetQuestion("test.example.org.", dns.TypeA) + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + ctx := context.Background() + + code, err := a.ServeDNS(ctx, rec, m) + + if code != dns.RcodeServerFailure { + t.Errorf("Expected RcodeServerFailure for missing zone, got %d", code) + } + if err != nil { + t.Errorf("Expected no error for missing zone, got: %v", err) + } +} + +// Helper functions for testing + +func createTestAuto(zones []string) *Auto { + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + origins: zones, + names: zones, + }, + Next: nil, // No next plugin for testing + } + + // Initialize with empty zones for the tests + for _, zone := range zones { + a.Z[zone] = &file.Zone{} + } + + return a +} diff --git a/plugin/auto/regexp_test.go b/plugin/auto/regexp_test.go index 17c35eb90..23bf094de 100644 --- a/plugin/auto/regexp_test.go +++ b/plugin/auto/regexp_test.go @@ -1,8 +1,12 @@ package auto -import "testing" +import ( + "fmt" + "testing" +) func TestRewriteToExpand(t *testing.T) { + t.Parallel() tests := []struct { in string expected string @@ -12,9 +16,12 @@ func TestRewriteToExpand(t *testing.T) { {in: "{1", expected: "${1"}, } for i, tc := range tests { - got := rewriteToExpand(tc.in) - if got != tc.expected { - t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got) - } + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + got := rewriteToExpand(tc.in) + if got != tc.expected { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expected, got) + } + }) } } diff --git a/plugin/auto/setup_test.go b/plugin/auto/setup_test.go index d66ca074e..9f0ede848 100644 --- a/plugin/auto/setup_test.go +++ b/plugin/auto/setup_test.go @@ -1,6 +1,7 @@ package auto import ( + "fmt" "testing" "time" @@ -8,6 +9,7 @@ import ( ) func TestAutoParse(t *testing.T) { + t.Parallel() tests := []struct { inputFileRules string shouldErr bool @@ -93,6 +95,13 @@ func TestAutoParse(t *testing.T) { }`, true, "/tmp", "${1}", ``, 60 * time.Second, }, + // non-existent directory. + { + `auto example.org { + directory /foobar/coredns * {1} + }`, + true, "/tmp", "${1}", ``, 60 * time.Second, + }, // unexpected argument. { `auto example.org { @@ -100,34 +109,54 @@ func TestAutoParse(t *testing.T) { }`, true, "/tmp", "${1}", ``, 60 * time.Second, }, + // upstream directive should not error and should consume args + { + `auto example.org { + directory /tmp + upstream 8.8.8.8 1.1.1.1 + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, + // upstream directive with no args should not error + { + `auto example.org { + directory /tmp + upstream + }`, + false, "/tmp", "${1}", `db\.(.*)`, 60 * time.Second, + }, } for i, test := range tests { - c := caddy.NewTestController("dns", test.inputFileRules) - a, err := autoParse(c) + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + c := caddy.NewTestController("dns", test.inputFileRules) + a, err := autoParse(c) - if err == nil && test.shouldErr { - t.Fatalf("Test %d expected errors, but got no error", i) - } else if err != nil && !test.shouldErr { - t.Fatalf("Test %d expected no errors, but got '%v'", i, err) - } else if !test.shouldErr { - if a.directory != test.expectedDirectory { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.directory) + if err == nil && test.shouldErr { + t.Fatalf("Test %d expected errors, but got no error", i) + } else if err != nil && !test.shouldErr { + t.Fatalf("Test %d expected no errors, but got '%v'", i, err) + } else if !test.shouldErr { + if a.directory != test.expectedDirectory { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedDirectory, a.directory) + } + if a.template != test.expectedTempl { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.template) + } + if a.re.String() != test.expectedRe { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.re) + } + if a.ReloadInterval != test.expectedReloadInterval { + t.Fatalf("Test %d expected %v, got %v", i, test.expectedReloadInterval, a.ReloadInterval) + } } - if a.template != test.expectedTempl { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedTempl, a.template) - } - if a.re.String() != test.expectedRe { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedRe, a.re) - } - if a.ReloadInterval != test.expectedReloadInterval { - t.Fatalf("Test %d expected %v, got %v", i, test.expectedReloadInterval, a.ReloadInterval) - } - } + }) } } func TestSetupReload(t *testing.T) { + t.Parallel() tests := []struct { name string config string @@ -168,6 +197,7 @@ func TestSetupReload(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() ctr := caddy.NewTestController("dns", tt.config) if err := setup(ctr); (err != nil) != tt.wantErr { t.Errorf("Error: setup() error = %v, wantErr %v", err, tt.wantErr) diff --git a/plugin/auto/walk_test.go b/plugin/auto/walk_test.go index 473f08cdf..551708579 100644 --- a/plugin/auto/walk_test.go +++ b/plugin/auto/walk_test.go @@ -18,6 +18,7 @@ www IN A 127.0.0.1 ` func TestWalk(t *testing.T) { + t.Parallel() tempdir, err := createFiles(t) if err != nil { t.Fatal(err) @@ -45,6 +46,7 @@ func TestWalk(t *testing.T) { } func TestWalkNonExistent(t *testing.T) { + t.Parallel() nonExistingDir := "highly_unlikely_to_exist_dir" ldr := loader{ diff --git a/plugin/auto/watcher_test.go b/plugin/auto/watcher_test.go index 0c7e482e6..eb524678a 100644 --- a/plugin/auto/watcher_test.go +++ b/plugin/auto/watcher_test.go @@ -8,6 +8,7 @@ import ( ) func TestWatcher(t *testing.T) { + t.Parallel() tempdir, err := createFiles(t) if err != nil { t.Fatal(err) @@ -50,6 +51,7 @@ func TestWatcher(t *testing.T) { } func TestSymlinks(t *testing.T) { + t.Parallel() tempdir, err := createFiles(t) if err != nil { t.Fatal(err) diff --git a/plugin/auto/xfr_test.go b/plugin/auto/xfr_test.go new file mode 100644 index 000000000..51d75fe36 --- /dev/null +++ b/plugin/auto/xfr_test.go @@ -0,0 +1,98 @@ +package auto + +import ( + "testing" + + "github.com/coredns/coredns/plugin/file" +) + +func TestAutoNotify(t *testing.T) { + t.Parallel() + + a := &Auto{ + Zones: &Zones{ + names: []string{"example.org.", "test.org."}, + }, + transfer: nil, + } + + err := a.Notify() + if err != nil { + t.Errorf("Expected no error, got %v", err) + } +} + +func TestAutoTransferZoneCase(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + zone string + expectError bool + errorType string + }{ + { + name: "exact match", + zone: "example.org.", + expectError: true, + errorType: "no SOA", + }, + { + name: "case different", + zone: "EXAMPLE.ORG.", + expectError: true, + errorType: "not authoritative", + }, + { + name: "no match", + zone: "other.org.", + expectError: true, + errorType: "not authoritative", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + a := createTestAutoForTransfer(t, []string{"example.org."}) + + ch, err := a.Transfer(tt.zone, 1234) + + if !tt.expectError { + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if ch == nil { + t.Error("Expected non-nil channel") + } + } else { + if err == nil { + t.Error("Expected error, got nil") + } + if ch != nil { + t.Error("Expected nil channel when error occurs") + } + } + }) + } +} + +// Helper functions + +func createTestAutoForTransfer(t *testing.T, zones []string) *Auto { + t.Helper() + a := &Auto{ + Zones: &Zones{ + Z: make(map[string]*file.Zone), + names: zones, + }, + } + + // Initialize with real empty zones for the tests + for _, zone := range zones { + a.Z[zone] = &file.Zone{} + } + + return a +} diff --git a/plugin/auto/zone_test.go b/plugin/auto/zone_test.go new file mode 100644 index 000000000..6fc1da7f9 --- /dev/null +++ b/plugin/auto/zone_test.go @@ -0,0 +1,222 @@ +package auto + +import ( + "testing" + + "github.com/coredns/coredns/plugin/file" +) + +func TestZonesNames(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + zones []string + expected []string + }{ + { + name: "empty zones", + zones: []string{}, + expected: []string{}, + }, + { + name: "single zone", + zones: []string{"example.org."}, + expected: []string{"example.org."}, + }, + { + name: "multiple zones", + zones: []string{"example.org.", "test.org.", "another.com."}, + expected: []string{"example.org.", "test.org.", "another.com."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + z := &Zones{ + names: tt.zones, + } + + result := z.Names() + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d names, got %d", len(tt.expected), len(result)) + } + + for i, name := range tt.expected { + if i >= len(result) || result[i] != name { + t.Errorf("Expected name %s at index %d, got %s", name, i, result[i]) + } + } + }) + } +} + +func TestZonesOrigins(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + origins []string + expected []string + }{ + { + name: "empty origins", + origins: []string{}, + expected: []string{}, + }, + { + name: "single origin", + origins: []string{"example.org."}, + expected: []string{"example.org."}, + }, + { + name: "multiple origins", + origins: []string{"example.org.", "test.org."}, + expected: []string{"example.org.", "test.org."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + z := &Zones{ + origins: tt.origins, + } + + result := z.Origins() + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d origins, got %d", len(tt.expected), len(result)) + } + + for i, origin := range tt.expected { + if i >= len(result) || result[i] != origin { + t.Errorf("Expected origin %s at index %d, got %s", origin, i, result[i]) + } + } + }) + } +} + +func TestZonesZones(t *testing.T) { + t.Parallel() + + zone1 := &file.Zone{} + zone2 := &file.Zone{} + + z := &Zones{ + Z: map[string]*file.Zone{ + "example.org.": zone1, + "test.org.": zone2, + }, + } + + tests := []struct { + name string + zoneName string + expected *file.Zone + }{ + { + name: "existing zone", + zoneName: "example.org.", + expected: zone1, + }, + { + name: "another existing zone", + zoneName: "test.org.", + expected: zone2, + }, + { + name: "non-existent zone", + zoneName: "notfound.org.", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := z.Zones(tt.zoneName) + + if result != tt.expected { + t.Errorf("Expected zone %v, got %v", tt.expected, result) + } + }) + } +} + +func TestZonesAdd(t *testing.T) { + t.Parallel() + + z := &Zones{} + zone := &file.Zone{} + + // Test adding to empty zones + z.Add(zone, "example.org.", nil) + + if z.Z == nil { + t.Error("Expected Z map to be initialized") + } + + if z.Z["example.org."] != zone { + t.Error("Expected zone to be added to map") + } + + if len(z.names) != 1 || z.names[0] != "example.org." { + t.Errorf("Expected names to contain 'example.org.', got %v", z.names) + } + + // Test adding another zone + zone2 := &file.Zone{} + z.Add(zone2, "test.org.", nil) + + if len(z.Z) != 2 { + t.Errorf("Expected 2 zones in map, got %d", len(z.Z)) + } + + if z.Z["test.org."] != zone2 { + t.Error("Expected second zone to be added to map") + } + + if len(z.names) != 2 { + t.Errorf("Expected 2 names, got %d", len(z.names)) + } +} + +func TestZonesEmptyOperations(t *testing.T) { + t.Parallel() + + z := &Zones{} + + names := z.Names() + if len(names) != 0 { + t.Errorf("Expected empty names slice, got %v", names) + } + + origins := z.Origins() + if len(origins) != 0 { + t.Errorf("Expected empty origins slice, got %v", origins) + } + + zone := z.Zones("any.zone.") + if zone != nil { + t.Errorf("Expected nil zone, got %v", zone) + } + + z.Remove("any.zone.") + + testZone := &file.Zone{} + z.Add(testZone, "test.org.", nil) + + if z.Z == nil { + t.Error("Expected Z map to be initialized after Add") + } + if z.Z["test.org."] != testZone { + t.Error("Expected zone to be added") + } +}