package checker import ( "context" "encoding/json" "net" "strings" "testing" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) func TestIsValidHostname(t *testing.T) { good := []string{"example.com", "mx-1.example.com", "a.b.c.d", "MX.EXAMPLE.COM", "1.2.3.4"} for _, s := range good { if !isValidHostname(s) { t.Errorf("expected %q valid", s) } } bad := []string{ "", "a b.com", "a\r\nb.com", "a\nb.com", ".com", "under_score.com", "a@b.com", "spaces .com", strings.Repeat("a", 254), } for _, s := range bad { if isValidHostname(s) { t.Errorf("expected %q invalid", s) } } } func TestIsValidMailbox(t *testing.T) { good := []string{"a@b.com", "user.name+tag@mx.example.com", "postmaster@example.org"} for _, s := range good { if !isValidMailbox(s) { t.Errorf("expected %q valid", s) } } bad := []string{ "", "@example.com", "a@", "a b@example.com", // space in local "a\r\n@example.com", // CRLF "a@example.com", // bracket in local "a,b@example.com", // comma in local "\"quoted\"@example.com", // quoted local "a@with space.com", "a@.com", strings.Repeat("a", 65) + "@example.com", // too-long local } for _, s := range bad { if isValidMailbox(s) { t.Errorf("expected %q invalid", s) } } } func TestCollect_RejectsInvalidDomain(t *testing.T) { p := &smtpProvider{} _, err := p.Collect(context.Background(), sdk.CheckerOptions{"domain": "evil\r\nMAIL FROM:<>"}) if err == nil || !strings.Contains(err.Error(), "invalid domain") { t.Errorf("expected invalid-domain error, got %v", err) } } func TestCollect_RejectsInvalidHELO(t *testing.T) { p := &smtpProvider{} _, err := p.Collect(context.Background(), sdk.CheckerOptions{ "domain": "example.com", "helo_name": "evil\r\nRSET", }) if err == nil || !strings.Contains(err.Error(), "invalid helo_name") { t.Errorf("expected invalid-helo error, got %v", err) } } func TestCollect_RewritesInvalidProbeAddress(t *testing.T) { p := &smtpProvider{} body := json.RawMessage(`{"mx":[{"Preference":0,"Mx":"."}]}`) // null MX → returns immediately out, err := p.Collect(context.Background(), sdk.CheckerOptions{ "domain": "example.com", "service": body, "timeout": 1.0, "test_probe_address": "evil\r\nMAIL FROM:", }) if err != nil { t.Fatalf("collect: %v", err) } if !out.(*SMTPData).MX.NullMX { t.Error("expected null MX path") } } func TestSplitMail_MoreCases(t *testing.T) { cases := []struct { in string ok bool domain, local string }{ {"a@b.com", true, "b.com", "a"}, {"a@b@c.com", true, "c.com", "a@b"}, // last @ wins {"", false, "", ""}, {"@", false, "", ""}, } for _, c := range cases { d, l, ok := splitMail(c.in) if ok != c.ok || d != c.domain || l != c.local { t.Errorf("splitMail(%q) = (%q,%q,%v), want (%q,%q,%v)", c.in, d, l, ok, c.domain, c.local, c.ok) } } } func TestComputeCoverage_Empty(t *testing.T) { d := &SMTPData{} computeCoverage(d) if d.Coverage.AnyReachable { t.Errorf("empty endpoints should not be reachable") } } func TestComputeCoverage_AllPath(t *testing.T) { yes := true d := &SMTPData{ Endpoints: []EndpointProbe{ {IP: "1.2.3.4", IsIPv6: false, TCPConnected: true, BannerReceived: true, EHLOReceived: true, STARTTLSUpgraded: true, NullSenderAccepted: &yes, PostmasterAccepted: &yes}, {IP: "2001:db8::1", IsIPv6: true, TCPConnected: true, BannerReceived: true, EHLOReceived: true, STARTTLSUpgraded: true, NullSenderAccepted: &yes, PostmasterAccepted: &yes}, }, } computeCoverage(d) c := d.Coverage if !c.HasIPv4 || !c.HasIPv6 || !c.AnyReachable || !c.AnyBanner || !c.AnyEHLO || !c.AnySTARTTLS || !c.AllSTARTTLS || !c.AllAcceptMail { t.Errorf("expected all coverage flags set, got %+v", c) } } func TestComputeCoverage_PartialSTARTTLS(t *testing.T) { yes := true d := &SMTPData{ Endpoints: []EndpointProbe{ {IP: "1.2.3.4", TCPConnected: true, BannerReceived: true, EHLOReceived: true, STARTTLSUpgraded: true, NullSenderAccepted: &yes, PostmasterAccepted: &yes}, {IP: "1.2.3.5", TCPConnected: true, BannerReceived: true, EHLOReceived: true, STARTTLSUpgraded: false, NullSenderAccepted: &yes, PostmasterAccepted: &yes}, }, } computeCoverage(d) if !d.Coverage.AnySTARTTLS { t.Error("any STARTTLS expected") } if d.Coverage.AllSTARTTLS { t.Error("not all STARTTLS") } } func TestComputeCoverage_RejectedMailFlipsAccept(t *testing.T) { no := false yes := true d := &SMTPData{ Endpoints: []EndpointProbe{ {IP: "1.2.3.4", TCPConnected: true, BannerReceived: true, EHLOReceived: true, STARTTLSUpgraded: true, NullSenderAccepted: &no, PostmasterAccepted: &yes}, }, } computeCoverage(d) if d.Coverage.AllAcceptMail { t.Error("AllAcceptMail should be false when null sender rejected") } } func TestComputeCoverage_NoEHLOFlipsAccept(t *testing.T) { d := &SMTPData{ Endpoints: []EndpointProbe{ {IP: "1.2.3.4", TCPConnected: true, BannerReceived: true, EHLOReceived: false}, }, } computeCoverage(d) if d.Coverage.AllAcceptMail { t.Error("no EHLO must drop AllAcceptMail") } } func TestParseServiceBody_FullEnvelope(t *testing.T) { type mxitem struct { Hdr struct{ Name string } `json:"Hdr"` Preference uint16 Mx string } body := struct { MXs []mxitem `json:"mx"` }{ MXs: []mxitem{ {Preference: 10, Mx: "mx1.example.com."}, {Preference: 20, Mx: "mx2.example.com."}, }, } envelope := struct { Type string `json:"_svctype"` Service json.RawMessage `json:"Service"` }{Type: "svcs.MXs"} envelope.Service, _ = json.Marshal(body) raw, _ := json.Marshal(envelope) out := parseServiceBody(raw) if len(out) != 2 { t.Fatalf("got %d", len(out)) } if out[0].Preference != 10 || out[0].Target != "mx1.example.com." { t.Errorf("[0]: %+v", out[0]) } } func TestParseServiceBody_BareBody(t *testing.T) { raw := json.RawMessage(`{"mx":[{"Preference":5,"Mx":"mx.example.com."}]}`) out := parseServiceBody(raw) if len(out) != 1 || out[0].Preference != 5 { t.Errorf("got %+v", out) } } func TestParseServiceBody_BadJSON(t *testing.T) { if got := parseServiceBody(json.RawMessage(`not json`)); got != nil { t.Errorf("expected nil, got %+v", got) } } func TestParseServiceBody_NoMX(t *testing.T) { raw := json.RawMessage(`{"mx":[]}`) out := parseServiceBody(raw) if out == nil { t.Errorf("empty array should yield empty slice, not nil") } if len(out) != 0 { t.Errorf("got %+v", out) } } func TestLookupMX_NXDomainBecomesEmpty(t *testing.T) { // Use a TLD-style label that fails fast and cleanly. We rely on the // system resolver returning IsNotFound for invalid.example.invalid; // if the local resolver is unusual, the test is skipped. r := &net.Resolver{} out, err := lookupMX(context.Background(), r, "invalid.example.invalid") if err != nil { t.Skipf("resolver returned a different error: %v", err) } if out != nil { t.Errorf("expected nil for NXDOMAIN, got %+v", out) } } func TestCollect_RejectsEmptyDomain(t *testing.T) { p := &smtpProvider{} _, err := p.Collect(context.Background(), sdk.CheckerOptions{"domain": " "}) if err == nil || !strings.Contains(err.Error(), "domain is required") { t.Errorf("expected domain-required error, got %v", err) } } func TestCollect_NullMXFromService(t *testing.T) { p := &smtpProvider{} body := json.RawMessage(`{"mx":[{"Preference":0,"Mx":"."}]}`) out, err := p.Collect(context.Background(), sdk.CheckerOptions{ "domain": "example.com", "service": body, "timeout": 1.0, }) if err != nil { t.Fatalf("collect: %v", err) } d, ok := out.(*SMTPData) if !ok { t.Fatalf("type: %T", out) } if !d.MX.NullMX { t.Errorf("expected NullMX=true, got %+v", d.MX) } if len(d.Endpoints) != 0 { t.Errorf("null MX should not probe, got %d endpoints", len(d.Endpoints)) } } func TestCollect_RewritesProbeToAvoidLocalDomain(t *testing.T) { // Use a tiny timeout so the probe attempt against the bogus IP // fails fast; we only assert on the rewriting behavior, which // happens before any network call. p := &smtpProvider{} body := json.RawMessage(`{"mx":[{"Preference":10,"Mx":"127.255.255.255"}]}`) // unlikely to be an MX target opts := sdk.CheckerOptions{ "domain": "example.com", "service": body, "timeout": 1.0, "test_probe_address": "victim@example.com", // same domain → must be rewritten "test_open_relay": false, "test_null_sender": false, "test_postmaster": false, } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() out, err := p.Collect(ctx, opts) if err != nil { t.Fatalf("collect: %v", err) } d := out.(*SMTPData) for _, ep := range d.Endpoints { if strings.Contains(ep.OpenRelayRecipient, "example.com") { t.Errorf("probe recipient leaked into the local domain: %q", ep.OpenRelayRecipient) } } }