diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 9b063c510..79eabb066 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -49,7 +49,6 @@ jobs: TWILIO_API_SECRET: ${{ secrets.TWILIO_CLUSTER_TEST_API_KEY_SECRET }} TWILIO_FROM_NUMBER: ${{ secrets.TWILIO_FROM_NUMBER }} TWILIO_TO_NUMBER: ${{ secrets.TWILIO_TO_NUMBER }} - TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }} run: make cluster-test - name: Run Test Coverage diff --git a/Makefile b/Makefile index 24c1b6bc3..12b3ba9a0 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,12 @@ test-docker: docker build -t twilio/twilio-go . docker run twilio/twilio-go go test -race ./... -test-docker: - docker build -t twilio/twilio-go . - docker run twilio/twilio-go go test ./... - cluster-test: go test -race --tags=cluster +webhook-cluster-test: + go test -race --tags=webhook_cluster + goimports: go install golang.org/x/tools/cmd/goimports@latest goimports -w . diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 1b045bc23..f27c1edc0 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -12,6 +12,8 @@ Please enter each Issue number you are resolving in your PR after one of the fol e.g. Fixes #1 Closes #2 + +Note: If you made changes to the Request Validation logic, please run `make webhook-cluster-test` locally and ensure all test cases pass --> # Fixes # diff --git a/cluster_test.go b/cluster_test.go index c6ec97464..39a34628f 100644 --- a/cluster_test.go +++ b/cluster_test.go @@ -4,20 +4,12 @@ package twilio import ( - "encoding/json" - "fmt" - "net/http" - "os" - "testing" - "time" - - "github.com/localtunnel/go-localtunnel" "github.com/stretchr/testify/assert" - twilio "github.com/twilio/twilio-go/client" Api "github.com/twilio/twilio-go/rest/api/v2010" ChatV2 "github.com/twilio/twilio-go/rest/chat/v2" EventsV1 "github.com/twilio/twilio-go/rest/events/v1" - StudioV2 "github.com/twilio/twilio-go/rest/studio/v2" + "os" + "testing" ) var from string @@ -127,127 +119,6 @@ func TestListParams(t *testing.T) { assert.Nil(t, err) } -func createValidationServer(channel chan bool) *http.Server { - server := &http.Server{} - server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - url := r.Header["X-Forwarded-Proto"][0] + "://" + r.Header["X-Forwarded-Host"][0] + r.URL.RequestURI() - signatureHeader := r.Header["X-Twilio-Signature"] - r.ParseForm() - params := make(map[string]string) - for k, v := range r.PostForm { - params[k] = v[0] - } - requestValidator := twilio.NewRequestValidator(os.Getenv("TWILIO_AUTH_TOKEN")) - if len(signatureHeader) != 0 { - channel <- requestValidator.Validate(url, params, r.Header["X-Twilio-Signature"][0]) - } else { - channel <- false - } - }) - return server -} - -func createStudioFlowParams(url string, method string) *StudioV2.CreateFlowParams { - jsonStr := fmt.Sprintf(`{ - "description": "Studio Flow", - "states": [ - { - "name": "Trigger", - "type": "trigger", - "transitions": [ - { - "next": "httpRequest", - "event": "incomingRequest" - } - ], - "properties": { - } - }, - { - "name": "httpRequest", - "type": "make-http-request", - "transitions": [], - "properties": { - "method": "%s", - "content_type": "application/x-www-form-urlencoded;charset=utf-8", - "url": "%s" - } - } - ], - "initial_state": "Trigger", - "flags": { - "allow_concurrent_calls": true - } - }`, method, url) - - var definition interface{} - _ = json.Unmarshal([]byte(jsonStr), &definition) - - params := &StudioV2.CreateFlowParams{ - Definition: &definition, - } - params.SetFriendlyName("Go Cluster Test Flow") - params.SetStatus("published") - return params -} - -func createStudioExecutionParams() *StudioV2.CreateExecutionParams { - executionParams := &StudioV2.CreateExecutionParams{} - executionParams.SetTo("To") - executionParams.SetFrom("From") - return executionParams -} - -func executeFlow(t *testing.T, flowSid string) { - _, exeErr := testClient.StudioV2.CreateExecution(flowSid, createStudioExecutionParams()) - if exeErr != nil { - t.Fatal("Error with Studio Execution Creation: ", exeErr) - } -} - -func requestValidation(t *testing.T, method string) { - //Invoke Localtunnel - listener, ltErr := localtunnel.Listen(localtunnel.Options{}) - if ltErr != nil { - t.Fatal("Error with Localtunnel: ", ltErr) - } - //Create Validation Server & Listen - channel := make(chan bool) - server := createValidationServer(channel) - go server.Serve(listener) - - //Extra time for server to set up - time.Sleep(1 * time.Second) - - //Create Studio Flow - params := createStudioFlowParams(listener.URL(), method) - resp, flowErr := testClient.StudioV2.CreateFlow(params) - if flowErr != nil { - t.Fatal("Error with Studio Flow Creation: ", flowErr) - } - flowSid := *resp.Sid - executeFlow(t, flowSid) - - //Await for Request Validation - afterCh := time.After(5 * time.Second) - select { - case validate := <-channel: - assert.True(t, validate) - case <-afterCh: - t.Fatal("No request was sent to validation server") - } - defer testClient.StudioV2.DeleteFlow(flowSid) - defer server.Close() -} - -func TestRequestValidation_GETMethod(t *testing.T) { - requestValidation(t, "GET") -} - -func TestRequestValidation_POSTMethod(t *testing.T) { - requestValidation(t, "POST") -} - func TestListingAvailableNumber(t *testing.T) { params := &Api.ListAvailablePhoneNumberTollFreeParams{} params.SetLimit(2) diff --git a/webhook_cluster_test.go b/webhook_cluster_test.go new file mode 100644 index 000000000..c32fd6d54 --- /dev/null +++ b/webhook_cluster_test.go @@ -0,0 +1,152 @@ +//go:build webhook_cluster +// +build webhook_cluster + +package twilio + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "testing" + "time" + + "github.com/localtunnel/go-localtunnel" + "github.com/stretchr/testify/assert" + twilio "github.com/twilio/twilio-go/client" + StudioV2 "github.com/twilio/twilio-go/rest/studio/v2" +) + +var testClient *RestClient +var authToken string + +func TestMain(m *testing.M) { + var apiKey = os.Getenv("TWILIO_API_KEY") + var secret = os.Getenv("TWILIO_API_SECRET") + var accountSid = os.Getenv("TWILIO_ACCOUNT_SID") + authToken = os.Getenv("TWILIO_AUTH_TOKEN") + testClient = NewRestClientWithParams(ClientParams{apiKey, secret, accountSid, nil}) + ret := m.Run() + os.Exit(ret) +} + +func createValidationServer(channel chan bool) *http.Server { + server := &http.Server{} + server.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + url := r.Header["X-Forwarded-Proto"][0] + "://" + r.Header["X-Forwarded-Host"][0] + r.URL.RequestURI() + signatureHeader := r.Header["X-Twilio-Signature"] + r.ParseForm() + params := make(map[string]string) + for k, v := range r.PostForm { + params[k] = v[0] + } + requestValidator := twilio.NewRequestValidator(authToken) + if len(signatureHeader) != 0 { + channel <- requestValidator.Validate(url, params, r.Header["X-Twilio-Signature"][0]) + } else { + channel <- false + } + }) + return server +} + +func createStudioFlowParams(url string, method string) *StudioV2.CreateFlowParams { + jsonStr := fmt.Sprintf(`{ + "description": "Studio Flow", + "states": [ + { + "name": "Trigger", + "type": "trigger", + "transitions": [ + { + "next": "httpRequest", + "event": "incomingRequest" + } + ], + "properties": { + } + }, + { + "name": "httpRequest", + "type": "make-http-request", + "transitions": [], + "properties": { + "method": "%s", + "content_type": "application/x-www-form-urlencoded;charset=utf-8", + "url": "%s" + } + } + ], + "initial_state": "Trigger", + "flags": { + "allow_concurrent_calls": true + } + }`, method, url) + + var definition interface{} + _ = json.Unmarshal([]byte(jsonStr), &definition) + + params := &StudioV2.CreateFlowParams{ + Definition: &definition, + } + params.SetFriendlyName("Go Cluster Test Flow") + params.SetStatus("published") + return params +} + +func createStudioExecutionParams() *StudioV2.CreateExecutionParams { + executionParams := &StudioV2.CreateExecutionParams{} + executionParams.SetTo("To") + executionParams.SetFrom("From") + return executionParams +} + +func executeFlow(t *testing.T, flowSid string) { + _, exeErr := testClient.StudioV2.CreateExecution(flowSid, createStudioExecutionParams()) + if exeErr != nil { + t.Fatal("Error with Studio Execution Creation: ", exeErr) + } +} + +func requestValidation(t *testing.T, method string) { + //Invoke Localtunnel + listener, ltErr := localtunnel.Listen(localtunnel.Options{}) + if ltErr != nil { + t.Fatal("Error with Localtunnel: ", ltErr) + } + //Create Validation Server & Listen + channel := make(chan bool) + server := createValidationServer(channel) + go server.Serve(listener) + + //Extra time for server to set up + time.Sleep(1 * time.Second) + + //Create Studio Flow + params := createStudioFlowParams(listener.URL(), method) + resp, flowErr := testClient.StudioV2.CreateFlow(params) + if flowErr != nil { + t.Fatal("Error with Studio Flow Creation: ", flowErr) + } + flowSid := *resp.Sid + executeFlow(t, flowSid) + + //Await for Request Validation + afterCh := time.After(5 * time.Second) + select { + case validate := <-channel: + assert.True(t, validate) + case <-afterCh: + t.Fatal("No request was sent to validation server") + } + defer testClient.StudioV2.DeleteFlow(flowSid) + defer server.Close() +} + +func TestRequestValidation_GETMethod(t *testing.T) { + requestValidation(t, "GET") +} + +func TestRequestValidation_POSTMethod(t *testing.T) { + requestValidation(t, "POST") +}