Initial commit
This commit is contained in:
commit
485c5a4a1d
33 changed files with 5407 additions and 0 deletions
294
checker/rule_test.go
Normal file
294
checker/rule_test.go
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
func mustJSONForRule(t *testing.T, v any) json.RawMessage {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// stubObs is a minimal sdk.ObservationGetter for the rule tests. It is
|
||||
// keyed by ObservationKey so a single instance can serve a Get and any
|
||||
// number of GetRelated lookups.
|
||||
type stubObs struct {
|
||||
data *SMTPData
|
||||
getErr error
|
||||
related map[sdk.ObservationKey][]sdk.RelatedObservation
|
||||
}
|
||||
|
||||
func (s *stubObs) Get(_ context.Context, _ sdk.ObservationKey, dest any) error {
|
||||
if s.getErr != nil {
|
||||
return s.getErr
|
||||
}
|
||||
if s.data == nil {
|
||||
return errors.New("no data")
|
||||
}
|
||||
b, err := json.Marshal(s.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
|
||||
func (s *stubObs) GetRelated(_ context.Context, key sdk.ObservationKey) ([]sdk.RelatedObservation, error) {
|
||||
return s.related[key], nil
|
||||
}
|
||||
|
||||
func TestSeverityToStatus(t *testing.T) {
|
||||
cases := []struct {
|
||||
sev string
|
||||
want sdk.Status
|
||||
}{
|
||||
{SeverityCrit, sdk.StatusCrit},
|
||||
{SeverityWarn, sdk.StatusWarn},
|
||||
{SeverityInfo, sdk.StatusInfo},
|
||||
{"", sdk.StatusOK},
|
||||
{"bogus", sdk.StatusOK},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := severityToStatus(c.sev); got != c.want {
|
||||
t.Errorf("%q → %v, want %v", c.sev, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassAndNotTestedStates(t *testing.T) {
|
||||
p := passState("c.ok", "fine")
|
||||
if p.Status != sdk.StatusOK || p.Code != "c.ok" || p.Message != "fine" {
|
||||
t.Errorf("passState: %+v", p)
|
||||
}
|
||||
n := notTestedState("c.skip", "n/a")
|
||||
if n.Status != sdk.StatusUnknown || n.Code != "c.skip" {
|
||||
t.Errorf("notTestedState: %+v", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueToState(t *testing.T) {
|
||||
is := Issue{
|
||||
Code: "x", Severity: SeverityWarn, Message: "m", Fix: "do",
|
||||
Endpoint: "1.2.3.4:25", Target: "mx",
|
||||
}
|
||||
st := issueToState(is)
|
||||
if st.Status != sdk.StatusWarn {
|
||||
t.Errorf("status: %v", st.Status)
|
||||
}
|
||||
if st.Subject != "1.2.3.4:25" {
|
||||
t.Errorf("subject (endpoint preferred): %q", st.Subject)
|
||||
}
|
||||
if st.Meta["fix"] != "do" || st.Meta["endpoint"] != "1.2.3.4:25" || st.Meta["target"] != "mx" {
|
||||
t.Errorf("meta: %+v", st.Meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueToState_TargetFallbackSubject(t *testing.T) {
|
||||
is := Issue{Code: "x", Severity: SeverityCrit, Target: "mx"}
|
||||
st := issueToState(is)
|
||||
if st.Subject != "mx" {
|
||||
t.Errorf("expected target subject, got %q", st.Subject)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueToState_NoMeta(t *testing.T) {
|
||||
is := Issue{Code: "x", Severity: SeverityInfo}
|
||||
st := issueToState(is)
|
||||
if st.Meta != nil {
|
||||
t.Errorf("meta should be nil when no fields set, got %+v", st.Meta)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatesFromIssues(t *testing.T) {
|
||||
issues := []Issue{
|
||||
{Code: "a", Severity: SeverityCrit},
|
||||
{Code: "b", Severity: SeverityWarn},
|
||||
}
|
||||
states := statesFromIssues(issues)
|
||||
if len(states) != 2 {
|
||||
t.Fatalf("got %d", len(states))
|
||||
}
|
||||
if states[0].Code != "a" || states[1].Code != "b" {
|
||||
t.Errorf("order not preserved: %+v", states)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuesByCodes_FiltersAndKeepsOrder(t *testing.T) {
|
||||
d := &SMTPData{
|
||||
Domain: "x",
|
||||
MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}},
|
||||
Endpoints: []EndpointProbe{{Target: "mx.x", IP: "1.2.3.4", Address: "1.2.3.4:25", Error: "boom"}},
|
||||
}
|
||||
got := issuesByCodes(d, CodeTCPUnreachable, CodeAllEndpointsDown, "smtp.does-not-exist")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("want 2 issues, got %d", len(got))
|
||||
}
|
||||
codes := []string{got[0].Code, got[1].Code}
|
||||
want := []string{CodeTCPUnreachable, CodeAllEndpointsDown}
|
||||
if !reflect.DeepEqual(codes, want) {
|
||||
t.Errorf("order: got %v, want %v", codes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuesByCodes_EmptyCodes(t *testing.T) {
|
||||
if got := issuesByCodes(&SMTPData{}); got != nil {
|
||||
t.Errorf("expected nil, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRules_ContainsAllExpectedNames(t *testing.T) {
|
||||
rules := Rules()
|
||||
got := map[string]bool{}
|
||||
for _, r := range rules {
|
||||
got[r.Name()] = true
|
||||
if r.Description() == "" {
|
||||
t.Errorf("%s: empty description", r.Name())
|
||||
}
|
||||
}
|
||||
want := []string{
|
||||
"smtp.null_mx", "smtp.mx_present", "smtp.mx_sanity",
|
||||
"smtp.endpoint_reachable", "smtp.banner_sanity", "smtp.ehlo_supported",
|
||||
"smtp.starttls_offered", "smtp.starttls_handshake", "smtp.auth_posture",
|
||||
"smtp.reverse_dns", "smtp.null_sender", "smtp.postmaster",
|
||||
"smtp.open_relay", "smtp.extension_posture", "smtp.ipv6_reachable",
|
||||
"smtp.tls_quality",
|
||||
}
|
||||
for _, n := range want {
|
||||
if !got[n] {
|
||||
t.Errorf("missing rule %q", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullMXRule_Detected(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{MX: MXLookup{NullMX: true}}}
|
||||
st := (&nullMXRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusInfo || st[0].Code != CodeNullMX {
|
||||
t.Errorf("got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullMXRule_NotNull(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{Domain: "x", MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}}}}
|
||||
st := (&nullMXRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusOK {
|
||||
t.Errorf("expected pass, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullMXRule_LoadError(t *testing.T) {
|
||||
obs := &stubObs{getErr: errors.New("boom")}
|
||||
st := (&nullMXRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusError {
|
||||
t.Errorf("expected error, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleConcernRule_PassWhenNoMatchingIssues(t *testing.T) {
|
||||
yes := true
|
||||
obs := &stubObs{data: &SMTPData{
|
||||
Domain: "x",
|
||||
MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}},
|
||||
Endpoints: []EndpointProbe{{Target: "mx.x", IP: "1.2.3.4", Address: "1.2.3.4:25", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true, NullSenderAccepted: &yes, PostmasterAccepted: &yes, PTR: "mx.x", FCrDNSPass: true, HasPipelining: true, Has8BITMIME: true}},
|
||||
}}
|
||||
r := &simpleConcernRule{name: "smtp.endpoint_reachable", codes: []string{CodeTCPUnreachable}, passCode: "smtp.endpoint_reachable.ok", passMessage: "ok"}
|
||||
st := r.Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusOK {
|
||||
t.Errorf("expected single pass state, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleConcernRule_EmitsMatchingIssues(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{
|
||||
Domain: "x",
|
||||
MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}},
|
||||
Endpoints: []EndpointProbe{{Target: "mx.x", IP: "1.2.3.4", Address: "1.2.3.4:25", Error: "boom"}},
|
||||
}}
|
||||
r := &simpleConcernRule{name: "smtp.endpoint_reachable", codes: []string{CodeTCPUnreachable, CodeAllEndpointsDown}, passCode: "smtp.endpoint_reachable.ok", passMessage: "ok"}
|
||||
st := r.Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 2 {
|
||||
t.Fatalf("want 2 states, got %d (%+v)", len(st), st)
|
||||
}
|
||||
if st[0].Status != sdk.StatusCrit {
|
||||
t.Errorf("expected crit status, got %v", st[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleConcernRule_NullMXSkipped(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{MX: MXLookup{NullMX: true}}}
|
||||
r := &simpleConcernRule{name: "smtp.starttls_offered", codes: []string{CodeSTARTTLSMissing}, passCode: "smtp.starttls_offered.ok"}
|
||||
st := r.Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusUnknown {
|
||||
t.Errorf("null MX should yield not-tested, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleConcernRule_LoadError(t *testing.T) {
|
||||
obs := &stubObs{getErr: errors.New("nope")}
|
||||
r := &simpleConcernRule{name: "smtp.x", codes: []string{CodeTCPUnreachable}, passCode: "ok"}
|
||||
st := r.Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusError {
|
||||
t.Errorf("got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSQualityRule_NoRelated(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{Domain: "x", MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}}}}
|
||||
st := (&tlsQualityRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusUnknown {
|
||||
t.Errorf("expected not-tested, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSQualityRule_NullMXSkipped(t *testing.T) {
|
||||
obs := &stubObs{data: &SMTPData{MX: MXLookup{NullMX: true}}}
|
||||
st := (&tlsQualityRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusUnknown {
|
||||
t.Errorf("got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSQualityRule_PassWhenRelatedClean(t *testing.T) {
|
||||
yes := true
|
||||
notAfter := time.Now().Add(365 * 24 * time.Hour)
|
||||
payload := map[string]any{"host": "mx.x", "port": 25, "chain_valid": yes, "hostname_match": yes, "not_after": notAfter}
|
||||
related := map[sdk.ObservationKey][]sdk.RelatedObservation{
|
||||
TLSRelatedKey: {{Data: mustJSONForRule(t, payload)}},
|
||||
}
|
||||
obs := &stubObs{
|
||||
data: &SMTPData{Domain: "x", MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}}},
|
||||
related: related,
|
||||
}
|
||||
st := (&tlsQualityRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) != 1 || st[0].Status != sdk.StatusOK {
|
||||
t.Errorf("expected ok pass, got %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSQualityRule_RelatedIssuesFlow(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"host": "mx.x", "port": 25,
|
||||
"issues": []map[string]any{{"code": "cert.expired", "severity": "crit", "message": "expired"}},
|
||||
}
|
||||
related := map[sdk.ObservationKey][]sdk.RelatedObservation{
|
||||
TLSRelatedKey: {{Data: mustJSONForRule(t, payload)}},
|
||||
}
|
||||
obs := &stubObs{
|
||||
data: &SMTPData{Domain: "x", MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}}},
|
||||
related: related,
|
||||
}
|
||||
st := (&tlsQualityRule{}).Evaluate(context.Background(), obs, nil)
|
||||
if len(st) == 0 || st[0].Status != sdk.StatusCrit {
|
||||
t.Errorf("expected crit, got %+v", st)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue