Alerting: Add abstraction layer and testing hooks in front of SMTP dialer (#43875)

* Add abstraction layer above SMTP communication

* Fix issues with attachments and sync command

* Tests for bad SMTP behavior

* Separate tests between async and sync entry points. Test difference between them

* Return interface so Wire can properly map types

* Address feedback from George
pull/43783/head
Alexander Weaver 4 years ago committed by GitHub
parent 8114f6b065
commit c68eefd398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      pkg/server/wire.go
  2. 130
      pkg/services/notifications/mailer.go
  3. 41
      pkg/services/notifications/mailer_test.go
  4. 5
      pkg/services/notifications/notifications.go
  5. 270
      pkg/services/notifications/notifications_test.go
  6. 147
      pkg/services/notifications/smtp.go
  7. 108
      pkg/services/notifications/smtp_test.go
  8. 32
      pkg/services/notifications/testing.go

@ -150,6 +150,7 @@ var wireBasicSet = wire.NewSet(
libraryelements.ProvideService,
wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)),
notifications.ProvideService,
notifications.ProvideSmtpService,
tracing.ProvideService,
metrics.ProvideService,
testdatasource.ProvideService,

@ -6,21 +6,13 @@ package notifications
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"io"
"net"
"net/mail"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
gomail "gopkg.in/mail.v2"
)
var (
@ -42,6 +34,10 @@ func init() {
})
}
type Mailer interface {
Send(messages ...*Message) (int, error)
}
func (ns *NotificationService) Send(msg *Message) (int, error) {
messages := []*Message{}
@ -55,123 +51,7 @@ func (ns *NotificationService) Send(msg *Message) (int, error) {
}
}
return ns.dialAndSend(messages...)
}
func (ns *NotificationService) dialAndSend(messages ...*Message) (int, error) {
sentEmailsCount := 0
dialer, err := ns.createDialer()
if err != nil {
return sentEmailsCount, err
}
for _, msg := range messages {
m := ns.buildEmail(msg)
innerError := dialer.DialAndSend(m)
emailsSentTotal.Inc()
if innerError != nil {
// As gomail does not returned typed errors we have to parse the error
// to catch invalid error when the address is invalid.
// https://github.com/go-gomail/gomail/blob/81ebce5c23dfd25c6c67194b37d3dd3f338c98b1/send.go#L113
if !strings.HasPrefix(innerError.Error(), "gomail: invalid address") {
emailsSentFailed.Inc()
}
err = errutil.Wrapf(innerError, "Failed to send notification to email addresses: %s", strings.Join(msg.To, ";"))
continue
}
sentEmailsCount++
}
return sentEmailsCount, err
}
func (ns *NotificationService) buildEmail(msg *Message) *gomail.Message {
m := gomail.NewMessage()
m.SetHeader("From", msg.From)
m.SetHeader("To", msg.To...)
m.SetHeader("Subject", msg.Subject)
ns.setFiles(m, msg)
for _, replyTo := range msg.ReplyTo {
m.SetAddressHeader("Reply-To", replyTo, "")
}
// loop over content types from settings in reverse order as they are ordered in according to descending
// preference while the alternatives should be ordered according to ascending preference
for i := len(ns.Cfg.Smtp.ContentTypes) - 1; i >= 0; i-- {
if i == len(ns.Cfg.Smtp.ContentTypes)-1 {
m.SetBody(ns.Cfg.Smtp.ContentTypes[i], msg.Body[ns.Cfg.Smtp.ContentTypes[i]])
} else {
m.AddAlternative(ns.Cfg.Smtp.ContentTypes[i], msg.Body[ns.Cfg.Smtp.ContentTypes[i]])
}
}
return m
}
// setFiles attaches files in various forms
func (ns *NotificationService) setFiles(
m *gomail.Message,
msg *Message,
) {
for _, file := range msg.EmbeddedFiles {
m.Embed(file)
}
for _, file := range msg.AttachedFiles {
file := file
m.Attach(file.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
_, err := writer.Write(file.Content)
return err
}))
}
}
func (ns *NotificationService) createDialer() (*gomail.Dialer, error) {
host, port, err := net.SplitHostPort(ns.Cfg.Smtp.Host)
if err != nil {
return nil, err
}
iPort, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
tlsconfig := &tls.Config{
InsecureSkipVerify: ns.Cfg.Smtp.SkipVerify,
ServerName: host,
}
if ns.Cfg.Smtp.CertFile != "" {
cert, err := tls.LoadX509KeyPair(ns.Cfg.Smtp.CertFile, ns.Cfg.Smtp.KeyFile)
if err != nil {
return nil, fmt.Errorf("could not load cert or key file: %w", err)
}
tlsconfig.Certificates = []tls.Certificate{cert}
}
d := gomail.NewDialer(host, iPort, ns.Cfg.Smtp.User, ns.Cfg.Smtp.Password)
d.TLSConfig = tlsconfig
d.StartTLSPolicy = getStartTLSPolicy(ns.Cfg.Smtp.StartTLSPolicy)
if ns.Cfg.Smtp.EhloIdentity != "" {
d.LocalName = ns.Cfg.Smtp.EhloIdentity
} else {
d.LocalName = setting.InstanceName
}
return d, nil
}
func getStartTLSPolicy(policy string) gomail.StartTLSPolicy {
switch policy {
case "NoStartTLS":
return -1
case "MandatoryStartTLS":
return 1
default:
return 0
}
return ns.mailer.Send(messages...)
}
func (ns *NotificationService) buildEmailMessage(cmd *models.SendEmailCommand) (*Message, error) {

@ -1,41 +0,0 @@
package notifications
import (
"bytes"
"strings"
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBuildMail(t *testing.T) {
ns := &NotificationService{
Cfg: setting.NewCfg(),
}
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
message := &Message{
To: []string{"to@address.com"},
From: "from@address.com",
Subject: "Some subject",
Body: map[string]string{
"text/html": "Some HTML body",
"text/plain": "Some plain text body",
},
ReplyTo: []string{"from@address.com"},
}
t.Run("When building email", func(t *testing.T) {
email := ns.buildEmail(message)
buf := new(bytes.Buffer)
_, err := email.WriteTo(buf)
require.NoError(t, err)
assert.Contains(t, buf.String(), "Some HTML body")
assert.Contains(t, buf.String(), "Some plain text body")
assert.Less(t, strings.Index(buf.String(), "Some plain text body"), strings.Index(buf.String(), "Some HTML body"))
})
}

@ -22,13 +22,14 @@ var tmplResetPassword = "reset_password"
var tmplSignUpStarted = "signup_started"
var tmplWelcomeOnSignUp = "welcome_on_signup"
func ProvideService(bus bus.Bus, cfg *setting.Cfg) (*NotificationService, error) {
func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer) (*NotificationService, error) {
ns := &NotificationService{
Bus: bus,
Cfg: cfg,
log: log.New("notifications"),
mailQueue: make(chan *Message, 10),
webhookQueue: make(chan *Webhook, 10),
mailer: mailer,
}
ns.Bus.AddHandler(ns.sendResetPasswordEmail)
@ -70,6 +71,7 @@ type NotificationService struct {
mailQueue chan *Message
webhookQueue chan *Webhook
mailer Mailer
log log.Logger
}
@ -125,6 +127,7 @@ func (ns *NotificationService) sendEmailCommandHandlerSync(ctx context.Context,
To: cmd.To,
SingleEmail: cmd.SingleEmail,
EmbeddedFiles: cmd.EmbeddedFiles,
AttachedFiles: cmd.AttachedFiles,
Subject: cmd.Subject,
ReplyTo: cmd.ReplyTo,
})

@ -11,30 +11,268 @@ import (
"github.com/stretchr/testify/require"
)
func TestNotificationService(t *testing.T) {
ns := &NotificationService{
Cfg: setting.NewCfg(),
}
ns.Cfg.StaticRootPath = "../../../public/"
ns.Cfg.Smtp.Enabled = true
ns.Cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
ns.Cfg.Smtp.FromAddress = "from@address.com"
ns.Cfg.Smtp.FromName = "Grafana Admin"
ns.Cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
ns.Bus = bus.New()
ns, err := ProvideService(bus.New(), ns.Cfg)
require.NoError(t, err)
func TestProvideService(t *testing.T) {
bus := bus.New()
t.Run("When invalid from_address in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.FromAddress = "@notanemail@"
_, _, err := createSutWithConfig(bus, cfg)
require.Error(t, err)
})
t.Run("When template_patterns fails to parse", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.TemplatesPatterns = append(cfg.Smtp.TemplatesPatterns, "/usr/not-a-dir/**")
_, _, err := createSutWithConfig(bus, cfg)
require.Error(t, err)
})
}
func TestSendEmailSync(t *testing.T) {
bus := bus.New()
t.Run("When sending emails synchronously", func(t *testing.T) {
_, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"asdf@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
},
}
err := bus.Dispatch(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, mailer.Sent)
sent := mailer.Sent[len(mailer.Sent)-1]
require.Equal(t, "subject", sent.Subject)
require.Equal(t, []string{"asdf@grafana.com"}, sent.To)
})
t.Run("When using Single Email mode with multiple recipients", func(t *testing.T) {
_, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: true,
Template: "welcome_on_signup",
},
}
err := bus.Dispatch(context.Background(), cmd)
require.NoError(t, err)
require.Len(t, mailer.Sent, 1)
})
t.Run("When using Multi Email mode with multiple recipients", func(t *testing.T) {
_, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
},
}
err := bus.Dispatch(context.Background(), cmd)
require.NoError(t, err)
require.Len(t, mailer.Sent, 3)
})
t.Run("When attaching files to emails", func(t *testing.T) {
_, mailer := createSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Template: "welcome_on_signup",
AttachedFiles: []*models.SendEmailAttachFile{
{
Name: "attachment.txt",
Content: []byte("text file content"),
},
},
},
}
err := bus.Dispatch(context.Background(), cmd)
require.NoError(t, err)
require.NotEmpty(t, mailer.Sent)
sent := mailer.Sent[len(mailer.Sent)-1]
require.Len(t, sent.AttachedFiles, 1)
file := sent.AttachedFiles[len(sent.AttachedFiles)-1]
require.Equal(t, "attachment.txt", file.Name)
require.Equal(t, []byte("text file content"), file.Content)
})
t.Run("When SMTP disabled in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Enabled = false
_, mailer, err := createSutWithConfig(bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: true,
Template: "welcome_on_signup",
},
}
err = bus.Dispatch(context.Background(), cmd)
require.ErrorIs(t, err, models.ErrSmtpNotEnabled)
require.Empty(t, mailer.Sent)
})
t.Run("When invalid content type in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data")
_, mailer, err := createSutWithConfig(bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
},
}
err = bus.Dispatch(context.Background(), cmd)
require.Error(t, err)
require.Empty(t, mailer.Sent)
})
t.Run("When SMTP dialer is disconnected", func(t *testing.T) {
_ = createDisconnectedSut(t, bus)
cmd := &models.SendEmailCommandSync{
SendEmailCommand: models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
},
}
err := bus.Dispatch(context.Background(), cmd)
require.Error(t, err)
})
}
func TestSendEmailAsync(t *testing.T) {
bus := bus.New()
t.Run("When sending reset email password", func(t *testing.T) {
err := ns.sendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}})
sut, _ := createSut(t, bus)
err := sut.sendResetPasswordEmail(context.Background(), &models.SendResetPasswordEmailCommand{User: &models.User{Email: "asd@asd.com"}})
require.NoError(t, err)
sentMsg := <-ns.mailQueue
sentMsg := <-sut.mailQueue
assert.Contains(t, sentMsg.Body["text/html"], "body")
assert.NotContains(t, sentMsg.Body["text/plain"], "body")
assert.Equal(t, "Reset your Grafana password - asd@asd.com", sentMsg.Subject)
assert.NotContains(t, sentMsg.Body["text/html"], "Subject")
assert.NotContains(t, sentMsg.Body["text/plain"], "Subject")
})
t.Run("When SMTP disabled in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Enabled = false
_, mailer, err := createSutWithConfig(bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: true,
Template: "welcome_on_signup",
}
err = bus.Dispatch(context.Background(), cmd)
require.ErrorIs(t, err, models.ErrSmtpNotEnabled)
require.Empty(t, mailer.Sent)
})
t.Run("When invalid content type in configuration", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.ContentTypes = append(cfg.Smtp.ContentTypes, "multipart/form-data")
_, mailer, err := createSutWithConfig(bus, cfg)
require.NoError(t, err)
cmd := &models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
}
err = bus.Dispatch(context.Background(), cmd)
require.Error(t, err)
require.Empty(t, mailer.Sent)
})
t.Run("When SMTP dialer is disconnected", func(t *testing.T) {
_ = createDisconnectedSut(t, bus)
cmd := &models.SendEmailCommand{
Subject: "subject",
To: []string{"1@grafana.com", "2@grafana.com", "3@grafana.com"},
SingleEmail: false,
Template: "welcome_on_signup",
}
err := bus.Dispatch(context.Background(), cmd)
// The async version should not surface connection errors via Bus. It should only log them.
require.NoError(t, err)
})
}
func createSut(t *testing.T, bus bus.Bus) (*NotificationService, *FakeMailer) {
t.Helper()
cfg := createSmtpConfig()
ns, fm, err := createSutWithConfig(bus, cfg)
require.NoError(t, err)
return ns, fm
}
func createSutWithConfig(bus bus.Bus, cfg *setting.Cfg) (*NotificationService, *FakeMailer, error) {
smtp := NewFakeMailer()
ns, err := ProvideService(bus, cfg, smtp)
return ns, smtp, err
}
func createDisconnectedSut(t *testing.T, bus bus.Bus) *NotificationService {
t.Helper()
cfg := createSmtpConfig()
smtp := NewFakeDisconnectedMailer()
ns, err := ProvideService(bus, cfg, smtp)
require.NoError(t, err)
return ns
}
func createSmtpConfig() *setting.Cfg {
cfg := setting.NewCfg()
cfg.StaticRootPath = "../../../public/"
cfg.Smtp.Enabled = true
cfg.Smtp.TemplatesPatterns = []string{"emails/*.html", "emails/*.txt"}
cfg.Smtp.FromAddress = "from@address.com"
cfg.Smtp.FromName = "Grafana Admin"
cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
return cfg
}

@ -0,0 +1,147 @@
package notifications
import (
"crypto/tls"
"fmt"
"io"
"net"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
gomail "gopkg.in/mail.v2"
)
type SmtpClient struct {
cfg setting.SmtpSettings
}
func ProvideSmtpService(cfg *setting.Cfg) (Mailer, error) {
return NewSmtpClient(cfg.Smtp)
}
func NewSmtpClient(cfg setting.SmtpSettings) (*SmtpClient, error) {
client := &SmtpClient{
cfg: cfg,
}
return client, nil
}
func (sc *SmtpClient) Send(messages ...*Message) (int, error) {
sentEmailsCount := 0
dialer, err := sc.createDialer()
if err != nil {
return sentEmailsCount, err
}
for _, msg := range messages {
m := sc.buildEmail(msg)
innerError := dialer.DialAndSend(m)
emailsSentTotal.Inc()
if innerError != nil {
// As gomail does not returned typed errors we have to parse the error
// to catch invalid error when the address is invalid.
// https://github.com/go-gomail/gomail/blob/81ebce5c23dfd25c6c67194b37d3dd3f338c98b1/send.go#L113
if !strings.HasPrefix(innerError.Error(), "gomail: invalid address") {
emailsSentFailed.Inc()
}
err = errutil.Wrapf(innerError, "Failed to send notification to email addresses: %s", strings.Join(msg.To, ";"))
continue
}
sentEmailsCount++
}
return sentEmailsCount, err
}
// buildEmail converts the Message DTO to a gomail message.
func (sc *SmtpClient) buildEmail(msg *Message) *gomail.Message {
m := gomail.NewMessage()
m.SetHeader("From", msg.From)
m.SetHeader("To", msg.To...)
m.SetHeader("Subject", msg.Subject)
sc.setFiles(m, msg)
for _, replyTo := range msg.ReplyTo {
m.SetAddressHeader("Reply-To", replyTo, "")
}
// loop over content types from settings in reverse order as they are ordered in according to descending
// preference while the alternatives should be ordered according to ascending preference
for i := len(sc.cfg.ContentTypes) - 1; i >= 0; i-- {
if i == len(sc.cfg.ContentTypes)-1 {
m.SetBody(sc.cfg.ContentTypes[i], msg.Body[sc.cfg.ContentTypes[i]])
} else {
m.AddAlternative(sc.cfg.ContentTypes[i], msg.Body[sc.cfg.ContentTypes[i]])
}
}
return m
}
// setFiles attaches files in various forms.
func (sc *SmtpClient) setFiles(
m *gomail.Message,
msg *Message,
) {
for _, file := range msg.EmbeddedFiles {
m.Embed(file)
}
for _, file := range msg.AttachedFiles {
file := file
m.Attach(file.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
_, err := writer.Write(file.Content)
return err
}))
}
}
func (sc *SmtpClient) createDialer() (*gomail.Dialer, error) {
host, port, err := net.SplitHostPort(sc.cfg.Host)
if err != nil {
return nil, err
}
iPort, err := strconv.Atoi(port)
if err != nil {
return nil, err
}
tlsconfig := &tls.Config{
InsecureSkipVerify: sc.cfg.SkipVerify,
ServerName: host,
}
if sc.cfg.CertFile != "" {
cert, err := tls.LoadX509KeyPair(sc.cfg.CertFile, sc.cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("could not load cert or key file: %w", err)
}
tlsconfig.Certificates = []tls.Certificate{cert}
}
d := gomail.NewDialer(host, iPort, sc.cfg.User, sc.cfg.Password)
d.TLSConfig = tlsconfig
d.StartTLSPolicy = getStartTLSPolicy(sc.cfg.StartTLSPolicy)
if sc.cfg.EhloIdentity != "" {
d.LocalName = sc.cfg.EhloIdentity
} else {
d.LocalName = setting.InstanceName
}
return d, nil
}
func getStartTLSPolicy(policy string) gomail.StartTLSPolicy {
switch policy {
case "NoStartTLS":
return -1
case "MandatoryStartTLS":
return 1
default:
return 0
}
}

@ -0,0 +1,108 @@
package notifications
import (
"bytes"
"strings"
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBuildMail(t *testing.T) {
cfg := setting.NewCfg()
cfg.Smtp.ContentTypes = []string{"text/html", "text/plain"}
sc, err := NewSmtpClient(cfg.Smtp)
require.NoError(t, err)
message := &Message{
To: []string{"to@address.com"},
From: "from@address.com",
Subject: "Some subject",
Body: map[string]string{
"text/html": "Some HTML body",
"text/plain": "Some plain text body",
},
ReplyTo: []string{"from@address.com"},
}
t.Run("When building email", func(t *testing.T) {
email := sc.buildEmail(message)
buf := new(bytes.Buffer)
_, err := email.WriteTo(buf)
require.NoError(t, err)
assert.Contains(t, buf.String(), "Some HTML body")
assert.Contains(t, buf.String(), "Some plain text body")
assert.Less(t, strings.Index(buf.String(), "Some plain text body"), strings.Index(buf.String(), "Some HTML body"))
})
}
func TestSmtpDialer(t *testing.T) {
t.Run("When SMTP hostname is invalid", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "invalid%hostname:123:456"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(message)
require.Equal(t, 0, count)
require.EqualError(t, err, "address invalid%hostname:123:456: too many colons in address")
})
t.Run("When SMTP port is invalid", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "invalid%hostname:123a"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(message)
require.Equal(t, 0, count)
require.EqualError(t, err, "strconv.Atoi: parsing \"123a\": invalid syntax")
})
t.Run("When TLS certificate does not exist", func(t *testing.T) {
cfg := createSmtpConfig()
cfg.Smtp.Host = "localhost:1234"
cfg.Smtp.CertFile = "/var/certs/does-not-exist.pem"
client, err := ProvideSmtpService(cfg)
require.NoError(t, err)
message := &Message{
To: []string{"asdf@grafana.com"},
SingleEmail: true,
Subject: "subject",
Body: map[string]string{
"text/html": "body",
"text/plain": "body",
},
}
count, err := client.Send(message)
require.Equal(t, 0, count)
require.EqualError(t, err, "could not load cert or key file: open /var/certs/does-not-exist.pem: no such file or directory")
})
}

@ -0,0 +1,32 @@
package notifications
import "fmt"
type FakeMailer struct {
Sent []*Message
}
func NewFakeMailer() *FakeMailer {
return &FakeMailer{
Sent: make([]*Message, 0),
}
}
func (fm *FakeMailer) Send(messages ...*Message) (int, error) {
sentEmailsCount := 0
for _, msg := range messages {
fm.Sent = append(fm.Sent, msg)
sentEmailsCount++
}
return sentEmailsCount, nil
}
type FakeDisconnectedMailer struct{}
func NewFakeDisconnectedMailer() *FakeDisconnectedMailer {
return &FakeDisconnectedMailer{}
}
func (fdm *FakeDisconnectedMailer) Send(messages ...*Message) (int, error) {
return 0, fmt.Errorf("connect: connection refused")
}
Loading…
Cancel
Save