Skip to content

Commit

Permalink
ROX-23252: Connect email sender service to the endpoint (#1848)
Browse files Browse the repository at this point in the history
* Add SendRawEmail method

* Connect email sender service to the endpoint

* Update emailsender/cmd/app/main.go

Co-authored-by: Johannes Malsam <[email protected]>

---------

Co-authored-by: Johannes Malsam <[email protected]>
  • Loading branch information
kurlov and johannes94 authored Jun 4, 2024
1 parent 4306c60 commit 929ec23
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 23 deletions.
13 changes: 12 additions & 1 deletion emailsender/cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/golang/glog"
"github.com/stackrox/acs-fleet-manager/emailsender/config"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/api"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/email"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/metrics"
)

Expand All @@ -39,9 +40,19 @@ func main() {

ctx := context.Background()

// initialize components
sesClient, err := email.NewSES(ctx)
if err != nil {
glog.Errorf("Failed to initialise SES Client: %v", err)
os.Exit(1)
}
temporarySenderName := "[email protected]"
emailSender := email.NewEmailSender(temporarySenderName, sesClient)
emailHandler := api.NewEmailHandler(emailSender)

// base router
router := mux.NewRouter()
api.SetupRoutes(router)
api.SetupRoutes(router, emailHandler)

server := http.Server{Addr: cfg.ServerAddress, Handler: router}

Expand Down
80 changes: 80 additions & 0 deletions emailsender/pkg/api/emailhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package api

import (
"encoding/json"
"fmt"
"github.com/golang/glog"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/email"
"net/http"
)

type EmailHandler struct {
emailSender email.Sender
}

// SendEmailRequest represents API requests for sending email
type SendEmailRequest struct {
To []string
RawMessage []byte
}

type Envelope map[string]interface{}

func NewEmailHandler(emailSender email.Sender) *EmailHandler {
return &EmailHandler{
emailSender: emailSender,
}
}

func (eh *EmailHandler) SendEmail(w http.ResponseWriter, r *http.Request) {
var request SendEmailRequest

jsonDecoder := json.NewDecoder(r.Body)
jsonDecoder.DisallowUnknownFields()

if err := jsonDecoder.Decode(&request); err != nil {
eh.errorResponse(w, "Cannot decode send email request payload", http.StatusBadRequest)
return
}

if err := eh.emailSender.Send(r.Context(), request.To, request.RawMessage); err != nil {
eh.errorResponse(w, "Cannot send email", http.StatusInternalServerError)
return
}

envelope := Envelope{
"status": "sent",
}
if err := eh.jsonResponse(w, envelope, http.StatusOK); err != nil {
glog.Errorf("Failed creating json response: %v", err)
http.Error(w, "Cannot create json response", http.StatusInternalServerError)
}
}

func (eh *EmailHandler) jsonResponse(w http.ResponseWriter, envelop Envelope, statusCode int) error {
j, err := json.Marshal(envelop)
if err != nil {
return fmt.Errorf("failed to marshal: %v", err)
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)

_, err = w.Write(j)
if err != nil {
return fmt.Errorf("failed to write json response: %v", err)
}

return nil
}

func (eh *EmailHandler) errorResponse(w http.ResponseWriter, message string, statusCode int) {
envelope := Envelope{
"error": message,
}

if err := eh.jsonResponse(w, envelope, statusCode); err != nil {
glog.Errorf("Failed creating error json response: %v", err)
http.Error(w, "Can not create error json response", http.StatusInternalServerError)
}
}
96 changes: 96 additions & 0 deletions emailsender/pkg/api/emailhandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package api

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/stackrox/acs-fleet-manager/emailsender/pkg/email"
"net/http"
"net/http/httptest"
"testing"
)

type MockEmailSender struct {
SendFunc func(ctx context.Context, to []string, rawMessage []byte) error
}

func (m *MockEmailSender) Send(ctx context.Context, to []string, rawMessage []byte) error {
return m.SendFunc(ctx, to, rawMessage)
}

var simpleEmailSender = &MockEmailSender{
SendFunc: func(ctx context.Context, to []string, rawMessage []byte) error {
return nil
},
}

func TestEmailHandler_SendEmail(t *testing.T) {
subject := "Test subject"
textBody := "text body"
var messageBuf bytes.Buffer
messageBuf.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
messageBuf.WriteString(textBody)
rawMessage := messageBuf.Bytes()

sendEmailRequest := SendEmailRequest{
To: []string{"[email protected]", "[email protected]"},
RawMessage: rawMessage,
}
jsonReq, _ := json.Marshal(sendEmailRequest)
invalidJsonReq, _ := json.Marshal(map[string]string{
"invalid": "JSON",
})

tests := []struct {
name string
emailSender email.Sender
req *http.Request
wantCode int
wantBody string
}{
{
name: "should return JSON response with StatusOK to a valid email request",
emailSender: simpleEmailSender,
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonReq)),
wantCode: http.StatusOK,
wantBody: `{"status":"sent"}`,
},
{
name: "should return JSON error with StatusBadRequest when cannot decode request",
emailSender: simpleEmailSender,
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(invalidJsonReq)),
wantCode: http.StatusBadRequest,
wantBody: `{"error":"Cannot decode send email request payload"}`,
},
{
name: "should return JSON error with StatusInternalServerError when cannot send email",
emailSender: &MockEmailSender{
SendFunc: func(ctx context.Context, to []string, rawMessage []byte) error {
return errors.New("failed to send email")
},
},
req: httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonReq)),
wantCode: http.StatusInternalServerError,
wantBody: `{"error":"Cannot send email"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
eh := &EmailHandler{
emailSender: tt.emailSender,
}
resp := httptest.NewRecorder()
eh.SendEmail(resp, tt.req)

if resp.Result().StatusCode != tt.wantCode {
t.Errorf("expected status code %d, got %d", tt.wantCode, resp.Result().StatusCode)
}

if resp.Body.String() != tt.wantBody {
t.Errorf("expected body %s, got %s", tt.wantBody, resp.Body.String())
}
})
}
}
19 changes: 0 additions & 19 deletions emailsender/pkg/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,9 @@
package api

import (
"encoding/json"
"net/http"
)

// EmailRequest represents API requests for sending email
type EmailRequest struct {
To []string
RawMessage []byte
}

// SendEmailHandler handles sending email API endpoint
func SendEmailHandler(w http.ResponseWriter, r *http.Request) {
var email EmailRequest

if err := json.NewDecoder(r.Body).Decode(&email); err != nil {
http.Error(w, "Can not decode request payload", http.StatusBadRequest)
return
}

w.WriteHeader(http.StatusOK)
}

// HealthCheckHandler returns 200 HTTP status code
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down
5 changes: 2 additions & 3 deletions emailsender/pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package api

import (
"github.com/gorilla/mux"

loggingMiddleware "github.com/stackrox/acs-fleet-manager/pkg/server/logging"
)

// SetupRoutes configures API route mapping
func SetupRoutes(router *mux.Router) {
func SetupRoutes(router *mux.Router, emailHandler *EmailHandler) {
// add middlewares
router.Use(loggingMiddleware.RequestLoggingMiddleware, EnsureJSONContentType)

router.HandleFunc("/health", HealthCheckHandler).Methods("GET")
router.HandleFunc("/api/v1/acscsemail", SendEmailHandler).Methods("POST")
router.HandleFunc("/api/v1/acscsemail", emailHandler.SendEmail).Methods("POST")
}
33 changes: 33 additions & 0 deletions emailsender/pkg/email/sender.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package email

import (
"context"
"fmt"
"github.com/golang/glog"
)

type Sender interface {
Send(ctx context.Context, to []string, rawMessage []byte) error
}

type MailSender struct {
from string
ses *SES
}

func NewEmailSender(from string, ses *SES) *MailSender {
return &MailSender{
from: from,
ses: ses,
}
}

func (s *MailSender) Send(ctx context.Context, to []string, rawMessage []byte) error {
_, err := s.ses.SendRawEmail(ctx, s.from, to, rawMessage)
if err != nil {
glog.Errorf("Failed sending email: %v", err)
return fmt.Errorf("failed to send email: %v", err)
}

return nil
}
43 changes: 43 additions & 0 deletions emailsender/pkg/email/sender_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package email

import (
"bytes"
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/assert"
"testing"

"github.com/aws/aws-sdk-go-v2/service/ses"
)

func TestSend_Success(t *testing.T) {
from := "[email protected]"
to := []string{"[email protected]", "[email protected]"}
subject := "Test subject"
textBody := "text body"
var messageBuf bytes.Buffer
messageBuf.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
messageBuf.WriteString(textBody)
rawMessage := messageBuf.Bytes()
called := false

mockClient := &MockSESClient{
SendRawEmailFunc: func(ctx context.Context, params *ses.SendRawEmailInput, optFns ...func(*ses.Options)) (*ses.SendRawEmailOutput, error) {
called = true
return &ses.SendRawEmailOutput{
MessageId: aws.String("test-message-id"),
}, nil
},
}
mockedSES := &SES{sesClient: mockClient}
mockedSender := MailSender{
from,
mockedSES,
}

err := mockedSender.Send(context.Background(), to, rawMessage)

assert.NoError(t, err)
assert.True(t, called)
}

0 comments on commit 929ec23

Please sign in to comment.