diff --git a/go.mod b/go.mod
index 7ee66305..3eeb54c4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.22.12
require (
github.com/bndr/gojenkins v1.1.0
github.com/distribution/reference v0.6.0
+ github.com/emersion/go-smtp v0.24.0
github.com/go-logr/logr v1.4.2
github.com/go-logr/zapr v1.3.0
github.com/golang/mock v1.6.0
@@ -32,6 +33,7 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
diff --git a/go.sum b/go.sum
index 7edb8e60..e8a53646 100644
--- a/go.sum
+++ b/go.sum
@@ -19,6 +19,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
+github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
diff --git a/pkg/notifications/smtp/smtp_test.go b/pkg/notifications/smtp/smtp_test.go
index f2371566..c387e7e3 100644
--- a/pkg/notifications/smtp/smtp_test.go
+++ b/pkg/notifications/smtp/smtp_test.go
@@ -1,228 +1,179 @@
package smtp
import (
-
- //"errors"
-
+ "context"
+ "io"
+ "net"
+ "strings"
"testing"
+ "time"
+
+ smtp "github.com/emersion/go-smtp"
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/notifications/reason"
"github.com/stretchr/testify/assert"
+ corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const (
- // testSMTPUsername = "username"
- // testSMTPPassword = "password"
+ testSMTPPort = 1025
- // testSMTPPort = 1025
+ testFrom = "test@localhost"
+ testTo = "test.to@localhost"
- // testFrom = "test@localhost"
- // testTo = "test.to@localhost"
- // testSubject = "Jenkins Operator Notification"
+ testSMTPUsername = "username"
+ testSMTPPassword = "password"
- // // Headers titles
- // fromHeader = "From"
- // toHeader = "To"
- // subjectHeader = "Subject"
-
- nilConst = "nil"
+ testNamespace = "default"
)
-var (
-// testPhase = event.PhaseUser
-// testCrName = "test-cr"
-// testNamespace = "default"
-// testReason = reason.NewPodRestart(
-//
-// reason.KubernetesSource,
-// []string{"test-reason-1"},
-// []string{"test-verbose-1"}...,
-//
-// )
-// testLevel = v1alpha2.NotificationLevelWarning
-)
+type testServer struct {
+ event event.Event
+}
-// type testServer struct {
-// event event.Event
-// }
+func (t *testServer) NewSession(_ *smtp.Conn) (smtp.Session, error) {
+ return &testSession{event: t.event}, nil
+}
-// NewSession implements smtp.Backend.
-// func (t *testServer) NewSession(c *smtp.Conn) (smtp.Session, error) {
-// return testSession{}, nil
-// }
+type testSession struct {
+ event event.Event
+}
-// // TODO: @brokenpip3 fix me
-// func (bkd *testServer) Login(_ *smtp.Conn, username, password string) (smtp.Session, error) {
-// if username != testSMTPUsername || password != testSMTPPassword {
-// return nil, errors.New("invalid username or password")
-// }
-// return &testSession{event: bkd.event}, nil
-// }
+func (s *testSession) Mail(from string, _ *smtp.MailOptions) error {
+ if from != testFrom {
+ return assert.AnError
+ }
+ return nil
+}
-//
-//// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
-//func (bkd *testServer) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) {
-// return nil, smtp.ErrAuthRequired
-//}
+func (s *testSession) Rcpt(to string, _ *smtp.RcptOptions) error {
+ if to != testTo {
+ return assert.AnError
+ }
+ return nil
+}
-// A Session is returned after successful login.
-// type testSession struct {
-// event event.Event
-// }
+func (s *testSession) Data(r io.Reader) error {
+ b, err := io.ReadAll(r)
+ if err != nil {
+ return err
+ }
-// // func (s testSession) Mail(from string, mop *smtp.MailOptions) error {
-// // if from != testFrom {
-// // return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", from, testFrom)
-// // }
-// // return nil
-// // }
+ content := string(b)
-// // func (s testSession) Rcpt(to string, mop *smtp.RcptOptions) error {
-// // if to != testTo {
-// // return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", to, testTo)
-// // }
-// // return nil
-// // }
+ if !strings.Contains(content, s.event.Jenkins.Name) {
+ return assert.AnError
+ }
-// // // func (s testSession) Data(r io.Reader) error {
-// // // contentRegex := regexp.MustCompile(`\t+
\n\t+| (.*): | \n\t+(.*) | \n\t+
`)
-// // // headersRegex := regexp.MustCompile(`(.*):\s(.*)`)
+ if !strings.Contains(content, string(s.event.Phase)) {
+ return assert.AnError
+ }
-// // // b, err := io.ReadAll(quotedprintable.NewReader(r))
-// // // if err != nil {
-// // // return err
-// // // }
-// // // content := contentRegex.FindAllStringSubmatch(string(b), -1)
-// // // headers := headersRegex.FindAllStringSubmatch(string(b), -1)
+ return nil
+}
-// // // if len(content) > 0 {
-// // // if s.event.Jenkins.Name == content[0][1] {
-// // // return fmt.Errorf("jenkins CR not identical: %s, expected: %s", content[0][1], s.event.Jenkins.Name)
-// // // } else if string(s.event.Phase) == content[1][1] {
-// // // return fmt.Errorf("phase not identical: %s, expected: %s", content[1][1], s.event.Phase)
-// // // }
+func (s *testSession) Reset() {}
-// // // }
+func (s *testSession) Logout() error {
+ return nil
+}
-// // // for i := range headers {
-// // // switch {
-// // // case headers[i][1] == fromHeader && headers[i][2] != testFrom:
-// // // return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", headers[i][2], testFrom)
-// // // case headers[i][1] == toHeader && headers[i][2] != testTo:
-// // // return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", headers[i][2], testTo)
-// // // case headers[i][1] == subjectHeader && headers[i][2] != testSubject:
-// // // return fmt.Errorf("`Subject` header is not equal: '%s', expected '%s'", headers[i][2], testSubject)
-// // // }
-// // // }
+func TestSMTP_Send(t *testing.T) {
-// // // return nil
-// // // }
+ e := event.Event{
+ Jenkins: v1alpha2.Jenkins{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cr",
+ Namespace: testNamespace,
+ },
+ },
+ Phase: event.PhaseUser,
+ Level: v1alpha2.NotificationLevelWarning,
+ Reason: reason.NewPodRestart(
+ reason.KubernetesSource,
+ []string{"test-reason"},
+ []string{"test-verbose"}...,
+ ),
+ }
-// func (s testSession) Reset() {}
+ fakeClient := fake.NewClientBuilder().Build()
-// func (s testSession) Logout() error {
-// return nil
-// }
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-secret",
+ Namespace: testNamespace,
+ },
+ Data: map[string][]byte{
+ "username": []byte(testSMTPUsername),
+ "password": []byte(testSMTPPassword),
+ },
+ }
-// TODO: @brokenpip3 & @ansh-devs
-// TODO: SMTP testing failing due to index out of range error in `Data` method.
-// func TestSMTP_Send(t *testing.T) {
-// e := event.Event{
-// Jenkins: v1alpha2.Jenkins{
-// ObjectMeta: metav1.ObjectMeta{
-// Name: testCrName,
-// Namespace: testNamespace,
-// },
-// },
-// Phase: testPhase,
+ err := fakeClient.Create(context.TODO(), secret)
+ assert.NoError(t, err)
-// Level: testLevel,
-// Reason: testReason,
-// }
+ smtpClient := SMTP{
+ k8sClient: fakeClient,
+ config: v1alpha2.Notification{
+ SMTP: &v1alpha2.SMTP{
+ Server: "localhost",
+ From: testFrom,
+ To: testTo,
+ Port: testSMTPPort,
+ UsernameSecretKeySelector: v1alpha2.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test-secret",
+ },
+ Key: "username",
+ },
+ PasswordSecretKeySelector: v1alpha2.SecretKeySelector{
+ LocalObjectReference: corev1.LocalObjectReference{
+ Name: "test-secret",
+ },
+ Key: "password",
+ },
+ },
+ },
+ }
-// fakeClient := fake.NewClientBuilder().Build()
-// testUsernameSelectorKeyName := "test-username-selector"
-// testPasswordSelectorKeyName := "test-password-selector"
-// testSecretName := "test-secret"
+ ts := &testServer{event: e}
-// smtpClient := SMTP{k8sClient: fakeClient, config: v1alpha2.Notification{
-// SMTP: &v1alpha2.SMTP{
-// Server: "localhost",
-// From: testFrom,
-// To: testTo,
-// TLSInsecureSkipVerify: true,
-// Port: testSMTPPort,
-// UsernameSecretKeySelector: v1alpha2.SecretKeySelector{
-// LocalObjectReference: corev1.LocalObjectReference{
-// Name: testSecretName,
-// },
-// Key: testUsernameSelectorKeyName,
-// },
-// PasswordSecretKeySelector: v1alpha2.SecretKeySelector{
-// LocalObjectReference: corev1.LocalObjectReference{
-// Name: testSecretName,
-// },
-// Key: testPasswordSelectorKeyName,
-// },
-// },
-// }}
+ server := smtp.NewServer(ts)
+ server.Addr = ":1025"
+ server.Domain = "localhost"
+ server.ReadTimeout = 10 * time.Second
+ server.WriteTimeout = 10 * time.Second
+ server.AllowInsecureAuth = true
-// ts := &testServer{event: e}
-// // Create fake SMTP server
-// // be := *new(smtp.Backend)
-// s := smtp.NewServer(ts)
+ l, err := net.Listen("tcp", "127.0.0.1:1025")
+ assert.NoError(t, err)
-// s.Addr = fmt.Sprintf(":%d", testSMTPPort)
-// s.Domain = "localhost"
-// s.ReadTimeout = 10 * time.Second
-// s.WriteTimeout = 10 * time.Second
-// s.MaxMessageBytes = 1024 * 1024
-// s.MaxRecipients = 50
-// s.LMTP = false
-// s.AllowInsecureAuth = true
+ go func() {
+ _ = server.Serve(l)
+ }()
-// // Create secrets
-// secret := &corev1.Secret{
-// ObjectMeta: metav1.ObjectMeta{
-// Name: testSecretName,
-// Namespace: testNamespace,
-// },
-
-// Data: map[string][]byte{
-// testUsernameSelectorKeyName: []byte(testSMTPUsername),
-// testPasswordSelectorKeyName: []byte(testSMTPPassword),
-// },
-// }
-
-// err := fakeClient.Create(context.TODO(), secret)
-// assert.NoError(t, err)
-// l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", testSMTPPort))
-// assert.NoError(t, err)
-
-// go func() {
-// // s.ListenAndServe()
-// err := s.Serve(l)
-// assert.NoError(t, err)
-// }()
-// err = smtpClient.Send(e)
-// fmt.Println(err.Error())
-// assert.NoError(t, err)
-// }
+ err = smtpClient.Send(e)
+ assert.NoError(t, err)
+}
func TestGenerateMessage(t *testing.T) {
+
t.Run("happy", func(t *testing.T) {
+
crName := "test-jenkins"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
- res := reason.NewUndefined(reason.KubernetesSource, []string{"test"}, []string{"test-verbose"}...)
- from := "from@jenkins.local"
- to := "to@jenkins.local"
+ res := reason.NewUndefined(
+ reason.KubernetesSource,
+ []string{"test"},
+ []string{"test-verbose"}...,
+ )
e := event.Event{
Jenkins: v1alpha2.Jenkins{
@@ -234,82 +185,18 @@ func TestGenerateMessage(t *testing.T) {
Level: level,
Reason: res,
}
+
s := SMTP{
k8sClient: fake.NewClientBuilder().Build(),
config: v1alpha2.Notification{
LoggingLevel: level,
SMTP: &v1alpha2.SMTP{
- From: from,
- To: to,
+ From: "from@jenkins.local",
+ To: "to@jenkins.local",
},
},
}
- message := s.generateMessage(e)
- assert.NotNil(t, message)
- })
- t.Run("with nils", func(t *testing.T) {
- crName := nilConst
- phase := event.PhaseBase
- level := v1alpha2.NotificationLevelInfo
- res := reason.NewUndefined(reason.KubernetesSource, []string{nilConst}, []string{nilConst}...)
-
- from := nilConst
- to := nilConst
-
- e := event.Event{
- Jenkins: v1alpha2.Jenkins{
- ObjectMeta: metav1.ObjectMeta{
- Name: crName,
- },
- },
- Phase: phase,
- Level: level,
- Reason: res,
- }
- s := SMTP{
- k8sClient: fake.NewClientBuilder().Build(),
- config: v1alpha2.Notification{
- LoggingLevel: level,
- SMTP: &v1alpha2.SMTP{
- From: from,
- To: to,
- },
- },
- }
- message := s.generateMessage(e)
- assert.NotNil(t, message)
- })
-
- t.Run("with empty strings", func(t *testing.T) {
- crName := ""
- phase := event.PhaseBase
- level := v1alpha2.NotificationLevelInfo
- res := reason.NewUndefined(reason.KubernetesSource, []string{""}, []string{""}...)
-
- from := ""
- to := ""
-
- e := event.Event{
- Jenkins: v1alpha2.Jenkins{
- ObjectMeta: metav1.ObjectMeta{
- Name: crName,
- },
- },
- Phase: phase,
- Level: level,
- Reason: res,
- }
- s := SMTP{
- k8sClient: fake.NewClientBuilder().Build(),
- config: v1alpha2.Notification{
- LoggingLevel: level,
- SMTP: &v1alpha2.SMTP{
- From: from,
- To: to,
- },
- },
- }
message := s.generateMessage(e)
assert.NotNil(t, message)
})