Add echo buffer to SMTP server tests

Refactored SMTP server tests to use an echo buffer for capturing responses. This allows for better validation of command responses without relying on error messages. Additionally, added a new test case to validate a full SendMail transaction with TLS and authentication.
This commit is contained in:
Winni Neessen 2024-11-11 13:31:25 +01:00
parent c3252626e3
commit c6da393676
Signed by: wneessen
GPG key ID: 385AC9889632126E

View file

@ -2215,11 +2215,12 @@ func TestClient_Mail(t *testing.T) {
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-8BITMIME\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoCommandAsError: "MAIL FROM:",
FeatureSet: featureSet,
ListenPort: serverPort,
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
@ -2237,13 +2238,13 @@ func TestClient_Mail(t *testing.T) {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.Mail("valid-from@domain.tld"); err == nil {
t.Error("server should echo the command as error but didn't")
if err = client.Mail("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set mail from address: %s", err)
}
sent := strings.Replace(err.Error(), "500 ", "", -1)
expected := "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME"
if !strings.EqualFold(sent, expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, sent)
resp := strings.Split(echoBuffer.String(), "\r\n")
if !strings.EqualFold(resp[5], expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5])
}
})
t.Run("from address and server supports SMTPUTF8", func(t *testing.T) {
@ -2252,11 +2253,12 @@ func TestClient_Mail(t *testing.T) {
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-SMTPUTF8\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoCommandAsError: "MAIL FROM:",
FeatureSet: featureSet,
ListenPort: serverPort,
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
@ -2274,13 +2276,13 @@ func TestClient_Mail(t *testing.T) {
t.Errorf("failed to close client: %s", err)
}
})
if err = client.Mail("valid-from@domain.tld"); err == nil {
t.Error("server should echo the command as error but didn't")
if err = client.Mail("valid-from@domain.tld"); err != nil {
t.Errorf("failed to set mail from address: %s", err)
}
sent := strings.Replace(err.Error(), "500 ", "", -1)
expected := "MAIL FROM:<valid-from@domain.tld> SMTPUTF8"
if !strings.EqualFold(sent, expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, sent)
resp := strings.Split(echoBuffer.String(), "\r\n")
if !strings.EqualFold(resp[5], expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5])
}
})
t.Run("from address and server supports DSN", func(t *testing.T) {
@ -2289,11 +2291,12 @@ func TestClient_Mail(t *testing.T) {
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-DSN\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoCommandAsError: "MAIL FROM:",
FeatureSet: featureSet,
ListenPort: serverPort,
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
@ -2315,10 +2318,10 @@ func TestClient_Mail(t *testing.T) {
if err = client.Mail("valid-from@domain.tld"); err == nil {
t.Error("server should echo the command as error but didn't")
}
sent := strings.Replace(err.Error(), "500 ", "", -1)
expected := "MAIL FROM:<valid-from@domain.tld> RET=FULL"
if !strings.EqualFold(sent, expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, sent)
resp := strings.Split(echoBuffer.String(), "\r\n")
if !strings.EqualFold(resp[5], expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[5])
}
})
t.Run("from address and server supports DSN, SMTPUTF8 and 8BITMIME", func(t *testing.T) {
@ -2327,11 +2330,12 @@ func TestClient_Mail(t *testing.T) {
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-DSN\r\n250-8BITMIME\r\n250-SMTPUTF8\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoCommandAsError: "MAIL FROM:",
FeatureSet: featureSet,
ListenPort: serverPort,
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
@ -2353,10 +2357,10 @@ func TestClient_Mail(t *testing.T) {
if err = client.Mail("valid-from@domain.tld"); err == nil {
t.Error("server should echo the command as error but didn't")
}
sent := strings.Replace(err.Error(), "500 ", "", -1)
expected := "MAIL FROM:<valid-from@domain.tld> BODY=8BITMIME SMTPUTF8 RET=FULL"
if !strings.EqualFold(sent, expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, sent)
resp := strings.Split(echoBuffer.String(), "\r\n")
if !strings.EqualFold(resp[7], expected) {
t.Errorf("expected mail from command to be %q, but sent %q", expected, resp[7])
}
})
}
@ -2428,11 +2432,12 @@ func TestClient_Rcpt(t *testing.T) {
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-DSN\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoCommandAsError: "RCPT TO:",
FeatureSet: featureSet,
ListenPort: serverPort,
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
@ -2456,10 +2461,10 @@ func TestClient_Rcpt(t *testing.T) {
if err = client.Rcpt("valid-to@domain.tld"); err == nil {
t.Error("recpient address with newlines should fail")
}
sent := strings.Replace(err.Error(), "500 ", "", -1)
expected := "RCPT TO:<valid-to@domain.tld> NOTIFY=SUCCESS"
if !strings.EqualFold(sent, expected) {
t.Errorf("expected rcpt to command to be %q, but sent %q", expected, sent)
resp := strings.Split(echoBuffer.String(), "\r\n")
if !strings.EqualFold(resp[5], expected) {
t.Errorf("expected rcpt to command to be %q, but sent %q", expected, resp[5])
}
})
}
@ -2537,6 +2542,45 @@ func TestClient_Data(t *testing.T) {
})
}
func TestSendMail(t *testing.T) {
t.Run("full SendMail transaction with TLS and auth", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH LOGIN\r\n250-DSN\r\n250 STARTTLS"
echoBuffer := bytes.NewBuffer(nil)
go func() {
if err := simpleSMTPServer(ctx, t, &serverProps{
EchoBuffer: echoBuffer,
FeatureSet: featureSet,
ListenPort: serverPort,
},
); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 30)
addr := fmt.Sprintf("%s:%d", TestServerAddr, serverPort)
testHookStartTLS = func(config *tls.Config) {
testConfig := getTLSConfig(t)
config.ServerName = testConfig.ServerName
config.RootCAs = testConfig.RootCAs
config.Certificates = testConfig.Certificates
}
auth := LoginAuth("username", "password", TestServerAddr, false)
if err := SendMail(addr, auth, "valid-from@domain.tld", []string{"valid-to@domain.tld"},
[]byte("test message")); err != nil {
t.Fatalf("failed to send mail: %s", err)
}
resp := strings.Split(echoBuffer.String(), "\r\n")
for i, line := range resp {
t.Logf("response line %d: %q", i, line)
}
})
}
/*
func TestBasic(t *testing.T) {
server := strings.Join(strings.Split(basicServer, "\n"), "\r\n")
@ -4055,28 +4099,28 @@ func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "
// serverProps represents the configuration properties for the SMTP server.
type serverProps struct {
EchoCommandAsError string
FailOnAuth bool
FailOnDataInit bool
FailOnDataClose bool
FailOnDial bool
FailOnEhlo bool
FailOnHelo bool
FailOnMailFrom bool
FailOnNoop bool
FailOnQuit bool
FailOnReset bool
FailOnSTARTTLS bool
FailTemp bool
FeatureSet string
ListenPort int
HashFunc func() hash.Hash
IsSCRAMPlus bool
IsTLS bool
SupportDSN bool
SSLListener bool
TestSCRAM bool
VRFYUserUnknown bool
EchoBuffer io.Writer
FailOnAuth bool
FailOnDataInit bool
FailOnDataClose bool
FailOnDial bool
FailOnEhlo bool
FailOnHelo bool
FailOnMailFrom bool
FailOnNoop bool
FailOnQuit bool
FailOnReset bool
FailOnSTARTTLS bool
FailTemp bool
FeatureSet string
ListenPort int
HashFunc func() hash.Hash
IsSCRAMPlus bool
IsTLS bool
SupportDSN bool
SSLListener bool
TestSCRAM bool
VRFYUserUnknown bool
}
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
@ -4151,6 +4195,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
if err != nil {
t.Logf("failed to write line: %s", err)
}
if props.EchoBuffer != nil {
if _, err := props.EchoBuffer.Write([]byte(data + "\r\n")); err != nil {
t.Errorf("failed write to echo buffer: %s", err)
}
}
_ = writer.Flush()
}
writeOK := func() {
@ -4171,13 +4220,15 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
break
}
time.Sleep(time.Millisecond)
if props.EchoBuffer != nil {
if _, err = props.EchoBuffer.Write([]byte(data)); err != nil {
t.Errorf("failed write to echo buffer: %s", err)
}
}
var datastring string
data = strings.TrimSpace(data)
switch {
case props.EchoCommandAsError != "" && strings.HasPrefix(data, props.EchoCommandAsError):
writeLine("500 " + data)
break
case strings.HasPrefix(data, "HELO"):
if len(strings.Split(data, " ")) != 2 {
writeLine("501 Syntax: HELO hostname")
@ -4260,6 +4311,11 @@ func handleTestServerConnection(connection net.Conn, t *testing.T, props *server
t.Logf("failed to read data from connection: %s", derr)
break
}
if props.EchoBuffer != nil {
if _, err = props.EchoBuffer.Write([]byte(ddata)); err != nil {
t.Errorf("failed write to echo buffer: %s", err)
}
}
ddata = strings.TrimSpace(ddata)
if ddata == "." {
if props.FailOnDataClose {