Skip to content

Commit

Permalink
Merge pull request #45 from manuelbcd/feature/test-coverage
Browse files Browse the repository at this point in the history
Increased test coverage for openproject.go and work-package.go. Added…
  • Loading branch information
manuelbcd authored Mar 30, 2021
2 parents 733612f + 513b664 commit d9e3f62
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 166 deletions.
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ignore:
- "examples/" # ignore examples folder
166 changes: 0 additions & 166 deletions openproject.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,16 @@ package openproject
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/google/go-querystring/query"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -404,168 +400,6 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
return http.DefaultTransport
}

// CookieAuthTransport is an http.RoundTripper that authenticates all requests
// using cookie-based authentication.
// Note that it is generally preferable to use HTTP BASIC authentication with the REST API.
type CookieAuthTransport struct {
Username string
Password string
AuthURL string

// SessionObject is the authenticated cookie string.s
// It's passed in each call to prove the client is authenticated.
SessionObject []*http.Cookie

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// RoundTrip adds the session object to the request.
func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.SessionObject == nil {
err := t.setSessionObject()
if err != nil {
return nil, errors.Wrap(err, "cookieauth: no session object has been set")
}
}

req2 := cloneRequest(req) // per RoundTripper contract
for _, cookie := range t.SessionObject {
// Don't add an empty value cookie to the request
if cookie.Value != "" {
req2.AddCookie(cookie)
}
}

return t.transport().RoundTrip(req2)
}

// Client returns an *http.Client that makes requests that are authenticated
// using cookie authentication
func (t *CookieAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

// setSessionObject attempts to authenticate the user and set
// the session object (e.g. cookie)
func (t *CookieAuthTransport) setSessionObject() error {
req, err := t.buildAuthRequest()
if err != nil {
return err
}

var authClient = &http.Client{
Timeout: time.Second * 60,
}
resp, err := authClient.Do(req)
if err != nil {
return err
}

t.SessionObject = resp.Cookies()
return nil
}

// getAuthRequest assembles the request to get the authenticated cookie
func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) {
body := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
t.Username,
t.Password,
}

b := new(bytes.Buffer)
json.NewEncoder(b).Encode(body)

req, err := http.NewRequest("POST", t.AuthURL, b)
if err != nil {
return nil, err
}

req.Header.Set("Content-Type", "application/json")
return req, nil
}

func (t *CookieAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// JWTAuthTransport is an http.RoundTripper that authenticates all requests
// using JWT based authentication.
// NOTE: this form of auth should be used by add-ons installed from the Atlassian marketplace.
type JWTAuthTransport struct {
Secret []byte
Issuer string

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// Client JWTAuthTransport
func (t *JWTAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

// transport JWTAuthTransport
func (t *JWTAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// RoundTrip adds the session object to the request.
func (t *JWTAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract
exp := time.Duration(59) * time.Second
qsh := t.createQueryStringHash(req.Method, req2.URL)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": t.Issuer,
"iat": time.Now().Unix(),
"exp": time.Now().Add(exp).Unix(),
"qsh": qsh,
})

jwtStr, err := token.SignedString(t.Secret)
if err != nil {
return nil, errors.Wrap(err, "jwtAuth: error signing JWT")
}

req2.Header.Set("Authorization", fmt.Sprintf("JWT %s", jwtStr))
return t.transport().RoundTrip(req2)
}

// CreateQueryStringHash
func (t *JWTAuthTransport) createQueryStringHash(httpMethod string, openprojURL *url.URL) string {
canonicalRequest := t.canonicalizeRequest(httpMethod, openprojURL)
h := sha256.Sum256([]byte(canonicalRequest))
return hex.EncodeToString(h[:])
}

// CanonicalizeRequest
func (t *JWTAuthTransport) canonicalizeRequest(httpMethod string, openprojURL *url.URL) string {
path := "/" + strings.Replace(strings.Trim(openprojURL.Path, "/"), "&", "&", -1)

var canonicalQueryString []string
for k, v := range openprojURL.Query() {
if k == "jwt" {
continue
}
param := url.QueryEscape(k)
value := url.QueryEscape(strings.Join(v, ""))
canonicalQueryString = append(canonicalQueryString, strings.Replace(strings.Join([]string{param, value}, "="), "+", " ", -1))
}
sort.Strings(canonicalQueryString)
return fmt.Sprintf("%s&%s&%s", strings.ToUpper(httpMethod), path, strings.Join(canonicalQueryString, "&"))
}

// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
func cloneRequest(r *http.Request) *http.Request {
Expand Down
71 changes: 71 additions & 0 deletions work-package_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package openproject

import (
"encoding/json"
"fmt"
"github.com/trivago/tgo/tcontainer"
"io/ioutil"
"net/http"
"reflect"
"testing"
)

Expand Down Expand Up @@ -117,3 +120,71 @@ func TestWorkPackageService_Delete(t *testing.T) {
t.Errorf("Error given: %s", err)
}
}

func TestWorkPackageFields_MarshalJSON_OmitsEmptyFields(t *testing.T) {
i := &WorkPackage{
Description: &WPDescription{
Format: "html",
Raw: "Content example",
HTML: "<html>Content example</html>",
},
Type: "Task",
}

rawdata, err := json.Marshal(i)
if err != nil {
t.Errorf("Expected nil err, received %s", err)
}

// convert json to map and see if unset keys are there
issuef := tcontainer.NewMarshalMap()
err = json.Unmarshal(rawdata, &issuef)
if err != nil {
t.Errorf("Expected nil err, received %s", err)
}

_, err = issuef.Int("description/html")
if err == nil {
t.Error("Expected non nil error, received nil")
}

// verify that the field that should be there, is.
name, err := issuef.String("description/raw")
if err != nil {
t.Errorf("Expected nil err, received %s", err)
}

if name != "Content example" {
t.Errorf("Expected Story, received %s", name)
}

}

func TestWorkPackageCustomFields_MarshalJSON_Success(t *testing.T) {
i := &WorkPackage{
Description: &WPDescription{
Format: "html",
Raw: "Content example",
HTML: "<html>Content example</html>",
},
Custom: tcontainer.MarshalMap{
"customfield_A": "testA",
},
}

bytes, err := json.Marshal(i)
if err != nil {
t.Errorf("Expected nil err, received %s", err)
}

received := new(WorkPackage)
// the order of json might be different. so unmarshal it again and compare objects
err = json.Unmarshal(bytes, received)
if err != nil {
t.Errorf("Expected nil err, received %s", err)
}

if !reflect.DeepEqual(i, received) {
t.Errorf("Received object different from expected. Expected %+v, received %+v", i, received)
}
}

0 comments on commit d9e3f62

Please sign in to comment.