checker-smtp/checker/issues_test.go

326 lines
10 KiB
Go

package checker
import (
"strings"
"testing"
)
// hasIssue reports whether the issue list contains an entry with the
// given code. Used by the table-driven tests below.
func hasIssue(issues []Issue, code string) bool {
for _, is := range issues {
if is.Code == code {
return true
}
}
return false
}
func issueByCode(issues []Issue, code string) *Issue {
for i := range issues {
if issues[i].Code == code {
return &issues[i]
}
}
return nil
}
func TestDeriveIssues_NullMXShortCircuits(t *testing.T) {
d := &SMTPData{
Domain: "example.com",
MX: MXLookup{NullMX: true, Records: []MXRecord{{Target: "."}}},
Endpoints: []EndpointProbe{
{Target: "mx", IP: "1.2.3.4", Address: "1.2.3.4:25"}, // would normally yield issues
},
}
issues := deriveIssues(d)
if len(issues) != 1 {
t.Fatalf("null MX should short-circuit; got %d issues", len(issues))
}
if issues[0].Code != CodeNullMX {
t.Errorf("want CodeNullMX, got %q", issues[0].Code)
}
if issues[0].Severity != SeverityInfo {
t.Errorf("null MX is informational, got %q", issues[0].Severity)
}
}
func TestDeriveIssues_MXLookupFailed(t *testing.T) {
d := &SMTPData{Domain: "x", MX: MXLookup{Error: "servfail"}}
issues := deriveIssues(d)
if !hasIssue(issues, CodeMXLookupFailed) {
t.Fatalf("expected mx lookup failed, got %+v", issues)
}
}
func TestDeriveIssues_ImplicitMX(t *testing.T) {
d := &SMTPData{Domain: "x", MX: MXLookup{ImplicitMX: true}}
issues := deriveIssues(d)
if !hasIssue(issues, CodeImplicitMX) {
t.Fatalf("expected implicit MX issue")
}
is := issueByCode(issues, CodeImplicitMX)
if is.Severity != SeverityWarn {
t.Errorf("implicit MX should be warn, got %q", is.Severity)
}
}
func TestDeriveIssues_NoMX(t *testing.T) {
d := &SMTPData{Domain: "x"}
issues := deriveIssues(d)
if !hasIssue(issues, CodeNoMX) {
t.Fatalf("expected no-mx issue")
}
}
func TestDeriveIssues_MXIPLiteral(t *testing.T) {
d := &SMTPData{
Domain: "x",
MX: MXLookup{Records: []MXRecord{
{Preference: 10, Target: "192.0.2.1", IsIPLiteral: true, IPv4: []string{"192.0.2.1"}},
}},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeMXIPLiteral) {
t.Fatalf("expected ip-literal issue")
}
}
func TestDeriveIssues_MXResolveFailed(t *testing.T) {
d := &SMTPData{
Domain: "x",
MX: MXLookup{Records: []MXRecord{
{Preference: 10, Target: "mx.x", ResolveError: "nxdomain"},
}},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeMXResolveFailed) {
t.Fatalf("expected resolve-failed issue")
}
}
func TestDeriveIssues_MXNoAddresses(t *testing.T) {
d := &SMTPData{
Domain: "x",
MX: MXLookup{Records: []MXRecord{
{Preference: 10, Target: "mx.x"}, // no IPs, no error
}},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeNoAddresses) {
t.Fatalf("expected no-addresses issue")
}
}
func TestDeriveIssues_TCPUnreachable(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: "connection refused"},
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeTCPUnreachable) {
t.Fatalf("expected tcp-unreachable")
}
if !hasIssue(issues, CodeAllEndpointsDown) {
t.Fatalf("expected all-endpoints-down summary")
}
}
func TestDeriveIssues_BannerMissingAndInvalid(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", TCPConnected: true},
{Target: "mx.x", IP: "1.2.3.5", Address: "1.2.3.5:25", TCPConnected: true, BannerReceived: true, BannerCode: 421},
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeBannerMissing) {
t.Errorf("expected banner-missing")
}
if !hasIssue(issues, CodeBannerInvalid) {
t.Errorf("expected banner-invalid")
}
}
func TestDeriveIssues_EHLOFailedAndFallback(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", TCPConnected: true, BannerReceived: true, BannerCode: 220},
{Target: "mx.x", IP: "1.2.3.5", Address: "1.2.3.5:25", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, EHLOFallbackHELO: true},
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeEHLOFailed) {
t.Errorf("expected ehlo-failed")
}
if !hasIssue(issues, CodeEHLOFallback) {
t.Errorf("expected ehlo-fallback")
}
}
func TestDeriveIssues_STARTTLSMissingAndFailed(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", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true}, // no STARTTLS offered
{Target: "mx.x", IP: "1.2.3.5", Address: "1.2.3.5:25", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, STARTTLSOffered: true, Error: "ssl bad"}, // offered but not upgraded
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeSTARTTLSMissing) {
t.Errorf("expected starttls missing")
}
if !hasIssue(issues, CodeSTARTTLSFailed) {
t.Errorf("expected starttls failed")
}
if !hasIssue(issues, CodeAllNoSTARTTLS) {
t.Errorf("expected summary all-no-starttls")
}
}
func TestDeriveIssues_AUTHOverPlain(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", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, AUTHPreTLS: []string{"PLAIN", "LOGIN"}},
},
}
issues := deriveIssues(d)
is := issueByCode(issues, CodeAUTHOverPlain)
if is == nil {
t.Fatalf("expected auth-over-plain issue")
}
if !strings.Contains(is.Message, "PLAIN") || !strings.Contains(is.Message, "LOGIN") {
t.Errorf("auth message should list mechanisms, got %q", is.Message)
}
}
func TestDeriveIssues_PTRAndFCrDNS(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", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true},
{Target: "mx.x", IP: "1.2.3.5", Address: "1.2.3.5:25", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true, PTR: "wrong.example.com", FCrDNSPass: false},
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodePTRMissing) {
t.Errorf("expected ptr-missing")
}
if !hasIssue(issues, CodeFCrDNSMismatch) {
t.Errorf("expected fcrdns-mismatch")
}
}
func TestDeriveIssues_NullSenderRejected(t *testing.T) {
no := false
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", TCPConnected: true, BannerReceived: true, BannerCode: 220,
EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true,
PTR: "mx.x", FCrDNSPass: true, HasPipelining: true, Has8BITMIME: true,
NullSenderAccepted: &no, NullSenderResponse: "550 nope",
},
},
}
issues := deriveIssues(d)
is := issueByCode(issues, CodeNullSenderReject)
if is == nil {
t.Fatalf("expected null-sender-reject")
}
if is.Severity != SeverityCrit {
t.Errorf("severity: want crit, got %q", is.Severity)
}
}
func TestDeriveIssues_PostmasterRejected(t *testing.T) {
no := false
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", TCPConnected: true, BannerReceived: true, BannerCode: 220,
EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true,
PTR: "mx.x", FCrDNSPass: true, HasPipelining: true, Has8BITMIME: true,
PostmasterAccepted: &no, PostmasterResponse: "550 no postmaster",
},
},
}
if !hasIssue(deriveIssues(d), CodePostmasterReject) {
t.Fatalf("expected postmaster-reject")
}
}
func TestDeriveIssues_NoIPv6(t *testing.T) {
d := &SMTPData{
Domain: "x",
MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}},
Coverage: Coverage{HasIPv4: true, HasIPv6: false},
}
if !hasIssue(deriveIssues(d), CodeNoIPv6) {
t.Fatalf("expected no-ipv6 info issue")
}
}
func TestDeriveIssues_NoExtensionInfoIssues(t *testing.T) {
d := &SMTPData{
Domain: "x",
MX: MXLookup{Records: []MXRecord{{Target: "mx.x", IPv4: []string{"1.2.3.4"}}}},
Coverage: Coverage{HasIPv4: true, HasIPv6: true},
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,
PTR: "mx.x", FCrDNSPass: true,
HasPipelining: false, Has8BITMIME: false,
},
},
}
issues := deriveIssues(d)
if !hasIssue(issues, CodeNoPipelining) {
t.Errorf("expected no-pipelining info")
}
if !hasIssue(issues, CodeNo8BITMIME) {
t.Errorf("expected no-8bitmime info")
}
}
func TestDeriveIssues_HappyPath(t *testing.T) {
yes := true
no := false
d := &SMTPData{
Domain: "example.com",
MX: MXLookup{Records: []MXRecord{{Preference: 10, Target: "mx.example.com", IPv4: []string{"1.2.3.4"}, IPv6: []string{"2001:db8::1"}}}},
Coverage: Coverage{HasIPv4: true, HasIPv6: true},
Endpoints: []EndpointProbe{
{
Target: "mx.example.com", IP: "1.2.3.4", Address: "1.2.3.4:25",
TCPConnected: true, BannerReceived: true, BannerCode: 220,
EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true,
PTR: "mx.example.com", FCrDNSPass: true,
HasPipelining: true, Has8BITMIME: true,
NullSenderAccepted: &yes, PostmasterAccepted: &yes, OpenRelay: &no,
},
},
}
issues := deriveIssues(d)
if len(issues) != 0 {
t.Errorf("happy-path should have no issues, got: %+v", issues)
}
}