Skip to content

Commit

Permalink
Refactor client to support mocking in unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
kengio committed Feb 2, 2021
1 parent 74e811c commit a5ebbfe
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 85 deletions.
20 changes: 13 additions & 7 deletions mngapi/barong/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import (
"github.com/openware/pkg/mngapi"
)

type BarongMngAPIV2 struct {
cli *mngapi.Client
}
// Client is barong management api client instance
type Client struct{}

func New(rootAPIUrl, endpointPrefix, jwtIssuer, jwtPrivateKey string) *BarongMngAPIV2 {
cli, _ := mngapi.New(rootAPIUrl, endpointPrefix, jwtIssuer, "RS256", jwtPrivateKey)
var (
mngapiClient mngapi.DefaultClient
)

return &BarongMngAPIV2{
cli: cli,
// New return barong management api client
func New(rootAPIUrl, endpointPrefix, jwtIssuer, jwtAlgo, jwtPrivateKey string) (*Client, error) {
client, err := mngapi.New(rootAPIUrl, endpointPrefix, jwtIssuer, jwtAlgo, jwtPrivateKey)
if err != nil {
return nil, err
}

mngapiClient = client
return &Client{}, nil
}
56 changes: 56 additions & 0 deletions mngapi/barong/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package barong

import (
"testing"

"github.com/openware/pkg/mngapi"
"github.com/stretchr/testify/assert"
)

const (
rootURL = "https://www.openware.com"
barongPrefix = "/api/v2/barong/management/"
jwtIssuer = "applogic"
jwtAlgo = "RS256"
jwtPrivateKey = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlKS0FJQkFBS0NBZ0VBelNSeHpxZkhpVFg1bzl5N0JBdE1NM0lxcmtrSWNMZmhia3FHS0V3VFJXYkMyam5ZCmRDaXQ5SC9BV25zYTdpcnlOQ0hwS1lhUVozMkJ4MVVycnQrVk9kMm1YSDEwZHJ5VUtQcTZDdk1rSnBqYitNTncKTXlmd0dxKzdNdmMrUDBWcXp3dE5oNnplVThubVRzMTY2eWd0SVdzREEydWprc1R3ZHY3bEVFK0xMY1djbC90Ugp2dCtKcWtqeVNDYm1UOWl2bXUyeWh4UGFWbmU1TGxLQ2JnOWVJZEZTWEV1R2JFSnBpVGNhZ0lsSUh3VmJ6VnpSCllCbzlobjRXbHhXSmVUSkNQYmxIN0U0cmtrNUdJUDJqUnlvYmdUb2pTNERHS1hqc1ZwamJtc0l1Vk15OE5qQjUKWTJVcU54MVRaWFcvZTV2Nk9jUk9mOFFXSmxhQW1jNTd3S09La3Y0QXdoZ3QzWnluTXpnaVZaQ0Q2MzFDMmszYgpIUitTeHNneXFocHFZSEhUMThxM1hlVnI3d2lsR1Q1WnBremlIbk9SbjlFRjIxa21jczc2MWNBS3hIU3lENkNwCmdTVERObkc0Sm5sWjVnM1BpSk9YR1IvajgvZzNmZ3YydnAzL05nVm55Q2IrTUhCT0ZSaStXOXM1ZytMRU9XdTEKNmp1b2lWOHpaNlk5UmZFdHhVZjRzZ0ErNFJjUGZxQkJ5U2JPRW9neDJ5dDB3aVVJeUwyTEpZMmpBQUNxTVpSWApVUnB0bEtsVjAzbWZuL0I2aHkzNWR5VmN4ME9WRlpXK01tZUgzaHNHSWQwZE92UWdQaHpHOVBkcTVCUWVoQkp5ClpvdlU4RE54U0p5dnA1N0tqYlJWY2VIeklzUUw0MGZXdW81VlU0bHdnLzZueDdmYWlsdkRKSS9hZXQwQ0F3RUEKQVFLQ0FnQU1YRzdUSWY3L0FKYWJUaGlpeEwrQnRoWm1UQlpMSEhsaitPK2VpLzc1UnBqbEoya29qcTcwdGFIMAprY2hzbzMvV3JsaHJYU1ZrWndhajZUanBuNlZSU0U3VzhlUkxwMDlTTE5GN0NXMmJPY2kvYzU5V0pjanRBcnZICjlXZjF6Z3dDajg3TEp4cDZlQWI5cHBvS2czQTh2RU1CT01JeGZOWjBoU1Z1Vnl5dXhHS01NZU9hR2NRazA2SnQKd0pKT0syTmhkWU0xYW5mVWtBQkRqMHMyc0l4ZWcwdHdMa2phU3lJcTEzd3NWSmxZN1N5NzhpVFhvcDBrZG9LTAo5Z3REbDBpd2lYS1JCYURRZnhEd3VmZlZ1TzdSV1p4NDF6aVpsU1RBanhOa2Z1RGwwVFJpRzRlaytwcVJtWjNGCjFsT0Vja0NnckhpQ2NHRlpUQXNSdVlSeGRpbEtXSSthRU15L2lxUzFja0dPR0NuNUVMSmNpT21HV0hTQTF2eFgKZ1FMaVZ1Nkh1b0ZpcW1Jd3NWeHF5MFczN1ZDUEpVWFRXVmhCOGRXbjJscklyd3U0WWhhZzEwcXJ3cnJIQzFIWAo3a0hyU3JJRGdCSzBNdlQyTFh1TUU2eFcxT2k2N2k2RG05UHNrVXRIanVUNEpWTk94YUdRVUVBQm5YSkd5Y1MzCkRFVUFoR25qRmdpY29vM2JoQXZrdmh4WXNFdUdVRDZiVlZ1UTN0MytnbW0rWWVoSlUxdms4bldLYVdhOG14WEYKQWYrZmJIZ2c4TWxaVnJjOWpQQmZJZDQxdEZlUFpQbXpaaWFHRThnbHJ1d2xzQkhHa2F5WG4xYWtqM3JiV2NscgpLSWlRemJ4UjczTmZVTHNhZ2ZtL3lJWjBQeDRjWHgydG9zYUZiU3lkbGZLVDhaNGNEUUtDQVFFQTVyUVRwT3QzCjQ3VkZZUkw1ZTNaZTc3Rjc1MG9WN0pob3Q5YkZjenJFQTl6cVRFTC9Ua2pGbVE3RGR4aFgwTVo5YVZnMklMY3EKaThOdnpZWWhiMDlwd1ZsUWpzQmtocXA5WjlYWWU5bkNJRHBCMFhQWnRUM2pqbUtFcUYwZC8yT0FHeW16bWhVcwp6ajhnNDRsbFV5K1dvc1p2L2NicmF1RHZaUDBqbWd6cTRWSDlRK21VR2RKZlRDNXpBYldnZ3RodjhyWXd4YUVyCnhxOE9DNVp0V1BWK3BwN1dKV3ZSRWs4WWtTcFZVWjJVV2R0V2VaUk5hNWIxUHBUS3J4VGRxNkUvb3V4d1UwOTMKYWIwWDVlMGs3bnErVyttK1Y5Uks0NEUyL0x1c3JUWUlkRVRuWUpsRVo0aU9EUEJGNit5QlBtMVBKbGtSN3RjQwpNelhsUlNXRllUQjA0d0tDQVFFQTQ2TGNNUlZpdHd5QWkzamY1STd0amc4TmFpREw4dkJ1QzFzRXlMMndKaFNUClVXU0tiWmZIcEVpNlI1UVR2L01sd0RHNUZ4VThUWk1pSHl1RFFMR0pwYnQ3Vk5mZFVpWlozZkJiRkNKK3htaDMKOU1FRWszcWZJZlVHN0d3RmlkUEx4R3Q0OGMrREFNZW5nYnVhNlpMRTdvd21SZW8wK3k3cmRpQ3d3MEY3MWxUSApzbVd1aEhCa1hoTFlpU1lpZ1ZRTVJDR0U3aW5LdUdkQTdLd2dpTWQ4VVhrV3NibDJZRmFmd3JSRlVZVmJRUlJ2ClZTVnVMYVVoYTZCbzFkbGxLZ1lrVFIwOEZRNWhrVisvaGtZNlZoM1BCRzZEZXhJR1FiNlJrQjdDNWprSVhDdlMKUzFvOWF6aENreTFralo4YXpuMUg2Ky9xM25qckl5UXNtcWdoZUpBZFB3S0NBUUJBNG1Tai9aVzZkVUVPREVnZQpjU3hDUGFpYlpEckdVQmNqblVQckpKdjhlaVZyVFd5QWwvYjdGU3ZrVXZSZnczT0NMVTBMNW5nUTF1YWE1eDZBCkw5V09pNUFjbGYrdjRFTms4TC95RlV5RHc5Ni9DZFl4SXpiYzFOaDZnYlh1SGczcGxkRHRoUWNVK3F4RlVsOHQKQmpWWGtuZnM2QVZPQ2ZWS2NlZVJiQkNqVG12c3JjVDVmakZQTzhFY3VmaHExSFNuenBYby8ydFFkZXQ5VnRGcQpNNkZyTzBEL1JWT0gwcmNXSE5IaUltK1cxaGw4R0RtdUNNYncwdWd1VmJBQ2xWZFFleThjUHoxV2Y5ZzQwbm1RCm1QVHc1TXlqNXhFbzZ5Nkw1anlxZW9mbUszcm5zRE9NNnRzSXlJcmh6NktKN0RSV2xMWjJkZ0lvWlFBV2NuY1EKM3BBQkFvSUJBQmQ2Q1dtS2doYk0xRWtPRzFFd0tISFpQWkh2ZGZsRk1LUTlLOTRrS2hHVFY2b3lTMUNJTWMvUQpyRjJMZVFuMzRySFNydnNoZG9tdG5meEcrWTluZ0FHMnR6NkYwTTZUSS91T3VXWDNOTW56cGtONDBLY0JJMzVXCkRmTytKRWdWcnROQUhrWWFGN0d4NWFXc21vcHlWNXNlbXlma3dyZ1JHN21nSDNyVHV4amN2NGUza3VzWHlGSW4KY1d1Ym9qMWlWSzJHSTNhSW10NnZ6M05aUVRXNkZTazE2dEJEaDJEaUxqSGZjN0szcFRTdURkbGpOZHpCUmhRYQpoQlZpQ1Z2dkxEbER4Wm1LVlNld0QwbWkzb3RaSWF1Y1ZqVVFJOU1OKzJjNHRQTVhlTFJBMUx4dXZ4emF2WXIrClNIdU9xQzRabjV4R3J4dG9yeDk5c0pmMnRSVUJEL01DZ2dFQkFMRExRNzg0SDU2QjhlZU5zZE1wUk1mTitxZDQKclFlVTB2NUxUOElxdldTcmpLZTlHS3J4NTNSZ3RPNEtRTkdYc3FoMkROdktobFNDMys0amxCdms2UmxxUFdFYwpOSnRZSEw4UFBwby9hOEphM0F1RlpYL0NLNFFCeHR2SEdodjdPeG9JMG9zd0EzNG5NcVk2QXJaSktuSjEvcDljCmYxQk03TGRZQk1VNmVEeDBsSDVFM2xkM2lXVFN1ZUdWVk5PdzBpNmpoeDl3MUp0LzZwRis5NDJqdDFiRUoyN3YKYVdXT2REQ1g0SVIxMStiRlhhOEZJcEhCbStoTm1FdWRRc2hwN2pId2hCTjNiZnNSeHJXWGUyd1cvYkthdFBqWAo1N0p1bEFQVlN3L3h1TGJZZFZiVGlvdmRsMWxObXFJZEpqYVZma2ZZSzVJUVR1R0pxVHNzdVkvbWNITT0KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K"
)

// Mock client
type MockClient struct {
response []byte
apiError *mngapi.APIError
}

// Mock request function
func (m *MockClient) Request(method string, path string, body interface{}) ([]byte, *mngapi.APIError) {
return m.response, m.apiError
}

func TestCreateNewClient(t *testing.T) {
t.Run("Success creation", func(t *testing.T) {
_, err := New(rootURL, barongPrefix, jwtIssuer, jwtAlgo, jwtPrivateKey)

assert.Nil(t, err)
})

t.Run("JWT issuer unset", func(t *testing.T) {
_, err := New(rootURL, barongPrefix, "", jwtAlgo, jwtPrivateKey)

assert.NotNil(t, err)
assert.EqualError(t, err, "JWT issuer unset")
})

t.Run("Invalid signing algorithm", func(t *testing.T) {
_, err := New(rootURL, barongPrefix, jwtIssuer, "RS999", jwtPrivateKey)

assert.NotNil(t, err)
assert.EqualError(t, err, "Unsupported signing method RS999")
})

t.Run("Invalid private key", func(t *testing.T) {
_, err := New(rootURL, barongPrefix, jwtIssuer, jwtAlgo, "")

assert.NotNil(t, err)
assert.EqualError(t, err, "Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
})
}
74 changes: 49 additions & 25 deletions mngapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"path/filepath"
Expand All @@ -18,15 +17,30 @@ import (
)

const (
// RequestTimeout default value
// RequestTimeout default value to 30 seconds
RequestTimeout = time.Duration(30 * time.Second)

// JWTExpireDuration default value to 1 hour
JWTExpireDuration = time.Hour

//JWTAlgorithm default value to RS256
JWTAlgorithm = "RS256"
)

// Client struct to define common data and function
// HTTPClient interface
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}

// DefaultClient interface
type DefaultClient interface {
Request(method string, path string, body interface{}) ([]byte, *APIError)
}

// Client instance
type Client struct {
rootAPIUrl string
endpointPrefix string
client *http.Client
jwtIssuer string
jwtSigningMethod jwtgo.SigningMethod
jwtPrivateKey *rsa.PrivateKey
Expand All @@ -35,19 +49,23 @@ type Client struct {
// APIError response from management API
type APIError struct {
StatusCode int `json:"code"`
Error string `json:"error",omitempty`
Errors []string `json:"errors",omitempty`
Error string `json:"error,omitempty"`
Errors []string `json:"errors,omitempty"`
}

// New to return ManagementAPIV2 struct
var (
httpClient HTTPClient
)

// New to return Client struct
func New(rootAPIUrl string, endpointPrefix string, jwtIssuer string, jwtAlgo string, jwtPrivateKey string) (*Client, error) {
pk, err := loadPrivateKeyFromString(jwtPrivateKey)
if err != nil {
return nil, err
}

if jwtAlgo == "" {
jwtAlgo = "RS256"
jwtAlgo = JWTAlgorithm
}

sm := jwtgo.GetSigningMethod(jwtAlgo)
Expand All @@ -59,10 +77,12 @@ func New(rootAPIUrl string, endpointPrefix string, jwtIssuer string, jwtAlgo str
return nil, fmt.Errorf("JWT issuer unset")
}

// Create default http client
httpClient = &http.Client{Timeout: RequestTimeout}

return &Client{
rootAPIUrl: rootAPIUrl,
endpointPrefix: endpointPrefix,
client: &http.Client{Timeout: RequestTimeout},
jwtIssuer: jwtIssuer,
jwtSigningMethod: sm,
jwtPrivateKey: pk,
Expand All @@ -73,57 +93,57 @@ func New(rootAPIUrl string, endpointPrefix string, jwtIssuer string, jwtAlgo str
func (m *Client) Request(method string, path string, body interface{}) ([]byte, *APIError) {
// Check for allowed HTTP methods
if !allowedHTTPMethods(method) {
log.Fatalln("Only PUT and POST are allowed")
return nil, &APIError{StatusCode: 500, Error: "HTTP method is not allowed, accept only POST and PUT"}
}

url, err := url.Parse(m.rootAPIUrl)
url.Path = filepath.Join(url.Path, m.endpointPrefix, path)

// Generate jwt multisig
jwt, err := m.generateJWT(convertToStringInterface(body), time.Hour)
// TODO: Add to support JWT with multiple signatures
// Generate JWT
jwt, err := m.generateJWT(convertToStringInterface(body), JWTExpireDuration)
if err != nil {
log.Fatalln(err)
return nil, &APIError{StatusCode: 500, Error: err.Error()}
}

// Convert jwt to json string
jwtstr, err := json.Marshal(jwt)
if err != nil {
log.Fatalln(err)
return nil, &APIError{StatusCode: 500, Error: err.Error()}
}

// Create new HTTP request
req, err := http.NewRequest(method, url.String(), bytes.NewBuffer(jwtstr))
if err != nil {
log.Fatalln("Request", "Can not create new request: "+err.Error())
return nil, &APIError{StatusCode: 500, Error: err.Error()}
}

req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json")

// Call HTTP request
res, err := m.client.Do(req)
res, err := httpClient.Do(req)
if err != nil {
log.Fatalln(err)
return nil, &APIError{StatusCode: 500, Error: err.Error()}
}

defer res.Body.Close()

// Convert response body to []byte
resbody, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalln(err)
return nil, &APIError{StatusCode: 500, Error: err.Error()}
}

// Check for API error
if !(res.StatusCode == 200 || res.StatusCode == 201) {
apiError := APIError{}
apiError.StatusCode = res.StatusCode

if res.StatusCode > 500 {
apiError.Error = res.Status
} else {
_ = json.Unmarshal(resbody, &apiError)
apiError := APIError{
StatusCode: res.StatusCode,
Error: res.Status,
}

_ = json.Unmarshal(resbody, &apiError)

return nil, &apiError
}

Expand Down Expand Up @@ -193,6 +213,10 @@ func loadPrivateKeyFromString(str string) (*rsa.PrivateKey, error) {
}

func allowedHTTPMethods(method string) bool {
if len(method) == 0 {
return false
}

var methods = []string{http.MethodPost, http.MethodPut}

for _, v := range methods {
Expand Down
Loading

0 comments on commit a5ebbfe

Please sign in to comment.