diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3912072..0c06d4e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,4 +71,10 @@ jobs: - name: Send coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: goveralls -coverprofile=coverage.out -service=github \ No newline at end of file + run: goveralls -coverprofile=coverage.out -service=github + + - name: Quality Gate - Test coverage shall be above threshold + env: + TESTCOVERAGE_THRESHOLD: 80.0 + run: | + bash scripts/coverage.sh \ No newline at end of file diff --git a/client.go b/client.go index c95085f..014c0b0 100644 --- a/client.go +++ b/client.go @@ -179,8 +179,8 @@ func (s *client) refreshAccessToken() { // MakeRequest performs a HTTP request to the provided path and parameters func (s *client) MakeRequest(ctx context.Context, method, path string, queryParams map[string]string, body interface{}, authorised bool) (*http.Response, error) { urlPath := fmt.Sprintf("%s%s", BaseURL, path) - var request *http.Request + var request *http.Request switch method { case http.MethodGet: req, err := http.NewRequestWithContext(ctx, method, urlPath, nil) @@ -188,6 +188,7 @@ func (s *client) MakeRequest(ctx context.Context, method, path string, queryPara return nil, err } request = req + case http.MethodPost: encoded, err := json.Marshal(body) if err != nil { @@ -202,6 +203,7 @@ func (s *client) MakeRequest(ctx context.Context, method, path string, queryPara } request = req + default: return nil, fmt.Errorf("s.MakeRequest() unsupported http method: %s", method) diff --git a/client_test.go b/client_test.go index 6f66993..fbe7a08 100644 --- a/client_test.go +++ b/client_test.go @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/brianvoe/gofakeit" "github.com/jarcoal/httpmock" ) @@ -25,27 +26,41 @@ func MockLogin() { } func TestSILComms_Login(t *testing.T) { - type fields struct { - client *http.Client - } tests := []struct { - name string - fields fields + name string + wantPanic bool }{ { - name: "happy case: successful login", - fields: fields{ - client: &http.Client{}, - }, + name: "happy case: successful login", + wantPanic: false, + }, + { + name: "sad case: invalid status code", + wantPanic: true, + }, + { + name: "sad case: invalid api response", + wantPanic: true, + }, + { + name: "sad case: invalid token response", + wantPanic: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer func() { + r := recover() + if (r != nil) != tt.wantPanic { + t.Errorf("login() recover = %v, wantPanic = %v", r, tt.wantPanic) + } + }() + httpmock.Activate() defer httpmock.DeactivateAndReset() if tt.name == "happy case: successful login" { - httpmock.RegisterResponder(http.MethodPost, "/auth/token/", func(r *http.Request) (*http.Response, error) { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/auth/token/", BaseURL), func(r *http.Request) (*http.Response, error) { resp := APIResponse{ Status: StatusSuccess, Message: "success", @@ -58,34 +73,83 @@ func TestSILComms_Login(t *testing.T) { }) } - s := &client{ - client: tt.fields.client, + if tt.name == "sad case: invalid status code" { + httpmock.RegisterResponder(http.MethodPost, "/auth/token/", func(r *http.Request) (*http.Response, error) { + resp := APIResponse{ + Status: StatusSuccess, + Message: "success", + Data: nil, + } + return httpmock.NewJsonResponse(http.StatusBadRequest, resp) + }) + } + + if tt.name == "sad case: invalid api response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/auth/token/", BaseURL), func(r *http.Request) (*http.Response, error) { + resp := map[string]interface{}{ + "status": 1234, + "message": 1234, + } + return httpmock.NewJsonResponse(http.StatusOK, resp) + }) } + + if tt.name == "sad case: invalid token response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/auth/token/", BaseURL), func(r *http.Request) (*http.Response, error) { + resp := APIResponse{ + Status: StatusSuccess, + Message: "success", + Data: map[string]interface{}{ + "refresh": 1234, + "access": 1234, + }, + } + return httpmock.NewJsonResponse(http.StatusOK, resp) + }) + } + + s := newClient() + s.login() }) } } func TestSILclient_refreshAccessToken(t *testing.T) { - type fields struct { - client *http.Client - refreshToken string - } tests := []struct { - name string - fields fields + name string + wantPanic bool }{ { - name: "happy case: refresh access token", - fields: fields{ - client: &http.Client{}, - }, + name: "happy case: refresh access token", + wantPanic: false, + }, + { + name: "sad case: invalid status code", + wantPanic: true, + }, + { + name: "sad case: invalid api response", + wantPanic: true, + }, + { + name: "sad case: invalid token response", + wantPanic: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer func() { + r := recover() + if (r != nil) != tt.wantPanic { + t.Errorf("refreshAccessToken() recover = %v, wantPanic = %v", r, tt.wantPanic) + } + }() + httpmock.Activate() defer httpmock.DeactivateAndReset() + MockLogin() + s := newClient() if tt.name == "happy case: refresh access token" { httpmock.RegisterResponder(http.MethodPost, "/auth/token/refresh/", func(r *http.Request) (*http.Response, error) { @@ -101,10 +165,41 @@ func TestSILclient_refreshAccessToken(t *testing.T) { }) } - s := &client{ - client: tt.fields.client, - refreshToken: tt.fields.refreshToken, + if tt.name == "sad case: invalid status code" { + httpmock.RegisterResponder(http.MethodPost, "/auth/token/refresh/", func(r *http.Request) (*http.Response, error) { + resp := APIResponse{ + Status: StatusSuccess, + Message: "success", + Data: nil, + } + return httpmock.NewJsonResponse(http.StatusBadRequest, resp) + }) + } + + if tt.name == "sad case: invalid api response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/auth/token/refresh/", BaseURL), func(r *http.Request) (*http.Response, error) { + resp := map[string]interface{}{ + "status": 1234, + "message": 1234, + } + return httpmock.NewJsonResponse(http.StatusOK, resp) + }) } + + if tt.name == "sad case: invalid token response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/auth/token/refresh/", BaseURL), func(r *http.Request) (*http.Response, error) { + resp := APIResponse{ + Status: StatusSuccess, + Message: "success", + Data: map[string]interface{}{ + "refresh": 1234, + "access": 1234, + }, + } + return httpmock.NewJsonResponse(http.StatusOK, resp) + }) + } + s.refreshAccessToken() }) } @@ -125,7 +220,7 @@ func TestSILclient_MakeRequest(t *testing.T) { wantErr bool }{ { - name: "happy case: make authenticated request", + name: "happy case: make unauthenticated request", args: args{ ctx: context.Background(), method: http.MethodPost, @@ -137,7 +232,7 @@ func TestSILclient_MakeRequest(t *testing.T) { wantErr: false, }, { - name: "happy case: make unauthenticated request", + name: "happy case: make authenticated POST request", args: args{ ctx: context.Background(), method: http.MethodPost, @@ -148,46 +243,84 @@ func TestSILclient_MakeRequest(t *testing.T) { }, wantErr: false, }, + { + name: "happy case: make authenticated GET request", + args: args{ + ctx: context.Background(), + method: http.MethodGet, + path: "/v1/sms/bulk/", + queryParams: map[string]string{ + "app": gofakeit.UUID(), + }, + body: nil, + authorised: true, + }, + wantErr: false, + }, + { + name: "sad case: make unsupported protocol request", + args: args{ + ctx: context.Background(), + method: http.MethodOptions, + path: "/v1/sms/bulk/", + queryParams: map[string]string{ + "app": gofakeit.UUID(), + }, + body: nil, + authorised: true, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() MockLogin() + s := newClient() - httpmock.RegisterResponder(http.MethodPost, "/v1/sms/bulk/", func(r *http.Request) (*http.Response, error) { - resp := APIResponse{ - Status: StatusSuccess, - Message: "success", - Data: BulkSMSResponse{ - GUID: "", - Sender: "", - Message: "", - Recipients: []string{}, - State: "", - SMS: []string{}, - Created: "", - Updated: "", - }, - } - return httpmock.NewJsonResponse(http.StatusOK, resp) - }) + if tt.name == "happy case: make authenticated POST request" { + httpmock.RegisterResponder(http.MethodPost, "/v1/sms/bulk/", func(r *http.Request) (*http.Response, error) { + resp := APIResponse{ + Status: StatusSuccess, + Message: "success", + Data: BulkSMSResponse{ + GUID: "", + Sender: "", + Message: "", + Recipients: []string{}, + State: "", + SMS: []string{}, + Created: "", + Updated: "", + }, + } + return httpmock.NewJsonResponse(http.StatusOK, resp) + }) + } - if tt.name == "happy case: make authenticated request" { - httpmock.RegisterResponder(http.MethodPost, "/auth/token/", func(r *http.Request) (*http.Response, error) { + if tt.name == "happy case: make authenticated GET request" { + httpmock.RegisterResponder(http.MethodGet, "/v1/sms/bulk/", func(r *http.Request) (*http.Response, error) { resp := APIResponse{ Status: StatusSuccess, Message: "success", - Data: TokenResponse{ - Refresh: "refresh", - Access: "access", + Data: []BulkSMSResponse{ + { + GUID: "", + Sender: "", + Message: "", + Recipients: []string{}, + State: "", + SMS: []string{}, + Created: "", + Updated: "", + }, }, } return httpmock.NewJsonResponse(http.StatusOK, resp) }) } - s := newClient() got, err := s.MakeRequest(tt.args.ctx, tt.args.method, tt.args.path, tt.args.queryParams, tt.args.body, tt.args.authorised) if (err != nil) != tt.wantErr { t.Errorf("SILclient.MakeRequest() error = %v, wantErr %v", err, tt.wantErr) diff --git a/models.go b/models.go index 2b6b39d..0b64ded 100644 --- a/models.go +++ b/models.go @@ -7,6 +7,14 @@ type APIResponse struct { Data interface{} `json:"data,omitempty"` } +// ResultsResponse is the base response from a paginated list of results +type ResultsResponse struct { + Count int `json:"count"` + Next *string `json:"next"` + Previous *string `json:"previous"` + Results []interface{} `json:"results"` +} + // TokenResponse is the data in the API response when logging in // The access token is used as the bearer token when making API requests // The refresh token is used to obtain a new access token when it expires diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100644 index 0000000..14889a4 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,12 @@ +#!/bin/bash +echo "Quality Gate: checking if test coverage is above threshold ..." +echo "Threshold : ${TESTCOVERAGE_THRESHOLD} %" +totalCoverage=`go tool cover -func=coverage.out | grep total | grep -Eo '[0-9]+\.[0-9]+'` +echo "Current test coverage : $totalCoverage %" +if (( $(echo "$totalCoverage ${TESTCOVERAGE_THRESHOLD}" | awk '{print ($1 >= $2)}') )); then + echo "OK" +else + echo "Current test coverage is below threshold. Please add more tests" + echo "Failed" + exit 1 +fi \ No newline at end of file diff --git a/sms_test.go b/sms_test.go index d394cd6..c66bcf4 100644 --- a/sms_test.go +++ b/sms_test.go @@ -33,6 +33,39 @@ func TestSILCommsLib_SendBulkSMS(t *testing.T) { }, wantErr: false, }, + { + name: "sad case: invalid status code", + args: args{ + ctx: context.Background(), + message: "This is a test", + recipients: []string{ + gofakeit.Phone(), + }, + }, + wantErr: true, + }, + { + name: "sad case: invalid API response", + args: args{ + ctx: context.Background(), + message: "This is a test", + recipients: []string{ + gofakeit.Phone(), + }, + }, + wantErr: true, + }, + { + name: "sad case: invalid bulk SMS data response", + args: args{ + ctx: context.Background(), + message: "This is a test", + recipients: []string{ + gofakeit.Phone(), + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -54,6 +87,37 @@ func TestSILCommsLib_SendBulkSMS(t *testing.T) { return httpmock.NewJsonResponse(http.StatusAccepted, resp) }) } + if tt.name == "sad case: invalid status code" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/v1/sms/bulk/", silcomms.BaseURL), func(r *http.Request) (*http.Response, error) { + return httpmock.NewJsonResponse(http.StatusUnauthorized, nil) + }) + } + + if tt.name == "sad case: invalid API response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/v1/sms/bulk/", silcomms.BaseURL), func(r *http.Request) (*http.Response, error) { + resp := map[string]interface{}{ + "status": 1234, + "message": 1234, + } + + return httpmock.NewJsonResponse(http.StatusAccepted, resp) + }) + } + + if tt.name == "sad case: invalid bulk SMS data response" { + httpmock.RegisterResponder(http.MethodPost, fmt.Sprintf("%s/v1/sms/bulk/", silcomms.BaseURL), func(r *http.Request) (*http.Response, error) { + resp := silcomms.APIResponse{ + Status: silcomms.StatusSuccess, + Message: "success", + Data: map[string]interface{}{ + "guid": 123456, + "sender": 123456, + "message": 123456, + }, + } + return httpmock.NewJsonResponse(http.StatusAccepted, resp) + }) + } got, err := l.SendBulkSMS(tt.args.ctx, tt.args.message, tt.args.recipients) if (err != nil) != tt.wantErr {