diff --git a/examples/apis/oauth/create/main.go b/examples/apis/oauth/create/main.go new file mode 100644 index 00000000..981a14bd --- /dev/null +++ b/examples/apis/oauth/create/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/oauth" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := oauth.NewClient(cfg) + authorizationCode := "{{AUTHORIZATION_CODE}}" + redirectURI := "{{REDIRECT_URI}}" + + cred, err := client.Create(context.Background(), authorizationCode, redirectURI) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(cred) +} diff --git a/examples/apis/oauth/getauthorizationurl/main.go b/examples/apis/oauth/getauthorizationurl/main.go new file mode 100644 index 00000000..8d6d77d9 --- /dev/null +++ b/examples/apis/oauth/getauthorizationurl/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/oauth" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := oauth.NewClient(cfg) + + clientID := "{{CLIENT_ID}}" + redirectURI := "{{REDIRECT_URI}}" + state := "state" + + url := client.GetAuthorizationURL(clientID, redirectURI, state) + + fmt.Println(url) +} diff --git a/examples/apis/oauth/refresh/main.go b/examples/apis/oauth/refresh/main.go new file mode 100644 index 00000000..97c18d08 --- /dev/null +++ b/examples/apis/oauth/refresh/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/oauth" +) + +func main() { + accessToken := "{{ACCESS_TOKEN}}" + + cfg, err := config.New(accessToken) + if err != nil { + fmt.Println(err) + return + } + + client := oauth.NewClient(cfg) + authorizationCode := "{{AUTHORIZATION_CODE}}" + redirectURI := "{{REDIRECT_URI}}" + + cred, err := client.Create(context.Background(), authorizationCode, redirectURI) + if err != nil { + fmt.Println(err) + return + } + + refreshToken := cred.RefreshToken + cred, err = client.Refresh(context.Background(), refreshToken) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(cred) +} diff --git a/pkg/oauth/client.go b/pkg/oauth/client.go new file mode 100644 index 00000000..2df773fb --- /dev/null +++ b/pkg/oauth/client.go @@ -0,0 +1,98 @@ +package oauth + +import ( + "context" + "net/url" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/internal/baseclient" +) + +const ( + urlBase = "https://api.mercadopago.com/oauth/token" + urlAuth = "https://auth.mercadopago.com/authorization" +) + +// Client contains the method to interact with the Oauth API. +type Client interface { + + // Create oauth credentials to operate on behalf of a seller + // It is a post request to the endpoint: "https://api.mercadopago.com/oauth/token" + // Reference: https://www.mercadopago.com/developers/en/reference/oauth/_oauth_token/post + Create(ctx context.Context, authorizationCode, redirectURI string) (*Response, error) + + // Get url for oauth authorization. + GetAuthorizationURL(clientID, redirectURI, state string) string + + // Refresh token received when you create credentials. + // It is a post request to the endpoint: "https://api.mercadopago.com/oauth/token" + // Reference: https://www.mercadopago.com/developers/en/reference/oauth/_oauth_token/post + Refresh(ctx context.Context, refreshToken string) (*Response, error) +} + +// client is the implementation of Client. +type client struct { + cfg *config.Config +} + +// NewClient returns a new Oauth API Client. +func NewClient(c *config.Config) Client { + return &client{ + cfg: c, + } +} + +func (c *client) Create(ctx context.Context, authorizationCode, redirectURI string) (*Response, error) { + request := &Request{ + ClientSecret: c.cfg.AccessToken, + Code: authorizationCode, + RedirectURI: redirectURI, + GrantType: "authorization_code", + } + + res, err := baseclient.Post[*Response](ctx, c.cfg, urlBase, request) + if err != nil { + return nil, err + } + + return res, nil +} + +func (c *client) GetAuthorizationURL(clientID, redirectURI, state string) string { + params := map[string]string{ + "client_id": clientID, + "response_type": "code", + "platform_id": "mp", + "redirect_uri": redirectURI, + } + + parsedURL, err := url.Parse(urlAuth) + if err != nil { + return "" + } + + queryParams := url.Values{} + + for k, v := range params { + queryParams.Add(k, v) + } + + parsedURL.RawQuery = queryParams.Encode() + + return parsedURL.String() +} + +func (c *client) Refresh(ctx context.Context, refreshToken string) (*Response, error) { + request := &RefreshTokenRequest{ + ClientSecret: c.cfg.AccessToken, + RefreshToken: refreshToken, + GrantType: "refresh_token", + } + + res, err := baseclient.Post[*Response](ctx, c.cfg, urlBase, request) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/pkg/oauth/client_test.go b/pkg/oauth/client_test.go new file mode 100644 index 00000000..76224ddc --- /dev/null +++ b/pkg/oauth/client_test.go @@ -0,0 +1,228 @@ +package oauth + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "reflect" + "strings" + "testing" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/internal/httpclient" +) + +var ( + oauthResponseJSON, _ = os.Open("../../resources/mocks/oauth/response.json") + oauthResponse, _ = io.ReadAll(oauthResponseJSON) +) + +func TestCreate(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want *Response + wantErr string + }{ + { + name: "should_return_error_when_send_request", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("some error") + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(oauthResponse)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + }, + want: &Response{ + AccessToken: "APP_USR-1223334455", + Scope: "offline_access payments read write", + RefreshToken: "TG-65cf4eed634", + PublicKey: "APP_USR-5b5b91b7", + TokenType: "Bearer", + LiveMode: true, + ExpiresIn: 15552000, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{ + cfg: tt.fields.config, + } + got, err := c.Create(tt.args.ctx, "TG-65cf4eed634", "http://test.com") + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.Create() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.Create() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRefresh(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want *Response + wantErr string + }{ + { + name: "should_return_error_when_send_request", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("some error") + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + }, + want: nil, + wantErr: "transport level error: some error", + }, + { + name: "should_return_response", + fields: fields{ + config: &config.Config{ + Requester: &httpclient.Mock{ + DoMock: func(req *http.Request) (*http.Response, error) { + stringReader := strings.NewReader(string(oauthResponse)) + stringReadCloser := io.NopCloser(stringReader) + return &http.Response{ + Body: stringReadCloser, + }, nil + }, + }, + }, + }, + args: args{ + ctx: context.Background(), + }, + want: &Response{ + AccessToken: "APP_USR-1223334455", + Scope: "offline_access payments read write", + RefreshToken: "TG-65cf4eed634", + PublicKey: "APP_USR-5b5b91b7", + TokenType: "Bearer", + LiveMode: true, + ExpiresIn: 15552000, + }, + wantErr: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{ + cfg: tt.fields.config, + } + got, err := c.Refresh(tt.args.ctx, "TG-65cf4eed634") + gotErr := "" + if err != nil { + gotErr = err.Error() + } + + if gotErr != tt.wantErr { + t.Errorf("client.Refresh() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("client.Refresh() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetAuthorizationURL(t *testing.T) { + type fields struct { + config *config.Config + } + type args struct { + clientID string + redirectURI string + state string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "should_return_authorization_url", + fields: fields{ + config: &config.Config{ + AccessToken: "accessToken", + }, + }, + args: args{ + clientID: "323123123", + redirectURI: "redirectURI", + state: "state", + }, + want: "https://auth.mercadopago.com/authorization?client_id=323123123&platform_id=mp&redirect_uri=redirectURI&response_type=code", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &client{ + cfg: tt.fields.config, + } + got := c.GetAuthorizationURL(tt.args.clientID, tt.args.redirectURI, tt.args.state) + + if got != tt.want { + t.Errorf("client.getAuthorizationURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/oauth/request.go b/pkg/oauth/request.go new file mode 100644 index 00000000..aa0a5ebb --- /dev/null +++ b/pkg/oauth/request.go @@ -0,0 +1,16 @@ +package oauth + +// Request represents credential information to perform a create credential request. +type Request struct { + GrantType string `json:"grant_type,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + Code string `json:"code,omitempty"` + RedirectURI string `json:"redirect_uri,omitempty"` +} + +// RefreshTokenRequest represents credential information to perform a refresh credential request. +type RefreshTokenRequest struct { + GrantType string `json:"grant_type,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` +} diff --git a/pkg/oauth/response.go b/pkg/oauth/response.go new file mode 100644 index 00000000..a86ae257 --- /dev/null +++ b/pkg/oauth/response.go @@ -0,0 +1,12 @@ +package oauth + +// Response represents credential information for an Oauth authorization +type Response struct { + AccessToken string `json:"access_token"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` + PublicKey string `json:"public_key"` + TokenType string `json:"token_type"` + LiveMode bool `json:"live_mode"` + ExpiresIn int64 `json:"expires_in"` +} diff --git a/pkg/user/user_test.go b/pkg/user/client_test.go similarity index 100% rename from pkg/user/user_test.go rename to pkg/user/client_test.go diff --git a/resources/mocks/oauth/response.json b/resources/mocks/oauth/response.json new file mode 100644 index 00000000..01018ae8 --- /dev/null +++ b/resources/mocks/oauth/response.json @@ -0,0 +1,11 @@ +{ + "access_token": "APP_USR-1223334455", + "scope": "offline_access payments read write", + "refresh_token": "TG-65cf4eed634", + "public_key": "APP_USR-5b5b91b7", + "token_type": "Bearer", + "live_mode": true, + "expires_in": 15552000 +} + + diff --git a/test/integration/oauth/oauth_test.go b/test/integration/oauth/oauth_test.go new file mode 100644 index 00000000..a9488b2c --- /dev/null +++ b/test/integration/oauth/oauth_test.go @@ -0,0 +1,64 @@ +package integration + +import ( + "context" + "os" + "testing" + + "github.com/mercadopago/sdk-go/pkg/config" + "github.com/mercadopago/sdk-go/pkg/oauth" +) + +func TestOauth(t *testing.T) { + t.Run("should_create_credentials", func(t *testing.T) { + cfg, err := config.New(os.Getenv("ACCESS_TOKEN")) + if err != nil { + t.Fatal(err) + } + + client := oauth.NewClient(cfg) + authorizationCode := "authorization_code" + redirectURI := "redirect_uri" + + cred, err := client.Create(context.Background(), authorizationCode, redirectURI) + + if cred == nil { + t.Error("credential can't be nil") + } + if err != nil { + t.Errorf(err.Error()) + } + }) + + t.Run("should_refresh_token", func(t *testing.T) { + cfg, err := config.New(os.Getenv("ACCESS_TOKEN")) + if err != nil { + t.Fatal(err) + } + + client := oauth.NewClient(cfg) + + authorizationCode := "authorization_code" + redirectURI := "redirect_uri" + + cred, err := client.Create(context.Background(), authorizationCode, redirectURI) + + if cred == nil { + t.Error("credential can't be nil") + return + } + if err != nil { + t.Errorf(err.Error()) + } + + refreshToken := cred.RefreshToken + cred, err = client.Refresh(context.Background(), refreshToken) + + if cred == nil { + t.Error("credential can't be nil") + } + if err != nil { + t.Errorf(err.Error()) + } + }) +}