298 lines
8.7 KiB
Go
298 lines
8.7 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// fakeObs is a minimal ObservationGetter for tests. If err is non-nil, Get
|
|
// returns it; otherwise, it JSON-roundtrips data into dest.
|
|
type fakeObs struct {
|
|
data any
|
|
err error
|
|
}
|
|
|
|
func (f *fakeObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
|
|
if f.err != nil {
|
|
return f.err
|
|
}
|
|
b, err := json.Marshal(f.data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(b, dest)
|
|
}
|
|
|
|
func (f *fakeObs) GetRelated(_ context.Context, _ sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestLevelToStatus(t *testing.T) {
|
|
cases := []struct {
|
|
level string
|
|
want sdk.Status
|
|
}{
|
|
{"CRITICAL", sdk.StatusCrit},
|
|
{"ERROR", sdk.StatusCrit},
|
|
{"critical", sdk.StatusCrit}, // case-insensitive
|
|
{"WARNING", sdk.StatusWarn},
|
|
{"NOTICE", sdk.StatusInfo},
|
|
{"INFO", sdk.StatusInfo},
|
|
{"DEBUG", sdk.StatusInfo},
|
|
{"", sdk.StatusUnknown},
|
|
{"BANANA", sdk.StatusUnknown},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.level, func(t *testing.T) {
|
|
if got := levelToStatus(tc.level); got != tc.want {
|
|
t.Errorf("levelToStatus(%q) = %v, want %v", tc.level, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorstStatus(t *testing.T) {
|
|
// Severity ordering used by worstStatus:
|
|
// Error > Crit > Warn > Info > OK > Unknown
|
|
cases := []struct {
|
|
a, b, want sdk.Status
|
|
}{
|
|
{sdk.StatusOK, sdk.StatusOK, sdk.StatusOK},
|
|
{sdk.StatusOK, sdk.StatusInfo, sdk.StatusInfo},
|
|
{sdk.StatusInfo, sdk.StatusWarn, sdk.StatusWarn},
|
|
{sdk.StatusWarn, sdk.StatusCrit, sdk.StatusCrit},
|
|
{sdk.StatusCrit, sdk.StatusError, sdk.StatusError},
|
|
{sdk.StatusError, sdk.StatusCrit, sdk.StatusError},
|
|
{sdk.StatusUnknown, sdk.StatusOK, sdk.StatusOK},
|
|
{sdk.StatusUnknown, sdk.StatusUnknown, sdk.StatusUnknown},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := worstStatus(tc.a, tc.b); got != tc.want {
|
|
t.Errorf("worstStatus(%v, %v) = %v, want %v", tc.a, tc.b, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFilterByModules(t *testing.T) {
|
|
results := []ZonemasterTestResult{
|
|
{Module: "DNSSEC", Message: "a"},
|
|
{Module: "Delegation", Message: "b"},
|
|
{Module: "dnssec", Message: "c"},
|
|
{Module: "Syntax", Message: "d"},
|
|
}
|
|
|
|
t.Run("matches case-insensitively", func(t *testing.T) {
|
|
got := filterByModules(results, []string{"dnssec"})
|
|
if len(got) != 2 {
|
|
t.Fatalf("got %d results, want 2: %+v", len(got), got)
|
|
}
|
|
if got[0].Message != "a" || got[1].Message != "c" {
|
|
t.Errorf("unexpected results: %+v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("multiple modules", func(t *testing.T) {
|
|
got := filterByModules(results, []string{"delegation", "syntax"})
|
|
if len(got) != 2 {
|
|
t.Errorf("got %d, want 2", len(got))
|
|
}
|
|
})
|
|
|
|
t.Run("empty modules returns nil", func(t *testing.T) {
|
|
if got := filterByModules(results, nil); got != nil {
|
|
t.Errorf("got %+v, want nil", got)
|
|
}
|
|
})
|
|
|
|
t.Run("no match returns empty", func(t *testing.T) {
|
|
if got := filterByModules(results, []string{"nope"}); len(got) != 0 {
|
|
t.Errorf("got %+v, want empty", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateZonemasterOptions(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
opts sdk.CheckerOptions
|
|
wantErr string // substring; empty means no error expected
|
|
}{
|
|
{"empty opts", sdk.CheckerOptions{}, ""},
|
|
{"empty url", sdk.CheckerOptions{"zonemasterAPIURL": ""}, ""},
|
|
{"valid http", sdk.CheckerOptions{"zonemasterAPIURL": "http://localhost:5000/api"}, ""},
|
|
{"valid https", sdk.CheckerOptions{"zonemasterAPIURL": "https://zonemaster.net/api"}, ""},
|
|
{"non-string", sdk.CheckerOptions{"zonemasterAPIURL": 42}, "must be a string"},
|
|
{"bad scheme", sdk.CheckerOptions{"zonemasterAPIURL": "ftp://x/api"}, "http or https"},
|
|
{"no host", sdk.CheckerOptions{"zonemasterAPIURL": "http:///api"}, "must include a host"},
|
|
{"unparseable", sdk.CheckerOptions{"zonemasterAPIURL": "http://[::1"}, "zonemasterAPIURL"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := validateZonemasterOptions(tc.opts)
|
|
switch {
|
|
case tc.wantErr == "" && err != nil:
|
|
t.Errorf("unexpected error: %v", err)
|
|
case tc.wantErr != "" && err == nil:
|
|
t.Errorf("expected error containing %q, got nil", tc.wantErr)
|
|
case tc.wantErr != "" && !strings.Contains(err.Error(), tc.wantErr):
|
|
t.Errorf("error %q does not contain %q", err.Error(), tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormLevel(t *testing.T) {
|
|
cases := map[string]string{
|
|
"": "",
|
|
"info": "INFO",
|
|
"WaRnInG": "WARNING",
|
|
"CRITICAL": "CRITICAL",
|
|
}
|
|
for in, want := range cases {
|
|
if got := normLevel(in); got != want {
|
|
t.Errorf("normLevel(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCategoryRuleEvaluate_NoData(t *testing.T) {
|
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
|
obs := &fakeObs{data: ZonemasterData{Results: nil}}
|
|
|
|
states := r.Evaluate(context.Background(), obs, nil)
|
|
if len(states) != 1 {
|
|
t.Fatalf("got %d states, want 1", len(states))
|
|
}
|
|
if states[0].Status != sdk.StatusUnknown {
|
|
t.Errorf("status = %v, want StatusUnknown", states[0].Status)
|
|
}
|
|
if states[0].Code != "zonemaster.dnssec.not_tested" {
|
|
t.Errorf("code = %q", states[0].Code)
|
|
}
|
|
}
|
|
|
|
func TestCategoryRuleEvaluate_ObservationError(t *testing.T) {
|
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
|
obs := &fakeObs{err: errors.New("boom")}
|
|
|
|
states := r.Evaluate(context.Background(), obs, nil)
|
|
if len(states) != 1 {
|
|
t.Fatalf("got %d states, want 1", len(states))
|
|
}
|
|
if states[0].Status != sdk.StatusError {
|
|
t.Errorf("status = %v, want StatusError", states[0].Status)
|
|
}
|
|
if states[0].Code != "zonemaster.observation_error" {
|
|
t.Errorf("code = %q", states[0].Code)
|
|
}
|
|
}
|
|
|
|
func TestCategoryRuleEvaluate_AllOK(t *testing.T) {
|
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
|
obs := &fakeObs{data: ZonemasterData{Results: []ZonemasterTestResult{
|
|
{Module: "dnssec", Level: "INFO", Message: "ok1"},
|
|
{Module: "dnssec", Level: "NOTICE", Message: "ok2"},
|
|
{Module: "delegation", Level: "ERROR", Message: "ignored, wrong module"},
|
|
}}}
|
|
|
|
states := r.Evaluate(context.Background(), obs, nil)
|
|
if len(states) != 1 {
|
|
t.Fatalf("got %d states, want 1 (summary only): %+v", len(states), states)
|
|
}
|
|
if states[0].Status != sdk.StatusOK {
|
|
t.Errorf("status = %v, want StatusOK", states[0].Status)
|
|
}
|
|
if got, _ := states[0].Meta["total"].(int); got != 2 {
|
|
t.Errorf("total = %d, want 2", got)
|
|
}
|
|
}
|
|
|
|
func TestCategoryRuleEvaluate_MixedSeverities(t *testing.T) {
|
|
r := &categoryRule{name: "zonemaster.dnssec", modules: []string{"dnssec"}}
|
|
obs := &fakeObs{data: ZonemasterData{Results: []ZonemasterTestResult{
|
|
{Module: "DNSSEC", Level: "INFO", Message: "i"},
|
|
{Module: "dnssec", Level: "WARNING", Message: "w", Testcase: "tc-w"},
|
|
{Module: "dnssec", Level: "ERROR", Message: "e", Testcase: "tc-e"},
|
|
{Module: "dnssec", Level: "CRITICAL", Message: "c", Testcase: "tc-c"},
|
|
}}}
|
|
|
|
states := r.Evaluate(context.Background(), obs, nil)
|
|
// Expect 1 summary + 3 issue states (warning + error + critical).
|
|
if len(states) != 4 {
|
|
t.Fatalf("got %d states, want 4: %+v", len(states), states)
|
|
}
|
|
|
|
summary := states[0]
|
|
if summary.Status != sdk.StatusCrit {
|
|
t.Errorf("summary status = %v, want StatusCrit", summary.Status)
|
|
}
|
|
if got, _ := summary.Meta["critical"].(int); got != 1 {
|
|
t.Errorf("critical = %d, want 1", got)
|
|
}
|
|
if got, _ := summary.Meta["error"].(int); got != 1 {
|
|
t.Errorf("error = %d, want 1", got)
|
|
}
|
|
if got, _ := summary.Meta["warning"].(int); got != 1 {
|
|
t.Errorf("warning = %d, want 1", got)
|
|
}
|
|
if got, _ := summary.Meta["info"].(int); got != 1 {
|
|
t.Errorf("info = %d, want 1", got)
|
|
}
|
|
|
|
// Issue states: codes should be dotted, lowercased levels.
|
|
wantCodes := map[string]bool{
|
|
"zonemaster.dnssec.warning": false,
|
|
"zonemaster.dnssec.error": false,
|
|
"zonemaster.dnssec.critical": false,
|
|
}
|
|
for _, s := range states[1:] {
|
|
if _, ok := wantCodes[s.Code]; !ok {
|
|
t.Errorf("unexpected issue code: %q", s.Code)
|
|
continue
|
|
}
|
|
wantCodes[s.Code] = true
|
|
if s.Subject == "" {
|
|
t.Errorf("issue state %q missing Subject", s.Code)
|
|
}
|
|
}
|
|
for code, seen := range wantCodes {
|
|
if !seen {
|
|
t.Errorf("missing issue state for %q", code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRulesContainsAllCategories(t *testing.T) {
|
|
got := Rules()
|
|
wantNames := []string{
|
|
"zonemaster.dnssec",
|
|
"zonemaster.delegation",
|
|
"zonemaster.consistency",
|
|
"zonemaster.connectivity",
|
|
"zonemaster.nameserver",
|
|
"zonemaster.syntax",
|
|
"zonemaster.zone",
|
|
"zonemaster.address",
|
|
"zonemaster.basic",
|
|
}
|
|
if len(got) != len(wantNames) {
|
|
t.Fatalf("Rules() returned %d rules, want %d", len(got), len(wantNames))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, r := range got {
|
|
seen[r.Name()] = true
|
|
if r.Description() == "" {
|
|
t.Errorf("rule %q has empty description", r.Name())
|
|
}
|
|
}
|
|
for _, n := range wantNames {
|
|
if !seen[n] {
|
|
t.Errorf("Rules() missing %q", n)
|
|
}
|
|
}
|
|
}
|