checker-smtp/checker/report_test.go

240 lines
7.1 KiB
Go

package checker
import (
"encoding/json"
"strings"
"testing"
"time"
sdk "git.happydns.org/checker-sdk-go/checker"
)
func TestStatusToSeverity(t *testing.T) {
cases := []struct {
in sdk.Status
want string
}{
{sdk.StatusCrit, SeverityCrit},
{sdk.StatusError, SeverityCrit},
{sdk.StatusWarn, SeverityWarn},
{sdk.StatusInfo, SeverityInfo},
{sdk.StatusOK, ""},
{sdk.StatusUnknown, ""},
}
for _, c := range cases {
if got := statusToSeverity(c.in); got != c.want {
t.Errorf("status %v: want %q, got %q", c.in, c.want, got)
}
}
}
func TestOverallStatus_NullMX(t *testing.T) {
d := &SMTPData{MX: MXLookup{NullMX: true}}
label, class := overallStatus(d, nil, nil)
if label != "NULL MX" || class != "info" {
t.Errorf("got (%q,%q)", label, class)
}
}
func TestOverallStatus_DataOnly(t *testing.T) {
d := &SMTPData{}
label, class := overallStatus(d, nil, nil)
if label != "data only" || class != "muted" {
t.Errorf("got (%q,%q)", label, class)
}
}
func TestOverallStatus_FromFixes(t *testing.T) {
d := &SMTPData{}
states := []sdk.CheckState{{Status: sdk.StatusOK}}
cases := []struct {
fixes []reportFix
wantLabel string
wantClass string
caseLabel string
}{
{[]reportFix{{Severity: SeverityCrit}}, "FAIL", "fail", "crit"},
{[]reportFix{{Severity: SeverityWarn}}, "WARN", "warn", "warn"},
{[]reportFix{{Severity: SeverityInfo}}, "INFO", "info", "info"},
{nil, "OK", "ok", "ok"},
}
for _, c := range cases {
label, class := overallStatus(d, states, c.fixes)
if label != c.wantLabel || class != c.wantClass {
t.Errorf("%s: got (%q,%q)", c.caseLabel, label, class)
}
}
}
func TestOverallStatus_CritWinsOverWarn(t *testing.T) {
d := &SMTPData{}
states := []sdk.CheckState{{Status: sdk.StatusOK}}
fixes := []reportFix{{Severity: SeverityWarn}, {Severity: SeverityCrit}, {Severity: SeverityInfo}}
if label, _ := overallStatus(d, states, fixes); label != "FAIL" {
t.Errorf("crit must dominate, got %q", label)
}
}
func TestFixesFromStates_OnlyFindings(t *testing.T) {
states := []sdk.CheckState{
{Status: sdk.StatusOK, Code: "skip-me"},
{Status: sdk.StatusUnknown, Code: "skip-me-too"},
{Status: sdk.StatusWarn, Code: "warn-1", Message: "msg", Meta: map[string]any{"fix": "do x", "endpoint": "1.2.3.4:25", "target": "mx"}},
{Status: sdk.StatusCrit, Code: "crit-1", Message: "boom"},
}
out := fixesFromStates(states)
if len(out) != 2 {
t.Fatalf("want 2 fixes, got %d", len(out))
}
w := out[0]
if w.Severity != SeverityWarn || w.Fix != "do x" || w.Endpoint != "1.2.3.4:25" || w.Target != "mx" {
t.Errorf("warn fix wrong: %+v", w)
}
}
func TestFixesFromStates_MetaWrongTypesIgnored(t *testing.T) {
states := []sdk.CheckState{
{Status: sdk.StatusWarn, Code: "x", Meta: map[string]any{"fix": 42, "endpoint": nil}},
}
out := fixesFromStates(states)
if len(out) != 1 {
t.Fatalf("got %d", len(out))
}
if out[0].Fix != "" || out[0].Endpoint != "" {
t.Errorf("non-string meta values must be ignored, got %+v", out[0])
}
}
func TestIndexTLSByAddress(t *testing.T) {
yes := true
notAfter := time.Now().Add(30 * 24 * time.Hour)
payload := map[string]any{
"host": "mx.example.com", "port": 25,
"chain_valid": yes, "hostname_match": yes, "not_after": notAfter,
"issues": []map[string]any{
{"code": "x", "severity": "warn", "message": "m"},
{"code": "y", "severity": "bogus"}, // dropped
},
}
related := []sdk.RelatedObservation{{Data: mustJSON(t, payload), CollectedAt: time.Now()}}
idx := indexTLSByAddress(related)
posture, ok := idx["mx.example.com:25"]
if !ok {
t.Fatalf("expected entry, got %+v", idx)
}
if posture.ChainValid == nil || !*posture.ChainValid {
t.Errorf("ChainValid: %+v", posture.ChainValid)
}
if len(posture.Issues) != 1 {
t.Errorf("issues: want 1, got %d", len(posture.Issues))
}
}
func TestBuildReportData_StatusByEndpoint(t *testing.T) {
yes := true
relay := true
d := &SMTPData{
Domain: "example.com",
MX: MXLookup{Records: []MXRecord{{Preference: 10, Target: "mx.example.com", IPv4: []string{"1.2.3.4"}}}},
Endpoints: []EndpointProbe{
// healthy
{
Target: "mx.example.com", IP: "1.2.3.4", Port: 25, Address: "1.2.3.4:25",
TCPConnected: true, BannerReceived: true, BannerCode: 220,
EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true,
NullSenderAccepted: &yes, PostmasterAccepted: &yes,
},
// unreachable
{Target: "mx.example.com", IP: "1.2.3.5", Port: 25, Address: "1.2.3.5:25"},
// open relay
{
Target: "mx.example.com", IP: "1.2.3.6", Port: 25, Address: "1.2.3.6:25",
TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true,
STARTTLSOffered: true, STARTTLSUpgraded: true,
OpenRelay: &relay,
},
},
}
view := buildReportData(d, nil, []sdk.CheckState{{Status: sdk.StatusOK}})
if len(view.Endpoints) != 3 {
t.Fatalf("want 3 endpoints, got %d", len(view.Endpoints))
}
wantStatuses := []string{"OK", "unreachable", "OPEN RELAY"}
for i, want := range wantStatuses {
if view.Endpoints[i].StatusLabel != want {
t.Errorf("endpoint[%d]: got %q, want %q", i, view.Endpoints[i].StatusLabel, want)
}
}
}
func TestBuildReportData_FixesSortedBySeverity(t *testing.T) {
d := &SMTPData{Domain: "x"}
states := []sdk.CheckState{
{Status: sdk.StatusInfo, Code: "info-1"},
{Status: sdk.StatusCrit, Code: "crit-1"},
{Status: sdk.StatusWarn, Code: "warn-1"},
}
view := buildReportData(d, nil, states)
if len(view.Fixes) != 3 {
t.Fatalf("got %d fixes", len(view.Fixes))
}
if view.Fixes[0].Severity != SeverityCrit ||
view.Fixes[1].Severity != SeverityWarn ||
view.Fixes[2].Severity != SeverityInfo {
t.Errorf("not sorted: %+v", view.Fixes)
}
}
func TestRenderReport_ContainsDomain(t *testing.T) {
view := reportData{
Domain: "example.com",
StatusLabel: "OK",
StatusClass: "ok",
}
html, err := renderReport(view)
if err != nil {
t.Fatalf("render: %v", err)
}
if !strings.Contains(html, "example.com") {
t.Errorf("html missing domain")
}
if !strings.Contains(html, "<!DOCTYPE html>") {
t.Errorf("not an html doc")
}
}
func TestGetHTMLReport_RoundTrip(t *testing.T) {
yes := true
d := &SMTPData{
Domain: "example.com",
RunAt: "2026-01-01T00:00:00Z",
MX: MXLookup{Records: []MXRecord{{Preference: 10, Target: "mx.example.com", IPv4: []string{"1.2.3.4"}}}},
Endpoints: []EndpointProbe{{
Target: "mx.example.com", IP: "1.2.3.4", Port: 25, Address: "1.2.3.4:25",
TCPConnected: true, BannerReceived: true, BannerCode: 220,
EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true,
NullSenderAccepted: &yes, PostmasterAccepted: &yes,
}},
}
body, err := json.Marshal(d)
if err != nil {
t.Fatalf("marshal: %v", err)
}
rctx := sdk.StaticReportContext(body)
p := &smtpProvider{}
html, err := p.GetHTMLReport(rctx)
if err != nil {
t.Fatalf("GetHTMLReport: %v", err)
}
if !strings.Contains(html, "mx.example.com") {
t.Errorf("html missing target hostname")
}
}
func TestGetHTMLReport_BadJSON(t *testing.T) {
rctx := sdk.StaticReportContext(json.RawMessage("{not json"))
p := &smtpProvider{}
if _, err := p.GetHTMLReport(rctx); err == nil {
t.Fatal("expected error on bad json")
}
}