193 lines
5.4 KiB
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)
|
|
}
|
|
}
|
|
}
|