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) } }