Skip to content

Commit

Permalink
SDP-1316 Update send and auto-retry invitation scheduler job to work …
Browse files Browse the repository at this point in the history
…with both SMS and email
  • Loading branch information
marwen-abid committed Sep 11, 2024
1 parent a559246 commit 62a60f7
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 136 deletions.
29 changes: 28 additions & 1 deletion internal/data/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ func ClearAndCreateCountryFixtures(t *testing.T, ctx context.Context, sqlExec db
}

func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, r *Receiver) *Receiver {
t.Helper()

randomSuffix, err := utils.RandomString(5)
require.NoError(t, err)

Expand Down Expand Up @@ -336,6 +338,31 @@ func CreateReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExec
return &receiver
}

func InsertReceiverFixture(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter, r *ReceiverInsert) *Receiver {
t.Helper()

if r.ExternalId == nil {
randString, err := utils.RandomString(56)
require.NoError(t, err)
r.ExternalId = &randString
}

const query = `
INSERT INTO receivers
(email, phone_number, external_id)
VALUES
($1, $2, $3)
RETURNING
id, COALESCE(phone_number, '') as phone_number, COALESCE(email, '') as email, external_id, created_at, updated_at
`

var receiver Receiver
err := sqlExec.GetContext(ctx, &receiver, query, r.Email, r.PhoneNumber, r.ExternalId)
require.NoError(t, err)

return &receiver
}

func DeleteAllReceiversFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter) {
const query = "DELETE FROM receivers"
_, err := sqlExec.ExecContext(ctx, query)
Expand Down Expand Up @@ -419,7 +446,7 @@ func CreateReceiverWalletFixture(t *testing.T, ctx context.Context, sqlExec db.S
SELECT
rw.id, rw.stellar_address, rw.stellar_memo, rw.stellar_memo_type, rw.status, rw.status_history, rw.created_at, rw.updated_at,
rw.anchor_platform_transaction_id, rw.anchor_platform_transaction_synced_at,
r.id, r.email, r.phone_number, r.external_id, r.created_at, r.updated_at,
r.id, COALESCE(r.phone_number, '') as phone_number, COALESCE(r.email, '') as email, r.external_id, r.created_at, r.updated_at,
w.id, w.name, w.homepage, w.deep_link_schema, w.created_at, w.updated_at
FROM
inserted_receiver_wallet AS rw
Expand Down
16 changes: 10 additions & 6 deletions internal/message/message_dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
//go:generate mockery --name MessageDispatcherInterface --case=underscore --structname=MockMessageDispatcher --inpackage
type MessageDispatcherInterface interface {
RegisterClient(ctx context.Context, channel MessageChannel, client MessengerClient)
SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error
SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) (MessengerType, error)
GetClient(channel MessageChannel) (MessengerClient, error)
}

Expand All @@ -36,14 +36,17 @@ func (d *MessageDispatcher) RegisterClient(ctx context.Context, channel MessageC
d.clients[channel] = client
}

func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) error {
func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, channelPriority []MessageChannel) (MessengerType, error) {
// default to the highest priority channel messenger type.
messengerType := d.clients[channelPriority[0]].MessengerType()

supportedChannels := make(map[MessageChannel]bool)
for _, ch := range message.SupportedChannels() {
supportedChannels[ch] = true
}

if len(supportedChannels) == 0 {
return fmt.Errorf("no valid channel found for message %s", message)
return messengerType, fmt.Errorf("no valid channel found for message %s", message)
}

for _, channel := range channelPriority {
Expand All @@ -57,16 +60,17 @@ func (d *MessageDispatcher) SendMessage(ctx context.Context, message Message, ch
log.Ctx(ctx).Warnf("No client registered for channel %q", channel)
continue
}
messengerType = client.MessengerType()

err := client.SendMessage(message)
if err == nil {
return nil
return messengerType, nil
}

log.Ctx(ctx).Errorf("Error sending message %s using channel %q: %v", message, channel, err)
log.Ctx(ctx).Errorf("Error sending %s through messenger type %s: %v", channel, messengerType, err)
}

return fmt.Errorf("unable to send message %s using any of the supported channels [%v]", message, supportedChannels)
return messengerType, fmt.Errorf("unable to send message %s using any of the supported channels [%v]", message, supportedChannels)
}

func (d *MessageDispatcher) GetClient(channel MessageChannel) (MessengerClient, error) {
Expand Down
32 changes: 19 additions & 13 deletions internal/message/message_dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,13 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {
emptyMessage := Message{}

tests := []struct {
name string
message Message
channelPriority []MessageChannel
supportedChannels []MessageChannel
setupMock func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock)
expectedErr error
name string
message Message
channelPriority []MessageChannel
supportedChannels []MessageChannel
setupMock func(emailClientMock *MessengerClientMock, smsClientMock *MessengerClientMock)
expectedMessengerType MessengerType
expectedErr error
}{
{
name: "fail when no supported channels",
Expand Down Expand Up @@ -129,7 +130,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {

smsClientMock.AssertNotCalled(t, "SendMessage", emailMessage)
},
expectedErr: nil,
expectedMessengerType: MessengerTypeAWSEmail,
expectedErr: nil,
},
{
name: "successful when single supported channel (sms)",
Expand All @@ -144,7 +146,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {

emailClientMock.AssertNotCalled(t, "SendMessage", smsMessage)
},
expectedErr: nil,
expectedMessengerType: MessengerTypeTwilioSMS,
expectedErr: nil,
},
{
name: "successful when multiple supported channels",
Expand All @@ -159,7 +162,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {

emailClientMock.AssertNotCalled(t, "SendMessage", multiChannelMessage)
},
expectedErr: nil,
expectedMessengerType: MessengerTypeTwilioSMS,
expectedErr: nil,
},
{
name: "successful when first channel fails (sms) but second succeeds (e-mail)",
Expand All @@ -177,7 +181,8 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {
Return(nil).
Once()
},
expectedErr: nil,
expectedMessengerType: MessengerTypeAWSEmail,
expectedErr: nil,
},
{
name: "fail when all channels fail",
Expand Down Expand Up @@ -205,19 +210,20 @@ func Test_MessageDispatcher_SendMessage(t *testing.T) {
dispatcher := NewMessageDispatcher()

emailClient := NewMessengerClientMock(t)
emailClient.On("MessengerType").Return(MessengerTypeDryRun).Once()
emailClient.On("MessengerType").Return(MessengerTypeAWSEmail).Maybe()
dispatcher.RegisterClient(ctx, MessageChannelEmail, emailClient)

smsClient := NewMessengerClientMock(t)
smsClient.On("MessengerType").Return(MessengerTypeDryRun).Once()
smsClient.On("MessengerType").Return(MessengerTypeTwilioSMS).Maybe()
dispatcher.RegisterClient(ctx, MessageChannelSMS, smsClient)

tt.setupMock(emailClient, smsClient)

err := dispatcher.SendMessage(ctx, tt.message, tt.channelPriority)
messengerType, err := dispatcher.SendMessage(ctx, tt.message, tt.channelPriority)
if tt.expectedErr != nil {
assert.EqualError(t, err, tt.expectedErr.Error())
} else {
assert.Equal(t, tt.expectedMessengerType, messengerType)
assert.NoError(t, err)
}
})
Expand Down
20 changes: 15 additions & 5 deletions internal/message/mock_message_dispatcher_interface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {

t.Run("executes the service successfully", func(t *testing.T) {
messageDispatcherMock := message.NewMockMessageDispatcher(t)
messengerClientMock := message.NewMessengerClientMock(t)

crashTrackerClientMock := &crashtracker.MockCrashTrackerClient{}

s, err := services.NewSendReceiverWalletInviteService(
Expand Down Expand Up @@ -217,6 +215,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
deepLink1, err := walletDeepLink1.GetSignedRegistrationLink(stellarSecretKey)
require.NoError(t, err)
contentWallet1 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink1)
titleWallet1 := "You have a payment waiting for you from " + walletDeepLink1.OrganizationName

walletDeepLink2 := services.WalletDeepLink{
DeepLink: wallet2.DeepLinkSchema,
Expand All @@ -228,30 +227,27 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
deepLink2, err := walletDeepLink2.GetSignedRegistrationLink(stellarSecretKey)
require.NoError(t, err)
contentWallet2 := fmt.Sprintf("You have a payment waiting for you from the MyCustomAid. Click %s to register.", deepLink2)
titleWallet2 := "You have a payment waiting for you from " + walletDeepLink2.OrganizationName

mockErr := errors.New("unexpected error")
messageDispatcherMock.
On("GetClient", message.MessageChannelSMS).
Return(messengerClientMock, nil).
Twice().
On("SendMessage", mock.Anything, message.Message{
ToPhoneNumber: receiver1.PhoneNumber,
ToEmail: receiver1.Email,
Message: contentWallet1,
Title: titleWallet1,
}, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(mockErr).
Return(message.MessengerTypeTwilioSMS, mockErr).
Once().
On("SendMessage", mock.Anything, message.Message{
ToPhoneNumber: receiver2.PhoneNumber,
ToEmail: receiver2.Email,
Message: contentWallet2,
Title: titleWallet2,
}, []message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(nil).
Return(message.MessengerTypeTwilioSMS, nil).
Once()
messengerClientMock.
On("MessengerType").
Return(message.MessengerTypeTwilioSMS).
Twice()

mockMsg := fmt.Sprintf(
"error sending message to receiver ID %s for receiver wallet ID %s using messenger type %s",
receiver1.ID, rec1RW.ID, message.MessengerTypeTwilioSMS,
Expand Down Expand Up @@ -279,7 +275,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
assert.Equal(t, wallet1.ID, msg.WalletID)
assert.Equal(t, rec1RW.ID, *msg.ReceiverWalletID)
assert.Equal(t, data.FailureMessageStatus, msg.Status)
assert.Empty(t, msg.TitleEncrypted)
assert.Equal(t, titleWallet1, msg.TitleEncrypted)
assert.Equal(t, contentWallet1, msg.TextEncrypted)
assert.Len(t, msg.StatusHistory, 2)
assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
Expand All @@ -295,7 +291,7 @@ func Test_SendReceiverWalletsSMSInvitationJob_Execute(t *testing.T) {
assert.Equal(t, wallet2.ID, msg.WalletID)
assert.Equal(t, rec2RW.ID, *msg.ReceiverWalletID)
assert.Equal(t, data.SuccessMessageStatus, msg.Status)
assert.Empty(t, msg.TitleEncrypted)
assert.Equal(t, titleWallet2, msg.TitleEncrypted)
assert.Equal(t, contentWallet2, msg.TextEncrypted)
assert.Len(t, msg.StatusHistory, 2)
assert.Equal(t, data.PendingMessageStatus, msg.StatusHistory[0].Status)
Expand Down
2 changes: 1 addition & 1 deletion internal/serve/httphandler/receiver_send_otp_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func (h ReceiverSendOTPHandler) sendOTP(ctx context.Context, contactType data.Re
truncatedContactInfo := utils.TruncateString(contactInfo, 3)
contactTypeStr := utils.Humanize(string(contactType))
log.Ctx(ctx).Infof("sending OTP message to %s %s...", contactTypeStr, truncatedContactInfo)
err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority)
_, err = h.MessageDispatcher.SendMessage(ctx, msg, organization.MessageChannelPriority)
if err != nil {
return fmt.Errorf("cannot send OTP message through %s to %s: %w", contactTypeStr, truncatedContactInfo, err)
}
Expand Down
18 changes: 12 additions & 6 deletions internal/serve/httphandler/receiver_send_otp_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,16 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) {
for _, verificationField := range data.GetAllVerificationTypes() {
receiverSendOTPRequest := ReceiverSendOTPRequest{ReCAPTCHAToken: reCAPTCHAToken}
var contactInfo string
var messengerType message.MessengerType
switch contactType {
case data.ReceiverContactTypeSMS:
receiverSendOTPRequest.PhoneNumber = phoneNumber
contactInfo = phoneNumber
messengerType = message.MessengerTypeTwilioSMS
case data.ReceiverContactTypeEmail:
receiverSendOTPRequest.Email = email
contactInfo = email
messengerType = message.MessengerTypeAWSEmail
}
truncatedContactInfo := utils.TruncateString(contactInfo, 3)

Expand All @@ -314,7 +317,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) {
mock.Anything,
mock.AnythingOfType("message.Message"),
[]message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(errors.New("failed calling message dispatcher")).
Return(messengerType, errors.New("failed calling message dispatcher")).
Once().
Run(func(args mock.Arguments) {
msg := args.Get(1).(message.Message)
Expand Down Expand Up @@ -366,7 +369,7 @@ func Test_ReceiverSendOTPHandler_ServeHTTP_otpHandlerIsCalled(t *testing.T) {
mock.Anything,
mock.AnythingOfType("message.Message"),
[]message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(nil).
Return(messengerType, nil).
Once().
Run(func(args mock.Arguments) {
msg := args.Get(1).(message.Message)
Expand Down Expand Up @@ -501,13 +504,16 @@ func Test_ReceiverSendOTPHandler_sendOTP(t *testing.T) {
t.Run(fmt.Sprintf("%s/%s", contactType, tc.name), func(t *testing.T) {
var expectedMsg message.Message
var contactInfo string
var messengerType message.MessengerType
switch contactType {
case data.ReceiverContactTypeSMS:
expectedMsg = message.Message{ToPhoneNumber: phoneNumber, Message: tc.wantMessage}
contactInfo = phoneNumber
messengerType = message.MessengerTypeTwilioSMS
case data.ReceiverContactTypeEmail:
expectedMsg = message.Message{ToEmail: email, Message: tc.wantMessage, Title: "Your One-Time Password: " + otp}
contactInfo = email
messengerType = message.MessengerTypeAWSEmail
}

mockMessageDispatcher := message.NewMockMessageDispatcher(t)
Expand All @@ -517,9 +523,9 @@ func Test_ReceiverSendOTPHandler_sendOTP(t *testing.T) {
expectedMsg,
[]message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail})
if !tc.shouldDispatcherFail {
mockCall.Return(nil).Once()
mockCall.Return(messengerType, nil).Once()
} else {
mockCall.Return(errors.New("error sending message")).Once()
mockCall.Return(messengerType, errors.New("error sending message")).Once()
}

handler := ReceiverSendOTPHandler{
Expand Down Expand Up @@ -619,7 +625,7 @@ func Test_ReceiverSendOTPHandler_handleOTPForReceiver(t *testing.T) {
mock.Anything,
mock.AnythingOfType("message.Message"),
[]message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(errors.New("error sending message")).
Return(message.MessengerTypeTwilioSMS, errors.New("error sending message")).
Once()
},
wantVerificationField: data.VerificationTypeDateOfBirth,
Expand All @@ -642,7 +648,7 @@ func Test_ReceiverSendOTPHandler_handleOTPForReceiver(t *testing.T) {
mock.Anything,
mock.AnythingOfType("message.Message"),
[]message.MessageChannel{message.MessageChannelSMS, message.MessageChannelEmail}).
Return(nil).
Return(message.MessengerTypeTwilioSMS, nil).
Once()
},
wantVerificationField: data.VerificationTypePin,
Expand Down
Loading

0 comments on commit 62a60f7

Please sign in to comment.