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)
[![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)
[Docs](https://pkg.go.dev/github.com/plutov/paypal)
# Go client for PayPal REST API

View File

@ -13,17 +13,6 @@ import (
"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 {
@ -106,17 +95,6 @@ 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

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{
{
Operation: "replace",
Path: "/description",
Value: self.Description,
Value: p.Description,
},
{
Operation: "replace",
Path: "/category",
Value: self.Category,
Value: p.Category,
},
{
Operation: "replace",
Path: "/image_url",
Value: self.ImageUrl,
Value: p.ImageUrl,
},
{
Operation: "replace",
Path: "/home_url",
Value: self.HomeUrl,
Value: p.HomeUrl,
},
}
}

View File

@ -77,7 +77,7 @@ type (
// https://developer.paypal.com/docs/api/subscriptions/v1/#definition-plan_override
PlanOverride struct {
BillingCycles []BillingCycleOverride `json:"billing_cycles,omitempty"`
BillingCycles []BillingCycleOverride `json:"billing_cycles,omitempty"`
PaymentPreferences *PaymentPreferencesOverride `json:"payment_preferences,omitempty"`
Taxes *TaxesOverride `json:"taxes,omitempty"`
}
@ -104,12 +104,12 @@ type (
}
)
func (self *Subscription) GetUpdatePatch() []Patch {
func (p *Subscription) GetUpdatePatch() []Patch {
result := []Patch{
{
Operation: "replace",
Path: "/billing_info/outstanding_balance",
Value: self.BillingInfo.OutstandingBalance,
Value: p.BillingInfo.OutstandingBalance,
},
}
return result

View File

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

View File

@ -1,21 +1,10 @@
package paypal
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
)
var testBillingAgreementID = "BillingAgreementID"
type webprofileTestServer struct {
t *testing.T
}
func TestNewClient(t *testing.T) {
c, err := NewClient("", "", "")
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))
}
}
// 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 {
bodyBytes, _ = io.ReadAll(httpReq.Body)
} 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
httpReq.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))