192 lines
5.3 KiB
Go
192 lines
5.3 KiB
Go
package checker
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestReadResponse_SingleLine(t *testing.T) {
|
|
sc := newSMTPConn(newFakeConn("220 mx ESMTP\r\n"), 0)
|
|
code, text, lines, err := sc.readResponse()
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
if code != 220 || text != "mx ESMTP" || len(lines) != 1 {
|
|
t.Errorf("got code=%d text=%q lines=%v", code, text, lines)
|
|
}
|
|
}
|
|
|
|
func TestReadResponse_BadCode(t *testing.T) {
|
|
sc := newSMTPConn(newFakeConn("abc nope\r\n"), 0)
|
|
if _, _, _, err := sc.readResponse(); err == nil {
|
|
t.Fatal("expected error for non-numeric code")
|
|
}
|
|
}
|
|
|
|
func TestReadResponse_BadSeparator(t *testing.T) {
|
|
sc := newSMTPConn(newFakeConn("250?weird\r\n"), 0)
|
|
if _, _, _, err := sc.readResponse(); err == nil {
|
|
t.Fatal("expected error for bad separator")
|
|
}
|
|
}
|
|
|
|
func TestReadResponse_ShortLine(t *testing.T) {
|
|
sc := newSMTPConn(newFakeConn("ok\r\n"), 0)
|
|
if _, _, _, err := sc.readResponse(); err == nil {
|
|
t.Fatal("expected error for short line")
|
|
}
|
|
}
|
|
|
|
func TestReadResponse_EOF(t *testing.T) {
|
|
sc := newSMTPConn(newFakeConn(""), 0)
|
|
if _, _, _, err := sc.readResponse(); err == nil {
|
|
t.Fatal("expected EOF error")
|
|
}
|
|
}
|
|
|
|
func TestCmd_WritesAndReads(t *testing.T) {
|
|
fc := newFakeConn("250 ok\r\n")
|
|
sc := newSMTPConn(fc, 0)
|
|
code, text, _, err := sc.cmd("EHLO mx.example.com")
|
|
if err != nil {
|
|
t.Fatalf("cmd: %v", err)
|
|
}
|
|
if code != 250 || text != "ok" {
|
|
t.Errorf("got code=%d text=%q", code, text)
|
|
}
|
|
if got := fc.writer.String(); got != "EHLO mx.example.com\r\n" {
|
|
t.Errorf("wrote %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseEHLO_EmptyAndShort(t *testing.T) {
|
|
greeting, exts := parseEHLO([]string{"", "abc"}) // both too short to have a payload
|
|
if greeting != "" || len(exts) != 0 {
|
|
t.Errorf("expected empty greeting and no extensions, got %q %v", greeting, exts)
|
|
}
|
|
}
|
|
|
|
func TestExtensionLookup_HasMissing(t *testing.T) {
|
|
idx := buildExtensions([]string{"PIPELINING", "STARTTLS"})
|
|
if !idx.has("PIPELINING") || !idx.has("STARTTLS") {
|
|
t.Error("missing expected extension")
|
|
}
|
|
if idx.has("DSN") {
|
|
t.Error("DSN should be absent")
|
|
}
|
|
}
|
|
|
|
func TestExtensionLookup_ParseSizeNoArg(t *testing.T) {
|
|
if got := buildExtensions([]string{"SIZE"}).parseSize(); got != 0 {
|
|
t.Errorf("SIZE no-arg: want 0, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestExtensionLookup_ParseSizeJunk(t *testing.T) {
|
|
if got := buildExtensions([]string{"SIZE notanumber"}).parseSize(); got != 0 {
|
|
t.Errorf("SIZE junk: want 0, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestExtensionLookup_ParseAuthMixedCase(t *testing.T) {
|
|
got := buildExtensions([]string{"AUTH plain login crammd5"}).parseAuth()
|
|
want := []string{"PLAIN", "LOGIN", "CRAMMD5"}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("len: want %d got %d", len(want), len(got))
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("[%d]: want %q got %q", i, want[i], got[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtensionLookup_ParseAuthEmpty(t *testing.T) {
|
|
if got := buildExtensions(nil).parseAuth(); got != nil {
|
|
t.Errorf("expected nil, got %v", got)
|
|
}
|
|
if got := buildExtensions([]string{"AUTH"}).parseAuth(); got != nil {
|
|
t.Errorf("AUTH no-arg: expected nil, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestParseBanner_EdgeCases(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{"", ""},
|
|
{" ", ""},
|
|
{"foo@bar.com is not a hostname", ""}, // skipped: contains @
|
|
{"hello world", ""}, // no dot
|
|
{"mx.example.com,", "mx.example.com"}, // trailing punct stripped
|
|
{"mx.example.com.", "mx.example.com"}, // trailing dot stripped
|
|
{strings.Repeat("a", 254) + ".com", ""}, // too long
|
|
{"mx-1.example.com hi", "mx-1.example.com"}, // hyphen ok
|
|
}
|
|
for _, c := range cases {
|
|
if got := parseBanner(c.in); got != c.want {
|
|
t.Errorf("parseBanner(%q): want %q, got %q", c.in, c.want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeHostname(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", false},
|
|
{"a.b", true},
|
|
{"mx.example.com", true},
|
|
{"MX.EXAMPLE.COM", true},
|
|
{"mx_underscore.example.com", true}, // current implementation tolerates _
|
|
{"contains space", false},
|
|
{"contains\tab", false},
|
|
{"<bracket>", false},
|
|
{"emoji😀", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := looksLikeHostname(c.in); got != c.want {
|
|
t.Errorf("looksLikeHostname(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTLSProbeConfig(t *testing.T) {
|
|
cfg := tlsProbeConfig("mx.example.com")
|
|
if cfg.ServerName != "mx.example.com" {
|
|
t.Errorf("ServerName: got %q", cfg.ServerName)
|
|
}
|
|
if !cfg.InsecureSkipVerify {
|
|
t.Error("InsecureSkipVerify should be true (delegated to checker-tls)")
|
|
}
|
|
}
|
|
|
|
func TestSMTPConn_Close_DoesNotPanic(t *testing.T) {
|
|
// close should write QUIT, attempt to read a 221, swallow errors,
|
|
// and Close the underlying conn. With an empty reader, the read
|
|
// fails, but close should not panic.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("close panicked: %v", r)
|
|
}
|
|
}()
|
|
fc := newFakeConn("")
|
|
sc := newSMTPConn(fc, 0)
|
|
sc.close()
|
|
if !strings.Contains(fc.writer.String(), "QUIT") {
|
|
t.Errorf("expected QUIT to be written, got %q", fc.writer.String())
|
|
}
|
|
}
|
|
|
|
func TestSMTPConn_Swap(t *testing.T) {
|
|
a := newFakeConn("")
|
|
b := newFakeConn("250 second\r\n")
|
|
sc := newSMTPConn(a, 0)
|
|
sc.swap(b)
|
|
code, _, _, err := sc.readResponse()
|
|
if err != nil {
|
|
t.Fatalf("read after swap: %v", err)
|
|
}
|
|
if code != 250 {
|
|
t.Errorf("read should come from b, got code %d", code)
|
|
}
|
|
}
|