Skip to content

Commit 62b0ea1

Browse files
committed
tests(smtp): add more test coverage and test utils
1 parent 78ccf86 commit 62b0ea1

File tree

9 files changed

+815
-91
lines changed

9 files changed

+815
-91
lines changed

pkg/failures/failure.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package failures
2+
3+
import "fmt"
4+
5+
// FailureID is a number to be used to identify a specific error
6+
type FailureID int
7+
8+
const (
9+
// FailTestSetup is FailureID used to represent an error that is part of the setup for tests
10+
FailTestSetup FailureID = -1
11+
)
12+
13+
type failure struct {
14+
message string
15+
id FailureID
16+
stack string
17+
}
18+
19+
// Failure is an extended error that also includes an ID to be used to identify a specific error
20+
type Failure interface {
21+
error
22+
ID() FailureID
23+
}
24+
25+
func (f *failure) Error() string {
26+
return fmt.Sprintf("%s: %s", f.message, f.stack)
27+
}
28+
29+
func (f *failure) ID() FailureID {
30+
return f.id
31+
}
32+
33+
// Wrap returns a failure with the given message and id, saving the message of wrappedError for appending to Error()
34+
func Wrap(message string, id FailureID, wrappedError error, v ...interface{}) *failure {
35+
var stack string
36+
if wrappedError != nil {
37+
stack = wrappedError.Error()
38+
}
39+
40+
if len(v) > 0 {
41+
message = fmt.Sprintf(message, v...)
42+
}
43+
44+
return &failure{
45+
message: message,
46+
id: id,
47+
stack: stack,
48+
}
49+
}
50+
51+
// IsTestSetupFailure checks whether the given failure is due to the test setup being broken
52+
func IsTestSetupFailure(f Failure) (string, bool) {
53+
if f != nil && f.ID() == FailTestSetup {
54+
return fmt.Sprintf("test setup failed: %s", f.Error()), true
55+
}
56+
return "", false
57+
}
58+
59+
var _ error = &failure{}

pkg/services/smtp/smtp.go

+79-52
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"log"
88
"math/rand"
9+
"net"
910
"net/smtp"
1011
"net/url"
1112

@@ -29,11 +30,26 @@ const (
2930
// Initialize loads ServiceConfig from configURL and sets logger for this Service
3031
func (service *Service) Initialize(configURL *url.URL, logger *log.Logger) error {
3132
service.Logger.SetLogger(logger)
32-
service.config = &Config{}
33+
service.config = &Config{
34+
Port: 25,
35+
ToAddresses: nil,
36+
Subject: "",
37+
Auth: authTypes.Unknown,
38+
UseStartTLS: true,
39+
UseHTML: false,
40+
}
3341
if err := service.config.SetURL(configURL); err != nil {
3442
return err
3543
}
3644

45+
if service.config.Auth == authTypes.Unknown {
46+
if service.config.Username != "" {
47+
service.config.Auth = authTypes.Plain
48+
} else {
49+
service.config.Auth = authTypes.None
50+
}
51+
}
52+
3753
return nil
3854
}
3955

@@ -42,10 +58,28 @@ func (service *Service) Send(message string, params *map[string]string) error {
4258
if params == nil {
4359
params = &map[string]string{}
4460
}
45-
return service.doSend(message, *params)
61+
client, err := getClientConnection(service.config.Host, service.config.Port)
62+
if err != nil {
63+
return fail(FailGetSMTPClient, err)
64+
}
65+
return service.doSend(client, message, *params)
4666
}
4767

48-
func (service *Service) doSend(message string, params map[string]string) error {
68+
func getClientConnection(host string, port uint16) (*smtp.Client, error) {
69+
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
70+
if err != nil {
71+
return nil, fail(FailConnectToServer, err)
72+
}
73+
74+
client, err := smtp.NewClient(conn, host)
75+
if err != nil {
76+
return nil, fail(FailCreateSMTPClient, err)
77+
}
78+
79+
return client, nil
80+
}
81+
82+
func (service *Service) doSend(client *smtp.Client, message string, params map[string]string) failure {
4983
config := service.config
5084

5185
params["message"] = message
@@ -54,47 +88,42 @@ func (service *Service) doSend(message string, params map[string]string) error {
5488
service.multipartBoundry = fmt.Sprintf("%x", rand.Int63())
5589
}
5690

57-
client, err := smtp.Dial(fmt.Sprintf("%s:%d", config.Host, config.Port))
58-
if err != nil {
59-
return fmt.Errorf("error connecting to server: %s", err)
60-
}
61-
6291
if config.UseStartTLS {
6392
if err := client.StartTLS(&tls.Config{
6493
ServerName: config.Host,
6594
}); err != nil {
66-
return fmt.Errorf("error enabling StartTLS message: %s", err)
95+
return fail(FailEnableStartTLS, err)
6796
}
6897
}
6998

7099
if auth, err := service.getAuth(); err != nil {
71100
return err
72101
} else if auth != nil {
73102
if err := client.Auth(auth); err != nil {
74-
return fmt.Errorf("error authenticating: %s", err)
103+
return fail(FailAuthenticating, err)
75104
}
76105
}
77106

78107
for _, toAddress := range config.ToAddresses {
79108

80109
err := service.sendToRecipient(client, toAddress, &params)
81110
if err != nil {
82-
return fmt.Errorf("error sending message to recipient: %s", err)
111+
return fail(FailSendRecipient, err)
83112
}
84113

85114
service.Logf("Mail successfully sent to \"%s\"!\n", toAddress)
86115
}
87116

88117
// Send the QUIT command and close the connection.
89-
err = client.Quit()
118+
err := client.Quit()
90119
if err != nil {
91-
return fmt.Errorf("error closing session: %s", err)
120+
return fail(FailClosingSession, err)
92121
}
93122

94123
return nil
95124
}
96125

97-
func (service *Service) getAuth() (smtp.Auth, error) {
126+
func (service *Service) getAuth() (smtp.Auth, failure) {
98127

99128
config := service.config
100129

@@ -106,26 +135,26 @@ func (service *Service) getAuth() (smtp.Auth, error) {
106135
case authTypes.CRAMMD5:
107136
return smtp.CRAMMD5Auth(config.Username, config.Password), nil
108137
default:
109-
return nil, fmt.Errorf("invalid authorization method '%s'", config.Auth.String())
138+
return nil, fail(FailAuthType, nil, config.Auth.String())
110139
}
111140

112141
}
113142

114-
func (service *Service) sendToRecipient(client *smtp.Client, toAddress string, params *map[string]string) error {
143+
func (service *Service) sendToRecipient(client *smtp.Client, toAddress string, params *map[string]string) failure {
115144
conf := service.config
116145

117146
// Set the sender and recipient first
118147
if err := client.Mail(conf.FromAddress); err != nil {
119-
return fmt.Errorf("error creating new message: %s", err)
148+
return fail(FailSetSender, err)
120149
}
121150
if err := client.Rcpt(toAddress); err != nil {
122-
return fmt.Errorf("error setting RCPT: %s", err)
151+
return fail(FailSetRecipient, err)
123152
}
124153

125154
// Send the email body.
126155
wc, err := client.Data()
127156
if err != nil {
128-
return fmt.Errorf("error creating message stream: %s", err)
157+
return fail(FailOpenDataStream, err)
129158
}
130159

131160
// TODO: Move param override to shared service API
@@ -134,22 +163,23 @@ func (service *Service) sendToRecipient(client *smtp.Client, toAddress string, p
134163
subject = conf.Subject
135164
}
136165

137-
if err := writeHeaders(&wc, service.getHeaders(toAddress, subject)); err != nil {
138-
return fmt.Errorf("error writing message headers: %s", err)
166+
if err := writeHeaders(wc, service.getHeaders(toAddress, subject)); err != nil {
167+
return fail(FailWriteHeaders, err)
139168
}
140169

170+
var ferr failure
141171
if conf.UseHTML {
142-
err = service.writeMultipartMessage(&wc, params)
172+
ferr = service.writeMultipartMessage(wc, params)
143173
} else {
144-
err = writePlainMessage(&wc, (*params)["message"])
174+
ferr = service.writeMessagePart(wc, params, "plain")
145175
}
146176

147-
if err != nil {
148-
return err
177+
if ferr != nil {
178+
return ferr
149179
}
150180

151181
if err = wc.Close(); err != nil {
152-
return fmt.Errorf("error closing message stream: %s", err)
182+
return fail(FailCloseDataStream, err)
153183
}
154184

155185
return nil
@@ -174,72 +204,69 @@ func (service *Service) getHeaders(toAddress string, subject string) map[string]
174204
}
175205
}
176206

177-
func (service *Service) writeMultipartMessage(wc *io.WriteCloser, params *map[string]string) error {
178-
179-
message := (*params)["message"]
207+
func (service *Service) writeMultipartMessage(wc io.WriteCloser, params *map[string]string) failure {
180208

181209
if err := writeMultipartHeader(wc, service.multipartBoundry, contentPlain); err != nil {
182-
return fmt.Errorf("error writing message: %s", err)
210+
return fail(FailPlainHeader, err)
183211
}
184-
185-
if err := writePlainMessage(wc, message); err != nil {
212+
if err := service.writeMessagePart(wc, params, "plain"); err != nil {
186213
return err
187214
}
188215

189216
if err := writeMultipartHeader(wc, service.multipartBoundry, contentHTML); err != nil {
190-
return fmt.Errorf("error writing message: %s", err)
217+
return fail(FailHTMLHeader, err)
191218
}
192-
193-
if tpl, found := service.GetTemplate("message", ); found {
194-
if err := tpl.Execute(*wc, params); err != nil {
195-
return fmt.Errorf("error applying message template: %s", err)
196-
}
197-
} else {
198-
if _, err := fmt.Fprintf(*wc, message); err != nil {
199-
return fmt.Errorf("error writing message: %s", err)
200-
}
219+
if err := service.writeMessagePart(wc, params, "HTML"); err != nil {
220+
return err
201221
}
202222

203223
if err := writeMultipartHeader(wc, service.multipartBoundry, ""); err != nil {
204-
return fmt.Errorf("error writing message: %s", err)
224+
return fail(FailMultiEndHeader, err)
225+
205226
}
206227

207228
return nil
208229
}
209230

210-
func writePlainMessage(wc *io.WriteCloser, message string) error {
211-
if _, err := fmt.Fprintf(*wc, message); err != nil {
212-
return fmt.Errorf("error writing message: %s", err)
231+
func (service *Service) writeMessagePart(wc io.WriteCloser, params *map[string]string, template string) failure {
232+
if tpl, found := service.GetTemplate(template); found {
233+
if err := tpl.Execute(wc, params); err != nil {
234+
return fail(FailMessageTemplate, err)
235+
}
236+
} else {
237+
if _, err := fmt.Fprintf(wc, (*params)["message"]); err != nil {
238+
return fail(FailMessageRaw, err)
239+
}
213240
}
214241
return nil
215242
}
216243

217-
func writeMultipartHeader(wc *io.WriteCloser, boundry string, contentType string) error {
244+
func writeMultipartHeader(wc io.WriteCloser, boundry string, contentType string) error {
218245
suffix := "\n"
219246
if len(contentType) < 1 {
220247
suffix = "--"
221248
}
222249

223-
if _, err := fmt.Fprintf(*wc, "\n\n--%s%s", boundry, suffix); err != nil {
250+
if _, err := fmt.Fprintf(wc, "\n\n--%s%s", boundry, suffix); err != nil {
224251
return err
225252
}
226253

227254
if len(contentType) > 0 {
228-
if _, err := fmt.Fprintf(*wc, "Content-Type: %s\n\n", contentType); err != nil {
255+
if _, err := fmt.Fprintf(wc, "Content-Type: %s\n\n", contentType); err != nil {
229256
return err
230257
}
231258
}
232259

233260
return nil
234261
}
235262

236-
func writeHeaders(wc *io.WriteCloser, headers map[string]string) error {
263+
func writeHeaders(wc io.WriteCloser, headers map[string]string) error {
237264
for key, val := range headers {
238-
if _, err := fmt.Fprintf(*wc, "%s: %s\n", key, val); err != nil {
265+
if _, err := fmt.Fprintf(wc, "%s: %s\n", key, val); err != nil {
239266
return err
240267
}
241268
}
242269

243-
_, err := fmt.Fprintln(*wc)
270+
_, err := fmt.Fprintln(wc)
244271
return err
245272
}

0 commit comments

Comments
 (0)