From 46aae0620dfca4f774122c8da2c119358a7154cb Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Mon, 13 Nov 2023 12:39:16 +0100 Subject: [PATCH 1/2] feature: implement email sending settings The back office proxy supoports skipping the user resolution and overriding the email's sender and the email's default recipients, and the idea of these changes is for MBOP to support the same things too. RHCLOUD-29047 --- internal/handlers/send_email.go | 33 +++++++++++++++++---- internal/models/mail.go | 5 +++- internal/service/mailer/aws_ses_mailer.go | 4 +-- internal/service/mailer/mailer_interface.go | 2 +- internal/service/mailer/print_mailer.go | 7 +++-- 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/internal/handlers/send_email.go b/internal/handlers/send_email.go index 473b6afa..1a0d9def 100644 --- a/internal/handlers/send_email.go +++ b/internal/handlers/send_email.go @@ -36,21 +36,42 @@ func SendEmails(w http.ResponseWriter, r *http.Request) { return } + // The user might have wanted to override the sender that goes in the "from" field of the email. + var fromAddress string + if emails.EmailSender != "" { + fromAddress = emails.EmailSender + } else { + fromAddress = config.Get().FromEmail + } + for _, email := range emails.Emails { // creating a copy in order to pass it down into sub-functions email := email - err := mailer.LookupEmailsForUsernames(r.Context(), &email) - if err != nil { - l.Log.Error(err, "error translating usernames") - continue + // Lookup the emails for the given usernames unless the client specified otherwise. + if !emails.SkipUsersResolution { + err := mailer.LookupEmailsForUsernames(r.Context(), &email) + if err != nil { + l.Log.Error(err, "error translating usernames") + continue + } + + if len(email.Recipients) == 0 { + email.Recipients = []string{config.Get().ToEmail} + } } + // Should the user not specify any recipients, we need to determine if we should use the "default + // recipient" the user might have specified, or the default one that we set up in the configuration. if len(email.Recipients) == 0 { - email.Recipients = []string{config.Get().ToEmail} + if emails.DefaultRecipient != "" { + email.Recipients = []string{emails.DefaultRecipient} + } else { + email.Recipients = []string{config.Get().ToEmail} + } } - err = sender.SendEmail(r.Context(), &email) + err = sender.SendEmail(r.Context(), &email, fromAddress) if err != nil { l.Log.Error(err, "Error sending email", "email", email) } diff --git a/internal/models/mail.go b/internal/models/mail.go index 98d2b658..dbed0781 100644 --- a/internal/models/mail.go +++ b/internal/models/mail.go @@ -8,7 +8,10 @@ import ( ) type Emails struct { - Emails []Email `json:"emails,omitempty"` + Emails []Email `json:"emails,omitempty"` + EmailSender string `json:"emailSender,omitempty"` + DefaultRecipient string `json:"defaultRecipient,omitempty"` + SkipUsersResolution bool `json:"skipUsersResolution,omitempty"` } // taken from the BOP openapi spec diff --git a/internal/service/mailer/aws_ses_mailer.go b/internal/service/mailer/aws_ses_mailer.go index ec535a0a..9e400260 100644 --- a/internal/service/mailer/aws_ses_mailer.go +++ b/internal/service/mailer/aws_ses_mailer.go @@ -49,9 +49,9 @@ type awsSESEmailer struct { client *ses.Client } -func (s *awsSESEmailer) SendEmail(ctx context.Context, email *models.Email) error { +func (s *awsSESEmailer) SendEmail(ctx context.Context, email *models.Email, fromAddress string) error { out, err := s.client.SendEmail(ctx, &ses.SendEmailInput{ - FromEmailAddress: aws.String(config.Get().FromEmail), + FromEmailAddress: aws.String(fromAddress), Destination: &sesTypes.Destination{ ToAddresses: email.Recipients, CcAddresses: email.CcList, diff --git a/internal/service/mailer/mailer_interface.go b/internal/service/mailer/mailer_interface.go index 58fcb974..89756969 100644 --- a/internal/service/mailer/mailer_interface.go +++ b/internal/service/mailer/mailer_interface.go @@ -11,7 +11,7 @@ import ( ) type Emailer interface { - SendEmail(ctx context.Context, email *models.Email) error + SendEmail(ctx context.Context, email *models.Email, fromAddress string) error } func NewMailer() (Emailer, error) { diff --git a/internal/service/mailer/print_mailer.go b/internal/service/mailer/print_mailer.go index 3fcc0469..58271636 100644 --- a/internal/service/mailer/print_mailer.go +++ b/internal/service/mailer/print_mailer.go @@ -12,19 +12,20 @@ type printEmailer struct{} var _ = (Emailer)(&printEmailer{}) -func (p printEmailer) SendEmail(_ context.Context, email *models.Email) error { +func (p printEmailer) SendEmail(_ context.Context, email *models.Email, fromAddress string) error { l := 50 if len(email.Body) < 50 { l = len(email.Body) } - fmt.Printf(`To: %v + fmt.Printf(`From: %v +To: %v CC: %v BCC: %v Subject: %v BodyType: %v Message: %v...(truncated to 50 chars) -`, email.Recipients, email.CcList, email.BccList, email.Subject, email.BodyType, email.Body[:l]) +`, fromAddress, email.Recipients, email.CcList, email.BccList, email.Subject, email.BodyType, email.Body[:l]) return nil } From fd8e70ca98a518b2bc52baa26d8356cb574555a3 Mon Sep 17 00:00:00 2001 From: MikelAlejoBR Date: Mon, 25 Mar 2024 16:36:32 +0100 Subject: [PATCH 2/2] feature: tests for the "send_email" handler RHCLOUD-29047 --- internal/handlers/send_email.go | 102 ++-- internal/handlers/send_email_test.go | 622 ++++++++++++++++++++ internal/service/mailer/mailer_interface.go | 2 + internal/service/mailer/mock_mailer.go | 24 + 4 files changed, 701 insertions(+), 49 deletions(-) create mode 100644 internal/handlers/send_email_test.go create mode 100644 internal/service/mailer/mock_mailer.go diff --git a/internal/handlers/send_email.go b/internal/handlers/send_email.go index 1a0d9def..aa1c0fae 100644 --- a/internal/handlers/send_email.go +++ b/internal/handlers/send_email.go @@ -11,23 +11,10 @@ import ( "github.com/redhatinsights/mbop/internal/service/mailer" ) +// SendEmails sends the incoming payload's emails through the configured mailer module. func SendEmails(w http.ResponseWriter, r *http.Request) { switch config.Get().MailerModule { case awsModule, printModule: - body, err := io.ReadAll(r.Body) - if err != nil { - do500(w, "failed to read request body: "+err.Error()) - return - } - defer r.Body.Close() - - var emails models.Emails - err = json.Unmarshal(body, &emails) - if err != nil { - do400(w, "failed to parse request body: "+err.Error()) - return - } - // create our mailer (using the correct interface) sender, err := mailer.NewMailer() if err != nil { @@ -36,50 +23,67 @@ func SendEmails(w http.ResponseWriter, r *http.Request) { return } - // The user might have wanted to override the sender that goes in the "from" field of the email. - var fromAddress string - if emails.EmailSender != "" { - fromAddress = emails.EmailSender - } else { - fromAddress = config.Get().FromEmail - } + sendEmails(w, r, sender) + default: + CatchAll(w, r) + } +} - for _, email := range emails.Emails { - // creating a copy in order to pass it down into sub-functions - email := email +// sendEmails reads the unmarshalls the incoming body, and it verifies that it is correct. It determines if the email +// sender was overridden and resolves the specified non-email users through the configured user module. Finally, it +// determines if the default recipient must be grabbed from the config or if, on the other hand, we must set it to what +// the user asked it to be set. +func sendEmails(w http.ResponseWriter, r *http.Request, sender mailer.Emailer) { + body, err := io.ReadAll(r.Body) + if err != nil { + do500(w, "failed to read request body: "+err.Error()) + return + } + defer r.Body.Close() - // Lookup the emails for the given usernames unless the client specified otherwise. - if !emails.SkipUsersResolution { - err := mailer.LookupEmailsForUsernames(r.Context(), &email) - if err != nil { - l.Log.Error(err, "error translating usernames") - continue - } + var emails models.Emails + err = json.Unmarshal(body, &emails) + if err != nil { + do400(w, "failed to parse request body: "+err.Error()) + return + } - if len(email.Recipients) == 0 { - email.Recipients = []string{config.Get().ToEmail} - } - } + // The user might have wanted to override the sender that goes in the "from" field of the email. + var fromAddress string + if emails.EmailSender != "" { + fromAddress = emails.EmailSender + } else { + fromAddress = config.Get().FromEmail + } - // Should the user not specify any recipients, we need to determine if we should use the "default - // recipient" the user might have specified, or the default one that we set up in the configuration. - if len(email.Recipients) == 0 { - if emails.DefaultRecipient != "" { - email.Recipients = []string{emails.DefaultRecipient} - } else { - email.Recipients = []string{config.Get().ToEmail} - } - } + for _, email := range emails.Emails { + // creating a copy in order to pass it down into sub-functions + email := email - err = sender.SendEmail(r.Context(), &email, fromAddress) + // Lookup the emails for the given usernames unless the client specified otherwise. + if !emails.SkipUsersResolution { + err := mailer.LookupEmailsForUsernames(r.Context(), &email) if err != nil { - l.Log.Error(err, "Error sending email", "email", email) + l.Log.Error(err, "error translating usernames") + continue } } - sendJSON(w, newResponse("success")) + // Should the user not specify any recipients, we need to determine if we should use the "default + // recipient" the user might have specified, or the default one that we set up in the configuration. + if len(email.Recipients) == 0 { + if emails.DefaultRecipient == "" { + email.Recipients = []string{config.Get().ToEmail} + } else { + email.Recipients = []string{emails.DefaultRecipient} + } + } - default: - CatchAll(w, r) + err = sender.SendEmail(r.Context(), &email, fromAddress) + if err != nil { + l.Log.Error(err, "Error sending email", "email", email) + } } + + sendJSON(w, newResponse("success")) } diff --git a/internal/handlers/send_email_test.go b/internal/handlers/send_email_test.go new file mode 100644 index 00000000..b512a364 --- /dev/null +++ b/internal/handlers/send_email_test.go @@ -0,0 +1,622 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/redhatinsights/mbop/internal/config" + l "github.com/redhatinsights/mbop/internal/logger" + "github.com/redhatinsights/mbop/internal/models" + "github.com/redhatinsights/mbop/internal/service/mailer" +) + +// TestMain initializes the logger before running the tests so that we don't suffer from "panics" when the code under +// test attempts to log information. +func TestMain(m *testing.M) { + err := l.Init() + if err != nil { + log.Fatalln(err) + } + + exitCode := m.Run() + + os.Exit(exitCode) +} + +// slicesAreEqual returns true if the given slices are equal. +func slicesAreEqual(t *testing.T, s1 []string, s2 []string) bool { + if len(s1) != len(s2) { + t.Errorf(`slices are not of the same length. Want "%s" and "%s" to be equal`, s1, s2) + + return false + } + + for i := 0; i < len(s1); i++ { + if s1[i] != s2[i] { + t.Errorf(`slices do not contain the same elements. Want "%s" and "%s" to be equal`, s1, s2) + + return false + } + } + + return true +} + +// TestSendEmailsFailedParseBody tests that an invalid incoming payload results in a "bad request" response. +func TestSendEmailsFailedParseBody(t *testing.T) { + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", nil) + writer := httptest.NewRecorder() + + // Call the function under test. + SendEmails(writer, request) + + response := writer.Result() + defer response.Body.Close() + + if response.StatusCode != http.StatusBadRequest { + t.Errorf(`want "%d" status code when sending an invalid JSON payload, got "%d"`, http.StatusBadRequest, response.StatusCode) + } +} + +// TestSendEmails tests that the incoming emails are correctly parsed and processed, and that the output emails to be +// sent to the mailers contain the incoming email's data. +func TestSendEmails(t *testing.T) { + // Mock the incoming payload. + recipients := []string{"a@redhat.com", "a"} + ccList := []string{"copy@redhat.com", "b"} + bccList := []string{"hiddenCopy@redhat.com", "c"} + + email := &models.Email{ + Subject: "subject", + Body: "body", + Recipients: recipients, + CcList: ccList, + BccList: bccList, + BodyType: "html", + } + + emails := &models.Emails{ + Emails: []models.Email{*email}, + } + + // Marshal the mocked body. + requestBody, err := json.Marshal(emails) + if err != nil { + t.Fatalf("unable to marshal the emails model to JSON: %s", err) + } + + // Mock the request and the response writer. + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", bytes.NewBuffer(requestBody)) + writer := httptest.NewRecorder() + + // Change the mailer module to the "mock" one, and make sure that it gets reverted after the test. + originalMailerModule := config.Get().MailerModule + defer func() { + config.Get().MailerModule = originalMailerModule + }() + config.Get().MailerModule = "mock" + + // Use the "mock emailer" module for the test. + sender := &mailer.MockEmailer{} + + // Change the users' module to "mock", and then rever the value to the original one after the test. + originalUsersModule := config.Get().UsersModule + defer func() { + config.Get().UsersModule = originalUsersModule + }() + config.Get().UsersModule = "mock" + + // Call the function under test. + sendEmails(writer, request, sender) + + // Assert that the operation succeeded. + response := writer.Result() + defer response.Body.Close() + + // Assert that we are returning the expected status code. + if response.StatusCode != http.StatusOK { + t.Errorf(`want "%d" status code when the email sending operation succeeds, got "%d"`, http.StatusOK, response.StatusCode) + } + + // Assert that the returned body is the correct one. + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatalf("unable to read the response body after sending an emai: %s", err) + } + + expectedMessageBody := `{"message":"success"}` + if string(responseBody) != expectedMessageBody { + t.Errorf(`unexpected response body. Want "%s", got "%s"`, expectedMessageBody, string(responseBody)) + } + + // Assert that we did not accidentally modify the "from address". + if config.Get().FromEmail != sender.FromAddress { + t.Errorf(`the "from address" email should not have been modified by the test. Want "%s", got "%s"`, config.Get().FromEmail, sender.FromAddress) + } + + // Assert that only one email "was sent". + if 1 != len(sender.Emails) { + t.Errorf("want 1 email to be sent, got %d sent", len(sender.Emails)) + } + + // Assert that the sent email contains the expected fields. + sentEmail := sender.Emails[0] + + // Assert that the subject is correct. + if email.Subject != sentEmail.Subject { + t.Errorf(`unexpected email subject sent. Want "%s", got "%s"`, email.Subject, sentEmail.Subject) + } + + // Assert that the body is correct. + if email.Body != sentEmail.Body { + t.Errorf(`unexpected email body sent. Want "%s", got "%s"`, email.Body, sentEmail.Body) + } + + // Assert that the recipients are correct. + email.Recipients[1] = "a@mocked.biz" + if !slicesAreEqual(t, email.Recipients, sentEmail.Recipients) { + t.Error(`unexpected recipients specified in the sent email`) + } + + // Assert that the CC recipients are correct. + email.CcList[1] = "b@mocked.biz" + if !slicesAreEqual(t, email.CcList, sentEmail.CcList) { + t.Error(`unexpected CC recipients specified in the sent email`) + } + + // Assert that the BCC recipients are correct. + email.BccList[1] = "c@mocked.biz" + if !slicesAreEqual(t, email.BccList, sentEmail.BccList) { + t.Error(`unexpected BCC recipients specified in the sent email`) + } + + // Assert that the specified body type is correct. + if email.BodyType != sentEmail.BodyType { + t.Errorf(`unexpected email body type sent. Want "%s", got "%s"`, email.BodyType, sentEmail.BodyType) + } +} + +// TestSendEmailsOverrideFromAddress tests that the incoming emails are correctly parsed and processed, and that the +// output emails to be sent to the mailers contain the incoming email's data. The "from" address is overridden in the +// incoming payload. +func TestSendEmailsOverrideFromAddress(t *testing.T) { + // Mock the incoming payload. + recipients := []string{"a@redhat.com", "a"} + ccList := []string{"copy@redhat.com", "b"} + bccList := []string{"hiddenCopy@redhat.com", "c"} + + email := &models.Email{ + Subject: "subject", + Body: "body", + Recipients: recipients, + CcList: ccList, + BccList: bccList, + BodyType: "html", + } + + overriddenFromAddress := "custom-from-address@redhat.com" + emails := &models.Emails{ + Emails: []models.Email{*email}, + EmailSender: overriddenFromAddress, + } + + // Marshal the mocked body. + requestBody, err := json.Marshal(emails) + if err != nil { + t.Fatalf("unable to marshal the emails model to JSON: %s", err) + } + + // Mock the request and the response writer. + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", bytes.NewBuffer(requestBody)) + writer := httptest.NewRecorder() + + // Change the mailer module to the "mock" one, and make sure that it gets reverted after the test. + originalMailerModule := config.Get().MailerModule + defer func() { + config.Get().MailerModule = originalMailerModule + }() + config.Get().MailerModule = "mock" + + // Use the "mock emailer" module for the test. + sender := &mailer.MockEmailer{} + + // Change the users' module to "mock", and then rever the value to the original one after the test. + originalUsersModule := config.Get().UsersModule + defer func() { + config.Get().UsersModule = originalUsersModule + }() + config.Get().UsersModule = "mock" + + // Call the function under test. + sendEmails(writer, request, sender) + + // Assert that the operation succeeded. + response := writer.Result() + defer response.Body.Close() + + // Assert that we are returning the expected status code. + if response.StatusCode != http.StatusOK { + t.Errorf(`want "%d" status code when the email sending operation succeeds, got "%d"`, http.StatusOK, response.StatusCode) + } + + // Assert that the returned body is the correct one. + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatalf("unable to read the response body after sending an emai: %s", err) + } + + expectedMessageBody := `{"message":"success"}` + if string(responseBody) != expectedMessageBody { + t.Errorf(`unexpected response body. Want "%s", got "%s"`, expectedMessageBody, string(responseBody)) + } + + // Assert that we did not accidentally modify the "from address". + if overriddenFromAddress != sender.FromAddress { + t.Errorf(`the "from address" should have been overriden. Want "%s", got "%s"`, overriddenFromAddress, sender.FromAddress) + } + + // Assert that only one email "was sent". + if 1 != len(sender.Emails) { + t.Errorf("want 1 email to be sent, got %d sent", len(sender.Emails)) + } + + // Assert that the sent email contains the expected fields. + sentEmail := sender.Emails[0] + + // Assert that the subject is correct. + if email.Subject != sentEmail.Subject { + t.Errorf(`unexpected email subject sent. Want "%s", got "%s"`, email.Subject, sentEmail.Subject) + } + + // Assert that the body is correct. + if email.Body != sentEmail.Body { + t.Errorf(`unexpected email body sent. Want "%s", got "%s"`, email.Body, sentEmail.Body) + } + + // Assert that the recipients are correct. + email.Recipients[1] = "a@mocked.biz" + if !slicesAreEqual(t, email.Recipients, sentEmail.Recipients) { + t.Error(`unexpected recipients specified in the sent email`) + } + + // Assert that the CC recipients are correct. + email.CcList[1] = "b@mocked.biz" + if !slicesAreEqual(t, email.CcList, sentEmail.CcList) { + t.Error(`unexpected CC recipients specified in the sent email`) + } + + // Assert that the BCC recipients are correct. + email.BccList[1] = "c@mocked.biz" + if !slicesAreEqual(t, email.BccList, sentEmail.BccList) { + t.Error(`unexpected BCC recipients specified in the sent email`) + } + + // Assert that the specified body type is correct. + if email.BodyType != sentEmail.BodyType { + t.Errorf(`unexpected email body type sent. Want "%s", got "%s"`, email.BodyType, sentEmail.BodyType) + } +} + +// TestSendEmailsSkipUsersResolution tests that the incoming emails are correctly parsed and processed, and that the +// output emails to be sent to the mailers contain the incoming email's data. It skips the user resolution and asserts +// that the recipients are left untouched. +func TestSendEmailsSkipUsersResolution(t *testing.T) { + // Mock the incoming payload. + recipients := []string{"a@redhat.com", "a"} + ccList := []string{"copy@redhat.com", "b"} + bccList := []string{"hiddenCopy@redhat.com", "c"} + + email := &models.Email{ + Subject: "subject", + Body: "body", + Recipients: recipients, + CcList: ccList, + BccList: bccList, + BodyType: "html", + } + + emails := &models.Emails{ + Emails: []models.Email{*email}, + SkipUsersResolution: true, + } + + // Marshal the mocked body. + requestBody, err := json.Marshal(emails) + if err != nil { + t.Fatalf("unable to marshal the emails model to JSON: %s", err) + } + + // Mock the request and the response writer. + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", bytes.NewBuffer(requestBody)) + writer := httptest.NewRecorder() + + // Change the mailer module to the "mock" one, and make sure that it gets reverted after the test. + originalMailerModule := config.Get().MailerModule + defer func() { + config.Get().MailerModule = originalMailerModule + }() + config.Get().MailerModule = "mock" + + // Use the "mock emailer" module for the test. + sender := &mailer.MockEmailer{} + + // Change the users' module to "mock", and then rever the value to the original one after the test. + originalUsersModule := config.Get().UsersModule + defer func() { + config.Get().UsersModule = originalUsersModule + }() + config.Get().UsersModule = "mock" + + // Call the function under test. + sendEmails(writer, request, sender) + + // Assert that the operation succeeded. + response := writer.Result() + defer response.Body.Close() + + // Assert that we are returning the expected status code. + if response.StatusCode != http.StatusOK { + t.Errorf(`want "%d" status code when the email sending operation succeeds, got "%d"`, http.StatusOK, response.StatusCode) + } + + // Assert that the returned body is the correct one. + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatalf("unable to read the response body after sending an emai: %s", err) + } + + expectedMessageBody := `{"message":"success"}` + if string(responseBody) != expectedMessageBody { + t.Errorf(`unexpected response body. Want "%s", got "%s"`, expectedMessageBody, string(responseBody)) + } + + // Assert that we did not accidentally modify the "from address". + if config.Get().FromEmail != sender.FromAddress { + t.Errorf(`the "from address" email should not have been modified by the test. Want "%s", got "%s"`, config.Get().FromEmail, sender.FromAddress) + } + + // Assert that only one email "was sent". + if 1 != len(sender.Emails) { + t.Errorf("want 1 email to be sent, got %d sent", len(sender.Emails)) + } + + // Assert that the sent email contains the expected fields. + sentEmail := sender.Emails[0] + + // Assert that the subject is correct. + if email.Subject != sentEmail.Subject { + t.Errorf(`unexpected email subject sent. Want "%s", got "%s"`, email.Subject, sentEmail.Subject) + } + + // Assert that the body is correct. + if email.Body != sentEmail.Body { + t.Errorf(`unexpected email body sent. Want "%s", got "%s"`, email.Body, sentEmail.Body) + } + + // Assert that the recipients are correct. + if !slicesAreEqual(t, email.Recipients, sentEmail.Recipients) { + t.Error(`unexpected recipients specified in the sent email`) + } + + // Assert that the CC recipients are correct. + if !slicesAreEqual(t, email.CcList, sentEmail.CcList) { + t.Error(`unexpected CC recipients specified in the sent email`) + } + + // Assert that the BCC recipients are correct. + if !slicesAreEqual(t, email.BccList, sentEmail.BccList) { + t.Error(`unexpected BCC recipients specified in the sent email`) + } + + // Assert that the specified body type is correct. + if email.BodyType != sentEmail.BodyType { + t.Errorf(`unexpected email body type sent. Want "%s", got "%s"`, email.BodyType, sentEmail.BodyType) + } +} + +// TestSendEmailsDefaultRecipientFromConfig tests that the incoming emails are correctly parsed and processed, and that +// the output emails to be sent to the mailers contain the incoming email's data. The default recipient is not +// specified, so it is expected that the default recipient will be grabbed from the configuration. +func TestSendEmailsDefaultRecipientFromConfig(t *testing.T) { + // Mock the incoming payload. + email := &models.Email{ + Subject: "subject", + Body: "body", + BodyType: "html", + } + + emails := &models.Emails{ + Emails: []models.Email{*email}, + } + + // Marshal the mocked body. + requestBody, err := json.Marshal(emails) + if err != nil { + t.Fatalf("unable to marshal the emails model to JSON: %s", err) + } + + // Mock the request and the response writer. + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", bytes.NewBuffer(requestBody)) + writer := httptest.NewRecorder() + + // Change the mailer module to the "mock" one, and make sure that it gets reverted after the test. + originalMailerModule := config.Get().MailerModule + defer func() { + config.Get().MailerModule = originalMailerModule + }() + config.Get().MailerModule = "mock" + + // Use the "mock emailer" module for the test. + sender := &mailer.MockEmailer{} + + // Change the users' module to "mock", and then rever the value to the original one after the test. + originalUsersModule := config.Get().UsersModule + defer func() { + config.Get().UsersModule = originalUsersModule + }() + config.Get().UsersModule = "mock" + + // Call the function under test. + sendEmails(writer, request, sender) + + // Assert that the operation succeeded. + response := writer.Result() + defer response.Body.Close() + + // Assert that we are returning the expected status code. + if response.StatusCode != http.StatusOK { + t.Errorf(`want "%d" status code when the email sending operation succeeds, got "%d"`, http.StatusOK, response.StatusCode) + } + + // Assert that the returned body is the correct one. + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatalf("unable to read the response body after sending an emai: %s", err) + } + + expectedMessageBody := `{"message":"success"}` + if string(responseBody) != expectedMessageBody { + t.Errorf(`unexpected response body. Want "%s", got "%s"`, expectedMessageBody, string(responseBody)) + } + + // Assert that we did not accidentally modify the "from address". + if config.Get().FromEmail != sender.FromAddress { + t.Errorf(`the "from address" email should not have been modified by the test. Want "%s", got "%s"`, config.Get().FromEmail, sender.FromAddress) + } + + // Assert that only one email "was sent". + if 1 != len(sender.Emails) { + t.Errorf("want 1 email to be sent, got %d sent", len(sender.Emails)) + } + + // Assert that the sent email contains the expected fields. + sentEmail := sender.Emails[0] + + // Assert that the subject is correct. + if email.Subject != sentEmail.Subject { + t.Errorf(`unexpected email subject sent. Want "%s", got "%s"`, email.Subject, sentEmail.Subject) + } + + // Assert that the body is correct. + if email.Body != sentEmail.Body { + t.Errorf(`unexpected email body sent. Want "%s", got "%s"`, email.Body, sentEmail.Body) + } + + // Assert that the recipients are correct. + if !slicesAreEqual(t, []string{config.Get().ToEmail}, sentEmail.Recipients) { + t.Error("unexpected default recipient specified. Wanted the default recipient from the configuration.") + } + + // Assert that the specified body type is correct. + if email.BodyType != sentEmail.BodyType { + t.Errorf(`unexpected email body type sent. Want "%s", got "%s"`, email.BodyType, sentEmail.BodyType) + } +} + +// TestSendEmailsDefaultRecipientOverridden tests that the incoming emails are correctly parsed and processed, and that +// the output emails to be sent to the mailers contain the incoming email's data. The default recipient is specified +// in the incoming payload, so it should be overridden. +func TestSendEmailsDefaultRecipientOverridden(t *testing.T) { + // Mock the incoming payload. + email := &models.Email{ + Subject: "subject", + Body: "body", + BodyType: "html", + } + + // Override the default recipient. + defaultRecipient := "default-recipient@redhat.com" + emails := &models.Emails{ + Emails: []models.Email{*email}, + DefaultRecipient: defaultRecipient, + } + + // Marshal the mocked body. + requestBody, err := json.Marshal(emails) + if err != nil { + t.Fatalf("unable to marshal the emails model to JSON: %s", err) + } + + // Mock the request and the response writer. + request := httptest.NewRequest(http.MethodPost, "/v1/sendEmails", bytes.NewBuffer(requestBody)) + writer := httptest.NewRecorder() + + // Change the mailer module to the "mock" one, and make sure that it gets reverted after the test. + originalMailerModule := config.Get().MailerModule + defer func() { + config.Get().MailerModule = originalMailerModule + }() + config.Get().MailerModule = "mock" + + // Use the "mock emailer" module for the test. + sender := &mailer.MockEmailer{} + + // Change the users' module to "mock", and then rever the value to the original one after the test. + originalUsersModule := config.Get().UsersModule + defer func() { + config.Get().UsersModule = originalUsersModule + }() + config.Get().UsersModule = "mock" + + // Call the function under test. + sendEmails(writer, request, sender) + + // Assert that the operation succeeded. + response := writer.Result() + defer response.Body.Close() + + // Assert that we are returning the expected status code. + if response.StatusCode != http.StatusOK { + t.Errorf(`want "%d" status code when the email sending operation succeeds, got "%d"`, http.StatusOK, response.StatusCode) + } + + // Assert that the returned body is the correct one. + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatalf("unable to read the response body after sending an emai: %s", err) + } + + expectedMessageBody := `{"message":"success"}` + if string(responseBody) != expectedMessageBody { + t.Errorf(`unexpected response body. Want "%s", got "%s"`, expectedMessageBody, string(responseBody)) + } + + // Assert that we did not accidentally modify the "from address". + if config.Get().FromEmail != sender.FromAddress { + t.Errorf(`the "from address" email should not have been modified by the test. Want "%s", got "%s"`, config.Get().FromEmail, sender.FromAddress) + } + + // Assert that only one email "was sent". + if 1 != len(sender.Emails) { + t.Errorf("want 1 email to be sent, got %d sent", len(sender.Emails)) + } + + // Assert that the sent email contains the expected fields. + sentEmail := sender.Emails[0] + + // Assert that the subject is correct. + if email.Subject != sentEmail.Subject { + t.Errorf(`unexpected email subject sent. Want "%s", got "%s"`, email.Subject, sentEmail.Subject) + } + + // Assert that the body is correct. + if email.Body != sentEmail.Body { + t.Errorf(`unexpected email body sent. Want "%s", got "%s"`, email.Body, sentEmail.Body) + } + + // Assert that the recipients are correct. + if !slicesAreEqual(t, []string{defaultRecipient}, sentEmail.Recipients) { + t.Error("unexpected default recipient specified. Wanted the overridden default recipient specified in the payload.") + } + + // Assert that the specified body type is correct. + if email.BodyType != sentEmail.BodyType { + t.Errorf(`unexpected email body type sent. Want "%s", got "%s"`, email.BodyType, sentEmail.BodyType) + } +} diff --git a/internal/service/mailer/mailer_interface.go b/internal/service/mailer/mailer_interface.go index 89756969..bdf9266f 100644 --- a/internal/service/mailer/mailer_interface.go +++ b/internal/service/mailer/mailer_interface.go @@ -24,6 +24,8 @@ func NewMailer() (Emailer, error) { } sender = &awsSESEmailer{client: sesv2.NewFromConfig(*cfg)} + case "mock": + sender = &MockEmailer{} case "print": sender = &printEmailer{} default: diff --git a/internal/service/mailer/mock_mailer.go b/internal/service/mailer/mock_mailer.go new file mode 100644 index 00000000..3879d8eb --- /dev/null +++ b/internal/service/mailer/mock_mailer.go @@ -0,0 +1,24 @@ +package mailer + +import ( + "context" + + "github.com/redhatinsights/mbop/internal/models" +) + +// MockEmailer is a mocked mailer that is intended to be used for tests. +type MockEmailer struct { + Emails []*models.Email + FromAddress string +} + +// Build flag. +var _ = (Emailer)(&printEmailer{}) + +// SendEmail copies the incoming arguments into the struct to be able to check them afterwards in the tests. +func (m *MockEmailer) SendEmail(_ context.Context, email *models.Email, fromAddress string) error { + m.Emails = append(m.Emails, email) + m.FromAddress = fromAddress + + return nil +}