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
- name: Install sendmail
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
if: success()
run: |

View file

@ -5,12 +5,15 @@
package mail
import (
"crypto/rand"
"errors"
"strings"
"testing"
)
// TestRandomStringSecure tests the randomStringSecure method
func TestRandomStringSecure(t *testing.T) {
t.Run("randomStringSecure with varying length", func(t *testing.T) {
tt := []struct {
testName string
length int
@ -36,14 +39,47 @@ func TestRandomStringSecure(t *testing.T) {
}
})
}
})
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) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, err := randomStringSecure(22)
_, err := randomStringSecure(10)
if err != nil {
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:
// - A string representing the error message.
func (e *SendError) Error() string {
if e.Reason > 10 {
if e.Reason > ErrAmbiguous {
return "unknown reason"
}
@ -93,7 +93,7 @@ func (e *SendError) Error() string {
errMessage.WriteRune(' ')
errMessage.WriteString(e.errlist[i].Error())
if i != len(e.errlist)-1 {
errMessage.WriteString(", ")
errMessage.WriteString(",")
}
}
}

View file

@ -6,17 +6,17 @@ package mail
import (
"errors"
"fmt"
"strings"
"testing"
)
// TestSendError_Error tests the SendError and SendErrReason error handling methods
func TestSendError_Error(t *testing.T) {
tl := []struct {
n string
r SendErrReason
te bool
t.Run("TestSendError_Error with various reasons", func(t *testing.T) {
tests := []struct {
name string
reason SendErrReason
isTemp bool
}{
{"ErrGetSender/temp", ErrGetSender, true},
{"ErrGetSender/perm", ErrGetSender, false},
@ -43,126 +43,173 @@ func TestSendError_Error(t *testing.T) {
{"Unknown/temp", 9999, true},
{"Unknown/perm", 9999, false},
}
for _, tt := range tl {
t.Run(tt.n, func(t *testing.T) {
if err := returnSendError(tt.r, tt.te); err != nil {
exp := &SendError{Reason: tt.r, isTemp: tt.te}
if !errors.Is(err, exp) {
t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te,
exp.Error(), exp.isTemp)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := returnSendError(tt.reason, tt.isTemp)
if err == nil {
t.Fatalf("error expected, got nil")
}
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",
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) {
var se *SendError
err1 := returnSendError(ErrAmbiguous, true)
if !errors.As(err1, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
t.Run("TestSendError_IsTemp is true", func(t *testing.T) {
err := returnSendError(ErrAmbiguous, true)
if err == nil {
t.Fatalf("error expected, got nil")
}
if errors.As(err1, &se) && !se.IsTemp() {
t.Errorf("error mismatch, expected temporary error")
return
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Fatal("error expected to be of type *SendError")
}
err2 := returnSendError(ErrAmbiguous, false)
if !errors.As(err2, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
if !sendErr.IsTemp() {
t.Errorf("expected temporary error, got: temperr: %t", sendErr.IsTemp())
}
if errors.As(err2, &se) && se.IsTemp() {
t.Errorf("error mismatch, expected non-temporary error")
return
})
t.Run("TestSendError_IsTemp is false", func(t *testing.T) {
err := returnSendError(ErrAmbiguous, false)
if err == nil {
t.Fatalf("error expected, got nil")
}
}
func TestSendError_IsTempNil(t *testing.T) {
var sendErr *SendError
if !errors.As(err, &sendErr) {
t.Fatal("error expected to be of type *SendError")
}
if sendErr.IsTemp() {
t.Errorf("expected permanent error, got: temperr: %t", sendErr.IsTemp())
}
})
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) {
var se *SendError
t.Run("TestSendError_MessageID message ID is set", func(t *testing.T) {
var sendErr *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
if !errors.As(err, &sendErr) {
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 sendErr.MessageID() == "" {
t.Error("sendError expected message-id, but got empty string")
}
if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
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>",
se.MessageID())
sendErr.MessageID())
}
})
t.Run("TestSendError_MessageID message ID is not set", func(t *testing.T) {
var sendErr *SendError
message := testMessage(t)
err := &SendError{
affectedMsg: message,
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
}
}
func TestSendError_MessageIDNil(t *testing.T) {
var se *SendError
if se.MessageID() != "" {
t.Error("expected empty string on nil-senderror")
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) {
var se *SendError
t.Run("TestSendError_Msg message is set", func(t *testing.T) {
var sendErr *SendError
err := returnSendError(ErrAmbiguous, false)
if !errors.As(err, &se) {
t.Errorf("error mismatch, expected error to be of type *SendError")
return
if !errors.As(err, &sendErr) {
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")
msg := sendErr.Msg()
if msg == nil {
t.Fatalf("sendError expected msg pointer, but got nil")
}
from := se.Msg().GetFromString()
from := msg.GetFromString()
if len(from) == 0 {
t.Errorf("sendError expected msg from, but got empty string")
return
t.Fatal("sendError expected msg from, but got empty string")
}
if !strings.EqualFold(from[0], "<toni.tester@domain.tld>") {
t.Errorf("sendError message from expected: %s, but got: %s", "<toni.tester@domain.tld>",
from[0])
}
}
}
func TestSendError_MsgNil(t *testing.T) {
var se *SendError
if se.Msg() != nil {
t.Error("expected nil on nil-senderror")
}
}
func TestSendError_IsFail(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false)
err2 := returnSendError(ErrSMTPMailFrom, false)
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>`
})
t.Run("TestSendError_Msg message is not set", func(t *testing.T) {
var sendErr *SendError
err := &SendError{
Reason: ErrAmbiguous, isTemp: false, affectedMsg: nil,
rcpt: []string{"<email1@domain.tld>", "<email2@domain.tld>"},
errlist: []error{ErrNoRcptAddresses},
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"},
Reason: ErrAmbiguous,
}
if err.Error() != expected {
t.Errorf("error mismatch, expected: %s, got: %s", expected, err.Error())
if !errors.As(err, &sendErr) {
t.Fatal("error mismatch, expected error to be of type *SendError")
}
if sendErr.Msg() != nil {
t.Errorf("sendError expected nil msg pointer, got: %v", sendErr.Msg())
}
})
}
// 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.SetBodyString(TypeTextPlain, "This is the message body")
message.SetMessageIDWithValue("this.is.a.message.id")
return &SendError{Reason: r, isTemp: t, affectedMsg: message}
}