Add and update client tests

Reintroduces and enhances several client tests including `SetLogAuthData`, `Close`, `DialWithContext`, and others. Deprecated and non-functional tests are removed, and a new log parsing method is added for enhanced logging validation.
This commit is contained in:
Winni Neessen 2024-10-23 17:34:23 +02:00
parent ae7160ddba
commit 8a6cd2b448
Signed by: wneessen
GPG key ID: 385AC9889632126E

View file

@ -9,6 +9,7 @@ import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"net"
@ -34,11 +35,30 @@ const (
TestServerAddr = "127.0.0.1"
// TestServerPortBase is the base port for the simple SMTP test server
TestServerPortBase = 2025
// TestPasswordValid is the password that the test server accepts as valid for SMTP auth
TestPasswordValid = "V3ryS3cr3t+"
// TestUserValid is the username that the test server accepts as valid for SMTP auth
TestUserValid = "toni@tester.com"
)
// PortAdder is an atomic counter used to increment port numbers for the test SMTP server instances.
var PortAdder atomic.Int32
// logLine represents a log entry with time, level, message, and direction details.
type logLine struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Message string `json:"msg"`
Direction struct {
From string `json:"from"`
To string `json:"to"`
} `json:"direction"`
}
type logData struct {
Lines []logLine `json:"lines"`
}
func TestNewClient(t *testing.T) {
t.Run("create new Client", func(t *testing.T) {
client, err := NewClient(DefaultHost)
@ -1428,192 +1448,210 @@ func TestClient_SetSMTPAuthCustom(t *testing.T) {
})
}
func TestClient_SetLogAuthData(t *testing.T) {
t.Run("SetLogAuthData true", func(t *testing.T) {
client, err := NewClient(DefaultHost)
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
client.SetLogAuthData(true)
if !client.logAuthData {
t.Errorf("failed to set logAuthData, want: true, got: %t", client.logAuthData)
}
})
t.Run("SetLogAuthData false", func(t *testing.T) {
client, err := NewClient(DefaultHost)
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
client.SetLogAuthData(false)
if client.logAuthData {
t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData)
}
})
t.Run("SetLogAuthData override WithLogAuthData", func(t *testing.T) {
client, err := NewClient(DefaultHost, WithLogAuthData())
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
client.SetLogAuthData(false)
if client.logAuthData {
t.Errorf("failed to set logAuthData, want: false, got: %t", client.logAuthData)
}
})
}
func TestClient_Close(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
t.Run("connect and close the Client", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to the test server: %s", err)
}
if !client.smtpClient.HasConnection() {
t.Fatalf("client has no connection")
}
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
t.Run("connect and double close the Client", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to the test server: %s", err)
}
if !client.smtpClient.HasConnection() {
t.Fatalf("client has no connection")
}
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
}
func TestClient_DialWithContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
PortAdder.Add(1)
serverPort := int(TestServerPortBase + PortAdder.Load())
featureSet := "250-AUTH PLAIN\r\n250-8BITMIME\r\n250-DSN\r\n250 SMTPUTF8"
go func() {
if err := simpleSMTPServer(ctx, featureSet, false, serverPort); err != nil {
t.Errorf("failed to start test server: %s", err)
return
}
}()
time.Sleep(time.Millisecond * 300)
t.Run("connect and check connection", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to the test server: %s", err)
}
t.Cleanup(func() {
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
if !client.smtpClient.HasConnection() {
t.Fatalf("client has no connection")
}
})
t.Run("fail on base port use fallback", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
client.port = 12345
client.fallbackPort = serverPort
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to the test server: %s", err)
}
t.Cleanup(func() {
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
if !client.smtpClient.HasConnection() {
t.Fatalf("client has no connection")
}
})
t.Run("fail on base port and fallback", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
client.port = 12345
client.fallbackPort = 12346
if err = client.DialWithContext(ctxDial); err == nil {
t.Fatalf("connection was supposed to fail, but didn't")
}
if client.smtpClient != nil {
t.Fatalf("client has connection")
}
})
t.Run("connect with full debug logging and auth logging", func(t *testing.T) {
ctxDial, cancelDial := context.WithTimeout(ctx, time.Millisecond*500)
t.Cleanup(cancelDial)
logBuffer := bytes.NewBuffer(nil)
client, err := NewClient(DefaultHost, WithPort(serverPort), WithTLSPolicy(NoTLS),
WithDebugLog(), WithLogAuthData(), WithLogger(log.NewJSON(logBuffer, log.LevelDebug)),
WithSMTPAuth(SMTPAuthPlain), WithUsername(TestUserValid), WithPassword(TestPasswordValid))
if err != nil {
t.Fatalf("failed to create new client: %s", err)
}
if err = client.DialWithContext(ctxDial); err != nil {
t.Fatalf("failed to connect to the test server: %s", err)
}
t.Cleanup(func() {
if err = client.Close(); err != nil {
t.Errorf("failed to close the client: %s", err)
}
})
logs := parseJSONLog(t, logBuffer)
if len(logs.Lines) == 0 {
t.Errorf("failed to enable debug logging, but no logs were found")
}
authFound := false
for _, logline := range logs.Lines {
if strings.EqualFold(logline.Message, "AUTH PLAIN AHRvbmlAdGVzdGVyLmNvbQBWM3J5UzNjcjN0Kw==") &&
logline.Direction.From == "client" && logline.Direction.To == "server" {
authFound = true
}
}
if !authFound {
t.Errorf("logAuthData not working, no authentication info found in logs")
}
})
}
/*
// TestWithDSN tests the WithDSN method for the Client object
func TestWithDSN(t *testing.T) {
c, err := NewClient(DefaultHost, WithDSN())
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
if !c.requestDSN {
t.Errorf("WithDSN failed. c.requestDSN expected to be: %t, got: %t", true, c.requestDSN)
}
if c.dsnReturnType != DSNMailReturnFull {
t.Errorf("WithDSN failed. c.dsnReturnType expected to be: %s, got: %s", DSNMailReturnFull,
c.dsnReturnType)
}
if c.dsnRcptNotifyType[0] != string(DSNRcptNotifyFailure) {
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[0] expected to be: %s, got: %s", DSNRcptNotifyFailure,
c.dsnRcptNotifyType[0])
}
if c.dsnRcptNotifyType[1] != string(DSNRcptNotifySuccess) {
t.Errorf("WithDSN failed. c.dsnRcptNotifyType[1] expected to be: %s, got: %s", DSNRcptNotifySuccess,
c.dsnRcptNotifyType[1])
}
}
// TestWithDSNMailReturnType tests the WithDSNMailReturnType method for the Client object
func TestWithDSNMailReturnType(t *testing.T) {
tests := []struct {
name string
value DSNMailReturnOption
want string
sf bool
}{
{"WithDSNMailReturnType: FULL", DSNMailReturnFull, "FULL", false},
{"WithDSNMailReturnType: HDRS", DSNMailReturnHeadersOnly, "HDRS", false},
{"WithDSNMailReturnType: INVALID", "INVALID", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(DefaultHost, WithDSNMailReturnType(tt.value))
if err != nil && !tt.sf {
t.Errorf("failed to create new client: %s", err)
return
}
if string(c.dsnReturnType) != tt.want {
t.Errorf("WithDSNMailReturnType failed. Expected %s, got: %s", tt.want, string(c.dsnReturnType))
}
})
}
}
// TestWithDSNRcptNotifyType tests the WithDSNRcptNotifyType method for the Client object
func TestWithDSNRcptNotifyType(t *testing.T) {
tests := []struct {
name string
value DSNRcptNotifyOption
want string
sf bool
}{
{"WithDSNRcptNotifyType: NEVER", DSNRcptNotifyNever, "NEVER", false},
{"WithDSNRcptNotifyType: SUCCESS", DSNRcptNotifySuccess, "SUCCESS", false},
{"WithDSNRcptNotifyType: FAILURE", DSNRcptNotifyFailure, "FAILURE", false},
{"WithDSNRcptNotifyType: DELAY", DSNRcptNotifyDelay, "DELAY", false},
{"WithDSNRcptNotifyType: INVALID", "INVALID", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(DefaultHost, WithDSNRcptNotifyType(tt.value))
if err != nil && !tt.sf {
t.Errorf("failed to create new client: %s", err)
return
}
if len(c.dsnRcptNotifyType) <= 0 && !tt.sf {
t.Errorf("WithDSNRcptNotifyType failed. Expected at least one DSNRNType but got none")
}
if !tt.sf && c.dsnRcptNotifyType[0] != tt.want {
t.Errorf("WithDSNRcptNotifyType failed. Expected %s, got: %s", tt.want, c.dsnRcptNotifyType[0])
}
})
}
}
// TestWithoutNoop tests the WithoutNoop method for the Client object
func TestWithoutNoop(t *testing.T) {
c, err := NewClient(DefaultHost, WithoutNoop())
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
if !c.noNoop {
t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", true, c.noNoop)
}
c, err = NewClient(DefaultHost)
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
if c.noNoop {
t.Errorf("WithoutNoop failed. c.noNoop expected to be: %t, got: %t", false, c.noNoop)
}
}
func TestClient_SetLogAuthData(t *testing.T) {
c, err := NewClient(DefaultHost, WithLogAuthData())
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
if !c.logAuthData {
t.Errorf("WithLogAuthData failed. c.logAuthData expected to be: %t, got: %t", true,
c.logAuthData)
}
c.SetLogAuthData(false)
if c.logAuthData {
t.Errorf("SetLogAuthData failed. c.logAuthData expected to be: %t, got: %t", false,
c.logAuthData)
}
}
// TestSetSMTPAuthCustom tests the SetSMTPAuthCustom method for the Client object
func TestSetSMTPAuthCustom(t *testing.T) {
tests := []struct {
name string
value smtp.Auth
want string
sf bool
}{
{"SMTPAuth: CRAM-MD5", smtp.CRAMMD5Auth("", ""), "CRAM-MD5", false},
{"SMTPAuth: LOGIN", smtp.LoginAuth("", "", "", false), "LOGIN", false},
{"SMTPAuth: PLAIN", smtp.PlainAuth("", "", "", "", false), "PLAIN", false},
}
si := smtp.ServerInfo{TLS: true}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := NewClient(DefaultHost)
if err != nil {
t.Errorf("failed to create new client: %s", err)
return
}
c.SetSMTPAuthCustom(tt.value)
if c.smtpAuth == nil {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth method is empty")
}
if c.smtpAuthType != SMTPAuthCustom {
t.Errorf("failed to set custom SMTP auth method. SMTP Auth type is not custom: %s",
c.smtpAuthType)
}
p, _, err := c.smtpAuth.Start(&si)
if err != nil {
t.Errorf("SMTP Auth Start() method returned error: %s", err)
}
if p != tt.want {
t.Errorf("SMTP Auth Start() method is returned proto: %s, expected: %s", p, tt.want)
}
})
}
}
// TestClient_Close_double tests if a close on an already closed connection causes an error.
func TestClient_Close_double(t *testing.T) {
c, err := getTestConnection(true)
if err != nil {
t.Skipf("failed to create test client: %s. Skipping tests", err)
}
ctx := context.Background()
if err = c.DialWithContext(ctx); err != nil {
t.Errorf("failed to dial with context: %s", err)
return
}
if c.smtpClient == nil {
t.Errorf("DialWithContext didn't fail but no SMTP client found.")
return
}
if !c.smtpClient.HasConnection() {
t.Errorf("DialWithContext didn't fail but no connection found.")
}
if err = c.Close(); err != nil {
t.Errorf("failed to close connection: %s", err)
}
if err = c.Close(); err != nil {
t.Errorf("failed 2nd close connection: %s", err)
}
}
// TestClient_DialWithContext tests the DialWithContext method for the Client object
func TestClient_DialWithContext(t *testing.T) {
@ -3563,6 +3601,35 @@ func (f faker) SetReadDeadline(time.Time) error { return nil }
func (f faker) SetWriteDeadline(time.Time) error { return nil }
*/
// parseJSONLog parses a JSON encoded log from the provided buffer and returns a slice of logLine structs.
// In case of a decode error, it reports the error to the testing framework.
func parseJSONLog(t *testing.T, buf *bytes.Buffer) logData {
t.Helper()
builder := strings.Builder{}
builder.WriteString(`{"lines":[`)
lines := strings.Split(buf.String(), "\n")
for i, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
builder.WriteString(line)
if i < len(lines)-2 {
builder.WriteString(`,`)
}
}
builder.WriteString("]}")
var logdata logData
readBuffer := bytes.NewBuffer(nil)
readBuffer.WriteString(builder.String())
if err := json.NewDecoder(readBuffer).Decode(&logdata); err != nil {
t.Errorf("failed to decode json log: %s", err)
}
return logdata
}
// simpleSMTPServer starts a simple TCP server that resonds to SMTP commands.
// The provided featureSet represents in what the server responds to EHLO command
// failReset controls if a RSET succeeds