Repository: VictorAvelar/mollie-api-go
Pull request #1 (2019-08-25 18:43:31)

Feature create http client


Patch:
diff --git a/.travis.yaml b/.travis.yml
similarity index 92%
rename from .travis.yaml
rename to .travis.yml
index 9587f5e..e6a208b 100644
--- a/.travis.yaml
+++ b/.travis.yml
@@ -1,4 +1,4 @@
-dist: precise
+dist: xenial
 
 language: go
 
@@ -6,7 +6,7 @@ language: go
 # These will both be unnecessary when Go 1.13 lands.
 env:
   - GO111MODULE=on
-  - GOFLAGS='-mod vendor'
+  # - GOFLAGS='-mod vendor'
 
 # Only clone the most recent commit.
 git:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..f685e44
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,23 @@
+# Changelog
+
+All notable changes to `mollie-api-go` will be documented in this file.
+
+Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
+
+## master
+
+### Added
+- Base HTTP Client for interacting with Mollie REST API
+- Tests for the added code
+
+### Deprecated
+- Nothing
+
+### Fixed
+- Nothing
+
+### Removed
+- Nothing
+
+### Security
+- Nothing
\ No newline at end of file
diff --git a/README.md b/README.md
index 89f28b9..b867180 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ For leveraging [Mollie Connect](https://docs.mollie.com/oauth/overview) (advance
 
 ## Roadmap
 - [x] Project setup
-- [ ] Base HTTP Client
+- [x] Base HTTP Client
 - [ ] Authentication
     - [ ] API token
     - [ ] Organization token
diff --git a/mollie/mollie.go b/mollie/mollie.go
new file mode 100644
index 0000000..a2afb4e
--- /dev/null
+++ b/mollie/mollie.go
@@ -0,0 +1,148 @@
+package mollie
+
+import (
+	"context"
+	"errors"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+)
+
+// Mollie  constanst 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        = "MOLLIE_API_TOKEN"
+	OrgTokenEnv        = "MOLLIE_ORG_TOKEN"
+)
+
+var (
+	errEmptyAPIKey = errors.New("you must provide a non-empty API key")
+)
+
+type httpClient interface {
+	Do(req *http.Request) (res *http.Response, err error)
+}
+
+//APIClient struct contains the httpClient plus information related to the
+// different authentication methods provided by the API.
+type APIClient struct {
+	ClientContext     context.Context
+	HTTPClient        httpClient
+	BaseURL           *url.URL
+	APIKey            string
+	OrganizationToken string
+}
+
+//WithAPIKey offers a convenient setter with some base validation to attach
+//an API key to an APIClient.
+//Ideally your API key will be provided from and environment variable or
+//a secret management engine.
+func (c *APIClient) WithAPIKey(k string) error {
+	if k == "" {
+		return errEmptyAPIKey
+	}
+
+	c.APIKey = strings.Trim(k, " ")
+
+	return nil
+}
+
+//WithOrganizationToken offers a convenient token with some base validation to
+//attach a Mollie Organization Token to an APIClient.
+//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
+	}
+
+	c.OrganizationToken = t
+
+	return nil
+}
+
+//NewAPIRequest is a wrapper arount the http.NewRequest function.
+//It takes the same parameters plus a flag to indicate if the request needs
+//to have the authorization headers.
+//The http request method defaults to GET if no method is set.
+//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 attemp 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, bErr := http.NewRequest(m, uri, body)
+	if err != nil {
+		return nil, bErr
+	}
+
+	req.Header.Add("accept", "application/json")
+
+	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
+		}
+
+		v := strings.Join(parts, " ")
+		req.Header.Add(AuthHeader, v)
+	}
+	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.
+//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).
+//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()
+	}
+
+	if baseClient == nil {
+		baseClient = http.DefaultClient
+	}
+
+	u, err := url.Parse(uri)
+	if err != nil {
+		return
+	}
+
+	c := &APIClient{
+		ClientContext: ctx,
+		HTTPClient:    baseClient,
+		BaseURL:       u,
+	}
+
+	if tkn, ok := os.LookupEnv(APITokenEnv); ok {
+		c.APIKey = tkn
+	}
+
+	if orgTkn, ok := os.LookupEnv(OrgTokenEnv); ok {
+		c.OrganizationToken = orgTkn
+	}
+
+	return c, nil
+}


Test patch:
diff --git a/mollie/mollie_test.go b/mollie/mollie_test.go
new file mode 100644
index 0000000..2305bec
--- /dev/null
+++ b/mollie/mollie_test.go
@@ -0,0 +1,286 @@
+package mollie
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"reflect"
+	"testing"
+)
+
+// ---------- 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{
+		ClientContext:     context.Background(),
+		HTTPClient:        http.DefaultClient,
+		BaseURL:           u,
+		APIKey:            "",
+		OrganizationToken: "",
+	}
+
+	tests := []struct {
+		name      string
+		check     *APIClient
+		arguments args
+		wantErr   bool
+		err       error
+	}{
+		{
+			name:  "test client is build properly",
+			check: want,
+			arguments: args{
+				ctx: nil,
+				cl:  nil,
+				uri: "http://localhost",
+			},
+			wantErr: false,
+			err:     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"),
+		},
+	}
+
+	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)
+				}
+			}
+		})
+	}
+}
+
+func TestNewClient_WithEnvVars(t *testing.T) {
+	setEnv()
+	defer unsetEnv()
+	u, err := url.Parse("http://localhost:3000")
+	if err != nil {
+		t.Fail()
+	}
+	want := &APIClient{
+		ClientContext:     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)
+	}
+}
+
+func TestWithAPIToken(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:   "test_$DJAO@A##MKao23u#N",
+			wantErr: false,
+			err:     nil,
+		},
+	}
+
+	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()
+				}
+			}
+		})
+	}
+}
+
+func TestWithOrganizationToken(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,
+		},
+	}
+
+	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()
+				}
+			}
+		})
+	}
+}
+
+func TestNewAPIRequest_WellformedRequest(t *testing.T) {
+	setup()
+	defer teardown()
+
+	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 got.URL.String() != outURI {
+		t.Fatalf("mailformed request uri: want: %v, got: %v", outURI, got.URL)
+	}
+
+	if got.Method != outMethod {
+		t.Fatalf("method assignment failed: got: %v, want: %v", got.Method, outMethod)
+	}
+}
+
+func TestNewAPIRequest_WithAPIKeyAuthHeader(t *testing.T) {
+	setup()
+	defer teardown()
+
+	var testKey = "test_demoAPIKey1234"
+
+	testClient.WithAPIKey(testKey)
+
+	req, err := testClient.NewAPIRequest("", "/testing", nil, true)
+	if err != nil {
+		t.Fatalf("failed while building the API Request: %v", err)
+	}
+	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)
+	}
+}
+
+func TestNewAPIRequest_WithOrgTokenAuthHeader(t *testing.T) {
+	setup()
+	defer teardown()
+
+	var apiKey = "test_demoAPIKey1234"
+	var orgToken = "token_demoOrgToken"
+
+	// Accoring 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)
+	}
+	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)
+	}
+}
+
+func TestNewAPIRequest_AuthError(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := testClient.NewAPIRequest("", "/testing", nil, true)
+
+	if err != errEmptyAPIKey {
+		t.Fatalf("mismatching error message: got %v, want %v", err, errEmptyAPIKey)
+	}
+}