package checker import ( "context" "encoding/json" "fmt" "strings" "testing" "github.com/miekg/dns" sdk "git.happydns.org/checker-sdk-go/checker" ) // orphanService builds a fake "svcs.Orphan" service body whose embedded RR // header matches the given (rrtype, owner). Used by every test below to // avoid duplicating the JSON shape. func orphanService(rrtype uint16, owner string) rawService { body, _ := json.Marshal(map[string]any{ "record": map[string]any{ "Hdr": map[string]any{ "Name": owner, "Rrtype": rrtype, "Class": uint16(1), "Ttl": uint32(3600), }, }, }) return rawService{ Type: "svcs.Orphan", Domain: owner, Service: body, } } // modernService builds a non-orphan service body with no Hdr field, like // what a real svcs.MX or svcs.A would marshal. Used to assert the scanner // silently ignores services it cannot inspect. func modernService(svcType string) rawService { body, _ := json.Marshal(map[string]any{"preference": 10, "target": "mail.example.com."}) return rawService{ Type: svcType, Domain: "example.com.", Service: body, } } func runCollect(t *testing.T, zone *rawZone) *LegacyData { t.Helper() raw, err := json.Marshal(zone) if err != nil { t.Fatalf("marshal zone: %v", err) } var jsonZone map[string]any if err := json.Unmarshal(raw, &jsonZone); err != nil { t.Fatalf("unmarshal zone: %v", err) } p := &legacyProvider{} out, err := p.Collect(context.Background(), sdk.CheckerOptions{"zone": jsonZone}) if err != nil { t.Fatalf("Collect: %v", err) } data, ok := out.(*LegacyData) if !ok { t.Fatalf("Collect returned %T, want *LegacyData", out) } return data } func TestCollect_CleanZone(t *testing.T) { z := &rawZone{ Services: map[string][]rawService{ "": {modernService("svcs.A"), modernService("svcs.MX")}, "www": {modernService("svcs.CNAME")}, "mail": {modernService("svcs.A")}, }, } data := runCollect(t, z) if data.ServicesScanned != 4 { t.Errorf("ServicesScanned = %d, want 4", data.ServicesScanned) } if len(data.Findings) != 0 { t.Errorf("Findings = %+v, want empty", data.Findings) } if len(data.CollectErrors) != 0 { t.Errorf("CollectErrors = %v, want empty (modern services must be skipped silently)", data.CollectErrors) } } func TestCollect_DetectsCommonLegacyTypes(t *testing.T) { z := &rawZone{ Services: map[string][]rawService{ "": {orphanService(dns.TypeSPF, "example.com.")}, "old": {orphanService(38 /* A6 */, "old.example.com.")}, "sec": {orphanService(dns.TypeKEY, "sec.example.com."), orphanService(dns.TypeNXT, "sec.example.com.")}, "trash": {orphanService(11 /* WKS */, "trash.example.com.")}, }, } data := runCollect(t, z) if got := len(data.Findings); got != 5 { t.Fatalf("Findings count = %d, want 5", got) } want := map[string]bool{"SPF": false, "A6": false, "KEY": false, "NXT": false, "WKS": false} for _, f := range data.Findings { if _, ok := want[f.TypeName]; ok { want[f.TypeName] = true } } for k, ok := range want { if !ok { t.Errorf("missing finding for %s", k) } } } func TestEvaluate_GroupsAndRanksBySeverity(t *testing.T) { z := &rawZone{ Services: map[string][]rawService{ "": {orphanService(dns.TypeSPF, "example.com."), orphanService(dns.TypeSPF, "example.com.")}, "a": {orphanService(dns.TypeKEY, "a.example.com.")}, // critical "b": {orphanService(11 /* WKS */, "b.example.com.")}, // info "c": {orphanService(11 /* WKS */, "c.example.com.")}, // info, second occurrence "d": {orphanService(dns.TypeNULL, "d.example.com.")}, // info }, } data := runCollect(t, z) // Build a fake observation getter so we can call Evaluate without spinning a host. obs := staticObs{key: ObservationKeyLegacy, payload: mustMarshal(t, data)} rule := &legacyRecordsRule{} states := rule.Evaluate(context.Background(), obs, sdk.CheckerOptions{}) // 4 distinct types → 4 states. if len(states) != 4 { t.Fatalf("got %d states, want 4: %+v", len(states), states) } // First state must be the critical KEY (severity wins, not first-seen). if states[0].Subject != "KEY" || states[0].Status != sdk.StatusCrit { t.Errorf("top state = %+v, want KEY/Crit", states[0]) } // SPF (warn) must come before WKS / NULL (info). if states[1].Subject != "SPF" || states[1].Status != sdk.StatusWarn { t.Errorf("second state = %+v, want SPF/Warn", states[1]) } // SPF state should carry both occurrences in Meta.locations. locs, _ := states[1].Meta["locations"].([]FindingLocation) if len(locs) != 2 { t.Errorf("SPF Meta.locations length = %d, want 2", len(locs)) } } func TestEvaluate_EmptyZoneReturnsOK(t *testing.T) { data := &LegacyData{Zone: "example.com", ServicesScanned: 3} obs := staticObs{key: ObservationKeyLegacy, payload: mustMarshal(t, data)} states := (&legacyRecordsRule{}).Evaluate(context.Background(), obs, sdk.CheckerOptions{}) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Fatalf("want single OK state, got %+v", states) } if !strings.Contains(states[0].Message, "3 service(s) scanned") { t.Errorf("OK message = %q, want it to mention scanned count", states[0].Message) } } func TestCollect_MissingZoneOptionFails(t *testing.T) { p := &legacyProvider{} _, err := p.Collect(context.Background(), sdk.CheckerOptions{}) if err == nil { t.Fatal("expected error when 'zone' option is missing, got nil") } } func TestReport_TopCardMatchesWorstSeverity(t *testing.T) { // SPF (warn) + WKS (info) → top must be SPF. z := &rawZone{ Services: map[string][]rawService{ "a": {orphanService(dns.TypeSPF, "a.example.com.")}, "b": {orphanService(11 /* WKS */, "b.example.com.")}, }, } data := runCollect(t, z) html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{data: mustMarshal(t, data)}) if err != nil { t.Fatalf("GetHTMLReport: %v", err) } if !strings.Contains(html, "Fix this first") { t.Errorf("report missing 'Fix this first' card") } // The headline finding should reference SPF, not WKS. if i, j := strings.Index(html, "Fix this first"), strings.Index(html, "Other legacy records"); i < 0 || j < 0 || !strings.Contains(html[i:j], "SPF") { t.Errorf("'Fix this first' section does not reference SPF") } } func TestReport_OKBannerWhenNoFindings(t *testing.T) { html, err := (&legacyProvider{}).GetHTMLReport(staticReportCtx{ data: mustMarshal(t, &LegacyData{Zone: "example.com", ServicesScanned: 5}), }) if err != nil { t.Fatalf("GetHTMLReport: %v", err) } if !strings.Contains(html, "status-ok") { t.Errorf("report missing OK banner: %q", html[:min(300, len(html))]) } } // --- test helpers --------------------------------------------------------- func mustMarshal(t *testing.T, v any) []byte { t.Helper() b, err := json.Marshal(v) if err != nil { t.Fatalf("marshal: %v", err) } return b } type staticObs struct { key sdk.ObservationKey payload []byte } func (s staticObs) Get(_ context.Context, key sdk.ObservationKey, dest any) error { if key != s.key { return fmt.Errorf("staticObs: unexpected observation key %q (have %q)", key, s.key) } return json.Unmarshal(s.payload, dest) } func (s staticObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return nil, nil } type staticReportCtx struct { data []byte } func (s staticReportCtx) Data() json.RawMessage { return s.data } func (s staticReportCtx) Related(_ sdk.ObservationKey) []sdk.RelatedObservation { return nil } func (s staticReportCtx) States() []sdk.CheckState { return nil }