Add more GetBidding test and IsError

This commit is contained in:
JBP
2019-08-17 12:18:23 +02:00
parent 22c6e9d1bd
commit 1fb075c056
8 changed files with 193 additions and 60 deletions

View File

@@ -4,4 +4,4 @@ test:
.PHONY: integration .PHONY: integration
integration: integration:
go test -count=1 -v -run "Auction" ./test/integration -integration=true go test -count=1 -v -run "Auction" ./test/integration -integration=true -timeout=999999s

View File

@@ -160,7 +160,7 @@ type LegacyItem struct {
// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItemByLegacyId // eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItemByLegacyId
func (s *BrowseService) GetItemByLegacyID(ctx context.Context, itemLegacyID string, opts ...Opt) (CompactItem, error) { func (s *BrowseService) GetItemByLegacyID(ctx context.Context, itemLegacyID string, opts ...Opt) (CompactItem, error) {
u := fmt.Sprintf("buy/browse/v1/item/get_item_by_legacy_id?legacy_item_id=%s", itemLegacyID) u := fmt.Sprintf("buy/browse/v1/item/get_item_by_legacy_id?legacy_item_id=%s", itemLegacyID)
req, err := s.client.NewRequest(http.MethodGet, u, opts...) req, err := s.client.NewRequest(http.MethodGet, u, nil, opts...)
if err != nil { if err != nil {
return CompactItem{}, err return CompactItem{}, err
} }
@@ -190,7 +190,7 @@ type CompactItem struct {
// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItem // eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItem
func (s *BrowseService) GetCompactItem(ctx context.Context, itemID string, opts ...Opt) (CompactItem, error) { func (s *BrowseService) GetCompactItem(ctx context.Context, itemID string, opts ...Opt) (CompactItem, error) {
u := fmt.Sprintf("buy/browse/v1/item/%s?fieldgroups=COMPACT", itemID) u := fmt.Sprintf("buy/browse/v1/item/%s?fieldgroups=COMPACT", itemID)
req, err := s.client.NewRequest(http.MethodGet, u, opts...) req, err := s.client.NewRequest(http.MethodGet, u, nil, opts...)
if err != nil { if err != nil {
return CompactItem{}, err return CompactItem{}, err
} }
@@ -368,7 +368,7 @@ type Item struct {
// eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItem // eBay API docs: https://developer.ebay.com/api-docs/buy/browse/resources/item/methods/getItem
func (s *BrowseService) GetItem(ctx context.Context, itemID string, opts ...Opt) (Item, error) { func (s *BrowseService) GetItem(ctx context.Context, itemID string, opts ...Opt) (Item, error) {
u := fmt.Sprintf("buy/browse/v1/item/%s?fieldgroups=PRODUCT", itemID) u := fmt.Sprintf("buy/browse/v1/item/%s?fieldgroups=PRODUCT", itemID)
req, err := s.client.NewRequest(http.MethodGet, u, opts...) req, err := s.client.NewRequest(http.MethodGet, u, nil, opts...)
if err != nil { if err != nil {
return Item{}, err return Item{}, err
} }

80
ebay.go
View File

@@ -1,9 +1,11 @@
package ebay package ebay
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
@@ -78,7 +80,7 @@ type Opt func(*http.Request)
// NewRequest creates an API request. // NewRequest creates an API request.
// url should always be specified without a preceding slash. // url should always be specified without a preceding slash.
func (c *Client) NewRequest(method, url string, opts ...Opt) (*http.Request, error) { func (c *Client) NewRequest(method, url string, body interface{}, opts ...Opt) (*http.Request, error) {
if strings.HasPrefix(url, "/") { if strings.HasPrefix(url, "/") {
return nil, errors.New("url should always be specified without a preceding slash") return nil, errors.New("url should always be specified without a preceding slash")
} }
@@ -86,7 +88,16 @@ func (c *Client) NewRequest(method, url string, opts ...Opt) (*http.Request, err
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
req, err := http.NewRequest(method, u.String(), nil) var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(body); err != nil {
return nil, err
}
}
req, err := http.NewRequest(method, u.String(), buf)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@@ -112,30 +123,36 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error
return errors.WithStack(json.NewDecoder(resp.Body).Decode(v)) return errors.WithStack(json.NewDecoder(resp.Body).Decode(v))
} }
// ErrorData reports one or more errors caused by an API request. // Error describes one error caused by an eBay API request.
//
// eBay API docs: https://developer.ebay.com/api-docs/static/handling-error-messages.html
type Error struct {
ErrorID int `json:"errorId,omitempty"`
Domain string `json:"domain,omitempty"`
SubDomain string `json:"subDomain,omitempty"`
Category string `json:"category,omitempty"`
Message string `json:"message,omitempty"`
LongMessage string `json:"longMessage,omitempty"`
InputRefIds []string `json:"inputRefIds,omitempty"`
OuputRefIds []string `json:"outputRefIds,omitempty"`
Parameters []struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
} `json:"parameters,omitempty"`
}
// ErrorData describes one or more errors caused by an eBay API request.
// //
// eBay API docs: https://developer.ebay.com/api-docs/static/handling-error-messages.html // eBay API docs: https://developer.ebay.com/api-docs/static/handling-error-messages.html
type ErrorData struct { type ErrorData struct {
Errors []struct { Errors []Error `json:"errors,omitempty"`
ErrorID int `json:"errorId,omitempty"`
Domain string `json:"domain,omitempty"` response *http.Response
SubDomain string `json:"subDomain,omitempty"` requestDump string
Category string `json:"category,omitempty"`
Message string `json:"message,omitempty"`
LongMessage string `json:"longMessage,omitempty"`
InputRefIds []string `json:"inputRefIds,omitempty"`
OuputRefIds []string `json:"outputRefIds,omitempty"`
Parameters []struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
} `json:"parameters,omitempty"`
} `json:"errors,omitempty"`
Response *http.Response
RequestDump string
} }
func (e *ErrorData) Error() string { func (e *ErrorData) Error() string {
return fmt.Sprintf("%d %s: %+v", e.Response.StatusCode, e.RequestDump, e.Errors) return fmt.Sprintf("%d %s: %+v", e.response.StatusCode, e.requestDump, e.Errors)
} }
// CheckResponse checks the API response for errors, and returns them if present. // CheckResponse checks the API response for errors, and returns them if present.
@@ -144,7 +161,28 @@ func CheckResponse(req *http.Request, resp *http.Response) error {
return nil return nil
} }
dump, _ := httputil.DumpRequest(req, true) dump, _ := httputil.DumpRequest(req, true)
errorData := &ErrorData{Response: resp, RequestDump: string(dump)} errorData := &ErrorData{response: resp, requestDump: string(dump)}
_ = json.NewDecoder(resp.Body).Decode(errorData) _ = json.NewDecoder(resp.Body).Decode(errorData)
return errorData return errorData
} }
// IsError allows to check if err is a specific error codes returned by the eBay API.
//
// eBay API docs: https://developer.ebay.com/devzone/xml/docs/Reference/ebay/Errors/errormessages.htm
func IsError(err error, codes ...int) bool {
if err == nil {
return false
}
errData, ok := err.(*ErrorData)
if !ok {
return false
}
for _, e := range errData.Errors {
for _, code := range codes {
if e.ErrorID == code {
return true
}
}
}
return false
}

View File

@@ -10,15 +10,28 @@ import (
"testing" "testing"
"github.com/jybp/ebay" "github.com/jybp/ebay"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// setup sets up a test HTTP server.
func setup(t *testing.T) (client *ebay.Client, mux *http.ServeMux, teardown func()) {
mux = http.NewServeMux()
server := httptest.NewServer(mux)
var err error
client, err = ebay.NewCustomClient(nil, server.URL+"/")
if err != nil {
t.Fatal(err)
}
return client, mux, server.Close
}
func TestNewRequest(t *testing.T) { func TestNewRequest(t *testing.T) {
testOpt := func(r *http.Request) { testOpt := func(r *http.Request) {
r.URL.RawQuery = "q=1" r.URL.RawQuery = "q=1"
} }
client, _ := ebay.NewCustomClient(nil, "https://api.ebay.com/") client, _ := ebay.NewCustomClient(nil, "https://api.ebay.com/")
r, _ := client.NewRequest(http.MethodPost, "test", testOpt) r, _ := client.NewRequest(http.MethodPost, "test", nil, testOpt)
assert.Equal(t, "https://api.ebay.com/test?q=1", fmt.Sprint(r.URL)) assert.Equal(t, "https://api.ebay.com/test?q=1", fmt.Sprint(r.URL))
assert.Equal(t, http.MethodPost, r.Method) assert.Equal(t, http.MethodPost, r.Method)
} }
@@ -69,14 +82,29 @@ func TestCheckResponse(t *testing.T) {
assert.Equal(t, "2200077988|0", err.Errors[0].Parameters[0].Value) assert.Equal(t, "2200077988|0", err.Errors[0].Parameters[0].Value)
} }
// setup sets up a test HTTP server func TestIsErrorMatches(t *testing.T) {
func setup(t *testing.T) (client *ebay.Client, mux *http.ServeMux, teardown func()) { var err error = &ebay.ErrorData{
mux = http.NewServeMux() Errors: []ebay.Error{
server := httptest.NewServer(mux) ebay.Error{ErrorID: 1},
var err error },
client, err = ebay.NewCustomClient(nil, server.URL+"/")
if err != nil {
t.Fatal(err)
} }
return client, mux, server.Close assert.True(t, ebay.IsError(err, 1, 2, 3))
}
func TestIsErrorNoMatches(t *testing.T) {
var err error = &ebay.ErrorData{
Errors: []ebay.Error{
ebay.Error{ErrorID: 4},
},
}
assert.False(t, ebay.IsError(err, 1, 2, 3))
}
func TestIsErrorWrongType(t *testing.T) {
var err error = errors.New("test")
assert.False(t, ebay.IsError(err, 1, 2, 3))
}
func TestIsErrorNil(t *testing.T) {
assert.False(t, ebay.IsError(nil, 1, 2, 3))
} }

View File

@@ -65,16 +65,80 @@ type Bidding struct {
} `json:"currentProxyBid"` } `json:"currentProxyBid"`
} }
// GetBidding retrieves the buyer's bidding details on an auction. // Some valid eBay error codes for the GetBidding method.
//
// eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/getBidding#h2-error-codes
const (
ErrGetBiddingMarketplaceNotSupported = 120017
ErrGetBiddingNoBiddingActivity = 120033
)
// GetBidding retrieves the buyer's bidding details on a specific auction item.
// //
// eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/getBidding // eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/getBidding
func (s *OfferService) GetBidding(ctx context.Context, itemID, marketplaceID string, opts ...Opt) (Item, error) { func (s *OfferService) GetBidding(ctx context.Context, itemID, marketplaceID string, opts ...Opt) (Item, error) {
u := fmt.Sprintf("buy/offer/v1_beta/bidding/%s", itemID) u := fmt.Sprintf("buy/offer/v1_beta/bidding/%s", itemID)
opts = append(opts, OptBuyMarketplace(marketplaceID)) opts = append(opts, OptBuyMarketplace(marketplaceID))
req, err := s.client.NewRequest(http.MethodGet, u, opts...) req, err := s.client.NewRequest(http.MethodGet, u, nil, opts...)
if err != nil { if err != nil {
return Item{}, err return Item{}, err
} }
var it Item var it Item
return it, s.client.Do(ctx, req, &it) return it, s.client.Do(ctx, req, &it)
} }
// ProxyBid represents an eBay proxy bid.
type ProxyBid struct {
ProxyBidID string `json:"proxyBidId"`
}
// Some valid eBay error codes for the PlaceProxyBid method.
//
// eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/getBidding#h2-error-codes
const (
ErrPlaceProxyBidAuctionEndedBecauseOfBuyItNow = 120002
ErrPlaceProxyBidBidCannotBeGreaterThanBuyItNowPrice = 120005
ErrPlaceProxyBidAmountTooHigh = 120007
ErrPlaceProxyBidAmountTooLow = 120008
ErrPlaceProxyBidCurrencyMustMatchItemPriceCurrency = 120009
ErrPlaceProxyBidCannotLowerYourProxyBid = 120010
ErrPlaceProxyBidAmountExceedsLimit = 120011
ErrPlaceProxyBidAuctionHasEnded = 120012
ErrPlaceProxyBidAmountInvalid = 120013
ErrPlaceProxyBidCurrencyInvalid = 120014
ErrPlaceProxyBidMaximumBidAmountMissing = 120016
)
// PlaceProxyBid places a proxy bid for the buyer on a specific auction item.
//
// You must ensure the user agrees to the "Terms of use for Adult Only category"
// (https://signin.ebay.com/ws/eBayISAPI.dll?AdultSignIn2) if he wishes to bid on on a adult-only item.
//
// eBay API docs: https://developer.ebay.com/api-docs/buy/offer/resources/bidding/methods/getBidding
func (s *OfferService) PlaceProxyBid(ctx context.Context, itemID, marketplaceID, maxAmount, currency string, userConsentAdultOnlyItem bool, opts ...Opt) (ProxyBid, error) {
u := fmt.Sprintf("buy/offer/v1_beta/bidding/%s/place_proxy_bid", itemID)
opts = append(opts, OptBuyMarketplace(marketplaceID))
pl := struct {
MaxAmount struct {
Currency string `json:"currency"`
Value string `json:"value"`
} `json:"maxAmount"`
UserConsent struct {
AdultOnlyItem bool `json:"adultOnlyItem"`
} `json:"userConsent"`
}{
MaxAmount: struct {
Currency string `json:"currency"`
Value string `json:"value"`
}{currency, maxAmount},
UserConsent: struct {
AdultOnlyItem bool `json:"adultOnlyItem"`
}{userConsentAdultOnlyItem},
}
req, err := s.client.NewRequest(http.MethodPost, u, &pl, opts...)
if err != nil {
return ProxyBid{}, err
}
var p ProxyBid
return p, s.client.Do(ctx, req, &p)
}

View File

@@ -1,6 +1,8 @@
package ebay_test package ebay_test
import ( import (
"context"
"fmt"
"net/http" "net/http"
"testing" "testing"
@@ -13,3 +15,17 @@ func TestOptBuyMarketplace(t *testing.T) {
ebay.OptBuyMarketplace("EBAY_US")(r) ebay.OptBuyMarketplace("EBAY_US")(r)
assert.Equal(t, "EBAY_US", r.Header.Get("X-EBAY-C-MARKETPLACE-ID")) assert.Equal(t, "EBAY_US", r.Header.Get("X-EBAY-C-MARKETPLACE-ID"))
} }
func TestGetBidding(t *testing.T) {
client, mux, teardown := setup(t)
defer teardown()
mux.HandleFunc("/buy/offer/v1_beta/bidding/v1|202117468662|0", func(w http.ResponseWriter, r *http.Request) {
marketplaceID := r.Header.Get("X-EBAY-C-MARKETPLACE-ID")
fmt.Fprintf(w, `{"itemId": "%s"}`, marketplaceID)
})
bidding, err := client.Buy.Offer.GetBidding(context.Background(), "v1|202117468662|0", ebay.BuyMarketplaceUSA)
assert.Nil(t, err)
assert.Equal(t, ebay.BuyMarketplaceUSA, bidding.ItemID)
}

View File

@@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -45,8 +44,9 @@ func TestAuction(t *testing.T) {
t.SkipNow() t.SkipNow()
} }
// Manually create an auction in the sandbox and copy/paste the url: // Manually create an auction in the sandbox and copy/paste the url.
const auctionURL = "https://www.sandbox.ebay.com/itm/110439278158" // Auctions can't be created using the rest api (yet?).
const auctionURL = "https://www.sandbox.ebay.com/itm/110440008951"
ctx := context.Background() ctx := context.Background()
@@ -80,7 +80,7 @@ func TestAuction(t *testing.T) {
t.Fatalf("item %s is not an auction. BuyingOptions are: %+v", it.ItemID, it.BuyingOptions) t.Fatalf("item %s is not an auction. BuyingOptions are: %+v", it.ItemID, it.BuyingOptions)
} }
if time.Now().UTC().After(it.ItemEndDate) { if time.Now().UTC().After(it.ItemEndDate) {
// t.Fatalf("item %s end date has been reached. ItemEndDate is: %s", it.ItemID, it.ItemEndDate.String()) t.Fatalf("item %s end date has been reached. ItemEndDate is: %s", it.ItemID, it.ItemEndDate.String())
} }
t.Logf("item %s UniqueBidderCount:%d minimumBidPrice: %+v currentPriceToBid: %+v\n", it.ItemID, it.UniqueBidderCount, it.MinimumPriceToBid, it.CurrentBidPrice) t.Logf("item %s UniqueBidderCount:%d minimumBidPrice: %+v currentPriceToBid: %+v\n", it.ItemID, it.UniqueBidderCount, it.MinimumPriceToBid, it.CurrentBidPrice)
@@ -92,7 +92,6 @@ func TestAuction(t *testing.T) {
} }
state := url.QueryEscape(string(b)) state := url.QueryEscape(string(b))
authCodeC := make(chan string) authCodeC := make(chan string)
var expiresIn time.Duration
http.HandleFunc("/accept", func(rw http.ResponseWriter, r *http.Request) { http.HandleFunc("/accept", func(rw http.ResponseWriter, r *http.Request) {
actualState, err := url.QueryUnescape(r.URL.Query().Get("state")) actualState, err := url.QueryUnescape(r.URL.Query().Get("state"))
if err != nil { if err != nil {
@@ -104,12 +103,6 @@ func TestAuction(t *testing.T) {
return return
} }
code := r.URL.Query().Get("code") code := r.URL.Query().Get("code")
expiresInSeconds, err := strconv.Atoi(r.URL.Query().Get("expires_in"))
if err != nil {
http.Error(rw, fmt.Sprintf("invalid expires_in: %+v", err), http.StatusBadRequest)
return
}
expiresIn = time.Second * time.Duration(expiresInSeconds)
authCodeC <- code authCodeC <- code
t.Logf("The authorization code is %s.\n", code) t.Logf("The authorization code is %s.\n", code)
t.Logf("The authorization code will expire in %s seconds.\n", r.URL.Query().Get("expires_in")) t.Logf("The authorization code will expire in %s seconds.\n", r.URL.Query().Get("expires_in"))
@@ -146,18 +139,13 @@ func TestAuction(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// Force token regen.
// tok.Expiry = time.Now().Add(-time.Hour * 24) // not working?
fmt.Printf("Sleeping %v so token expires\n", expiresIn)
time.Sleep(expiresIn + time.Second*5)
client = ebay.NewSandboxClient(oauth2.NewClient(ctx, tokensource.New(oauthConf.TokenSource(ctx, tok)))) client = ebay.NewSandboxClient(oauth2.NewClient(ctx, tokensource.New(oauthConf.TokenSource(ctx, tok))))
bidding, err := client.Buy.Offer.GetBidding(ctx, it.ItemID, ebay.BuyMarketplaceUSA) _, err = client.Buy.Offer.GetBidding(ctx, it.ItemID, ebay.BuyMarketplaceUSA)
if err != nil { if !ebay.IsError(err, ebay.ErrGetBiddingNoBiddingActivity) {
t.Fatalf("%+v", err) t.Logf("Expected ErrNoBiddingActivity, got %+v.", err)
} }
t.Logf("item %s bidding: %+v\n", it.ItemID, bidding)
// err := client.Buy.Offer.PlaceProxyBid(ctx)
} }

View File

@@ -20,7 +20,6 @@ func New(base oauth2.TokenSource) *TokenSource {
func (ts *TokenSource) Token() (*oauth2.Token, error) { func (ts *TokenSource) Token() (*oauth2.Token, error) {
t, err := ts.base.Token() t, err := ts.base.Token()
if t != nil { if t != nil {
print("new token: " + t.AccessToken + "\n") // TODO remove
t.TokenType = "Bearer" t.TokenType = "Bearer"
} }
return t, err return t, err