Repository: supabase/gotrue
Pull request #676 (2022-09-12 06:58:22)

fix: add test for all sms providers
## What kind of change does this PR introduce?
* ~Introduce a custom `HttpClient` interface which can be overridden in tests.~ 
* ~For tests in `sms_provider`, we can overwrite the native `http.Client` with a `MockHttpClient` and define the expected API behaviour from the 3rd-party sms providers with `handleApiRequest`.~
* Used [gock](https://github.com/h2non/gock) for mocking external API requests instead

## To-Do
- [x] Implement the tests for `SendSms` for messagebird, textlocal and vonage



egor-romanov: LGTM 👍

I prefer to set up a real http(ws, grpc) server where possible, but this is ok too.

You may find examples here: https://github.com/supabase/cli/blob/main/test/mocks/supabase/server.go
https://github.com/supabase/cli/blob/e92784551f57d9861fdd80e1a5965ec9e909641b/test/secrets_test.go#L87-L101

or 
https://github.com/supabase/cli/blob/e92784551f57d9861fdd80e1a5965ec9e909641b/test/mocks/docker/server.go
https://github.com/supabase/cli/blob/e92784551f57d9861fdd80e1a5965ec9e909641b/test/db_test.go#L35


Patch:
diff --git a/api/sms_provider/textlocal.go b/api/sms_provider/textlocal.go
index f47bc9db..36eb5067 100644
--- a/api/sms_provider/textlocal.go
+++ b/api/sms_provider/textlocal.go
@@ -71,7 +71,7 @@ func (t *TextlocalProvider) SendSms(phone string, message string) error {
 		return derr
 	}
 
-	if len(resp.Errors) == 0 {
+	if len(resp.Errors) > 0 {
 		return errors.New("textlocal error: Internal Error")
 	}
 
diff --git a/api/sms_provider/twilio.go b/api/sms_provider/twilio.go
index 2ca84ab5..46c37ebc 100644
--- a/api/sms_provider/twilio.go
+++ b/api/sms_provider/twilio.go
@@ -61,7 +61,6 @@ func (t *TwilioProvider) SendSms(phone string, message string) error {
 		"From":    {t.Config.MessageServiceSid},
 		"Body":    {message},
 	}
-
 	client := &http.Client{Timeout: defaultTimeout}
 	r, err := http.NewRequest("POST", t.APIPath, strings.NewReader(body.Encode()))
 	if err != nil {
diff --git a/go.mod b/go.mod
index 8919f736..7f077e28 100644
--- a/go.mod
+++ b/go.mod
@@ -57,6 +57,7 @@ require (
 	github.com/google/go-cmp v0.5.2 // indirect
 	github.com/gorilla/context v1.1.1 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
+	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
 	github.com/jackc/pgio v1.0.0 // indirect
@@ -93,6 +94,7 @@ require (
 	google.golang.org/protobuf v1.25.0 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+	gopkg.in/h2non/gock.v1 v1.1.2 // indirect
 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.0 // indirect
diff --git a/go.sum b/go.sum
index ef19630d..3e4fc9df 100644
--- a/go.sum
+++ b/go.sum
@@ -235,6 +235,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
+github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
 github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
 github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -421,6 +423,7 @@ github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/nats-io/stan.go v0.4.5/go.mod h1:Ji7mK6gRZJSH1nc3ZJH6vi7zn/QnZhpR9Arm4iuzsUQ=
 github.com/nats-io/stan.go v0.5.0/go.mod h1:dYqB+vMN3C2F9pT1FRQpg9eHbjPj6mP0yYuyBNuXHZE=
+github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/netlify/mailme v1.1.1 h1:S/ANl+Hy/EIoJUgGiLJYYLZJ2QOTG452R73qTQudMns=
 github.com/netlify/mailme v1.1.1/go.mod h1:8g03BJmU+ps7ma5vcH+t8aMtaicQTMX3ffP7RJ8xY8g=
 github.com/netlify/netlify-commons v0.32.0 h1:IgpqedBa6aFrc+daRgGZ+SmU9eBXlDXzKSAjevWmshM=
@@ -869,6 +872,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/gomail.v2 v2.0.0-20150902115704-41f357289737/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
+gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=


Test patch:
diff --git a/api/sms_provider/sms_provider_test.go b/api/sms_provider/sms_provider_test.go
new file mode 100644
index 00000000..b13bd9cd
--- /dev/null
+++ b/api/sms_provider/sms_provider_test.go
@@ -0,0 +1,216 @@
+package sms_provider
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"net/url"
+	"testing"
+
+	"github.com/netlify/gotrue/conf"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
+	"gopkg.in/h2non/gock.v1"
+)
+
+var handleApiRequest func(*http.Request) (*http.Response, error)
+
+type SmsProviderTestSuite struct {
+	suite.Suite
+	Config *conf.GlobalConfiguration
+}
+
+type MockHttpClient struct {
+	mock.Mock
+}
+
+func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) {
+	return handleApiRequest(req)
+}
+
+func TestSmsProvider(t *testing.T) {
+	ts := &SmsProviderTestSuite{
+		Config: &conf.GlobalConfiguration{
+			Sms: conf.SmsProviderConfiguration{
+				Twilio: conf.TwilioProviderConfiguration{
+					AccountSid:        "test_account_sid",
+					AuthToken:         "test_auth_token",
+					MessageServiceSid: "test_message_service_id",
+				},
+				Messagebird: conf.MessagebirdProviderConfiguration{
+					AccessKey:  "test_access_key",
+					Originator: "test_originator",
+				},
+				Vonage: conf.VonageProviderConfiguration{
+					ApiKey:    "test_api_key",
+					ApiSecret: "test_api_secret",
+					From:      "test_from",
+				},
+				Textlocal: conf.TextlocalProviderConfiguration{
+					ApiKey: "test_api_key",
+					Sender: "test_sender",
+				},
+			},
+		},
+	}
+	suite.Run(t, ts)
+}
+
+func (ts *SmsProviderTestSuite) TestTwilioSendSms() {
+	defer gock.Off()
+	provider, err := NewTwilioProvider(ts.Config.Sms.Twilio)
+	require.NoError(ts.T(), err)
+
+	twilioProvider, ok := provider.(*TwilioProvider)
+	require.Equal(ts.T(), true, ok)
+
+	phone := "123456789"
+	message := "This is the sms code: 123456"
+
+	body := url.Values{
+		"To":      {"+" + phone},
+		"Channel": {"sms"},
+		"From":    {twilioProvider.Config.MessageServiceSid},
+		"Body":    {message},
+	}
+
+	cases := []struct {
+		Desc           string
+		TwilioResponse *gock.Response
+		ExpectedError  error
+	}{
+		{
+			Desc: "Successfully sent sms",
+			TwilioResponse: gock.New(twilioProvider.APIPath).Post("").
+				MatchHeader("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(twilioProvider.Config.AccountSid+":"+twilioProvider.Config.AuthToken))).
+				MatchType("url").BodyString(body.Encode()).
+				Reply(200).JSON(SmsStatus{
+				To:     "+" + phone,
+				From:   twilioProvider.Config.MessageServiceSid,
+				Status: "sent",
+				Body:   message,
+			}),
+			ExpectedError: nil,
+		},
+		{
+			Desc: "Sms status is failed / undelivered",
+			TwilioResponse: gock.New(twilioProvider.APIPath).Post("").
+				MatchHeader("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(twilioProvider.Config.AccountSid+":"+twilioProvider.Config.AuthToken))).
+				MatchType("url").BodyString(body.Encode()).
+				Reply(200).JSON(SmsStatus{
+				ErrorMessage: "failed to send sms",
+				ErrorCode:    "401",
+				Status:       "failed",
+			}),
+			ExpectedError: fmt.Errorf("twilio error: %v %v", "failed to send sms", "401"),
+		},
+		{
+			Desc: "Non-2xx status code returned",
+			TwilioResponse: gock.New(twilioProvider.APIPath).Post("").
+				MatchHeader("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(twilioProvider.Config.AccountSid+":"+twilioProvider.Config.AuthToken))).
+				MatchType("url").BodyString(body.Encode()).
+				Reply(500).JSON(twilioErrResponse{
+				Code:     500,
+				Message:  "Internal server error",
+				MoreInfo: "error",
+				Status:   500,
+			}),
+			ExpectedError: &twilioErrResponse{
+				Code:     500,
+				Message:  "Internal server error",
+				MoreInfo: "error",
+				Status:   500,
+			},
+		},
+	}
+
+	for _, c := range cases {
+		ts.Run(c.Desc, func() {
+			err = twilioProvider.SendSms(phone, message)
+			require.Equal(ts.T(), c.ExpectedError, err)
+		})
+	}
+}
+
+func (ts *SmsProviderTestSuite) TestMessagebirdSendSms() {
+	defer gock.Off()
+	provider, err := NewMessagebirdProvider(ts.Config.Sms.Messagebird)
+	require.NoError(ts.T(), err)
+
+	messagebirdProvider, ok := provider.(*MessagebirdProvider)
+	require.Equal(ts.T(), true, ok)
+
+	phone := "123456789"
+	message := "This is the sms code: 123456"
+	body := url.Values{
+		"originator": {messagebirdProvider.Config.Originator},
+		"body":       {message},
+		"recipients": {phone},
+		"type":       {"sms"},
+		"datacoding": {"unicode"},
+	}
+	gock.New(messagebirdProvider.APIPath).Post("").MatchHeader("Authorization", "AccessKey "+messagebirdProvider.Config.AccessKey).MatchType("url").BodyString(body.Encode()).Reply(200).JSON(MessagebirdResponse{
+		Recipients: MessagebirdResponseRecipients{
+			TotalSentCount: 1,
+		},
+	})
+
+	err = messagebirdProvider.SendSms(phone, message)
+	require.NoError(ts.T(), err)
+}
+
+func (ts *SmsProviderTestSuite) TestVonageSendSms() {
+	defer gock.Off()
+	provider, err := NewVonageProvider(ts.Config.Sms.Vonage)
+	require.NoError(ts.T(), err)
+
+	vonageProvider, ok := provider.(*VonageProvider)
+	require.Equal(ts.T(), true, ok)
+
+	phone := "123456789"
+	message := "This is the sms code: 123456"
+
+	body := url.Values{
+		"from":       {vonageProvider.Config.From},
+		"to":         {phone},
+		"text":       {message},
+		"api_key":    {vonageProvider.Config.ApiKey},
+		"api_secret": {vonageProvider.Config.ApiSecret},
+	}
+
+	gock.New(vonageProvider.APIPath).Post("").MatchType("url").BodyString(body.Encode()).Reply(200).JSON(VonageResponse{
+		Messages: []VonageResponseMessage{
+			{Status: "0"},
+		},
+	})
+
+	err = vonageProvider.SendSms(phone, message)
+	require.NoError(ts.T(), err)
+}
+
+func (ts *SmsProviderTestSuite) TestTextLocalSendSms() {
+	defer gock.Off()
+	provider, err := NewTextlocalProvider(ts.Config.Sms.Textlocal)
+	require.NoError(ts.T(), err)
+
+	textlocalProvider, ok := provider.(*TextlocalProvider)
+	require.Equal(ts.T(), true, ok)
+
+	phone := "123456789"
+	message := "This is the sms code: 123456"
+	body := url.Values{
+		"sender":  {textlocalProvider.Config.Sender},
+		"apikey":  {textlocalProvider.Config.ApiKey},
+		"message": {message},
+		"numbers": {phone},
+	}
+
+	gock.New(textlocalProvider.APIPath).Post("").MatchType("url").BodyString(body.Encode()).Reply(200).JSON(TextlocalResponse{
+		Status: "success",
+		Errors: []TextlocalError{},
+	})
+
+	err = textlocalProvider.SendSms(phone, message)
+	require.NoError(ts.T(), err)
+}