From 1bb626d559d803bc19f0932afad241a1f4f57f32 Mon Sep 17 00:00:00 2001 From: Akshay Prabhakant <32875270+akshayDev17@users.noreply.github.com> Date: Thu, 20 Oct 2022 12:41:49 +0530 Subject: [PATCH] Feature/private mutex (#248) * feat/mutexPrivate:init; trying whether changes get pushed * feat/privatedMutex: privated the mutex as a field in Client struct, added tests for this private mutex field * feature/privateMutex: commented the composition of sync.Mutex * feature/privateMutex: resolved issues with user credentials Co-authored-by: Akshay Prabhakant --- client.go | 10 ++-- client_test.go | 141 +++++++++++++++++++++++++++++++++++++++++++++++++ types.go | 15 +++--- 3 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 client_test.go diff --git a/client.go b/client.go index 43015b3..9e5fd74 100644 --- a/client.go +++ b/client.go @@ -138,7 +138,8 @@ func (c *Client) Send(req *http.Request, v interface{}) error { // making the main request // client.Token will be updated when changed func (c *Client) SendWithAuth(req *http.Request, v interface{}) error { - c.Lock() + // c.Lock() + c.mu.Lock() // Note: Here we do not want to `defer c.Unlock()` because we need `c.Send(...)` // to happen outside of the locked section. @@ -146,17 +147,18 @@ func (c *Client) SendWithAuth(req *http.Request, v interface{}) error { if !c.tokenExpiresAt.IsZero() && c.tokenExpiresAt.Sub(time.Now()) < RequestNewTokenBeforeExpiresIn { // c.Token will be updated in GetAccessToken call if _, err := c.GetAccessToken(req.Context()); err != nil { - c.Unlock() + // c.Unlock() + c.mu.Unlock() return err } } req.Header.Set("Authorization", "Bearer "+c.Token.Token) } - // Unlock the client mutex before sending the request, this allows multiple requests // to be in progress at the same time. - c.Unlock() + // c.Unlock() + c.mu.Unlock() return c.Send(req, v) } diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..b67cd5a --- /dev/null +++ b/client_test.go @@ -0,0 +1,141 @@ +package paypal + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// testClientID, testSecret imported from order_test.go + +// All test values are defined here +// var testClientID = "AXy9orp-CDaHhBZ9C78QHW2BKZpACgroqo85_NIOa9mIfJ9QnSVKzY-X_rivR_fTUUr6aLjcJsj6sDur" +// var testSecret = "EBoIiUSkCKeSk49hHSgTem1qnjzzJgRQHDEHvGpzlLEf_nIoJd91xu8rPOBDCdR_UYNKVxJE-UgS2iCw" +var testUserID = "https://www.paypal.com/webapps/auth/identity/user/VBqgHcgZwb1PBs69ybjjXfIW86_Hr93aBvF_Rgbh2II" +var testCardID = "CARD-54E6956910402550WKGRL6EA" + +var testProductId = "" // will be fetched in func TestProduct(t *testing.T) +var testBillingPlan = "" // will be fetched in func TestSubscriptionPlans(t *testing.T) + +const alphabet = "abcedfghijklmnopqrstuvwxyz" + +func RandomString(n int) string { + var sb strings.Builder + k := len(alphabet) + + for i := 0; i < n; i++ { + c := alphabet[rand.Intn(k)] + sb.WriteByte(c) + } + return sb.String() +} + +func createRandomProduct(t *testing.T) Product { + //create a product + productData := Product{ + Name: RandomString(10), + Description: RandomString(100), + Category: ProductCategorySoftware, + Type: ProductTypeService, + ImageUrl: "https://example.com/image.png", + HomeUrl: "https://example.com", + } + return productData +} + +// this is a simple copy of the SendWithAuth method, used to +// test the Lock and Unlock methods of the private mutex field +// of Client structure. +func (c *Client) sendWithAuth(req *http.Request, v interface{}) error { + // c.Lock() + c.mu.Lock() + // Note: Here we do not want to `defer c.Unlock()` because we need `c.Send(...)` + // to happen outside of the locked section. + + if c.mu.TryLock() { + // if the code is able to acquire a lock + // despite the mutex of c being locked, throw an error + err := errors.New("TryLock succeeded inside sendWithAuth with mutex locked") + return err + } + + if c.Token != nil { + if !c.tokenExpiresAt.IsZero() && c.tokenExpiresAt.Sub(time.Now()) < RequestNewTokenBeforeExpiresIn { + // c.Token will be updated in GetAccessToken call + if _, err := c.GetAccessToken(req.Context()); err != nil { + // c.Unlock() + c.mu.Unlock() + return err + } + } + + req.Header.Set("Authorization", "Bearer "+c.Token.Token) + } + // Unlock the client mutex before sending the request, this allows multiple requests + // to be in progress at the same time. + // c.Unlock() + c.mu.Unlock() + + if !c.mu.TryLock() { + // if the code is unable to acquire a lock + // despite the mutex of c being unlocked, throw an error + err := errors.New("TryLock failed inside sendWithAuth with mutex unlocked") + return err + } + c.mu.Unlock() // undo changes from the previous TryLock + + return c.Send(req, v) +} + +// this method is used to invoke the sendWithAuth method, which will then check +// operationally the privated mutex field of Client structure. +func (c *Client) createProduct(ctx context.Context, product Product) (*CreateProductResponse, error) { + req, err := c.NewRequest(ctx, http.MethodPost, fmt.Sprintf("%s%s", c.APIBase, "/v1/catalogs/products"), product) + response := &CreateProductResponse{} + if err != nil { + return response, err + } + err = c.sendWithAuth(req, response) + return response, err +} + +func TestClientMutex(t *testing.T) { + c, _ := NewClient(testClientID, testSecret, APIBaseSandBox) + c.GetAccessToken(context.Background()) + + // Basic Testing of the private mutex field + c.mu.Lock() + if c.mu.TryLock() { + t.Fatalf("TryLock succeeded with mutex locked") + } + c.mu.Unlock() + if !c.mu.TryLock() { + t.Fatalf("TryLock failed with mutex unlocked") + } + c.mu.Unlock() // undo changes from the previous TryLock + + // Operational testing of the private mutex field + n_iter := 2 + + errs := make(chan error) + + for i := 0; i < n_iter; i++ { + go func() { + _, err := c.createProduct(context.Background(), createRandomProduct(t)) + errs <- err + }() + } + + for i := 0; i < n_iter; i++ { + err := <-errs + assert.Equal(t, nil, err) + } + +} diff --git a/types.go b/types.go index 0f94934..3e5b0e4 100644 --- a/types.go +++ b/types.go @@ -405,7 +405,8 @@ type ( // Client represents a Paypal REST API Client Client struct { - sync.Mutex + // sync.Mutex + mu sync.Mutex Client *http.Client ClientID string Secret string @@ -495,12 +496,12 @@ type ( // ErrorResponseDetail struct ErrorResponseDetail struct { - Field string `json:"field"` - Issue string `json:"issue"` - Name string `json:"name"` - Message string `json:"message"` - Description string `json:"description"` - Links []Link `json:"link"` + Field string `json:"field"` + Issue string `json:"issue"` + Name string `json:"name"` + Message string `json:"message"` + Description string `json:"description"` + Links []Link `json:"link"` } // ErrorResponse https://developer.paypal.com/docs/api/errors/