Repository: VictorAvelar/mollie-api-go
Pull request #5 (2019-09-28 11:55:43)

Rework client and tests


Patch:
diff --git a/mollie/common_types.go b/mollie/common_types.go
new file mode 100644
index 0000000..2d7c1cb
--- /dev/null
+++ b/mollie/common_types.go
@@ -0,0 +1,91 @@
+package mollie
+
+import (
+	"strings"
+	"time"
+)
+
+// Amount represents a currency and value pair.
+type Amount struct {
+	Currency string `json:"currency,omitempty"`
+	Value    string `json:"value,omitempty"`
+}
+
+// Address provides a human friendly representation of a geographical space.
+//
+// When providing an address object as parameter to a request, the following conditions must be met:
+//
+// -If any of the fields is provided, all fields have to be provided with exception of the region field.
+// -If only the region field is given, one should provide all the other fields as per the previous condition.
+// -For certain PayPal payments the region field is required.
+type Address struct {
+	StreetAndNumber  string `json:"streetAndNumber,omitempty"`
+	StreetAdditional string `json:"streetAdditional,omitempty"`
+	PostalCode       string `json:"postalCode,omitempty"`
+	City             string `json:"city,omitempty"`
+	Region           string `json:"region,omitempty"`
+	Country          string `json:"country,omitempty"`
+}
+
+// ShortDate is a string representing a date in YYYY-MM-DD format.
+type ShortDate struct {
+	time.Time
+}
+
+// UnmarshalJSON overrides the default unmarshal action
+// for the Date struct, as we need links to be pointers
+// to the time.Time struct.
+func (d *ShortDate) UnmarshalJSON(b []byte) error {
+	s := string(b)
+	s = strings.Trim(s, "\"")
+	t, err := time.Parse("2006-01-02", s)
+	if err != nil {
+		return err
+	}
+	d.Time = t
+	return nil
+}
+
+// Locale represents a country and language in ISO-15897 format.
+type Locale string
+
+// Mollie supported locales
+const (
+	English       Locale = "en_US"
+	Dutch         Locale = "nl_NL"
+	DutchBelgium  Locale = "nl_BE"
+	French        Locale = "fr_FR"
+	FrenchBelgium Locale = "fr_BE"
+	German        Locale = "de_DE"
+	GermanAustria Locale = "de_AT"
+	GermanSwiss   Locale = "de_CH"
+	Spanish       Locale = "es_ES"
+	Catalan       Locale = "ca_ES"
+	Portuguese    Locale = "pt_PT"
+	Italian       Locale = "it_IT"
+	Norwegian     Locale = "nb_NO"
+	Swedish       Locale = "sv_SE"
+	Finish        Locale = "fi_FI"
+	Danish        Locale = "da_DK"
+	Icelandic     Locale = "is_IS"
+	Hungarian     Locale = "hu_HU"
+	Polish        Locale = "pl_PL"
+	Latvian       Locale = "lv_LV"
+	Lithuanian    Locale = "lt_LT"
+)
+
+// PhoneNumber represents a phone number in the E.164 format.
+type PhoneNumber string
+
+// QR code object represents an image of a QR code.
+type QRCode struct {
+	Height int    `json:"height,omitempty"`
+	Width  int    `json:"width,omitempty"`
+	Src    string `json:"src,omitempty"`
+}
+
+// URLs in Mollie are commonly represented as objects with an href and type field.
+type URL struct {
+	Href string `json:"href,omitempty"`
+	Type string `json:"type,omitempty"`
+}
diff --git a/mollie/config.go b/mollie/config.go
new file mode 100644
index 0000000..6250322
--- /dev/null
+++ b/mollie/config.go
@@ -0,0 +1,17 @@
+package mollie
+
+// Config contains information that helps during the setup of a new Mollie client.
+type Config struct {
+	testing bool
+	auth    string
+}
+
+//NewConfig build a Mollie configuration object,
+// it takes t to indicate if our client is meant to create requests for testing
+// and auth to indicate the authentication method we want to use.
+func NewConfig(t bool, auth string) *Config {
+	return &Config{
+		testing: t,
+		auth:    auth,
+	}
+}
diff --git a/mollie/mollie.go b/mollie/mollie.go
index 11263a7..a38a396 100644
--- a/mollie/mollie.go
+++ b/mollie/mollie.go
@@ -1,9 +1,12 @@
 package mollie
 
 import (
-	"context"
+	"bytes"
+	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -12,136 +15,195 @@ import (
 
 // Mollie  constants holding values to initialize the client and create requests.
 const (
-	BaseURL     string = "https://api.mollie.com"
-	APIVersion  string = "v2"
-	AuthHeader  string = "authorization"
-	TokenType   string = "Bearer"
-	APITokenEnv string = "MOLLIE_API_TOKEN"
-	OrgTokenEnv string = "MOLLIE_ORG_TOKEN"
+	BaseURLV2          string = "https://api.mollie.com/v2/"
+	AuthHeader         string = "authorization"
+	TokenType          string = "Bearer"
+	APITokenEnv        string = "MOLLIE_API_TOKEN"
+	OrgTokenEnv        string = "MOLLIE_ORG_TOKEN"
+	RequestContentType string = "application/json"
 )
 
 var (
 	errEmptyAPIKey = errors.New("you must provide a non-empty API key")
+	errBadBaseURL  = errors.New("malformed base url, it must contain a trailing slash")
 )
 
-type httpClient interface {
-	Do(req *http.Request) (res *http.Response, err error)
+// Client manages communication with Mollie's API.
+type Client struct {
+	BaseURL        *url.URL
+	authentication string
+	client         *http.Client
+	common         service // Reuse a single struct instead of allocating one for each service on the heap.
+	config         *Config
 }
 
-//APIClient for Mollie API plus information related to the
-// different authentication methods provided by the API.
-type APIClient struct {
-	Ctx               context.Context
-	HTTPClient        httpClient
-	BaseURL           *url.URL
-	APIKey            string
-	OrganizationToken string
+type service struct {
+	client *Client
 }
 
-//WithAPIKey offers a convenient setter with some base validation to attach
-//an API key to an APIClient.
+// WithAuthenticationValue offers a convenient setter for any of the valid authentication
+// tokens provided by Mollie.
 //
-//Ideally your API key will be provided from and environment variable or
-//a secret management engine.
-func (c *APIClient) WithAPIKey(k string) error {
+// Ideally your API key will be provided from and environment variable or
+// a secret management engine.
+// This should only be used when environment variables are "impossible" to be used.
+func (c *Client) WithAuthenticationValue(k string) error {
 	if k == "" {
 		return errEmptyAPIKey
 	}
 
-	c.APIKey = strings.TrimSpace(k)
+	c.authentication = strings.TrimSpace(k)
 
 	return nil
 }
 
-//WithOrganizationToken offers a convenient token with some base validation to
-//attach a Mollie Organization Token to an APIClient.
+// NewAPIRequest is a wrapper around the http.NewRequest function.
 //
-//Ideally your API key will be provided from and environment variable or
-//a secret management engine.
-func (c *APIClient) WithOrganizationToken(t string) error {
-	if t == "" {
-		return errEmptyAPIKey
+// It will setup the authentication headers/parameters according to the client config.
+func (c *Client) NewAPIRequest(method string, uri string, body interface{}) (req *http.Request, err error) {
+	if !strings.HasSuffix(c.BaseURL.Path, "/") {
+		return nil, errBadBaseURL
 	}
-	c.OrganizationToken = strings.TrimSpace(t)
-	return nil
-}
 
-//NewAPIRequest is a wrapper around the http.NewRequest function.
-//It takes the same parameters plus a flag to indicate if the request needs
-//to have the authorization headers.
-//
-//For setting up the headers it takes a hierarchical approach, this meaning that
-//if set the APIClient.OrganizationToken will be used, if this value is empty then
-//it will attempt to use the APIClient.APIKey, and if this value is also empty it
-//will return an error.
-func (c *APIClient) NewAPIRequest(m string, uri string, body io.Reader, auth bool) (req *http.Request, err error) {
-	uri = strings.Trim(uri, "/")
-	uri = strings.Join([]string{
-		APIVersion,
-		uri,
-	}, "/")
-	rel, _ := url.Parse(uri)
-
-	uri = c.BaseURL.ResolveReference(rel).String()
-	req, err = http.NewRequest(m, uri, body)
+	u, err := c.BaseURL.Parse(uri)
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	req.Header.Add("accept", "application/json")
+	if c.config.testing == true {
+		u.Query().Add("testmode", "true")
+	}
 
-	if auth {
-		var parts = []string{TokenType}
-		if c.OrganizationToken != "" {
-			parts = append(parts, c.OrganizationToken)
-		} else if c.APIKey != "" {
-			parts = append(parts, c.APIKey)
-		} else {
-			return nil, errEmptyAPIKey
+	var buf io.ReadWriter
+	if body != nil {
+		buf = new(bytes.Buffer)
+		enc := json.NewEncoder(buf)
+		enc.SetEscapeHTML(false)
+		err := enc.Encode(body)
+		if err != nil {
+			return nil, err
 		}
+	}
+
+	req, err = http.NewRequest(method, u.String(), buf)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Add(AuthHeader, strings.Join([]string{TokenType, c.authentication}, " "))
 
-		v := strings.Join(parts, " ")
-		req.Header.Add(AuthHeader, v)
+	if body != nil {
+		req.Header.Set("Content-Type", RequestContentType)
 	}
+	req.Header.Set("Accept", RequestContentType)
+
 	return
 }
 
-//NewClient returns a fully qualified Mollie HTTP API client with context.
-//It receives a context, a httpClient and a uri to initialize the client
-//but also accepts nil in some cases.
-//
-//If nil is passed to context, context.Background will be initialized.
-//if nil is passed as httpClient then the http.DefaultClient will be initialized.
-//The uri will be parsed with url.Parse function.
+// Do sends an API request and returns the API response or returned as an
+// error if an API error has occurred.
+func (c *Client) Do(req *http.Request) (*Response, error) {
+	resp, err := c.client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	response := newResponse(resp)
+	err = CheckResponse(resp)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, nil
+}
+
+// NewClient returns a new Mollie HTTP API client.
+// You can pass a previously build http client, if none is provided then
+// http.DefaultClient will be used.
 //
-//By default NewClient will lookup the environment for values to assign to the
-//API token (`MOLLIE_API_TOKEN`) and the Organization token (`MOLLIE_ORG_TOKEN`).
+// NewClient will lookup the environment for values to assign to the
+// API token (`MOLLIE_API_TOKEN`) and the Organization token (`MOLLIE_ORG_TOKEN`)
+// according to the provided Config object.
 //
-//You can also set the token values programmatically by using the APIClient
-//WithAPIKey and WithOrganizationKey functions.
-func NewClient(ctx context.Context, baseClient httpClient, uri string) (mollie *APIClient, err error) {
-	if ctx == nil {
-		ctx = context.Background()
-	}
+// You can also set the token values programmatically by using the Client
+// WithAPIKey and WithOrganizationKey functions.
+func NewClient(baseClient *http.Client, c *Config) (mollie *Client, err error) {
 	if baseClient == nil {
 		baseClient = http.DefaultClient
 	}
 
-	u, err := url.Parse(uri)
-	if err != nil {
-		return
-	}
+	u, _ := url.Parse(BaseURLV2)
 
-	mollie = &APIClient{
-		Ctx:        ctx,
-		HTTPClient: baseClient,
-		BaseURL:    u,
+	mollie = &Client{
+		BaseURL: u,
+		client:  baseClient,
+		config:  c,
 	}
+
+	mollie.common.client = mollie
+
+	// Parse authorization from environment
 	if tkn, ok := os.LookupEnv(APITokenEnv); ok {
-		mollie.APIKey = tkn
-	}
-	if orgTkn, ok := os.LookupEnv(OrgTokenEnv); ok {
-		mollie.OrganizationToken = orgTkn
+		mollie.authentication = tkn
 	}
 	return
 }
+
+/*
+Error reports details on a failed API request.
+The success or failure of each HTTP request is shown in the status field of the HTTP response header,
+which contains standard HTTP status codes:
+- a 2xx code for success
+- a 4xx or 5xx code for failure
+*/
+type Error struct {
+	Code     int            `json:"code"`
+	Message  string         `json:"message"`
+	Response *http.Response `json:"response"` // the full response that produced the error
+}
+
+// Error functions implement the Error interface on the zuora.Error struct.
+func (e *Error) Error() string {
+	return fmt.Sprintf("response failed with status %v", e.Message)
+}
+
+/*
+Constructor for Error
+*/
+func newError(r *http.Response) *Error {
+	var e Error
+	e.Response = r
+	e.Code = r.StatusCode
+	e.Message = r.Status
+	return &e
+}
+
+// Response is a Mollie API response. This wraps the standard http.Response
+// returned from Mollie and provides convenient access to things like
+// pagination links.
+type Response struct {
+	*http.Response
+	content []byte
+}
+
+func newResponse(r *http.Response) *Response {
+	var res Response
+	if c, err := ioutil.ReadAll(r.Body); err == nil {
+		res.content = c
+	}
+	json.NewDecoder(r.Body).Decode(&res)
+	res.Response = r
+	return &res
+}
+
+// CheckResponse checks the API response for errors, and returns them if
+// present. A response is considered an error if it has a status code outside
+// the 200 range.
+// API error responses are expected to have either no response
+// body, or a JSON response body.
+func CheckResponse(r *http.Response) error {
+	if r.StatusCode >= http.StatusMultipleChoices {
+		return newError(r)
+	}
+	return nil
+}
diff --git a/mollie/types.go b/mollie/types.go
deleted file mode 100644
index 67476fc..0000000
--- a/mollie/types.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package mollie
-
-import (
-	"net/url"
-	"strings"
-	"time"
-)
-
-//Locale is a string representing the country and language
-//in ISO 15897 format.
-type Locale string
-
-//PhoneNumber is a string in the E.164 format.
-//For example, `+31208202070`.
-type PhoneNumber string
-
-//Amount contains a currency code compliant to the ISO 4217
-//specification and a value with the exact amount in the
-//given currency.
-type Amount struct {
-	Currency string `json:"currency"`
-	Value    string `json:"value"`
-}
-
-//Address contains information to provide a means of physically
-//locating a place in the urban geographical space.
-//
-//For Mollie the country must follow an ISO 3166-1 alpha-2 format,
-//postal codes must belong to the specified city and country.
-//When providing an address object as parameter to a request,
-//the following conditions must be met:
-//
-//	- If any of the fields is provided, all fields have to be provided
-//	  with exception of the region field.
-//	- If only the region field is given, one should provide all the other
-//	  fields as per the previous condition.
-//	- For certain PayPal payments the region field is required.
-type Address struct {
-	StreetAndNumber  string `json:"streetAndNumber"`
-	PostalCode       string `json:"postalCode"`
-	City             string `json:"city"`
-	Country          string `json:"country"`
-	Region           string `json:"region,omitempty"`
-	StreetAdditional string `json:"streetAdditional,omitempty"`
-}
-
-//Date is represented on `YYYY-MM-DD` format for Mollie requests,
-//in order to comply with this the time.Time struct is embedded
-//on Date and custom marshal and unmarshal will be provided.
-type Date struct {
-	time.Time
-}
-
-//UnmarshalJSON overrides the default unmarshal action
-//for the Date struct, as we need links to be pointers
-//to the time.Time struct.
-func (d *Date) UnmarshalJSON(b []byte) error {
-	s := string(b)
-	s = strings.Trim(s, "\"")
-	t, err := time.Parse("2006-01-02", s)
-	if err != nil {
-		return err
-	}
-	d.Time = t
-	return nil
-}
-
-//QRCode represents an image of a QR code.
-type QRCode struct {
-	Height int `json:"height"`
-	Width  int `json:"width"`
-	Source int `json:"src"`
-}
-
-//Link for Mollie are commonly represented as objects
-//with an href and type field.
-type Link struct {
-	Href        HalURL `json:"href"`
-	ContentType string `json:"type"`
-}
-
-//HalURL has url.URL embedded to modify the default json
-//marshal behaviour.
-type HalURL struct {
-	*url.URL
-}
-
-//UnmarshalJSON overrides the default unmarshal action
-//for the HalURL struct, as we need links to be pointers
-//to the url.URL struct.
-func (hl *HalURL) UnmarshalJSON(b []byte) error {
-	s := string(b)
-	s = strings.Trim(s, "\"")
-	uri, err := url.Parse(s)
-	if err != nil {
-		return err
-	}
-	hl.URL = uri
-	return nil
-}
-
-//HAL is a map of Links that contains relevant resources
-//according to the response.
-type HAL map[string]Link
-
-//ResponseErr describes the content of the response body
-//when the http call to Mollie API is not successful.
-type ResponseErr struct {
-	Status int    `json:"status"`
-	Title  string `json:"title"`
-	Detail string `json:"detail"`
-	Field  string `json:"field,omitempty"`
-	Links  HAL    `json:"_links"`
-}


Test patch:
diff --git a/mollie/common_types_test.go b/mollie/common_types_test.go
new file mode 100644
index 0000000..52fe7e0
--- /dev/null
+++ b/mollie/common_types_test.go
@@ -0,0 +1,35 @@
+package mollie
+
+import (
+	"testing"
+)
+
+func TestShortDate_UnmarshalJSON(t *testing.T) {
+	type args struct {
+		b []byte
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			"unmarshal fails with invalid date format",
+			args{b: []byte("30-12-1991")},
+			true,
+		},
+		{
+			"unmarshal is successful",
+			args{b: []byte("1991-12-30")},
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := &ShortDate{}
+			if err := d.UnmarshalJSON(tt.args.b); (err != nil) != tt.wantErr {
+				t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/mollie/config_test.go b/mollie/config_test.go
new file mode 100644
index 0000000..146472c
--- /dev/null
+++ b/mollie/config_test.go
@@ -0,0 +1,70 @@
+package mollie
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestNewConfig(t *testing.T) {
+	type args struct {
+		t    bool
+		auth string
+	}
+	tests := []struct {
+		name string
+		args args
+		want *Config
+	}{
+		{
+			"config set to testing and with API token",
+			args{
+				t:    true,
+				auth: APITokenEnv,
+			},
+			&Config{
+				testing: true,
+				auth:    "MOLLIE_API_TOKEN",
+			},
+		},
+		{
+			"config set to testing and with ORG token",
+			args{
+				t:    true,
+				auth: OrgTokenEnv,
+			},
+			&Config{
+				testing: true,
+				auth:    "MOLLIE_ORG_TOKEN",
+			},
+		},
+		{
+			"config set to production and with ORG token",
+			args{
+				t:    true,
+				auth: OrgTokenEnv,
+			},
+			&Config{
+				testing: true,
+				auth:    "MOLLIE_ORG_TOKEN",
+			},
+		},
+		{
+			"config set to production and with API token",
+			args{
+				t:    true,
+				auth: APITokenEnv,
+			},
+			&Config{
+				testing: true,
+				auth:    "MOLLIE_API_TOKEN",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewConfig(tt.args.t, tt.args.auth); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("NewConfig() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/mollie/mollie_test.go b/mollie/mollie_test.go
index 7639fa1..33f988e 100644
--- a/mollie/mollie_test.go
+++ b/mollie/mollie_test.go
@@ -1,310 +1,349 @@
 package mollie
 
 import (
-	"context"
-	"errors"
+	"bytes"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"os"
 	"reflect"
+	"strings"
 	"testing"
+	"time"
 )
 
-// ---------- testing utilities ----------
-var (
-	testMux    *http.ServeMux
-	testClient *APIClient
-	testServer *httptest.Server
-)
-
-func setup() {
-	testMux = http.NewServeMux()
-	testServer = httptest.NewServer(testMux)
-	testClient, _ = NewClient(nil, nil, testServer.URL)
-}
-
-func teardown() {
-	testServer.Close()
-}
-
-func setEnv() {
-	_ = os.Setenv(APITokenEnv, "token_X12b31ggg23")
-	_ = os.Setenv(OrgTokenEnv, "ey1923n23123n1k3b123jv12g312h31v32g13")
-}
-
-func unsetEnv() {
-	_ = os.Unsetenv(APITokenEnv)
-	_ = os.Unsetenv(OrgTokenEnv)
-}
-
-// ---------- .testing utilities ----------
-
 func TestNewClient(t *testing.T) {
-
-	type args struct {
-		ctx context.Context
-		cl  httpClient
-		uri string
-	}
-
-	u, err := url.Parse("http://localhost")
-	if err != nil {
-		t.Fail()
-	}
-	want := &APIClient{
-		Ctx:               context.Background(),
-		HTTPClient:        http.DefaultClient,
-		BaseURL:           u,
-		APIKey:            "",
-		OrganizationToken: "",
+	var c = http.DefaultClient
+	{
+		c.Timeout = 25 * time.Second
 	}
 
 	tests := []struct {
-		name      string
-		check     *APIClient
-		arguments args
-		wantErr   bool
-		err       error
+		name   string
+		client *http.Client
 	}{
 		{
-			name:  "test client is build properly",
-			check: want,
-			arguments: args{
-				ctx: nil,
-				cl:  nil,
-				uri: "http://localhost",
-			},
-			wantErr: false,
-			err:     nil,
+			"nil returns a valid client",
+			nil,
 		},
 		{
-			name:  "test url parser breaks",
-			check: want,
-			arguments: args{
-				ctx: nil,
-				cl:  nil,
-				uri: " http:/:/localhost",
-			},
-			wantErr: true,
-			err:     errors.New("parse  http:/:/localhost: first path segment in URL cannot contain colon"),
+			"a passed client is decorated",
+			c,
 		},
 	}
 
+	conf := NewConfig(true, APITokenEnv)
+
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := NewClient(tt.arguments.ctx, tt.arguments.cl, tt.arguments.uri)
-			if tt.wantErr {
-				if err.Error() != tt.err.Error() {
-					t.Fatalf("failed while initializing the client: got: %v, want: %v", err, tt.err)
-				}
-			} else {
-				if !reflect.DeepEqual(got, tt.check) {
-					t.Fatalf("failed while initializing the client: got: %v, want: %v", got, tt.check)
-				}
+			_, err := NewClient(tt.client, conf)
+			if err != nil {
+				t.Errorf("not nil error received: %v", err)
 			}
 		})
 	}
 }
 
-func TestNewClient_WithEnvVars(t *testing.T) {
+func TestNewClientWithEnvVars(t *testing.T) {
 	setEnv()
 	defer unsetEnv()
-	u, err := url.Parse("http://localhost:3000")
-	if err != nil {
-		t.Fail()
-	}
-	want := &APIClient{
-		Ctx:               context.Background(),
-		HTTPClient:        http.DefaultClient,
-		BaseURL:           u,
-		APIKey:            "token_X12b31ggg23",
-		OrganizationToken: "ey1923n23123n1k3b123jv12g312h31v32g13",
-	}
-	got, _ := NewClient(nil, nil, "http://localhost:3000")
 
-	if !reflect.DeepEqual(got, want) {
-		t.Fatalf("client initialization error, got: %v, want %v", got, want)
+	var c = http.DefaultClient
+	{
+		c.Timeout = 25 * time.Second
 	}
-}
 
-func TestWithAPIToken(t *testing.T) {
-	setup()
-	defer teardown()
 	tests := []struct {
-		name       string
-		check      string
-		wantErr    bool
-		err        error
-		afterTest  func()
-		beforeTest func()
+		name   string
+		client *http.Client
 	}{
 		{
-			name:    "test an empty string returns an error",
-			check:   "",
-			wantErr: true,
-			err:     errEmptyAPIKey,
+			"nil returns a valid client",
+			nil,
 		},
 		{
-			name:    "test a valid test token is set",
-			check:   "test_$DJAO@A##MKao23u#N",
-			wantErr: false,
-			err:     nil,
+			"a passed client is decorated",
+			c,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got := testClient.WithAPIKey(tt.check)
-			if tt.wantErr {
-				if !reflect.DeepEqual(got, tt.err) {
-					t.Fatalf("failed while attaching API key, got: %v, want: %v", got, tt.err)
-				}
-			} else {
-				if testClient.APIKey != tt.check {
-					t.Fail()
-				}
+			got, err := NewClient(tt.client, tConf)
+			if err != nil {
+				t.Errorf("not nil error received: %v", err)
+			}
+
+			if got.authentication == "" {
+				t.Errorf("got empty api key %v, value %s expected", got.authentication, "token_X12b31ggg23")
 			}
 		})
 	}
 }
 
-func TestWithOrganizationToken(t *testing.T) {
+func TestClient_NewAPIRequest(t *testing.T) {
 	setup()
 	defer teardown()
-	tests := []struct {
-		name       string
-		check      string
-		wantErr    bool
-		err        error
-		afterTest  func()
-		beforeTest func()
-	}{
-		{
-			name:    "test an empty string returns an error",
-			check:   "",
-			wantErr: true,
-			err:     errEmptyAPIKey,
-		},
-		{
-			name:    "test a valid test token is set",
-			check:   "styduasjkdlmqwhdbw",
-			wantErr: false,
-			err:     nil,
-		},
+	b := []string{"hello", "bye"}
+	inURL, outURL := "test", tServer.URL+"/test"
+	inBody, outBody := b, `["hello","bye"]`+"\n"
+	_ = tClient.WithAuthenticationValue("test_token")
+	req, _ := tClient.NewAPIRequest("GET", inURL, inBody)
+
+	testHeader(t, req, "Accept", RequestContentType)
+	testHeader(t, req, AuthHeader, "Bearer test_token")
+	// test that relative URL was expanded
+	if got, want := req.URL.String(), outURL; got != want {
+		t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want)
 	}
 
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := testClient.WithOrganizationToken(tt.check)
-			if tt.wantErr {
-				if !reflect.DeepEqual(got, tt.err) {
-					t.Fatalf("failed while attaching API key, got: %v, want: %v", got, tt.err)
-				}
-			} else {
-				if testClient.OrganizationToken != tt.check {
-					t.Fail()
-				}
-			}
-		})
+	// test that body was JSON encoded
+	body, _ := ioutil.ReadAll(req.Body)
+	if got, want := string(body), outBody; got != want {
+		t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want)
 	}
 }
 
-func TestNewAPIRequest_WellFormedRequest(t *testing.T) {
+func TestClient_NewAPIRequest_ErrTrailingSlash(t *testing.T) {
+	uri, _ := url.Parse("http://localhost")
+	tClient = &Client{
+		BaseURL: uri,
+	}
+	_, err := tClient.NewAPIRequest("GET", "test", nil)
+
+	if !reflect.DeepEqual(err, errBadBaseURL) {
+		t.Errorf("expected error %v not ocurred, got %v", errBadBaseURL, err)
+	}
+}
+
+func TestClient_NewAPIRequest_HTTPReqNativeError(t *testing.T) {
 	setup()
 	defer teardown()
+	_, err := tClient.NewAPIRequest("\\\\\\", "test", nil)
 
-	inURI, outURI := "/testing", testServer.URL+"/v2/testing"
-	inMethod, outMethod := "", http.MethodGet
-
-	got, err := testClient.NewAPIRequest(inMethod, inURI, nil, false)
-	if err != nil {
-		t.Fatalf("failed while building the API Request: %v", err)
+	if err == nil {
+		t.Fatal("nil error produced")
 	}
 
-	if got.URL.String() != outURI {
-		t.Fatalf("mailformed request uri: want: %v, got: %v", outURI, got.URL)
+	if !strings.Contains(err.Error(), "invalid method") {
+		t.Errorf("unexpected err received %v", err)
 	}
+}
 
-	if got.Method != outMethod {
-		t.Fatalf("method assignment failed: got: %v, want: %v", got.Method, outMethod)
-	}
+func TestClient_NewAPIRequest_OrgTokenOverApiKey(t *testing.T) {
+	setup()
+	defer teardown()
+	_ = tClient.WithAuthenticationValue("org_token")
+	req, _ := tClient.NewAPIRequest("GET", "test", nil)
+
+	testHeader(t, req, AuthHeader, "Bearer org_token")
 }
 
-func TestNewAPIRequest_WithAPIKeyAuthHeader(t *testing.T) {
+func TestClient_WithAuthenticationValue_Error(t *testing.T) {
 	setup()
 	defer teardown()
+	err := tClient.WithAuthenticationValue("")
 
-	var testKey = "test_demoAPIKey1234"
+	if !reflect.DeepEqual(err, errEmptyAPIKey) {
+		t.Errorf("unexpected error, want %v and got %v", errEmptyAPIKey, err)
+	}
+}
 
-	_ = testClient.WithAPIKey(testKey)
+func TestClient_NewAPIRequest_ErrorBodySerialization(t *testing.T) {
+	setup()
+	defer teardown()
+	b := make(chan int)
+	_, err := tClient.NewAPIRequest("GET", "test", b)
 
-	req, err := testClient.NewAPIRequest("", "/testing", nil, true)
-	if err != nil {
-		t.Fatalf("failed while building the API Request: %v", err)
+	if err == nil {
+		t.Fatal("nil error produced")
 	}
-	got := req.Header.Get(AuthHeader)
-	want := "Bearer " + testKey
 
-	if got != want {
-		t.Fatalf("API Key header is not set: got %v, want %v", got, want)
+	if !strings.Contains(err.Error(), "unsupported type") {
+		t.Errorf("unexpected err received %v", err)
 	}
 }
 
-func TestNewAPIRequest_WithOrgTokenAuthHeader(t *testing.T) {
+func TestClient_NewAPIRequest_NativeURLParseError(t *testing.T) {
 	setup()
 	defer teardown()
+	_, err := tClient.NewAPIRequest("GET", ":", nil)
 
-	var apiKey = "test_demoAPIKey1234"
-	var orgToken = "token_demoOrgToken"
-
-	// According to the token hierarchy Org Token should be used.
-	_ = testClient.WithOrganizationToken(orgToken)
-	_ = testClient.WithAPIKey(apiKey)
-
-	req, err := testClient.NewAPIRequest("", "/testing", nil, true)
-	if err != nil {
-		t.Fatalf("failed while building the API Request: %v", err)
+	if err == nil {
+		t.Fatal("nil error produced")
 	}
-	got := req.Header.Get(AuthHeader)
-	want := "Bearer " + orgToken
 
-	if got != want {
-		t.Fatalf("API Key header is not set: got %v, want %v", got, want)
+	if !strings.Contains(err.Error(), "parse") {
+		t.Errorf("unexpected err received %v", err)
 	}
 }
 
-func TestNewAPIRequest_AuthError(t *testing.T) {
+func TestClient_Do(t *testing.T) {
 	setup()
 	defer teardown()
 
-	_, err := testClient.NewAPIRequest("", "/testing", nil, true)
+	tMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, AuthHeader, "Bearer test_token")
+		w.WriteHeader(http.StatusOK)
+	})
+	_ = tClient.WithAuthenticationValue("test_token")
+	req, _ := tClient.NewAPIRequest("GET", "test", nil)
+	res, err := tClient.Do(req)
 
-	if err != errEmptyAPIKey {
-		t.Fatalf("mismatching error message: got %v, want %v", err, errEmptyAPIKey)
+	if err != nil {
+		t.Errorf("unexpected error received: %v", err)
+	}
+
+	if res.StatusCode != http.StatusOK {
+		t.Errorf("request failed: %+v", res)
 	}
 }
 
-func TestNewAPIRequest_RequestError(t *testing.T) {
-	setup()
-	defer teardown()
-	m := "\\\\\\"
-	want := fmt.Errorf("net/http: invalid method %q", m)
-	_, err := testClient.NewAPIRequest(m, "https://google.com", nil, false)
+func TestCheckResponse(t *testing.T) {
+	res1 := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Status:     http.StatusText(http.StatusNotFound),
+	}
 
-	if err.Error() != want.Error() {
-		t.Errorf("wrong expected error, got: %v, want: %v", err, want)
+	res2 := &http.Response{
+		StatusCode: http.StatusOK,
+		Status:     http.StatusText(http.StatusOK),
+	}
+
+	tests := []struct {
+		name string
+		code string
+		arg  *http.Response
+	}{
+		{
+			"successful response",
+			"",
+			res2,
+		},
+		{
+			"not found response",
+			"Not Found",
+			res1,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := CheckResponse(tt.arg); err != nil {
+				if !strings.Contains(err.Error(), tt.code) {
+					t.Error(err)
+				}
+			}
+		})
 	}
 }
 
 // ----- mollie examples -----
-
 func ExampleNewClient() {
-	var ctx context.Context
-	c, err := NewClient(ctx, nil, BaseURL)
+	c, err := NewClient(nil, &Config{
+		testing: false,
+		auth:    APITokenEnv,
+	})
 	log.Printf("%+v", c)
 	fmt.Println(err == nil)
 	// Output: true
 }
+
+// <----- Testing helpers ----->
+
+var (
+	tMux    *http.ServeMux
+	tServer *httptest.Server
+	tClient *Client
+	tConf   *Config
+)
+
+// the parameter indicates if you want to prepare your tests against the US sandbox
+// just to be used when doing integration testing.
+func setup() {
+	tMux = http.NewServeMux()
+	tServer = httptest.NewServer(tMux)
+	tConf = NewConfig(true, APITokenEnv)
+	tClient, _ = NewClient(nil, tConf)
+	u, _ := url.Parse(tServer.URL + "/")
+	tClient.BaseURL = u
+}
+
+func teardown() {
+	tServer.Close()
+}
+
+func setEnv() {
+	_ = os.Setenv(APITokenEnv, "token_X12b31ggg23")
+	_ = os.Setenv(OrgTokenEnv, "ey1923n23123n1k3b123jv12g312h31v32g13")
+}
+
+func unsetEnv() {
+	_ = os.Unsetenv(APITokenEnv)
+	_ = os.Unsetenv(OrgTokenEnv)
+}
+
+func testMethod(t *testing.T, r *http.Request, want string) {
+	if got := r.Method; got != want {
+		t.Errorf("Request method: %v, want %v", got, want)
+	}
+}
+
+func testHeader(t *testing.T, r *http.Request, header string, want string) {
+	if got := r.Header.Get(header); got != want {
+		t.Errorf("Header.Get(%q) returned %q, want %q", header, got, want)
+	}
+}
+
+func testURLParseError(t *testing.T, err error) {
+	if err == nil {
+		t.Errorf("Expected error to be returned")
+	}
+	if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
+		t.Errorf("Expected URL parse error, got %+v", err)
+	}
+}
+
+func testBody(t *testing.T, r *http.Request, want string) {
+	b, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		t.Errorf("Error reading request body: %v", err)
+	}
+	if got := string(b); got != want {
+		t.Errorf("request Body is %s, want %s", got, want)
+	}
+}
+
+func testJSONMarshal(t *testing.T, v interface{}, want string) {
+	j, err := json.Marshal(v)
+	if err != nil {
+		t.Errorf("Unable to marshal JSON for %v", v)
+	}
+
+	w := new(bytes.Buffer)
+	err = json.Compact(w, []byte(want))
+	if err != nil {
+		t.Errorf("String is not valid json: %s", want)
+	}
+
+	if w.String() != string(j) {
+		t.Errorf("json.Marshal(%q) returned %s, want %s", v, j, w)
+	}
+
+	// now go the other direction and make sure things unmarshal as expected
+	u := reflect.ValueOf(v).Interface()
+	if err := json.Unmarshal([]byte(want), u); err != nil {
+		t.Errorf("Unable to unmarshal JSON for %v: %v", want, err)
+	}
+
+	if !reflect.DeepEqual(v, u) {
+		t.Errorf("json.Unmarshal(%q) returned %s, want %s", want, u, v)
+	}
+}
+
+// <----- .Testing helpers ----->
diff --git a/mollie/types_test.go b/mollie/types_test.go
deleted file mode 100644
index 8e2e65d..0000000
--- a/mollie/types_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package mollie
-
-import (
-	"testing"
-)
-
-func TestHalURL_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		b []byte
-	}
-	tests := []struct {
-		name    string
-		args    args
-		wantErr bool
-	}{
-		{
-			"Unmarshal google.com",
-			args{b: []byte("https://google.com")},
-			false,
-		},
-		{
-			"Unmarshal parse error",
-			args{b: []byte(" http://google.com")},
-			true,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			hl := &HalURL{}
-			if err := hl.UnmarshalJSON(tt.args.b); (err != nil) != tt.wantErr {
-				t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-			}
-		})
-	}
-}
-
-func TestDate_UnmarshalJSON(t *testing.T) {
-	type args struct {
-		b []byte
-	}
-	tests := []struct {
-		name    string
-		args    args
-		wantErr bool
-	}{
-		{
-			"unmarshal fails with invalid date format",
-			args{b: []byte("30-12-1991")},
-			true,
-		},
-		{
-			"unmarshal is successful",
-			args{b: []byte("1991-12-30")},
-			false,
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			d := &Date{}
-			if err := d.UnmarshalJSON(tt.args.b); (err != nil) != tt.wantErr {
-				t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
-			}
-		})
-	}
-}