diff --git a/.gitignore b/.gitignore index f1c181e..817039a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +.env \ No newline at end of file diff --git a/browse.go b/browse.go index bafd18c..bffd9ca 100644 --- a/browse.go +++ b/browse.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" ) // BrowseService handles communication with the Browse API @@ -11,24 +12,29 @@ import ( // eBay API docs: https://developer.ebay.com/api-docs/buy/browse/overview.html type BrowseService service -// OptContextualLocation adds the header containing contextualLocation. +// OptBrowseContextualLocation adds the header containing contextualLocation. // It is strongly recommended that you use it when submitting Browse API methods. // // eBay API docs: https://developer.ebay.com/api-docs/buy/static/api-browse.html#Headers -func OptContextualLocation(country, zip string) func(*http.Request) { +func OptBrowseContextualLocation(country, zip string) func(*http.Request) { return func(req *http.Request) { - // X-EBAY-C-ENDUSERCTX: contextualLocation=country=US,zip=19406 + v := req.Header.Get(headerEndUserCtx) + if len(v) > 0 { + v += "," + } + v += "contextualLocation=" + url.QueryEscape(fmt.Sprintf("country=%s,zip=%s", country, zip)) + req.Header.Set(headerEndUserCtx, v) } } -// Item represents a eBay item. +// Item represents an eBay item. type Item struct{} // GetItem retrieves the details of a specific item. // // 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) { - u := fmt.Sprintf("item/%s", itemID) + u := fmt.Sprintf("buy/browse/v1/item/%s", itemID) req, err := s.client.NewRequest(http.MethodGet, u, opts...) if err != nil { return Item{}, err diff --git a/browse_test.go b/browse_test.go index 9a5aa5b..6f84edc 100644 --- a/browse_test.go +++ b/browse_test.go @@ -8,8 +8,15 @@ import ( "github.com/stretchr/testify/assert" ) -func TestOptContextualLocation(t *testing.T) { +func TestOptBrowseContextualLocationn(t *testing.T) { r, _ := http.NewRequest("", "", nil) - ebay.OptContextualLocation("US", "19406")(r) - assert.Equal(t, "country%3DUS%2Czip%3D19406", r.Header.Get("X-EBAY-C-ENDUSERCTX")) + ebay.OptBrowseContextualLocation("US", "19406")(r) + assert.Equal(t, "contextualLocation=country%3DUS%2Czip%3D19406", r.Header.Get("X-EBAY-C-ENDUSERCTX")) +} + +func TestOptBrowseContextualLocationExistingHeader(t *testing.T) { + r, _ := http.NewRequest("", "", nil) + r.Header.Set("X-EBAY-C-ENDUSERCTX", "affiliateCampaignId=1") + ebay.OptBrowseContextualLocation("US", "19406")(r) + assert.Equal(t, "affiliateCampaignId=1,contextualLocation=country%3DUS%2Czip%3D19406", r.Header.Get("X-EBAY-C-ENDUSERCTX")) } diff --git a/ebay.go b/ebay.go index 66a83d1..d3e5a95 100644 --- a/ebay.go +++ b/ebay.go @@ -90,7 +90,7 @@ func (c *Client) NewRequest(method, url string, opts ...Opt) (*http.Request, err return req, nil } -// Do sends an API reauest and stores the JSON decoded value into v. +// Do sends an API request and stores the JSON decoded value into v. func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) error { resp, err := c.client.Do(req.WithContext(ctx)) if err != nil { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..62a163a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/jybp/ebay + +go 1.12 + +require ( + github.com/stretchr/testify v1.3.0 + golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dfcd274 --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0= +golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..fa3a07e --- /dev/null +++ b/integration_test.go @@ -0,0 +1,104 @@ +// +build integration + +package ebay_test + +import ( + "context" + _ "github.com/joho/godotenv/autoload" + "github.com/jybp/ebay" + "golang.org/x/oauth2" + "os" + "testing" + "net/http" + "strings" + "fmt" + "bytes" + "time" + "io/ioutil" + "encoding/json" + "net/url" +) + +var client *ebay.Client + +func init() { + clientID := os.Getenv("CLIENT_ID") + clientSecret := os.Getenv("CLIENT_SECRET") + if clientID == "" || clientSecret == "" { + panic("No CLIENT_ID or CLIENT_SECRET. Tests won't run.") + } + c := &http.Client{ + Transport: &oauth2.Transport{ + Source: oauth2.ReuseTokenSource(nil, TokenSource{ + Endpoint: "https://api.sandbox.ebay.com/identity/v1/oauth2/token", + ID: clientID, + Secret: clientSecret, + Scopes: []string{"https://api.ebay.com/oauth/api_scope"}, + Client: http.DefaultClient, + }), + Base: http.DefaultTransport, + }, + } + client = ebay.NewSandboxClient(c) +} + +type TokenSource struct { + Endpoint string + ID string + Secret string + Scopes []string + Client *http.Client +} + +func (ts TokenSource) Token() (*oauth2.Token, error) { + scopes := strings.Join(ts.Scopes, " ") + req, err := http.NewRequest(http.MethodPost, + ts.Endpoint, + strings.NewReader(fmt.Sprintf("grant_type=client_credentials&scope=%s", url.PathEscape(scopes)))) + if err != nil { + return nil, err + } + req.SetBasicAuth(ts.ID, ts.Secret) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := ts.Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if c := resp.StatusCode; c < 200 || c >= 300 { + return nil, fmt.Errorf("%s\nStatus:\n%d", req.URL, resp.StatusCode) + } + token := struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + }{} + if err = json.NewDecoder(bytes.NewReader(body)).Decode(&token); err != nil { + return nil, err + } + t := oauth2.Token{ + AccessToken: token.AccessToken, + TokenType: token.TokenType, + } + if secs := token.ExpiresIn; secs > 0 { + t.Expiry = time.Now().Add(time.Duration(secs) * time.Second) + } + print(t.TokenType) + print("\n") + print(t.AccessToken) + print("\n") + return &t, nil +} + +func TestAuthorization(t *testing.T) { + // TODO user token is reauired + req, err := client.NewRequest("GET", "buy/browse/v1/item_summary/search?q=drone&limit=3") + t.Log(req, err) + into := map[string]string{} + err = client.Do(context.Background(), req, &into) + t.Log(into, err) +}