package checker import ( "context" "encoding/json" "maps" "testing" sdk "git.happydns.org/checker-sdk-go/checker" ) // stubObs implements the minimal subset of sdk.ObservationGetter the rules use. type stubObs struct { data *ObservationData err error } func (s stubObs) Get(_ context.Context, _ sdk.ObservationKey, dst any) error { if s.err != nil { return s.err } b, err := json.Marshal(s.data) if err != nil { return err } return json.Unmarshal(b, dst) } func (s stubObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) { return nil, nil } func mkOpts(kv map[string]any) sdk.CheckerOptions { out := sdk.CheckerOptions{} maps.Copy(out, kv) return out } func TestNSDeclaredRule(t *testing.T) { rule := &nsDeclaredRule{} t.Run("ok with two NS", func(t *testing.T) { d := &ObservationData{ DeclaredNS: []string{"ns1.example.com.", "ns2.example.com."}, Probed: []string{"ns1.example.com.", "ns2.example.com."}, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(nil)) if len(states) != 1 || states[0].Status != sdk.StatusOK { t.Errorf("expected OK, got %#v", states) } }) t.Run("too few NS", func(t *testing.T) { d := &ObservationData{ DeclaredNS: []string{"ns1.example.com."}, Probed: []string{"ns1.example.com."}, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(map[string]any{"minNameServers": 2})) if len(states) != 1 || states[0].Code != CodeTooFewNS { t.Errorf("expected TooFewNS, got %#v", states) } }) t.Run("no NS at all", func(t *testing.T) { d := &ObservationData{} states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(map[string]any{"useParentNS": false})) var hasNoNS bool for _, st := range states { if st.Code == CodeNoNS { hasNoNS = true } } if !hasNoNS { t.Errorf("expected NoNS finding, got %#v", states) } }) } func TestNSReachableRule(t *testing.T) { rule := &nsReachableRule{} t.Run("UDP fail is critical", func(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1."}, Results: map[string]*NSResult{ "ns1.": {UDPReachable: false, TCPReachable: false}, }, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(nil)) if len(states) != 1 || states[0].Code != CodeNSUDPFailed || states[0].Status != sdk.StatusCrit { t.Errorf("expected critical UDP fail, got %#v", states) } }) t.Run("TCP fail crit when requireTCP", func(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1."}, Results: map[string]*NSResult{ "ns1.": {UDPReachable: true, TCPReachable: false}, }, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(map[string]any{"requireTCP": true})) if len(states) != 1 || states[0].Code != CodeNSTCPFailed || states[0].Status != sdk.StatusCrit { t.Errorf("got %#v", states) } }) t.Run("TCP fail warn when not required", func(t *testing.T) { d := &ObservationData{ Probed: []string{"ns1."}, Results: map[string]*NSResult{ "ns1.": {UDPReachable: true, TCPReachable: false}, }, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(map[string]any{"requireTCP": false})) if len(states) != 1 || states[0].Status != sdk.StatusWarn { t.Errorf("got %#v", states) } }) } func TestAuthoritativeRule_Lame(t *testing.T) { rule := &authoritativeRule{} d := &ObservationData{ Zone: "example.com.", HasSOA: true, Probed: []string{"ns1."}, Results: map[string]*NSResult{ "ns1.": {UDPReachable: true, Authoritative: false}, }, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(nil)) if len(states) != 1 || states[0].Code != CodeLame { t.Errorf("expected lame finding, got %#v", states) } } func TestLatencyRule(t *testing.T) { rule := &latencyRule{} d := &ObservationData{ Probed: []string{"fast.", "slow."}, Results: map[string]*NSResult{ "fast.": {UDPReachable: true, LatencyMs: 50}, "slow.": {UDPReachable: true, LatencyMs: 1000}, }, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(map[string]any{"latencyThresholdMs": 500})) if len(states) != 1 || states[0].Code != CodeSlowNS || states[0].Subject != "slow." { t.Errorf("expected single slow finding for slow., got %#v", states) } } func TestParentDelegationRule_Drift(t *testing.T) { rule := &parentDelegationRule{} d := &ObservationData{ DeclaredNS: []string{"ns1.example.com.", "ns2.example.com."}, ParentNS: []string{"ns1.example.com.", "ns3.example.com."}, } states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(nil)) if len(states) != 1 || states[0].Code != CodeParentDrift { t.Errorf("expected ParentDrift, got %#v", states) } } func TestParentDelegationRule_QueryFailed(t *testing.T) { rule := &parentDelegationRule{} d := &ObservationData{ParentQueryError: "boom"} states := rule.Evaluate(context.Background(), stubObs{data: d}, mkOpts(nil)) if len(states) != 1 || states[0].Code != CodeParentQueryFailed { t.Errorf("expected ParentQueryFailed, got %#v", states) } } func TestRulesRegistry(t *testing.T) { rules := Rules() if len(rules) == 0 { t.Fatal("Rules() returned empty list") } seen := map[string]bool{} for _, r := range rules { name := r.Name() if name == "" { t.Error("rule with empty name") } if seen[name] { t.Errorf("duplicate rule name: %s", name) } seen[name] = true if r.Description() == "" { t.Errorf("rule %s has empty description", name) } } }