Compare commits

..

7 commits

Author SHA1 Message Date
Michael Fuchs
188386098e
Merge e56a563286 into 9ca7d24f6a 2024-11-06 10:19:17 +01:00
9ca7d24f6a
Merge pull request #352 from wneessen/more-test-improvements
More test improvements
2024-11-01 16:39:34 +01:00
ec10e0b132
Remove redundant upgrade command in CI workflow
The `sudo apt-get -y upgrade` command was removed from the CI workflow's "Install sendmail" step. This change simplifies the installation process by ensuring only the necessary updates and installations are performed, which can contribute to faster and more reliable CI runs.
2024-11-01 16:36:06 +01:00
0fcde10768
Remove output redirection from sendmail install
This change ensures that the output of the apt-get commands is no longer redirected to /dev/null. This aids in debugging by making command outputs visible in the CI logs.
2024-11-01 16:33:48 +01:00
e37dd39654
Refactor senderror_test.go for improved test clarity
Consolidated multiple duplicate test cases into grouped sub-tests with clear names. This enhances readability and maintainability, ensures proper test isolation, and removes redundant code.
2024-11-01 15:58:11 +01:00
25b7f81e3b
Refactor error handling logic and string formatting
Replaced constant with named error for readability and maintainability in the error handling condition. Adjusted error message formatting by removing an extra space for consistency.
2024-11-01 15:57:59 +01:00
27a3985240
Refactor and expand random string tests
Refactored the test for `randomStringSecure` to better organize test cases using subtests. Added new test cases to check failures with a broken rand.Reader, improving test coverage and robustness.
2024-11-01 15:24:47 +01:00
4 changed files with 240 additions and 158 deletions

View file

@ -50,7 +50,7 @@ jobs:
check-latest: true check-latest: true
- name: Install sendmail - name: Install sendmail
run: | run: |
sudo apt-get -y update >/dev/null && sudo apt-get -y upgrade >/dev/null && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer >/dev/null && which sendmail sudo apt-get -y update && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail
- name: Run go test - name: Run go test
if: success() if: success()
run: | run: |

View file

@ -5,45 +5,81 @@
package mail package mail
import ( import (
"crypto/rand"
"errors"
"strings" "strings"
"testing" "testing"
) )
// TestRandomStringSecure tests the randomStringSecure method // TestRandomStringSecure tests the randomStringSecure method
func TestRandomStringSecure(t *testing.T) { func TestRandomStringSecure(t *testing.T) {
tt := []struct { t.Run("randomStringSecure with varying length", func(t *testing.T) {
testName string tt := []struct {
length int testName string
mustNotMatch string length int
}{ mustNotMatch string
{"20 chars", 20, "'"}, }{
{"100 chars", 100, "'"}, {"20 chars", 20, "'"},
{"1000 chars", 1000, "'"}, {"100 chars", 100, "'"},
} {"1000 chars", 1000, "'"},
}
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) { t.Run(tc.testName, func(t *testing.T) {
rs, err := randomStringSecure(tc.length) rs, err := randomStringSecure(tc.length)
if err != nil { if err != nil {
t.Errorf("random string generation failed: %s", err) t.Errorf("random string generation failed: %s", err)
} }
if strings.Contains(rs, tc.mustNotMatch) { if strings.Contains(rs, tc.mustNotMatch) {
t.Errorf("random string contains unexpected character. got: %s, not-expected: %s", t.Errorf("random string contains unexpected character. got: %s, not-expected: %s",
rs, tc.mustNotMatch) rs, tc.mustNotMatch)
} }
if len(rs) != tc.length { if len(rs) != tc.length {
t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs)) t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs))
} }
}) })
} }
})
t.Run("randomStringSecure fails on broken rand Reader (first read)", func(t *testing.T) {
defaultRandReader := rand.Reader
t.Cleanup(func() { rand.Reader = defaultRandReader })
rand.Reader = &randReader{failon: 1}
if _, err := randomStringSecure(22); err == nil {
t.Fatalf("expected failure on broken rand Reader")
}
})
t.Run("randomStringSecure fails on broken rand Reader (second read)", func(t *testing.T) {
defaultRandReader := rand.Reader
t.Cleanup(func() { rand.Reader = defaultRandReader })
rand.Reader = &randReader{failon: 0}
if _, err := randomStringSecure(22); err == nil {
t.Fatalf("expected failure on broken rand Reader")
}
})
} }
func BenchmarkGenerator_RandomStringSecure(b *testing.B) { func BenchmarkGenerator_RandomStringSecure(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := randomStringSecure(22) _, err := randomStringSecure(10)
if err != nil { if err != nil {
b.Errorf("RandomStringFromCharRange() failed: %s", err) b.Errorf("RandomStringFromCharRange() failed: %s", err)
} }
} }
} }
// randReader is type that satisfies the io.Reader interface. It can fail on a specific read
// operations and is therefore useful to test consecutive reads with errors
type randReader struct {
failon uint8
call uint8
}
// Read implements the io.Reader interface for the randReader type
func (r *randReader) Read(p []byte) (int, error) {
if r.call == r.failon {
r.call++
return len(p), nil
}
return 0, errors.New("broken reader")
}

View file

@ -81,7 +81,7 @@ type SendErrReason int
// Returns: // Returns:
// - A string representing the error message. // - A string representing the error message.
func (e *SendError) Error() string { func (e *SendError) Error() string {
if e.Reason > 10 { if e.Reason > ErrAmbiguous {
return "unknown reason" return "unknown reason"
} }
@ -93,7 +93,7 @@ func (e *SendError) Error() string {
errMessage.WriteRune(' ') errMessage.WriteRune(' ')
errMessage.WriteString(e.errlist[i].Error()) errMessage.WriteString(e.errlist[i].Error())
if i != len(e.errlist)-1 { if i != len(e.errlist)-1 {
errMessage.WriteString(", ") errMessage.WriteString(",")
} }
} }
} }

View file

@ -6,163 +6,210 @@ package mail
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
) )
// TestSendError_Error tests the SendError and SendErrReason error handling methods // TestSendError_Error tests the SendError and SendErrReason error handling methods
func TestSendError_Error(t *testing.T) { func TestSendError_Error(t *testing.T) {
tl := []struct { t.Run("TestSendError_Error with various reasons", func(t *testing.T) {
n string tests := []struct {
r SendErrReason name string
te bool reason SendErrReason
}{ isTemp bool
{"ErrGetSender/temp", ErrGetSender, true}, }{
{"ErrGetSender/perm", ErrGetSender, false}, {"ErrGetSender/temp", ErrGetSender, true},
{"ErrGetRcpts/temp", ErrGetRcpts, true}, {"ErrGetSender/perm", ErrGetSender, false},
{"ErrGetRcpts/perm", ErrGetRcpts, false}, {"ErrGetRcpts/temp", ErrGetRcpts, true},
{"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, {"ErrGetRcpts/perm", ErrGetRcpts, false},
{"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true},
{"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false},
{"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true},
{"ErrSMTPData/temp", ErrSMTPData, true}, {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false},
{"ErrSMTPData/perm", ErrSMTPData, false}, {"ErrSMTPData/temp", ErrSMTPData, true},
{"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, {"ErrSMTPData/perm", ErrSMTPData, false},
{"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true},
{"ErrSMTPReset/temp", ErrSMTPReset, true}, {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false},
{"ErrSMTPReset/perm", ErrSMTPReset, false}, {"ErrSMTPReset/temp", ErrSMTPReset, true},
{"ErrWriteContent/temp", ErrWriteContent, true}, {"ErrSMTPReset/perm", ErrSMTPReset, false},
{"ErrWriteContent/perm", ErrWriteContent, false}, {"ErrWriteContent/temp", ErrWriteContent, true},
{"ErrConnCheck/temp", ErrConnCheck, true}, {"ErrWriteContent/perm", ErrWriteContent, false},
{"ErrConnCheck/perm", ErrConnCheck, false}, {"ErrConnCheck/temp", ErrConnCheck, true},
{"ErrNoUnencoded/temp", ErrNoUnencoded, true}, {"ErrConnCheck/perm", ErrConnCheck, false},
{"ErrNoUnencoded/perm", ErrNoUnencoded, false}, {"ErrNoUnencoded/temp", ErrNoUnencoded, true},
{"ErrAmbiguous/temp", ErrAmbiguous, true}, {"ErrNoUnencoded/perm", ErrNoUnencoded, false},
{"ErrAmbiguous/perm", ErrAmbiguous, false}, {"ErrAmbiguous/temp", ErrAmbiguous, true},
{"Unknown/temp", 9999, true}, {"ErrAmbiguous/perm", ErrAmbiguous, false},
{"Unknown/perm", 9999, false}, {"Unknown/temp", 9999, true},
} {"Unknown/perm", 9999, false},
}
for _, tt := range tl { for _, tt := range tests {
t.Run(tt.n, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if err := returnSendError(tt.r, tt.te); err != nil { err := returnSendError(tt.reason, tt.isTemp)
exp := &SendError{Reason: tt.r, isTemp: tt.te} if err == nil {
if !errors.Is(err, exp) { t.Fatalf("error expected, got nil")
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te,
exp.Error(), exp.isTemp)
} }
if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) { want := &SendError{Reason: tt.reason, isTemp: tt.isTemp}
if !errors.Is(err, want) {
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)",
tt.reason, tt.isTemp, want.Error(), want.isTemp)
}
if !strings.Contains(err.Error(), tt.reason.String()) {
t.Errorf("error string mismatch, expected: %s, got: %s", t.Errorf("error string mismatch, expected: %s, got: %s",
tt.r.String(), fmt.Sprintf("%s", err)) tt.reason.String(), err.Error())
} }
} })
}) }
} })
t.Run("TestSendError_Error with multiple errors", func(t *testing.T) {
message := testMessage(t)
err := &SendError{
affectedMsg: message,
errlist: []error{ErrNoRcptAddresses, ErrNoFromAddress},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
}
if !strings.Contains(err.Error(), "ambiguous reason, check Msg.SendError for message specific reasons") {
t.Errorf("error string mismatch, expected: ambiguous reason, check Msg.SendError for message "+
"specific reasons, got: %s", err.Error())
}
if !strings.Contains(err.Error(), "no recipient addresses set, no FROM address set") {
t.Errorf("error string mismatch, expected: no recipient addresses set, no FROM address set, got: %s",
err.Error())
}
if !strings.Contains(err.Error(), "affected recipient(s): <toni.tester@domain.tld>, "+
"<tina.tester@domain.tld>") {
t.Errorf("error string mismatch, expected: affected recipient(s): <toni.tester@domain.tld>, "+
"<tina.tester@domain.tld>, got: %s", err.Error())
}
})
}
func TestSendError_Is(t *testing.T) {
t.Run("TestSendError_Is errors match", func(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false)
err2 := returnSendError(ErrAmbiguous, false)
if !errors.Is(err1, err2) {
t.Error("error mismatch, expected ErrAmbiguous to be equal to ErrAmbiguous")
}
})
t.Run("TestSendError_Is errors mismatch", func(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false)
err2 := returnSendError(ErrSMTPMailFrom, false)
if errors.Is(err1, err2) {
t.Error("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom")
}
})
t.Run("TestSendError_Is on nil", func(t *testing.T) {
var err *SendError
if err.Is(ErrNoFromAddress) {
t.Error("expected false on nil-senderror")
}
})
} }
func TestSendError_IsTemp(t *testing.T) { func TestSendError_IsTemp(t *testing.T) {
var se *SendError t.Run("TestSendError_IsTemp is true", func(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, true) err := returnSendError(ErrAmbiguous, true)
if !errors.As(err1, &se) { if err == nil {
t.Errorf("error mismatch, expected error to be of type *SendError") t.Fatalf("error expected, got nil")
return }
} var sendErr *SendError
if errors.As(err1, &se) && !se.IsTemp() { if !errors.As(err, &sendErr) {
t.Errorf("error mismatch, expected temporary error") t.Fatal("error expected to be of type *SendError")
return }
} if !sendErr.IsTemp() {
err2 := returnSendError(ErrAmbiguous, false) t.Errorf("expected temporary error, got: temperr: %t", sendErr.IsTemp())
if !errors.As(err2, &se) { }
t.Errorf("error mismatch, expected error to be of type *SendError") })
return t.Run("TestSendError_IsTemp is false", func(t *testing.T) {
} err := returnSendError(ErrAmbiguous, false)
if errors.As(err2, &se) && se.IsTemp() { if err == nil {
t.Errorf("error mismatch, expected non-temporary error") t.Fatalf("error expected, got nil")
return }
} var sendErr *SendError
} if !errors.As(err, &sendErr) {
t.Fatal("error expected to be of type *SendError")
func TestSendError_IsTempNil(t *testing.T) { }
var se *SendError if sendErr.IsTemp() {
if se.IsTemp() { t.Errorf("expected permanent error, got: temperr: %t", sendErr.IsTemp())
t.Error("expected false on nil-senderror") }
} })
t.Run("TestSendError_IsTemp is nil", func(t *testing.T) {
var se *SendError
if se.IsTemp() {
t.Error("expected false on nil-senderror")
}
})
} }
func TestSendError_MessageID(t *testing.T) { func TestSendError_MessageID(t *testing.T) {
var se *SendError t.Run("TestSendError_MessageID message ID is set", func(t *testing.T) {
err := returnSendError(ErrAmbiguous, false) var sendErr *SendError
if !errors.As(err, &se) { err := returnSendError(ErrAmbiguous, false)
t.Errorf("error mismatch, expected error to be of type *SendError") if !errors.As(err, &sendErr) {
return t.Fatal("error mismatch, expected error to be of type *SendError")
}
if errors.As(err, &se) {
if se.MessageID() == "" {
t.Errorf("sendError expected message-id, but got empty string")
} }
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") { if sendErr.MessageID() == "" {
t.Error("sendError expected message-id, but got empty string")
}
if !strings.EqualFold(sendErr.MessageID(), "<this.is.a.message.id>") {
t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>", t.Errorf("sendError message-id expected: %s, but got: %s", "<this.is.a.message.id>",
se.MessageID()) sendErr.MessageID())
} }
} })
} t.Run("TestSendError_MessageID message ID is not set", func(t *testing.T) {
var sendErr *SendError
func TestSendError_MessageIDNil(t *testing.T) { message := testMessage(t)
var se *SendError err := &SendError{
if se.MessageID() != "" { affectedMsg: message,
t.Error("expected empty string on nil-senderror") errlist: []error{ErrNoRcptAddresses},
} rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
}
if !errors.As(err, &sendErr) {
t.Fatal("error mismatch, expected error to be of type *SendError")
}
if sendErr.MessageID() != "" {
t.Errorf("sendError expected empty message-id, got: %s", sendErr.MessageID())
}
})
} }
func TestSendError_Msg(t *testing.T) { func TestSendError_Msg(t *testing.T) {
var se *SendError t.Run("TestSendError_Msg message is set", func(t *testing.T) {
err := returnSendError(ErrAmbiguous, false) var sendErr *SendError
if !errors.As(err, &se) { err := returnSendError(ErrAmbiguous, false)
t.Errorf("error mismatch, expected error to be of type *SendError") if !errors.As(err, &sendErr) {
return t.Fatal("error mismatch, expected error to be of type *SendError")
}
if errors.As(err, &se) {
if se.Msg() == nil {
t.Errorf("sendError expected msg pointer, but got nil")
} }
from := se.Msg().GetFromString() msg := sendErr.Msg()
if msg == nil {
t.Fatalf("sendError expected msg pointer, but got nil")
}
from := msg.GetFromString()
if len(from) == 0 { if len(from) == 0 {
t.Errorf("sendError expected msg from, but got empty string") t.Fatal("sendError expected msg from, but got empty string")
return
} }
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") { if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>", t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
from[0]) from[0])
} }
} })
} t.Run("TestSendError_Msg message is not set", func(t *testing.T) {
var sendErr *SendError
func TestSendError_MsgNil(t *testing.T) { err := &SendError{
var se *SendError errlist: []error{ErrNoRcptAddresses},
if se.Msg() != nil { rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
t.Error("expected nil on nil-senderror") Reason: ErrAmbiguous,
} }
} if !errors.As(err, &sendErr) {
t.Fatal("error mismatch, expected error to be of type *SendError")
func TestSendError_IsFail(t *testing.T) { }
err1 := returnSendError(ErrAmbiguous, false) if sendErr.Msg() != nil {
err2 := returnSendError(ErrSMTPMailFrom, false) t.Errorf("sendError expected nil msg pointer, got: %v", sendErr.Msg())
if errors.Is(err1, err2) { }
t.Errorf("error mismatch, ErrAmbiguous should not be equal to ErrSMTPMailFrom") })
}
}
func TestSendError_ErrorMulti(t *testing.T) {
expected := `ambiguous reason, check Msg.SendError for message specific reasons, ` +
`affected recipient(s): <email1@domain.tld>, <email2@domain.tld>`
err := &SendError{
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
}
if err.Error() != expected {
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
}
} }
// returnSendError is a helper method to retunr a SendError with a specific reason // returnSendError is a helper method to retunr a SendError with a specific reason
@ -173,6 +220,5 @@ func returnSendError(r SendErrReason, t bool) error {
message.Subject("This is the subject") message.Subject("This is the subject")
message.SetBodyString(TypeTextPlain, "This is the message body") message.SetBodyString(TypeTextPlain, "This is the message body")
message.SetMessageIDWithValue("this.is.a.message.id") message.SetMessageIDWithValue("this.is.a.message.id")
return &SendError{Reason: r, isTemp: t, affectedMsg: message} return &SendError{Reason: r, isTemp: t, affectedMsg: message}
} }