From bf5e62b45092087cf07d99a52cff07db8dcb7f65 Mon Sep 17 00:00:00 2001 From: MikeMwita Date: Sat, 6 Jul 2024 00:40:34 +0300 Subject: [PATCH 1/4] feat: Implement indempontecy-and-retries --- .idea/GitLink.xml | 6 ++++++ pkg/sms/sms_sender.go | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 .idea/GitLink.xml diff --git a/.idea/GitLink.xml b/.idea/GitLink.xml new file mode 100644 index 0000000..009597c --- /dev/null +++ b/.idea/GitLink.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/pkg/sms/sms_sender.go b/pkg/sms/sms_sender.go index 1b6626e..955ad89 100644 --- a/pkg/sms/sms_sender.go +++ b/pkg/sms/sms_sender.go @@ -3,6 +3,7 @@ package sms import ( "encoding/json" "fmt" + "math/rand" "net/http" "net/url" "strings" @@ -143,17 +144,21 @@ func (s *SmsSender) SendSMS() (SmsSenderResponse, error) { return smsSenderResponse, fmt.Errorf("status code: %d", res.StatusCode) } -// Retry sends an SMS with exponential backoff + func (s *SmsSender) RetrySendSMS(maxRetries int) (SmsSenderResponse, error) { for retry := 0; retry < maxRetries; retry++ { - response, err := s.SendSMS() - if err == nil { - return response, nil - } + response, err := s.SendSMS() + if err == nil { + return response, nil + } - delay := time.Duration(1< Date: Sat, 6 Jul 2024 01:32:12 +0300 Subject: [PATCH 2/4] feat: Implement retry logic test --- pkg/sms/sms_sender_test.go | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/pkg/sms/sms_sender_test.go b/pkg/sms/sms_sender_test.go index 2b0b08e..393538b 100644 --- a/pkg/sms/sms_sender_test.go +++ b/pkg/sms/sms_sender_test.go @@ -1,6 +1,7 @@ package sms import ( + "fmt" "testing" ) @@ -30,3 +31,105 @@ func TestSendSMS(t *testing.T) { t.Errorf("empty message received in response") } } + +func (s *SmsSender) MockSendSMS() (SmsSenderResponse, error) { + // Todo: Implement mock behavior + return SmsSenderResponse{ + SmsMessageData: SmsMessageData{ + Message: "Mocked success message", + Recipients: []Recipient{ + { + Key: "mock-recipient-key", + Cost: "0.05", + SmsKey: "mock-sms-key", + MessageId: "mock-message-id", + MessagePart: 1, + Number: "+254745617596", + Status: "Success", + StatusCode: "200", + }, + }, + }, + }, nil +} + +func TestRetrySendSMS(t *testing.T) { + sender := SmsSender{ + ApiKey: "your-api-key", + ApiUser: "your-api-user", + Recipients: []string{"+254745617596"}, + Message: "Hello, this is a test message.", + Sender: "YourSenderID", + } + + maxRetries := 5 + + sender.SendSMS = sender.MockSendSMS + + response, err := sender.RetrySendSMS(maxRetries) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if response.SmsMessageData.Message != "Mocked success message" { + t.Errorf("unexpected message received in response") + } +} + +func TestRetrySendSMS_Success(t *testing.T) { + sender := SmsSender{ + ApiKey: "your-api-key", + ApiUser: "your-api-user", + Recipients: []string{"+254745617596"}, + Message: "Hello, this is a test message.", + Sender: "YourSenderID", + } + + maxRetries := 5 + + retryCount := 0 + sender.SendSMS = func() (SmsSenderResponse, error) { + if retryCount < 3 { + retryCount++ + return SmsSenderResponse{}, fmt.Errorf("mocked error") + } + return SmsSenderResponse{ + SmsMessageData: SmsMessageData{ + Message: "Success!", + }, + }, nil + } + + response, err := sender.RetrySendSMS(maxRetries) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if response.SmsMessageData.Message != "Success!" { + t.Errorf("unexpected message received in response") + } +} + +func TestRetrySendSMS_Failure(t *testing.T) { + sender := SmsSender{ + ApiKey: "your-api-key", + ApiUser: "your-api-user", + Recipients: []string{"+254745617596"}, + Message: "Hello, this is a test message.", + Sender: "YourSenderID", + } + + maxRetries := 3 + + sender.SendSMS = func() (SmsSenderResponse, error) { + return SmsSenderResponse{}, fmt.Errorf("mocked error") + } + + _, err := sender.RetrySendSMS(maxRetries) + + if err == nil { + t.Errorf("expected error, got nil") + } +} From 0622f7979a1d8bbb4036e7cb4833e5249e6a8106 Mon Sep 17 00:00:00 2001 From: MikeMwita Date: Sat, 6 Jul 2024 01:42:12 +0300 Subject: [PATCH 3/4] chore: Code structure reorganization --- pkg/sms/client.go | 28 +++++++ pkg/sms/sms_sender.go | 166 ++++++++++++++---------------------------- pkg/sms/types.go | 35 +++++++++ 3 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 pkg/sms/client.go create mode 100644 pkg/sms/types.go diff --git a/pkg/sms/client.go b/pkg/sms/client.go new file mode 100644 index 0000000..d210b89 --- /dev/null +++ b/pkg/sms/client.go @@ -0,0 +1,28 @@ +package sms + +import "net/http" + +const ( + DefaultAPIURL = "https://api.africastalking.com/version1/messaging" + sandboxAPIURL = "https://api.sandbox.africastalking.com/version1/messaging" +) + +type Doer interface { + Do(req *http.Request) (*http.Response, error) +} + +type Client struct { + apiURL string + apiKey string + apiUser string + client Doer +} + +func NewClient(client Doer, apiKey, apiUser string) *Client { + return &Client{ + apiURL: DefaultAPIURL, + apiKey: apiKey, + apiUser: apiUser, + client: client, + } +} diff --git a/pkg/sms/sms_sender.go b/pkg/sms/sms_sender.go index 955ad89..579282e 100644 --- a/pkg/sms/sms_sender.go +++ b/pkg/sms/sms_sender.go @@ -1,6 +1,7 @@ package sms import ( + "context" "encoding/json" "fmt" "math/rand" @@ -12,149 +13,90 @@ import ( "github.com/google/uuid" ) -type SmsSender struct { - ApiKey string `json:"api_key"` - ApiUser string `json:"api_user"` - Recipients []string `json:"recipients"` - Message string `json:"message"` - Sender string `json:"sender"` - SmsKey string `json:"sms_key"` -} - -type Recipient struct { - Key string `json:"key"` - Cost string `json:"cost"` - SmsKey string `json:"sms_key"` - MessageId string `json:"message_id"` - MessagePart int `json:"message_part"` - Number string `json:"number"` - Status string `json:"status"` - StatusCode string `json:"status_code"` -} - -type SmsMessageData struct { - Message string `json:"message"` - Cost string `json:"cost"` - Recipients []Recipient `json:"recipients"` -} - -type ErrorResponse struct { - HasError bool `json:"has_error"` - Message string `json:"message"` -} - -type SmsSenderResponse struct { - ErrorResponse ErrorResponse `json:"error_response"` - SmsMessageData SmsMessageData `json:"sms_message_data"` -} - -// SendSMS sends an SMS using the Africa's Talking API -func (s *SmsSender) SendSMS() (SmsSenderResponse, error) { - endpoint := "https://api.africastalking.com/version1/messaging" - parsedURL, err := url.Parse(endpoint) - if err != nil { - return SmsSenderResponse{}, err - } - - body := map[string][]string{ - "username": {s.ApiUser}, - "to": s.Recipients, - "message": {s.Message}, - "from": {s.Sender}, - } - +func (s *SmsSender) SendSMS(ctx context.Context) (SmsSenderResponse, error) { form := url.Values{} - for key, values := range body { - for _, value := range values { - form.Add(key, value) - } - } + form.Add("username", s.Client.apiUser) + form.Add("to", strings.Join(s.Recipients, ",")) + form.Add("message", s.Message) + form.Add("from", s.Sender) - req, err := http.NewRequest(http.MethodPost, parsedURL.String(), strings.NewReader(form.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.Client.apiURL, strings.NewReader(form.Encode())) if err != nil { return SmsSenderResponse{}, err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.Header.Set("apiKey", s.ApiKey) + req.Header.Set("apiKey", s.Client.apiKey) - client := &http.Client{} - res, err := client.Do(req) + res, err := s.Client.client.Do(req) if err != nil { return SmsSenderResponse{}, err } defer res.Body.Close() - if res.StatusCode == http.StatusCreated { - recipients := make([]Recipient, 0) - - var data map[string]interface{} - err = json.NewDecoder(res.Body).Decode(&data) - if err != nil { - return SmsSenderResponse{}, err - } + if res.StatusCode != http.StatusCreated { + return SmsSenderResponse{ + ErrorResponse: ErrorResponse{ + HasError: true, + Message: "Message not sent", + }, + }, fmt.Errorf("status code: %d", res.StatusCode) + } - smsMessageData := data["SMSMessageData"].(map[string]interface{}) - message := smsMessageData["Message"].(string) + var data map[string]interface{} + err = json.NewDecoder(res.Body).Decode(&data) + if err != nil { + return SmsSenderResponse{}, err + } - cost := "" - for _, word := range strings.Split(message, " ") { - cost = word - } - recipientsData := smsMessageData["Recipients"].([]interface{}) - - for _, recipient := range recipientsData { - recipientData := recipient.(map[string]interface{}) - - rct := Recipient{ - Key: uuid.New().String(), - Cost: recipientData["cost"].(string), - SmsKey: s.SmsKey, - MessageId: recipientData["messageId"].(string), - MessagePart: int(recipientData["messageParts"].(float64)), - Number: recipientData["number"].(string), - Status: recipientData["status"].(string), - StatusCode: fmt.Sprintf("%v", recipientData["statusCode"]), - } - - recipients = append(recipients, rct) - } + smsMessageData := data["SMSMessageData"].(map[string]interface{}) + message := smsMessageData["Message"].(string) + cost := "" + for _, word := range strings.Split(message, " ") { + cost = word + } - smsSenderResponse := SmsSenderResponse{ - ErrorResponse: ErrorResponse{ - HasError: false, - }, - SmsMessageData: SmsMessageData{ - Message: message, - Cost: cost, - Recipients: recipients, - }, + recipientsData := smsMessageData["Recipients"].([]interface{}) + recipients := make([]Recipient, 0) + + for _, recipient := range recipientsData { + recipientData := recipient.(map[string]interface{}) + + rct := Recipient{ + Key: uuid.New().String(), + Cost: recipientData["cost"].(string), + SmsKey: s.SmsKey, + MessageId: recipientData["messageId"].(string), + MessagePart: int(recipientData["messageParts"].(float64)), + Number: recipientData["number"].(string), + Status: recipientData["status"].(string), + StatusCode: fmt.Sprintf("%v", recipientData["statusCode"]), } - return smsSenderResponse, nil + recipients = append(recipients, rct) } - smsSenderResponse := SmsSenderResponse{ + return SmsSenderResponse{ ErrorResponse: ErrorResponse{ - HasError: true, - Message: "Message not sent", + HasError: false, }, - } - - return smsSenderResponse, fmt.Errorf("status code: %d", res.StatusCode) + SmsMessageData: SmsMessageData{ + Message: message, + Cost: cost, + Recipients: recipients, + }, + }, nil } -func (s *SmsSender) RetrySendSMS(maxRetries int) (SmsSenderResponse, error) { +func (s *SmsSender) RetrySendSMS(ctx context.Context, maxRetries int) (SmsSenderResponse, error) { for retry := 0; retry < maxRetries; retry++ { - response, err := s.SendSMS() + response, err := s.SendSMS(ctx) if err == nil { return response, nil } delay := time.Duration(1< Date: Sat, 6 Jul 2024 01:45:52 +0300 Subject: [PATCH 4/4] chore: Code structure reorganization --- examples/send_sms_example.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/examples/send_sms_example.go b/examples/send_sms_example.go index 6621073..e520b9c 100644 --- a/examples/send_sms_example.go +++ b/examples/send_sms_example.go @@ -1,24 +1,34 @@ package sms_sender import ( - "fmt" + "context" "github.com/MikeMwita/africastalking-go/pkg/sms" "log" + "net/http" + "os" + "time" ) func main() { - // Example usage - sender := sms.SmsSender{ - ApiKey: "your_api_key", - ApiUser: "your_api_user", + apiKey := os.Getenv("API_KEY") + apiUser := os.Getenv("API_USER") + + client := sms.NewClient(&http.Client{}, apiKey, apiUser) + sender := &sms.SmsSender{ + Client: client, Recipients: []string{"+1234567890"}, - Message: "Hello, world!", + Message: "Test message", + Sender: "YourSenderID", + SmsKey: "unique_sms_key", } - response, err := sender.SendSMS() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resp, err := sender.RetrySendSMS(ctx, 3) if err != nil { - log.Fatal(err) + log.Fatalf("Failed to send SMS: %v", err) } - fmt.Printf("Response: %+v\n", response) + log.Printf("SMS Response: %+v", resp) }