package checker import ( "strings" "testing" "github.com/miekg/dns" ) func mkSOA(serial uint32) *dns.SOA { return &dns.SOA{ Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeSOA}, Ns: "ns1.example.com.", Mbox: "hostmaster.example.com.", Serial: serial, Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 300, } } func TestCollectSerialDrift_NoDrift(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1.example.com.", "ns2.example.com."}, Results: map[string]*NSResult{ "ns1.example.com.": {Authoritative: true, SOA: mkSOA(10), Serial: 10}, "ns2.example.com.": {Authoritative: true, SOA: mkSOA(10), Serial: 10}, }, } if got := collectSerialDrift(d); len(got) != 0 { t.Errorf("expected no findings, got %v", got) } } func TestCollectSerialDrift_Drift(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1.example.com.", "ns2.example.com.", "ns3.example.com."}, Results: map[string]*NSResult{ "ns1.example.com.": {Authoritative: true, SOA: mkSOA(10), Serial: 10}, "ns2.example.com.": {Authoritative: true, SOA: mkSOA(11), Serial: 11}, "ns3.example.com.": {Authoritative: false, SOA: mkSOA(99), Serial: 99}, // ignored }, } got := collectSerialDrift(d) if len(got) != 1 || got[0].Code != CodeSerialDrift || got[0].Severity != SeverityCrit { t.Fatalf("unexpected findings: %#v", got) } if !strings.Contains(got[0].Message, "serial 10") || !strings.Contains(got[0].Message, "serial 11") { t.Errorf("message missing serials: %q", got[0].Message) } if strings.Contains(got[0].Message, "99") { t.Errorf("non-authoritative server should not appear: %q", got[0].Message) } } func TestCollectSerialVsSaved(t *testing.T) { tests := []struct { name string saved uint32 nsSerials map[string]uint32 warn bool wantCodes []string wantSeverity []Severity }{ { name: "matches saved", saved: 50, nsSerials: map[string]uint32{"ns1.": 50, "ns2.": 50}, warn: true, }, { name: "saved newer than live -> stale", saved: 50, nsSerials: map[string]uint32{"ns1.": 49, "ns2.": 50}, warn: true, wantCodes: []string{CodeSerialStaleVsSaved}, wantSeverity: []Severity{SeverityWarn}, }, { name: "saved newer but warn disabled", saved: 50, nsSerials: map[string]uint32{"ns1.": 49}, warn: false, }, { name: "live ahead of saved -> info", saved: 50, nsSerials: map[string]uint32{"ns1.": 51}, warn: true, wantCodes: []string{CodeSerialAheadOfSaved}, wantSeverity: []Severity{SeverityInfo}, }, { name: "mixed", saved: 50, nsSerials: map[string]uint32{"ns1.": 49, "ns2.": 51}, warn: true, wantCodes: []string{CodeSerialStaleVsSaved, CodeSerialAheadOfSaved}, wantSeverity: []Severity{SeverityWarn, SeverityInfo}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := &ObservationData{DeclaredSerial: tt.saved, Results: map[string]*NSResult{}} for ns, s := range tt.nsSerials { d.Probed = append(d.Probed, ns) d.Results[ns] = &NSResult{Authoritative: true, SOA: mkSOA(s), Serial: s} } got := collectSerialVsSaved(d, tt.warn) if len(got) != len(tt.wantCodes) { t.Fatalf("got %d findings, want %d: %#v", len(got), len(tt.wantCodes), got) } codes := map[string]Severity{} for _, f := range got { codes[f.Code] = f.Severity } for i, c := range tt.wantCodes { if sev, ok := codes[c]; !ok || sev != tt.wantSeverity[i] { t.Errorf("missing or wrong-severity %s: got %v", c, codes) } } }) } } func TestCollectSOAFieldsDrift(t *testing.T) { soaA := mkSOA(10) soaB := mkSOA(10) soaB.Refresh = 9999 // different RDATA soaC := mkSOA(11) // same RDATA as A but different serial; should NOT trigger this rule d := &ObservationData{ Probed: []string{"ns1.", "ns2.", "ns3."}, Results: map[string]*NSResult{ "ns1.": {SOA: soaA}, "ns2.": {SOA: soaB}, "ns3.": {SOA: soaC}, }, } got := collectSOAFieldsDrift(d) if len(got) != 1 || got[0].Code != CodeSOAFieldsDrift { t.Fatalf("expected one SOAFieldsDrift finding, got %#v", got) } // Two distinct RDATA buckets (A+C grouped, B alone). if !strings.Contains(got[0].Message, "refresh=3600") || !strings.Contains(got[0].Message, "refresh=9999") { t.Errorf("message missing refresh values: %q", got[0].Message) } } func TestCollectSOAFieldsDrift_NoDriftWhenOnlySerialDiffers(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1.", "ns2."}, Results: map[string]*NSResult{ "ns1.": {SOA: mkSOA(10)}, "ns2.": {SOA: mkSOA(11)}, }, } if got := collectSOAFieldsDrift(d); len(got) != 0 { t.Errorf("serial-only difference should not be flagged here: %v", got) } } func TestCollectNSRRsetDrift_Consistent(t *testing.T) { rrset := []string{"ns1.example.com.", "ns2.example.com."} d := &ObservationData{ Probed: []string{"ns1.example.com.", "ns2.example.com."}, DeclaredNS: rrset, Results: map[string]*NSResult{ "ns1.example.com.": {Authoritative: true, NSRRset: rrset}, "ns2.example.com.": {Authoritative: true, NSRRset: rrset}, }, } if got := collectNSRRsetDrift(d); len(got) != 0 { t.Errorf("expected no findings, got %v", got) } } func TestCollectNSRRsetDrift_Drift(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1.example.com.", "ns2.example.com."}, DeclaredNS: []string{"ns1.example.com.", "ns2.example.com."}, Results: map[string]*NSResult{ "ns1.example.com.": {Authoritative: true, NSRRset: []string{"ns1.example.com.", "ns2.example.com."}}, "ns2.example.com.": {Authoritative: true, NSRRset: []string{"ns1.example.com."}}, }, } got := collectNSRRsetDrift(d) codes := map[string]bool{} for _, f := range got { codes[f.Code] = true } if !codes[CodeNSRRsetDrift] { t.Errorf("expected NSRRsetDrift, got %v", codes) } } func TestCollectNSRRsetDrift_MismatchConfig(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1.example.com."}, DeclaredNS: []string{"ns1.example.com.", "ns2.example.com."}, Results: map[string]*NSResult{ "ns1.example.com.": {Authoritative: true, NSRRset: []string{"ns1.example.com.", "ns3.example.com."}}, }, } got := collectNSRRsetDrift(d) var found bool for _, f := range got { if f.Code == CodeNSRRsetMismatchConfig { found = true if !strings.Contains(f.Message, "ns2.example.com") || !strings.Contains(f.Message, "ns3.example.com") { t.Errorf("message missing missing/extra entries: %q", f.Message) } } } if !found { t.Errorf("expected NSRRsetMismatchConfig in %v", got) } }