checker-authoritative-consi.../checker/rules_discovery_test.go

193 lines
5.4 KiB
Go

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)
}
}
}