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}, {"", 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) } }