checker-dnsviz/checker/collect_test.go

223 lines
6.6 KiB
Go

// SPDX-License-Identifier: MIT
package checker
import (
"reflect"
"sort"
"strings"
"testing"
)
func TestLabelDepth(t *testing.T) {
cases := map[string]int{
"": 0,
".": 0,
"com.": 1,
"example.com.": 2,
"www.example.com": 3,
"a.b.c.d.e.": 5,
}
for in, want := range cases {
if got := labelDepth(in); got != want {
t.Errorf("labelDepth(%q) = %d, want %d", in, got, want)
}
}
}
func TestJoinPath(t *testing.T) {
cases := []struct {
parent, key, want string
}{
{"", "errors", "errors"},
{"delegation", "errors", "delegation/errors"},
{"queries/example.com.", "answer", "queries/example.com./answer"},
}
for _, c := range cases {
if got := joinPath(c.parent, c.key); got != c.want {
t.Errorf("joinPath(%q,%q) = %q, want %q", c.parent, c.key, got, c.want)
}
}
}
func TestParseGrokOutput_OrderAndShape(t *testing.T) {
raw := []byte(`{
"example.com.": {
"status": "NOERROR",
"delegation": {"status": "SECURE"},
"queries": {"example.com./A": {"errors": [{"code": "X", "description": "boom"}]}}
},
"com.": {"delegation": {"status": "SECURE"}},
".": {"delegation": {"status": "SECURE"}},
"_meta": {"ignored": true}
}`)
zones, order, err := ParseGrokOutput(raw)
if err != nil {
t.Fatalf("ParseGrokOutput: %v", err)
}
if _, ok := zones["_meta"]; ok {
t.Errorf("expected _meta-prefixed key to be skipped, got it in zones")
}
if len(zones) != 3 {
t.Errorf("expected 3 zones, got %d (%v)", len(zones), zones)
}
// Order: most-specific first (example.com.), root last.
if !reflect.DeepEqual(order, []string{"example.com.", "com.", "."}) {
t.Errorf("unexpected order: %v", order)
}
if zones["example.com."].Status != "SECURE" {
t.Errorf("expected delegation.status to win for example.com., got %q", zones["example.com."].Status)
}
if zones["example.com."].DNSStatus != "NOERROR" {
t.Errorf("expected DNSStatus=NOERROR, got %q", zones["example.com."].DNSStatus)
}
if len(zones["example.com."].Errors) != 1 {
t.Fatalf("expected 1 error, got %v", zones["example.com."].Errors)
}
if zones["example.com."].Errors[0].Code != "X" {
t.Errorf("expected code=X, got %q", zones["example.com."].Errors[0].Code)
}
}
func TestParseGrokOutput_InvalidJSON(t *testing.T) {
if _, _, err := ParseGrokOutput([]byte("not json")); err == nil {
t.Fatal("expected error for invalid JSON")
}
}
func TestParseGrokOutput_StringZone(t *testing.T) {
// Old grok: a zone may collapse into a bare string status.
raw := []byte(`{"missing.example.": "NON_EXISTENT"}`)
zones, _, err := ParseGrokOutput(raw)
if err != nil {
t.Fatalf("ParseGrokOutput: %v", err)
}
if zones["missing.example."].Status != "NON_EXISTENT" {
t.Errorf("got %q, want NON_EXISTENT", zones["missing.example."].Status)
}
}
func TestDecodeZone_StatusFallbacks(t *testing.T) {
// Only top-level status; no delegation block. Status must fall back to it.
raw := []byte(`{"status": "NOERROR"}`)
z := decodeZone(raw)
if z.DNSStatus != "NOERROR" || z.Status != "NOERROR" {
t.Errorf("expected Status and DNSStatus = NOERROR, got %+v", z)
}
}
func TestCollectFindings_Nested(t *testing.T) {
raw := []byte(`{
"delegation": {
"errors": [{"code": "DS", "description": "missing"}]
},
"queries": {
"example.com./A": {
"answer": [
{"warnings": [{"code": "W1", "description": "smelly"}]}
]
}
}
}`)
z := decodeZone(raw)
if len(z.Errors) != 1 || z.Errors[0].Path != "delegation" {
t.Errorf("expected one error tagged delegation, got %+v", z.Errors)
}
if len(z.Warnings) != 1 {
t.Fatalf("expected one warning, got %+v", z.Warnings)
}
w := z.Warnings[0]
if !strings.HasPrefix(w.Path, "queries/example.com./A/answer[") {
t.Errorf("unexpected warning path: %q", w.Path)
}
if w.Code != "W1" || w.Description != "smelly" {
t.Errorf("unexpected warning content: %+v", w)
}
}
func TestAsFindings_VariantShapes(t *testing.T) {
// Object-keyed-by-code variant.
out := asFindings(map[string]any{
"CODE_B": map[string]any{"description": "second"},
"CODE_A": map[string]any{"description": "first"},
}, "p")
if len(out) != 2 {
t.Fatalf("expected 2 findings, got %v", out)
}
// Sorted by key for stability.
if out[0].Code != "CODE_A" || out[1].Code != "CODE_B" {
t.Errorf("findings not sorted by key: %+v", out)
}
for _, f := range out {
if f.Path != "p" {
t.Errorf("expected path=p, got %q", f.Path)
}
}
// []string variant (rare but supported via direct call).
strs := asFindings([]string{"raw1", "raw2"}, "p")
if len(strs) != 2 || strs[0].Description != "raw1" {
t.Errorf("string-list shape mishandled: %+v", strs)
}
// Unsupported scalar shape returns nil.
if asFindings(42, "p") != nil {
t.Errorf("expected nil for non-list non-map non-string-slice")
}
}
func TestMakeFinding_FallbackAndServers(t *testing.T) {
// description missing, message present.
f := makeFinding(map[string]any{
"message": "use-message",
"servers": []any{"ns1.example.", "ns2.example.", 42 /*ignored*/},
}, "fallback_code", "p")
if f.Description != "use-message" {
t.Errorf("wanted message fallback, got %q", f.Description)
}
if !reflect.DeepEqual(f.Servers, []string{"ns1.example.", "ns2.example."}) {
t.Errorf("non-string server entries should be skipped, got %v", f.Servers)
}
if f.Code != "fallback_code" {
t.Errorf("expected codeHint to be used when item has no code, got %q", f.Code)
}
// neither description nor message: keep the raw payload in Extra
// instead of synthesising a JSON blob into Description (which would
// then render as ugly text in the report).
f2 := makeFinding(map[string]any{"weird": 1}, "", "p")
if f2.Description != "" {
t.Errorf("expected empty Description when no human text available, got %q", f2.Description)
}
if f2.Extra == nil || f2.Extra["weird"] != 1 {
t.Errorf("expected raw payload in Extra, got %+v", f2.Extra)
}
// Plain string item.
f3 := makeFinding("just a string", "h", "p")
if f3.Description != "just a string" || f3.Code != "h" {
t.Errorf("string item mishandled: %+v", f3)
}
// Item explicit code overrides codeHint.
f4 := makeFinding(map[string]any{"code": "REAL", "description": "d"}, "hint", "p")
if f4.Code != "REAL" {
t.Errorf("expected explicit code to win, got %q", f4.Code)
}
}
func TestParseGrokOutput_OrderStable(t *testing.T) {
// Same-depth zones should still produce a deterministic slice (keys order
// in Go maps is randomized) - just checks the zones each appear once.
raw := []byte(`{"a.": {}, "b.": {}}`)
_, order, err := ParseGrokOutput(raw)
if err != nil {
t.Fatal(err)
}
cp := append([]string(nil), order...)
sort.Strings(cp)
if !reflect.DeepEqual(cp, []string{"a.", "b."}) {
t.Errorf("missing zones in order: %v", order)
}
}