Compare commits

..

1 commit

Author SHA1 Message Date
Michael Fuchs
b564960d71
Merge e56a563286 into d7e0b48567 2024-10-30 15:50:55 +01:00
4 changed files with 160 additions and 242 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 && sudo DEBIAN_FRONTEND=noninteractive apt-get -y install nullmailer && which sendmail 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
- name: Run go test - name: Run go test
if: success() if: success()
run: | run: |

View file

@ -5,81 +5,45 @@
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) {
t.Run("randomStringSecure with varying length", func(t *testing.T) { tt := []struct {
tt := []struct { testName string
testName string length int
length int mustNotMatch string
mustNotMatch string }{
}{ {"20 chars", 20, "'"},
{"20 chars", 20, "'"}, {"100 chars", 100, "'"},
{"100 chars", 100, "'"}, {"1000 chars", 1000, "'"},
{"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(10) _, err := randomStringSecure(22)
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 > ErrAmbiguous { if e.Reason > 10 {
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,210 +6,163 @@ 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) {
t.Run("TestSendError_Error with various reasons", func(t *testing.T) { tl := []struct {
tests := []struct { n string
name string r SendErrReason
reason SendErrReason te bool
isTemp bool }{
}{ {"ErrGetSender/temp", ErrGetSender, true},
{"ErrGetSender/temp", ErrGetSender, true}, {"ErrGetSender/perm", ErrGetSender, false},
{"ErrGetSender/perm", ErrGetSender, false}, {"ErrGetRcpts/temp", ErrGetRcpts, true},
{"ErrGetRcpts/temp", ErrGetRcpts, true}, {"ErrGetRcpts/perm", ErrGetRcpts, false},
{"ErrGetRcpts/perm", ErrGetRcpts, false}, {"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true},
{"ErrSMTPMailFrom/temp", ErrSMTPMailFrom, true}, {"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false},
{"ErrSMTPMailFrom/perm", ErrSMTPMailFrom, false}, {"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true},
{"ErrSMTPRcptTo/temp", ErrSMTPRcptTo, true}, {"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false},
{"ErrSMTPRcptTo/perm", ErrSMTPRcptTo, false}, {"ErrSMTPData/temp", ErrSMTPData, true},
{"ErrSMTPData/temp", ErrSMTPData, true}, {"ErrSMTPData/perm", ErrSMTPData, false},
{"ErrSMTPData/perm", ErrSMTPData, false}, {"ErrSMTPDataClose/temp", ErrSMTPDataClose, true},
{"ErrSMTPDataClose/temp", ErrSMTPDataClose, true}, {"ErrSMTPDataClose/perm", ErrSMTPDataClose, false},
{"ErrSMTPDataClose/perm", ErrSMTPDataClose, false}, {"ErrSMTPReset/temp", ErrSMTPReset, true},
{"ErrSMTPReset/temp", ErrSMTPReset, true}, {"ErrSMTPReset/perm", ErrSMTPReset, false},
{"ErrSMTPReset/perm", ErrSMTPReset, false}, {"ErrWriteContent/temp", ErrWriteContent, true},
{"ErrWriteContent/temp", ErrWriteContent, true}, {"ErrWriteContent/perm", ErrWriteContent, false},
{"ErrWriteContent/perm", ErrWriteContent, false}, {"ErrConnCheck/temp", ErrConnCheck, true},
{"ErrConnCheck/temp", ErrConnCheck, true}, {"ErrConnCheck/perm", ErrConnCheck, false},
{"ErrConnCheck/perm", ErrConnCheck, false}, {"ErrNoUnencoded/temp", ErrNoUnencoded, true},
{"ErrNoUnencoded/temp", ErrNoUnencoded, true}, {"ErrNoUnencoded/perm", ErrNoUnencoded, false},
{"ErrNoUnencoded/perm", ErrNoUnencoded, false}, {"ErrAmbiguous/temp", ErrAmbiguous, true},
{"ErrAmbiguous/temp", ErrAmbiguous, true}, {"ErrAmbiguous/perm", ErrAmbiguous, false},
{"ErrAmbiguous/perm", ErrAmbiguous, false}, {"Unknown/temp", 9999, true},
{"Unknown/temp", 9999, true}, {"Unknown/perm", 9999, false},
{"Unknown/perm", 9999, false}, }
}
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")
}
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.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) { for _, tt := range tl {
t.Run("TestSendError_Is errors match", func(t *testing.T) { t.Run(tt.n, func(t *testing.T) {
err1 := returnSendError(ErrAmbiguous, false) if err := returnSendError(tt.r, tt.te); err != nil {
err2 := returnSendError(ErrAmbiguous, false) exp := &SendError{Reason: tt.r, isTemp: tt.te}
if !errors.Is(err1, err2) { if !errors.Is(err, exp) {
t.Error("error mismatch, expected ErrAmbiguous to be equal to ErrAmbiguous") t.Errorf("error mismatch, expected: %s (temp: %t), got: %s (temp: %t)", tt.r, tt.te,
} exp.Error(), exp.isTemp)
}) }
t.Run("TestSendError_Is errors mismatch", func(t *testing.T) { if !strings.Contains(fmt.Sprintf("%s", err), tt.r.String()) {
err1 := returnSendError(ErrAmbiguous, false) t.Errorf("error string mismatch, expected: %s, got: %s",
err2 := returnSendError(ErrSMTPMailFrom, false) tt.r.String(), fmt.Sprintf("%s", err))
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) {
t.Run("TestSendError_IsTemp is true", func(t *testing.T) { var se *SendError
err := returnSendError(ErrAmbiguous, true) err1 := returnSendError(ErrAmbiguous, true)
if err == nil { if !errors.As(err1, &se) {
t.Fatalf("error expected, got nil") t.Errorf("error mismatch, expected error to be of type *SendError")
} return
var sendErr *SendError }
if !errors.As(err, &sendErr) { if errors.As(err1, &se) && !se.IsTemp() {
t.Fatal("error expected to be of type *SendError") t.Errorf("error mismatch, expected temporary error")
} return
if !sendErr.IsTemp() { }
t.Errorf("expected temporary error, got: temperr: %t", sendErr.IsTemp()) err2 := returnSendError(ErrAmbiguous, false)
} if !errors.As(err2, &se) {
}) t.Errorf("error mismatch, expected error to be of type *SendError")
t.Run("TestSendError_IsTemp is false", func(t *testing.T) { return
err := returnSendError(ErrAmbiguous, false) }
if err == nil { if errors.As(err2, &se) && se.IsTemp() {
t.Fatalf("error expected, got nil") t.Errorf("error mismatch, expected non-temporary error")
} return
var sendErr *SendError }
if !errors.As(err, &sendErr) { }
t.Fatal("error expected to be of type *SendError")
} func TestSendError_IsTempNil(t *testing.T) {
if sendErr.IsTemp() { var se *SendError
t.Errorf("expected permanent error, got: temperr: %t", sendErr.IsTemp()) if se.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) {
t.Run("TestSendError_MessageID message ID is set", func(t *testing.T) { var se *SendError
var sendErr *SendError err := returnSendError(ErrAmbiguous, false)
err := returnSendError(ErrAmbiguous, false) if !errors.As(err, &se) {
if !errors.As(err, &sendErr) { t.Errorf("error mismatch, expected error to be of type *SendError")
t.Fatal("error mismatch, expected error to be of type *SendError") return
}
if errors.As(err, &se) {
if se.MessageID() == "" {
t.Errorf("sendError expected message-id, but got empty string")
} }
if sendErr.MessageID() == "" { if !strings.EqualFold(se.MessageID(), "<this.is.a.message.id>") {
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>",
sendErr.MessageID()) se.MessageID())
} }
}) }
t.Run("TestSendError_MessageID message ID is not set", func(t *testing.T) { }
var sendErr *SendError
message := testMessage(t) func TestSendError_MessageIDNil(t *testing.T) {
err := &SendError{ var se *SendError
affectedMsg: message, if se.MessageID() != "" {
errlist: []error{ErrNoRcptAddresses}, t.Error("expected empty string on nil-senderror")
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) {
t.Run("TestSendError_Msg message is set", func(t *testing.T) { var se *SendError
var sendErr *SendError err := returnSendError(ErrAmbiguous, false)
err := returnSendError(ErrAmbiguous, false) if !errors.As(err, &se) {
if !errors.As(err, &sendErr) { t.Errorf("error mismatch, expected error to be of type *SendError")
t.Fatal("error mismatch, expected error to be of type *SendError") return
}
if errors.As(err, &se) {
if se.Msg() == nil {
t.Errorf("sendError expected msg pointer, but got nil")
} }
msg := sendErr.Msg() from := se.Msg().GetFromString()
if msg == nil {
t.Fatalf("sendError expected msg pointer, but got nil")
}
from := msg.GetFromString()
if len(from) == 0 { if len(from) == 0 {
t.Fatal("sendError expected msg from, but got empty string") t.Errorf("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
err := &SendError{ func TestSendError_MsgNil(t *testing.T) {
errlist: []error{ErrNoRcptAddresses}, var se *SendError
rcpt: []string{"<toni.tester@domain.tld>", "<tina.tester@domain.tld>"}, if se.Msg() != nil {
Reason: ErrAmbiguous, t.Error("expected nil on nil-senderror")
} }
if !errors.As(err, &sendErr) { }
t.Fatal("error mismatch, expected error to be of type *SendError")
} func TestSendError_IsFail(t *testing.T) {
if sendErr.Msg() != nil { err1 := returnSendError(ErrAmbiguous, false)
t.Errorf("sendError expected nil msg pointer, got: %v", sendErr.Msg()) 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>`
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
@ -220,5 +173,6 @@ 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}
} }