checker-smtp/checker/smtp_extra_test.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)
}
}