Use github actions

This commit is contained in:
Alex Pliutau 2024-05-27 12:36:25 +02:00
parent 5fddf59f71
commit b8f2c8c573
10 changed files with 60 additions and 601 deletions

40
.github/workflows/lint-test.yaml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Lint and Test
on: push
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
- name: Install dependencies
run: go get .
- name: Install linters
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
go install mvdan.cc/unparam@latest
- name: go vet
run: go vet ${{ inputs.path }}
- name: staticcheck
run: staticcheck ${{ inputs.path }}
- name: unparam
run: unparam ${{ inputs.path }}
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.21.x"
- name: Install dependencies
run: go get .
- name: Run Tests
run: go test -v -race ./...

View File

@ -1,9 +0,0 @@
language: go
go:
- 1.13
- 1.14
- 1.15
install:
- export PATH=$PATH:$HOME/gopath/bin
script:
- go test -v -race

View File

@ -1,6 +1,4 @@
[![Go Report Card](https://goreportcard.com/badge/plutov/paypal)](https://goreportcard.com/report/plutov/paypal) [Docs](https://pkg.go.dev/github.com/plutov/paypal)
[![Build Status](https://travis-ci.org/plutov/paypal.svg?branch=master)](https://travis-ci.org/plutov/paypal)
[![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/plutov/paypal)
# Go client for PayPal REST API # Go client for PayPal REST API

View File

@ -13,17 +13,6 @@ import (
"github.com/stretchr/testify/assert" "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" const alphabet = "abcedfghijklmnopqrstuvwxyz"
func RandomString(n int) string { func RandomString(n int) string {
@ -106,17 +95,6 @@ func TestClientMutex(t *testing.T) {
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox) c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken(context.Background()) 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 // Operational testing of the private mutex field
n_iter := 2 n_iter := 2

View File

@ -1,59 +0,0 @@
package paypal_test
import (
"context"
"github.com/plutov/paypal/v4"
)
func Example() {
// Initialize client
c, err := paypal.NewClient("clientID", "secretID", paypal.APIBaseSandBox)
if err != nil {
panic(err)
}
// Retrieve access token
_, err = c.GetAccessToken(context.Background())
if err != nil {
panic(err)
}
}
func ExampleClient_CreatePayout_Venmo() {
// Initialize client
c, err := paypal.NewClient("clientID", "secretID", paypal.APIBaseSandBox)
if err != nil {
panic(err)
}
// Retrieve access token
_, err = c.GetAccessToken(context.Background())
if err != nil {
panic(err)
}
// Set payout item with Venmo wallet
payout := paypal.Payout{
SenderBatchHeader: &paypal.SenderBatchHeader{
SenderBatchID: "Payouts_2018_100007",
EmailSubject: "You have a payout!",
EmailMessage: "You have received a payout! Thanks for using our service!",
},
Items: []paypal.PayoutItem{
{
RecipientType: "EMAIL",
RecipientWallet: paypal.VenmoRecipientWallet,
Receiver: "receiver@example.com",
Amount: &paypal.AmountPayout{
Value: "9.87",
Currency: "USD",
},
Note: "Thanks for your patronage!",
SenderItemID: "201403140001",
},
},
}
c.CreatePayout(context.Background(), payout)
}

View File

@ -33,27 +33,27 @@ type (
} }
) )
func (self *Product) GetUpdatePatch() []Patch { func (p *Product) GetUpdatePatch() []Patch {
return []Patch{ return []Patch{
{ {
Operation: "replace", Operation: "replace",
Path: "/description", Path: "/description",
Value: self.Description, Value: p.Description,
}, },
{ {
Operation: "replace", Operation: "replace",
Path: "/category", Path: "/category",
Value: self.Category, Value: p.Category,
}, },
{ {
Operation: "replace", Operation: "replace",
Path: "/image_url", Path: "/image_url",
Value: self.ImageUrl, Value: p.ImageUrl,
}, },
{ {
Operation: "replace", Operation: "replace",
Path: "/home_url", Path: "/home_url",
Value: self.HomeUrl, Value: p.HomeUrl,
}, },
} }
} }

View File

@ -104,12 +104,12 @@ type (
} }
) )
func (self *Subscription) GetUpdatePatch() []Patch { func (p *Subscription) GetUpdatePatch() []Patch {
result := []Patch{ result := []Patch{
{ {
Operation: "replace", Operation: "replace",
Path: "/billing_info/outstanding_balance", Path: "/billing_info/outstanding_balance",
Value: self.BillingInfo.OutstandingBalance, Value: p.BillingInfo.OutstandingBalance,
}, },
} }
return result return result

View File

@ -82,29 +82,29 @@ type (
} }
) )
func (self *SubscriptionPlan) GetUpdatePatch() []Patch { func (p *SubscriptionPlan) GetUpdatePatch() []Patch {
result := []Patch{ result := []Patch{
{ {
Operation: "replace", Operation: "replace",
Path: "/description", Path: "/description",
Value: self.Description, Value: p.Description,
}, },
} }
if self.Taxes != nil { if p.Taxes != nil {
result = append(result, Patch{ result = append(result, Patch{
Operation: "replace", Operation: "replace",
Path: "/taxes/percentage", Path: "/taxes/percentage",
Value: self.Taxes.Percentage, Value: p.Taxes.Percentage,
}) })
} }
if self.PaymentPreferences != nil { if p.PaymentPreferences != nil {
if self.PaymentPreferences.SetupFee != nil { if p.PaymentPreferences.SetupFee != nil {
result = append(result, Patch{ result = append(result, Patch{
Operation: "replace", Operation: "replace",
Path: "/payment_preferences/setup_fee", Path: "/payment_preferences/setup_fee",
Value: self.PaymentPreferences.SetupFee, Value: p.PaymentPreferences.SetupFee,
}, },
) )
} }
@ -112,17 +112,17 @@ func (self *SubscriptionPlan) GetUpdatePatch() []Patch {
result = append(result, []Patch{{ result = append(result, []Patch{{
Operation: "replace", Operation: "replace",
Path: "/payment_preferences/auto_bill_outstanding", Path: "/payment_preferences/auto_bill_outstanding",
Value: self.PaymentPreferences.AutoBillOutstanding, Value: p.PaymentPreferences.AutoBillOutstanding,
}, },
{ {
Operation: "replace", Operation: "replace",
Path: "/payment_preferences/payment_failure_threshold", Path: "/payment_preferences/payment_failure_threshold",
Value: self.PaymentPreferences.PaymentFailureThreshold, Value: p.PaymentPreferences.PaymentFailureThreshold,
}, },
{ {
Operation: "replace", Operation: "replace",
Path: "/payment_preferences/setup_fee_failure_action", Path: "/payment_preferences/setup_fee_failure_action",
Value: self.PaymentPreferences.SetupFeeFailureAction, Value: p.PaymentPreferences.SetupFeeFailureAction,
}}...) }}...)
} }

View File

@ -1,21 +1,10 @@
package paypal package paypal
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing" "testing"
) )
var testBillingAgreementID = "BillingAgreementID"
type webprofileTestServer struct {
t *testing.T
}
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
c, err := NewClient("", "", "") c, err := NewClient("", "", "")
if err == nil { if err == nil {
@ -482,481 +471,3 @@ func TestTypePaymentPatchMarshal(t *testing.T) {
t.Errorf("PaymentPatch response2 is incorrect,\n Given: %+v\n Expected: %+v", string(response2), string(p2expectedresponse)) t.Errorf("PaymentPatch response2 is incorrect,\n Given: %+v\n Expected: %+v", string(response2), string(p2expectedresponse))
} }
} }
// ServeHTTP implements http.Handler
func (ts *webprofileTestServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ts.t.Log(r.RequestURI)
if r.RequestURI == "/v1/payment-experience/web-profiles" {
if r.Method == "POST" {
ts.create(w, r)
}
if r.Method == "GET" {
ts.list(w, r)
}
}
if r.RequestURI == "/v1/payment-experience/web-profiles/XP-CP6S-W9DY-96H8-MVN2" {
if r.Method == "GET" {
ts.getvalid(w, r)
}
if r.Method == "PUT" {
ts.updatevalid(w, r)
}
if r.Method == "DELETE" {
ts.deletevalid(w, r)
}
}
if r.RequestURI == "/v1/payment-experience/web-profiles/foobar" {
if r.Method == "GET" {
ts.getinvalid(w, r)
}
if r.Method == "PUT" {
ts.updateinvalid(w, r)
}
if r.Method == "DELETE" {
ts.deleteinvalid(w, r)
}
}
if r.RequestURI == "/v1/billing-agreements/agreement-tokens" {
if r.Method == "POST" {
ts.createWithoutName(w, r)
}
}
if r.RequestURI == "/v1/billing-agreements/agreements" {
if r.Method == "POST" {
ts.createWithoutName(w, r)
}
}
if r.RequestURI == fmt.Sprintf("/v1/billing-agreements/agreements/%s/cancel", testBillingAgreementID) {
if r.Method == "POST" {
ts.deletevalid(w, r)
}
}
}
func (ts *webprofileTestServer) create(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
var raw map[string]string
w.Header().Set("Content-Type", "application/json")
if name, ok := data["name"]; !ok || name == "" {
raw = map[string]string{
"name": "VALIDATION_ERROR",
"message": "should have name",
}
w.WriteHeader(http.StatusBadRequest)
} else {
raw = map[string]string{
"id": "XP-CP6S-W9DY-96H8-MVN2",
}
w.WriteHeader(http.StatusCreated)
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) createWithoutName(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
var raw map[string]string
w.Header().Set("Content-Type", "application/json")
raw = map[string]string{
"id": "B-12345678901234567",
}
w.WriteHeader(http.StatusCreated)
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) updatevalid(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = json.Unmarshal(body, &data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
if ID, ok := data["id"]; !ok || ID != "XP-CP6S-W9DY-96H8-MVN2" {
raw := map[string]string{
"name": "INVALID_RESOURCE_ID",
"message": "id invalid",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res, _ := json.Marshal(raw)
w.Write(res)
return
}
if name, ok := data["name"]; !ok || name == "" {
raw := map[string]string{
"name": "VALIDATION_ERROR",
"message": "should have name",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res, _ := json.Marshal(raw)
w.Write(res)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (ts *webprofileTestServer) updateinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) getvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
raw := map[string]interface{}{
"id": "XP-CP6S-W9DY-96H8-MVN2",
"name": "YeowZa! T-Shirt Shop",
"presentation": map[string]interface{}{
"brand_name": "YeowZa! Paypal",
"logo_image": "http://www.yeowza.com",
"locale_code": "US",
},
"input_fields": map[string]interface{}{
"allow_note": true,
"no_shipping": 0,
"address_override": 1,
},
"flow_config": map[string]interface{}{
"landing_page_type": "Billing",
"bank_txn_pending_url": "http://www.yeowza.com",
},
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) getinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) list(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
raw := []interface{}{
map[string]interface{}{
"id": "XP-CP6S-W9DY-96H8-MVN2",
"name": "YeowZa! T-Shirt Shop",
},
map[string]interface{}{
"id": "XP-96H8-MVN2-CP6S-W9DY",
"name": "Shop T-Shirt YeowZa! ",
},
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) deleteinvalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
raw := map[string]interface{}{
"name": "INVALID_RESOURCE_ID",
"message": "foobar not found",
}
res, _ := json.Marshal(raw)
w.Write(res)
}
func (ts *webprofileTestServer) deletevalid(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
}
func TestCreateWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
Name: "YeowZa! T-Shirt Shop",
Presentation: Presentation{
BrandName: "YeowZa! Paypal",
LogoImage: "http://www.yeowza.com",
LocaleCode: "US",
},
InputFields: InputFields{
AllowNote: true,
NoShipping: NoShippingDisplay,
AddressOverride: AddrOverrideFromCall,
},
FlowConfig: FlowConfig{
LandingPageType: LandingPageTypeBilling,
BankTXNPendingURL: "http://www.yeowza.com",
},
}
res, err := c.CreateWebProfile(context.Background(), wp)
if err != nil {
t.Fatal(err)
}
if res.ID != "XP-CP6S-W9DY-96H8-MVN2" {
t.Fatalf("expecting response to have ID = `XP-CP6S-W9DY-96H8-MVN2` got `%s`", res.ID)
}
}
func TestCreateWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{}
_, err := c.CreateWebProfile(context.Background(), wp)
if err == nil {
t.Fatalf("expecting an error got nil")
}
}
func TestGetWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
res, err := c.GetWebProfile(context.Background(), "XP-CP6S-W9DY-96H8-MVN2")
if err != nil {
t.Fatal(err)
}
if res.ID != "XP-CP6S-W9DY-96H8-MVN2" {
t.Fatalf("expecting res.ID to have value = `XP-CP6S-W9DY-96H8-MVN2` but got `%s`", res.ID)
}
if res.Name != "YeowZa! T-Shirt Shop" {
t.Fatalf("expecting res.Name to have value = `YeowZa! T-Shirt Shop` but got `%s`", res.Name)
}
}
func TestGetWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
_, err := c.GetWebProfile(context.Background(), "foobar")
if err == nil {
t.Fatalf("expecting an error got nil")
}
}
func TestGetWebProfiles(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
res, err := c.GetWebProfiles(context.Background())
if err != nil {
t.Fatal(err)
}
if len(res) != 2 {
t.Fatalf("expecting two results got %d", len(res))
}
}
func TestSetWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "XP-CP6S-W9DY-96H8-MVN2",
Name: "Shop T-Shirt YeowZa!",
}
err := c.SetWebProfile(context.Background(), wp)
if err != nil {
t.Fatal(err)
}
}
func TestSetWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "foobar",
}
err := c.SetWebProfile(context.Background(), wp)
if err == nil {
t.Fatal(err)
}
wp = WebProfile{}
err = c.SetWebProfile(context.Background(), wp)
if err == nil {
t.Fatal(err)
}
}
func TestDeleteWebProfile_valid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
wp := WebProfile{
ID: "XP-CP6S-W9DY-96H8-MVN2",
Name: "Shop T-Shirt YeowZa!",
}
err := c.SetWebProfile(context.Background(), wp)
if err != nil {
t.Fatal(err)
}
}
func TestDeleteWebProfile_invalid(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
err := c.DeleteWebProfile(context.Background(), "foobar")
if err == nil {
t.Fatal(err)
}
}
func TestCreateBillingAgreementToken(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
description := "name A"
_, err := c.CreateBillingAgreementToken(
context.Background(),
&description,
&ShippingAddress{RecipientName: "Name", Type: "Type", Line1: "Line1", Line2: "Line2"},
&Payer{PaymentMethod: "paypal"},
&BillingPlan{ID: "id B", Name: "name B", Description: "description B", Type: "type B"})
if err != nil {
t.Fatal(err)
}
}
func TestCreateBillingAgreementFromToken(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
_, err := c.CreateBillingAgreementFromToken(context.Background(), "BillingAgreementToken")
if err != nil {
t.Fatal(err)
}
}
func TestCancelBillingAgreement(t *testing.T) {
ts := httptest.NewServer(&webprofileTestServer{t: t})
defer ts.Close()
c, _ := NewClient("foo", "bar", ts.URL)
err := c.CancelBillingAgreement(context.Background(), testBillingAgreementID)
if err != nil {
t.Fatal(err)
}
}

View File

@ -98,7 +98,7 @@ func (c *Client) VerifyWebhookSignature(ctx context.Context, httpReq *http.Reque
if httpReq.Body != nil { if httpReq.Body != nil {
bodyBytes, _ = io.ReadAll(httpReq.Body) bodyBytes, _ = io.ReadAll(httpReq.Body)
} else { } else {
return nil, errors.New("Cannot verify webhook for HTTP Request with empty body.") return nil, errors.New("cannot verify webhook for HTTP Request with empty body")
} }
// Restore the io.ReadCloser to its original state // Restore the io.ReadCloser to its original state
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))