From 01fcd41b9482cdf65421e49575bec3a63cd32cf2 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sun, 18 Oct 2020 15:49:58 +0200
Subject: [PATCH 01/11] Add some To-Do's to the README.md

---
 README.md | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 51fa550..9ffa422 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,12 @@
 # gsave
-Database to save all your data from [gstat](https://github.com/hamburghammer/gstat).
+Database to save all your data from [gstat](https://github.com/hamburghammer/gstat). 
+In the near future [gmon](https://github.com/hamburghammer/gmon) should use it to monitor hosts and to create alerts.
+
+## TODO
+- [ ] Configure CI/CD pipeline
+- [ ] HTTP testing
+- [ ] Auth Middleware with Basic Auth
+- [ ] Flag configurable
+- [ ] Logging
+- [ ] SQLite DB implementation
+- [ ] Env Variable configurable
\ No newline at end of file

From 092739985c57d2d5e22ae5c2fd245c10d7e0da52 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sun, 18 Oct 2020 16:40:34 +0200
Subject: [PATCH 02/11] Implement flag to change the http port

---
 main.go | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/main.go b/main.go
index 3443302..4811dc6 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	"errors"
+	"flag"
 	"fmt"
 	"log"
 	"net/http"
@@ -16,7 +17,12 @@ import (
 	"github.com/hamburghammer/gsave/db"
 )
 
-const serveAddr = "127.0.0.1:8080"
+var servePort int
+
+func init() {
+	flag.IntVar(&servePort, "port", 8080, "The port for the HTTP server.")
+	flag.Parse()
+}
 
 func main() {
 	log.Println("Initializing the DB...")
@@ -39,7 +45,7 @@ func main() {
 	log.Println("Starting the HTTP server...")
 	server := &http.Server{
 		Handler:      router,
-		Addr:         serveAddr,
+		Addr:         fmt.Sprintf(":%d", servePort),
 		WriteTimeout: 15 * time.Second,
 		ReadTimeout:  15 * time.Second,
 	}
@@ -75,7 +81,7 @@ func initRouter(hostDB db.HostDB, controllers []controller.Router) *mux.Router {
 
 func startHTTPServer(server *http.Server, wg *sync.WaitGroup) {
 	defer wg.Done()
-	log.Printf("The HTTP server is running: http://%s\n", serveAddr)
+	log.Printf("The HTTP server is running: http://localhost:%d/hosts\n", servePort)
 	if err := server.ListenAndServe(); err != nil {
 		if errors.Is(err, http.ErrServerClosed) {
 			log.Println("Shutting down the server...")

From 3dfb18f67d25d7fd8fb769ea51da99ed29ef325b Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sun, 18 Oct 2020 17:02:10 +0200
Subject: [PATCH 03/11] Change returned status code to 201

---
 controller/hosts.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/controller/hosts.go b/controller/hosts.go
index 7676309..989bede 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -122,6 +122,7 @@ func (hr *HostsRouter) postStats(w http.ResponseWriter, r *http.Request) {
 		http.Error(w, "Something with the DB went wrong.", http.StatusInternalServerError)
 		return
 	}
+	w.WriteHeader(http.StatusCreated)
 }
 
 // getSkipAndLimit from the query of the request.

From 7f817e166fd57cd649b5664058a492ffa47aad44 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sun, 18 Oct 2020 18:26:59 +0200
Subject: [PATCH 04/11] Implement logrus for logging

Fix wrong error handling if host was not found getting the stats for it.
Add configuration flags to configure the logging.
---
 README.md                |  4 ++--
 controller/controller.go | 15 ++++++++++++++-
 controller/hosts.go      | 19 +++++++++++--------
 go.mod                   |  1 +
 go.sum                   |  6 ++++++
 main.go                  | 40 ++++++++++++++++++++++++++++++----------
 6 files changed, 64 insertions(+), 21 deletions(-)

diff --git a/README.md b/README.md
index 9ffa422..7197b21 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ In the near future [gmon](https://github.com/hamburghammer/gmon) should use it t
 - [ ] Configure CI/CD pipeline
 - [ ] HTTP testing
 - [ ] Auth Middleware with Basic Auth
-- [ ] Flag configurable
-- [ ] Logging
+- [x] Flag configurable
+- [x] Logging
 - [ ] SQLite DB implementation
 - [ ] Env Variable configurable
\ No newline at end of file
diff --git a/controller/controller.go b/controller/controller.go
index 393f2d3..28966c6 100644
--- a/controller/controller.go
+++ b/controller/controller.go
@@ -1,6 +1,19 @@
 package controller
 
-import "github.com/gorilla/mux"
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+	log "github.com/sirupsen/logrus"
+)
+
+var (
+	logPackage             = log.WithField("Package", "controller")
+	logRequestError        = logPackage.WithField("RequestStatus", "Error")
+	logBadRequest          = logRequestError.WithField("StatusCode", http.StatusBadRequest)
+	logNotFound            = logRequestError.WithField("StatusCode", http.StatusNotFound)
+	logInternalServerError = logRequestError.WithField("StatusCode", http.StatusInternalServerError)
+)
 
 // Router is an interface that should be implemented by any controller
 // to give some information and to register the routes.
diff --git a/controller/hosts.go b/controller/hosts.go
index 989bede..d9fdda8 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"log"
 	"net/http"
 	"strconv"
 
@@ -46,6 +45,7 @@ func (hr *HostsRouter) getHosts(w http.ResponseWriter, r *http.Request) {
 	pagination, err := hr.getSkipAndLimit(r)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
+		logBadRequest.Error(err)
 		return
 	}
 
@@ -53,9 +53,11 @@ func (hr *HostsRouter) getHosts(w http.ResponseWriter, r *http.Request) {
 	if err != nil {
 		if errors.Is(err, db.ErrHostsNotFound) || errors.Is(err, db.ErrAllEntriesSkipped) {
 			http.Error(w, err.Error(), http.StatusNotFound)
+			logNotFound.Error(err)
 			return
 		}
 		http.Error(w, err.Error(), http.StatusInternalServerError)
+		logInternalServerError.Error(err)
 		return
 	}
 
@@ -65,15 +67,12 @@ func (hr *HostsRouter) getHosts(w http.ResponseWriter, r *http.Request) {
 
 func (hr *HostsRouter) getHost(w http.ResponseWriter, r *http.Request) {
 	hostname := mux.Vars(r)["hostname"]
-	if hostname == "" {
-		http.Error(w, fmt.Sprintf("Missing hostname: '%s' is not a valid hostname\n", hostname), http.StatusBadRequest)
-		return
-	}
 
 	host, err := hr.db.GetHost(hostname)
 	if err != nil {
 		if errors.Is(err, db.ErrHostNotFound) {
 			http.Error(w, fmt.Sprintf("No host with the name '%s' found\n", hostname), http.StatusNotFound)
+			logNotFound.Error(err)
 			return
 		}
 	}
@@ -88,16 +87,19 @@ func (hr *HostsRouter) getStats(w http.ResponseWriter, r *http.Request) {
 	pagination, err := hr.getSkipAndLimit(r)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
+		logBadRequest.Error(err)
 		return
 	}
 
 	stats, err := hr.db.GetStatsByHostname(hostname, pagination)
 	if err != nil {
-		if errors.Is(err, db.ErrHostsNotFound) || errors.Is(err, db.ErrAllEntriesSkipped) {
+		if errors.Is(err, db.ErrHostNotFound) || errors.Is(err, db.ErrAllEntriesSkipped) {
 			http.Error(w, err.Error(), http.StatusNotFound)
+			logNotFound.Error(err)
 			return
 		}
 		http.Error(w, err.Error(), http.StatusInternalServerError)
+		logInternalServerError.Error(err)
 		return
 	}
 
@@ -112,14 +114,15 @@ func (hr *HostsRouter) postStats(w http.ResponseWriter, r *http.Request) {
 	err := json.NewDecoder(r.Body).Decode(&stats)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
-		log.Println(err)
+		logBadRequest.Error(err)
 		return
 	}
-	log.Printf("Received stat: %+v", stats)
+	logPackage.Debugf("Received stat: %+v", stats)
 
 	err = hr.db.InsertStats(hostname, stats)
 	if err != nil {
 		http.Error(w, "Something with the DB went wrong.", http.StatusInternalServerError)
+		logInternalServerError.Error(err)
 		return
 	}
 	w.WriteHeader(http.StatusCreated)
diff --git a/go.mod b/go.mod
index 776bf89..9c0adb7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,6 @@ go 1.15
 
 require (
 	github.com/gorilla/mux v1.8.0
+	github.com/sirupsen/logrus v1.7.0
 	github.com/stretchr/testify v1.6.1
 )
diff --git a/go.sum b/go.sum
index b5db067..4dc6328 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,18 @@
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
diff --git a/main.go b/main.go
index 4811dc6..baec3ef 100644
--- a/main.go
+++ b/main.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"flag"
 	"fmt"
-	"log"
 	"net/http"
 	"os"
 	"os/signal"
@@ -15,17 +14,38 @@ import (
 	"github.com/gorilla/mux"
 	"github.com/hamburghammer/gsave/controller"
 	"github.com/hamburghammer/gsave/db"
+	log "github.com/sirupsen/logrus"
 )
 
-var servePort int
+var (
+	servePort  int
+	logPackage = log.WithField("Package", "main")
+)
 
 func init() {
 	flag.IntVar(&servePort, "port", 8080, "The port for the HTTP server.")
+	verbose := flag.Bool("verbose", false, "Enable debug logging output.")
+	quiet := flag.Bool("quiet", false, "Disable loging output only prints errors.")
+	jsonLogging := flag.Bool("json", false, "Set the logging format to json.")
 	flag.Parse()
+
+	log.SetFormatter(&log.TextFormatter{
+		FullTimestamp: true,
+	})
+
+	if *verbose {
+		log.SetLevel(log.DebugLevel)
+	}
+	if *quiet {
+		log.SetLevel(log.ErrorLevel)
+	}
+	if *jsonLogging {
+		log.SetFormatter(&log.JSONFormatter{})
+	}
 }
 
 func main() {
-	log.Println("Initializing the DB...")
+	logPackage.Info("Initializing the DB...")
 	stats := []db.Stats{
 		{Hostname: "foo", CPU: 0},
 		{Hostname: "foo", CPU: 1},
@@ -33,16 +53,16 @@ func main() {
 	}
 	hostDB, err := initDB(stats)
 	if err != nil {
-		log.Fatal(err)
+		logPackage.Fatal(err)
 	}
 
-	log.Println("Initializing the routes...")
+	logPackage.Info("Initializing the routes...")
 	controllers := []controller.Router{
 		controller.NewHostsRouter(hostDB),
 	}
 	router := initRouter(hostDB, controllers)
 
-	log.Println("Starting the HTTP server...")
+	logPackage.Info("Starting the HTTP server...")
 	server := &http.Server{
 		Handler:      router,
 		Addr:         fmt.Sprintf(":%d", servePort),
@@ -81,13 +101,13 @@ func initRouter(hostDB db.HostDB, controllers []controller.Router) *mux.Router {
 
 func startHTTPServer(server *http.Server, wg *sync.WaitGroup) {
 	defer wg.Done()
-	log.Printf("The HTTP server is running: http://localhost:%d/hosts\n", servePort)
+	logPackage.Infof("The HTTP server is running: http://localhost:%d/hosts\n", servePort)
 	if err := server.ListenAndServe(); err != nil {
 		if errors.Is(err, http.ErrServerClosed) {
-			log.Println("Shutting down the server...")
+			logPackage.Info("Shutting down the server...")
 			return
 		}
-		log.Fatalf("An unexpected error happend while running the HTTP server: %v\n", err)
+		logPackage.Fatalf("An unexpected error happend while running the HTTP server: %v\n", err)
 	}
 }
 
@@ -102,6 +122,6 @@ func listenToStopHTTPServer(server *http.Server, wg *sync.WaitGroup) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 	if err := server.Shutdown(ctx); err != nil {
-		log.Printf("An error happened on the shutdown of the server: %v", err)
+		logPackage.Errorf("An error happened on the shutdown of the server: %v", err)
 	}
 }

From 2aa6a848540e94c19e386932b5e8fb953dcb8e54 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Mon, 19 Oct 2020 17:40:40 +0200
Subject: [PATCH 05/11] Create .drone.yml

---
 .drone.yml | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)
 create mode 100644 .drone.yml

diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..12d68d7
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,37 @@
+kind: pipeline
+type: docker
+name: default
+
+steps:
+- name: unit-test
+  image: golang
+  volumes:
+  - name: cache
+    path: /go
+  commands:
+  - go mod download
+  - go test -coverprofile=coverage.out -covermode=count ./...
+  - go tool cover -html=coverage.out -o coverage.html
+  - go tool cover -func=coverage.out | grep total
+
+- name: race-test
+  image: golang
+  volumes:
+  - name: cache
+    path: /go
+  commands:
+  - go mod download
+  - go test -race -short ./...
+
+- name: build
+  image: golang
+  volumes:
+  - name: cache
+    path: /go
+  commands:
+  - go mod download
+  - go build
+
+volumes:
+- name: cache
+  temp: {}

From f02ef3122504af26aecc7969b8f9e2b7639680a7 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Mon, 19 Oct 2020 17:44:44 +0200
Subject: [PATCH 06/11] Add CI/CD pipeline status badge

---
 README.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 7197b21..6f8f3bd 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
 # gsave
+[![Build Status](https://cloud.drone.io/api/badges/hamburghammer/gsave/status.svg)](https://cloud.drone.io/hamburghammer/gsave)
+
 Database to save all your data from [gstat](https://github.com/hamburghammer/gstat). 
 In the near future [gmon](https://github.com/hamburghammer/gmon) should use it to monitor hosts and to create alerts.
 
@@ -9,4 +11,4 @@ In the near future [gmon](https://github.com/hamburghammer/gmon) should use it t
 - [x] Flag configurable
 - [x] Logging
 - [ ] SQLite DB implementation
-- [ ] Env Variable configurable
\ No newline at end of file
+- [ ] Env Variable configurable

From b8f6a1a65bbb4e2b213c4151e73c4cf754280c99 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Tue, 20 Oct 2020 21:01:53 +0200
Subject: [PATCH 07/11] Add first test for GetHosts -> proof of concept

Implementing first http test.
---
 controller/hosts.go      |   4 +-
 controller/hosts_test.go | 132 +++++++++++++++++++++++++++++++++++++++
 go.sum                   |   1 +
 3 files changed, 135 insertions(+), 2 deletions(-)
 create mode 100644 controller/hosts_test.go

diff --git a/controller/hosts.go b/controller/hosts.go
index d9fdda8..47ab47d 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -25,7 +25,7 @@ type HostsRouter struct {
 // Register registers all routes to the given subrouter.
 func (hr *HostsRouter) Register(subrouter *mux.Router) {
 	hr.subrouter = subrouter
-	subrouter.HandleFunc("", hr.getHosts).Methods(http.MethodGet).Name("GetHosts")
+	subrouter.HandleFunc("", hr.GetHosts).Methods(http.MethodGet).Name("GetHosts")
 	subrouter.HandleFunc("/{hostname}", hr.getHost).Methods(http.MethodGet).Name("GetHost")
 	subrouter.HandleFunc("/{hostname}/stats", hr.getStats).Methods(http.MethodGet).Name("GetStats")
 	subrouter.HandleFunc("/{hostname}/stats", hr.postStats).Methods(http.MethodPost).Name("PostStats")
@@ -41,7 +41,7 @@ func (hr *HostsRouter) GetRouteName() string {
 	return "Hosts"
 }
 
-func (hr *HostsRouter) getHosts(w http.ResponseWriter, r *http.Request) {
+func (hr *HostsRouter) GetHosts(w http.ResponseWriter, r *http.Request) {
 	pagination, err := hr.getSkipAndLimit(r)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusBadRequest)
diff --git a/controller/hosts_test.go b/controller/hosts_test.go
new file mode 100644
index 0000000..1eed4cd
--- /dev/null
+++ b/controller/hosts_test.go
@@ -0,0 +1,132 @@
+package controller_test
+
+import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/hamburghammer/gsave/controller"
+	"github.com/hamburghammer/gsave/db"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHostsRouter_GetHosts_WithoutPagination(t *testing.T) {
+	t.Run("should return hosts list", func(t *testing.T) {
+		stats := []db.HostInfo{
+			{Hostname: "foo"},
+		}
+		hostDB := &MockHostDB{}
+		hostDB.SetHosts(stats)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/hosts", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHosts)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusOK, rr.Code)
+		assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
+
+		var gotBody []db.HostInfo
+		json.Unmarshal(rr.Body.Bytes(), &gotBody)
+
+		assert.Equal(t, stats, gotBody)
+	})
+
+	// t.Run("", func(t *testing.T) {
+	// 	stats := []db.HostInfo{
+	// 		{Hostname: "foo"},
+	// 		{Hostname: "foo"},
+	// 		{Hostname: "bar"},
+	// 	}
+	// 	hostDB := &MockHostDB{}
+	// 	hostDB.SetHosts(stats)
+	// 	hostsRouter := controller.NewHostsRouter(hostDB)
+
+	// 	req, err := http.NewRequest("GET", "/entries", nil)
+	// 	if err != nil {
+	// 		t.Fatal(err)
+	// 	}
+	// 	rr := httptest.NewRecorder()
+	// 	handler := http.HandlerFunc(hostsRouter.GetHosts)
+	// 	handler.ServeHTTP(rr, req)
+	// 	if status := rr.Code; status != http.StatusOK {
+	// 		t.Errorf("handler returned wrong status code: got %v want %v",
+	// 			status, http.StatusOK)
+	// 	}
+	// })
+}
+
+type MockHostDB struct {
+	hosts      []db.HostInfo
+	hostsError error
+
+	host      db.HostInfo
+	hostError error
+
+	stats      []db.Stats
+	statsError error
+
+	insertedStat      db.Stats
+	insertedStatError error
+}
+
+// GetHosts
+func (m *MockHostDB) SetHosts(hosts []db.HostInfo) {
+	m.hosts = hosts
+}
+func (m *MockHostDB) SetHostsError(err error) {
+	m.hostsError = err
+}
+func (m *MockHostDB) GetHosts(pagination db.Pagination) ([]db.HostInfo, error) {
+	if m.hostsError != nil {
+		return []db.HostInfo{}, m.hostsError
+	}
+	return m.hosts, nil
+}
+
+// GetHost
+func (m *MockHostDB) SetHost(host db.HostInfo) {
+	m.host = host
+}
+func (m *MockHostDB) SetHostError(err error) {
+	m.hostError = err
+}
+func (m *MockHostDB) GetHost(hostname string) (db.HostInfo, error) {
+	if m.hostError != nil {
+		return db.HostInfo{}, m.hostError
+	}
+	return m.host, nil
+}
+
+// GetStatsByHostname
+func (m *MockHostDB) SetStatsByHostname(stats []db.Stats) {
+	m.stats = stats
+}
+func (m *MockHostDB) SetStatsByHostnameError(err error) {
+	m.statsError = err
+}
+func (m *MockHostDB) GetStatsByHostname(hostname string, pagination db.Pagination) ([]db.Stats, error) {
+	if m.statsError != nil {
+		return []db.Stats{}, m.hostError
+	}
+	return m.stats, nil
+}
+
+// InsertStats
+func (m *MockHostDB) GetInsertedStats() db.Stats {
+	return m.insertedStat
+}
+func (m *MockHostDB) SetInsertStatsError(err error) {
+	m.insertedStatError = err
+}
+func (m *MockHostDB) InsertStats(hostname string, stats db.Stats) error {
+	if m.statsError != nil {
+		return m.hostError
+	}
+	return nil
+}
diff --git a/go.sum b/go.sum
index 4dc6328..2c37952 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,6 @@
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=

From c550341eb92d06c02fa5bf12f90ee4369fdafe49 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sat, 24 Oct 2020 12:08:46 +0200
Subject: [PATCH 08/11] Add error response if pagination query is negative

---
 controller/hosts.go      |   7 ++
 controller/hosts_test.go | 237 +++++++++++++++++++++++++++++++++++----
 2 files changed, 221 insertions(+), 23 deletions(-)

diff --git a/controller/hosts.go b/controller/hosts.go
index 47ab47d..dbd96ad 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -41,6 +41,7 @@ func (hr *HostsRouter) GetRouteName() string {
 	return "Hosts"
 }
 
+// GetHosts is a HandleFunc to get hosts out of the db with optional pagination as query params.
 func (hr *HostsRouter) GetHosts(w http.ResponseWriter, r *http.Request) {
 	pagination, err := hr.getSkipAndLimit(r)
 	if err != nil {
@@ -141,6 +142,9 @@ func (hr *HostsRouter) getSkipAndLimit(r *http.Request) (db.Pagination, error) {
 	if err != nil {
 		return db.Pagination{}, fmt.Errorf("Query param 'limit' expected to be a number: %s is not a number", strLimit)
 	}
+	if limit < 0 {
+		return db.Pagination{}, fmt.Errorf("No negative number allowed for the query param 'limit'")
+	}
 
 	strSkip := r.FormValue("skip")
 	if strSkip == "" {
@@ -150,6 +154,9 @@ func (hr *HostsRouter) getSkipAndLimit(r *http.Request) (db.Pagination, error) {
 	if err != nil {
 		return db.Pagination{}, fmt.Errorf("Query param 'skip' expected to be a number: %s is not a number", strSkip)
 	}
+	if skip < 0 {
+		return db.Pagination{}, fmt.Errorf("No negative number allowed for the query param 'skip'")
+	}
 
 	return db.Pagination{Skip: int(skip), Limit: int(limit)}, nil
 }
diff --git a/controller/hosts_test.go b/controller/hosts_test.go
index 1eed4cd..23cab61 100644
--- a/controller/hosts_test.go
+++ b/controller/hosts_test.go
@@ -2,6 +2,8 @@ package controller_test
 
 import (
 	"encoding/json"
+	"errors"
+	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"testing"
@@ -11,8 +13,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestHostsRouter_GetHosts_WithoutPagination(t *testing.T) {
-	t.Run("should return hosts list", func(t *testing.T) {
+func TestHostsRouter_GetHosts(t *testing.T) {
+	t.Run("db has one item", func(t *testing.T) {
 		stats := []db.HostInfo{
 			{Hostname: "foo"},
 		}
@@ -37,28 +39,210 @@ func TestHostsRouter_GetHosts_WithoutPagination(t *testing.T) {
 		assert.Equal(t, stats, gotBody)
 	})
 
-	// t.Run("", func(t *testing.T) {
-	// 	stats := []db.HostInfo{
-	// 		{Hostname: "foo"},
-	// 		{Hostname: "foo"},
-	// 		{Hostname: "bar"},
-	// 	}
-	// 	hostDB := &MockHostDB{}
-	// 	hostDB.SetHosts(stats)
-	// 	hostsRouter := controller.NewHostsRouter(hostDB)
+	t.Run("db is empty", func(t *testing.T) {
+		stats := []db.HostInfo{}
+		hostDB := &MockHostDB{}
+		hostDB.SetHosts(stats)
+		hostsRouter := controller.NewHostsRouter(hostDB)
 
-	// 	req, err := http.NewRequest("GET", "/entries", nil)
-	// 	if err != nil {
-	// 		t.Fatal(err)
-	// 	}
-	// 	rr := httptest.NewRecorder()
-	// 	handler := http.HandlerFunc(hostsRouter.GetHosts)
-	// 	handler.ServeHTTP(rr, req)
-	// 	if status := rr.Code; status != http.StatusOK {
-	// 		t.Errorf("handler returned wrong status code: got %v want %v",
-	// 			status, http.StatusOK)
-	// 	}
-	// })
+		req, err := http.NewRequest("GET", "/hosts", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHosts)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusOK, rr.Code)
+
+		var gotBody []db.HostInfo
+		json.Unmarshal(rr.Body.Bytes(), &gotBody)
+
+		assert.Equal(t, stats, gotBody)
+	})
+
+	t.Run("db returns hosts not found error", func(t *testing.T) {
+		hostDB := &MockHostDB{}
+		hostDB.SetHostsError(db.ErrHostsNotFound)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/hosts", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHosts)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusNotFound, rr.Code)
+
+		wantBody := fmt.Sprintf("%s\n", db.ErrHostsNotFound.Error())
+		assert.Equal(t, wantBody, rr.Body.String())
+	})
+
+	t.Run("db returns all entities skipped error", func(t *testing.T) {
+		hostDB := &MockHostDB{}
+		hostDB.SetHostsError(db.ErrAllEntriesSkipped)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/hosts", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHosts)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusNotFound, rr.Code)
+
+		wantBody := fmt.Sprintf("%s\n", db.ErrAllEntriesSkipped.Error())
+		assert.Equal(t, wantBody, rr.Body.String())
+	})
+
+	t.Run("db returns unknown error", func(t *testing.T) {
+		unknownErr := errors.New("some error")
+		hostDB := &MockHostDB{}
+		hostDB.SetHostsError(unknownErr)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/hosts", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHosts)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusInternalServerError, rr.Code)
+
+		wantBody := fmt.Sprintf("%s\n", unknownErr.Error())
+		assert.Equal(t, wantBody, rr.Body.String())
+	})
+
+	t.Run("pagination", func(t *testing.T) {
+		t.Run("default pagination has a limit of 10", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 10, hostDB.GetPagination().Limit)
+		})
+
+		t.Run("default pagination has a skip of 0", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 0, hostDB.GetPagination().Skip)
+		})
+
+		t.Run("sets custom limit", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts?limit=2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusOK, rr.Code)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 2, hostDB.GetPagination().Limit)
+		})
+
+		t.Run("sets negative limit", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts?limit=-2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "No negative number allowed for the query param 'limit'\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets negative skip", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts?skip=-2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "No negative number allowed for the query param 'skip'\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets skip to not a number", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts?skip=a", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "Query param 'skip' expected to be a number: a is not a number\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets limit to not a number", func(t *testing.T) {
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/hosts?limit=a", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetHosts)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "Query param 'limit' expected to be a number: a is not a number\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+	})
 }
 
 type MockHostDB struct {
@@ -73,6 +257,8 @@ type MockHostDB struct {
 
 	insertedStat      db.Stats
 	insertedStatError error
+
+	pagination db.Pagination
 }
 
 // GetHosts
@@ -83,6 +269,7 @@ func (m *MockHostDB) SetHostsError(err error) {
 	m.hostsError = err
 }
 func (m *MockHostDB) GetHosts(pagination db.Pagination) ([]db.HostInfo, error) {
+	m.pagination = pagination
 	if m.hostsError != nil {
 		return []db.HostInfo{}, m.hostsError
 	}
@@ -130,3 +317,7 @@ func (m *MockHostDB) InsertStats(hostname string, stats db.Stats) error {
 	}
 	return nil
 }
+
+func (m *MockHostDB) GetPagination() db.Pagination {
+	return m.pagination
+}

From 1661266f9ed1b158b2184d19516f016203b94352 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Sat, 24 Oct 2020 15:52:11 +0200
Subject: [PATCH 09/11] Fix if an unknown error happens getting the host
 information

The function will now exit earlier and write an 500 status code
with the error message to the response.
---
 controller/hosts.go      |  10 ++--
 controller/hosts_test.go | 101 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+), 3 deletions(-)

diff --git a/controller/hosts.go b/controller/hosts.go
index dbd96ad..c6d1bbf 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -26,7 +26,7 @@ type HostsRouter struct {
 func (hr *HostsRouter) Register(subrouter *mux.Router) {
 	hr.subrouter = subrouter
 	subrouter.HandleFunc("", hr.GetHosts).Methods(http.MethodGet).Name("GetHosts")
-	subrouter.HandleFunc("/{hostname}", hr.getHost).Methods(http.MethodGet).Name("GetHost")
+	subrouter.HandleFunc("/{hostname}", hr.GetHost).Methods(http.MethodGet).Name("GetHost")
 	subrouter.HandleFunc("/{hostname}/stats", hr.getStats).Methods(http.MethodGet).Name("GetStats")
 	subrouter.HandleFunc("/{hostname}/stats", hr.postStats).Methods(http.MethodPost).Name("PostStats")
 }
@@ -66,16 +66,20 @@ func (hr *HostsRouter) GetHosts(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(hosts)
 }
 
-func (hr *HostsRouter) getHost(w http.ResponseWriter, r *http.Request) {
+// GetHost is a HandleFunc to get one host. The host name gets read out of the request path.
+func (hr *HostsRouter) GetHost(w http.ResponseWriter, r *http.Request) {
 	hostname := mux.Vars(r)["hostname"]
 
 	host, err := hr.db.GetHost(hostname)
 	if err != nil {
 		if errors.Is(err, db.ErrHostNotFound) {
-			http.Error(w, fmt.Sprintf("No host with the name '%s' found\n", hostname), http.StatusNotFound)
+			http.Error(w, fmt.Sprintf("No host with the name '%s' found", hostname), http.StatusNotFound)
 			logNotFound.Error(err)
 			return
 		}
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		logInternalServerError.Error(err)
+		return
 	}
 
 	w.Header().Set("Content-Type", "application/json")
diff --git a/controller/hosts_test.go b/controller/hosts_test.go
index 23cab61..224c3e6 100644
--- a/controller/hosts_test.go
+++ b/controller/hosts_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
+	"github.com/gorilla/mux"
 	"github.com/hamburghammer/gsave/controller"
 	"github.com/hamburghammer/gsave/db"
 	"github.com/stretchr/testify/assert"
@@ -245,12 +246,108 @@ func TestHostsRouter_GetHosts(t *testing.T) {
 	})
 }
 
+func TestGetHost(t *testing.T) {
+	t.Run("search with hostname from url", func(t *testing.T) {
+		hostname := "foo"
+		hostInfo := db.HostInfo{Hostname: hostname, DataPoints: 1}
+		hostDB := &MockHostDB{}
+		hostDB.SetHost(hostInfo)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHost)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, hostname, hostDB.GetHostHostname())
+	})
+
+	t.Run("db has an item", func(t *testing.T) {
+		hostname := "foo"
+		hostInfo := db.HostInfo{Hostname: hostname, DataPoints: 1}
+		hostDB := &MockHostDB{}
+		hostDB.SetHost(hostInfo)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHost)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusOK, rr.Code)
+
+		var gotBody db.HostInfo
+		err = json.NewDecoder(rr.Body).Decode(&gotBody)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, hostInfo, gotBody)
+	})
+
+	t.Run("db returns not found error", func(t *testing.T) {
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostDB.SetHostError(db.ErrHostNotFound)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHost)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusNotFound, rr.Code)
+
+		wantErr := fmt.Sprintf("No host with the name '%s' found\n", hostname)
+		assert.Equal(t, wantErr, rr.Body.String())
+	})
+
+	t.Run("db returns unknown error", func(t *testing.T) {
+		unknownErr := errors.New("unknown error")
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostDB.SetHostError(unknownErr)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetHost)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusInternalServerError, rr.Code)
+
+		wantErr := fmt.Sprintf("%s\n", unknownErr.Error())
+		assert.Equal(t, wantErr, rr.Body.String())
+	})
+}
+
 type MockHostDB struct {
 	hosts      []db.HostInfo
 	hostsError error
 
 	host      db.HostInfo
 	hostError error
+	hostname  string
 
 	stats      []db.Stats
 	statsError error
@@ -283,7 +380,11 @@ func (m *MockHostDB) SetHost(host db.HostInfo) {
 func (m *MockHostDB) SetHostError(err error) {
 	m.hostError = err
 }
+func (m *MockHostDB) GetHostHostname() string {
+	return m.hostname
+}
 func (m *MockHostDB) GetHost(hostname string) (db.HostInfo, error) {
+	m.hostname = hostname
 	if m.hostError != nil {
 		return db.HostInfo{}, m.hostError
 	}

From d4bcc3d453b2e70c86dfd57fecf9bd2279799495 Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Mon, 26 Oct 2020 21:51:54 +0100
Subject: [PATCH 10/11] Change error message in GetStats

Add more tests.
---
 controller/hosts.go      |  13 +-
 controller/hosts_test.go | 269 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 271 insertions(+), 11 deletions(-)

diff --git a/controller/hosts.go b/controller/hosts.go
index c6d1bbf..a1e7eba 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -27,7 +27,7 @@ func (hr *HostsRouter) Register(subrouter *mux.Router) {
 	hr.subrouter = subrouter
 	subrouter.HandleFunc("", hr.GetHosts).Methods(http.MethodGet).Name("GetHosts")
 	subrouter.HandleFunc("/{hostname}", hr.GetHost).Methods(http.MethodGet).Name("GetHost")
-	subrouter.HandleFunc("/{hostname}/stats", hr.getStats).Methods(http.MethodGet).Name("GetStats")
+	subrouter.HandleFunc("/{hostname}/stats", hr.GetStats).Methods(http.MethodGet).Name("GetStats")
 	subrouter.HandleFunc("/{hostname}/stats", hr.postStats).Methods(http.MethodPost).Name("PostStats")
 }
 
@@ -86,7 +86,8 @@ func (hr *HostsRouter) GetHost(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(host)
 }
 
-func (hr *HostsRouter) getStats(w http.ResponseWriter, r *http.Request) {
+// GetStats is a HandleFunc to get paginated stats for a host.
+func (hr *HostsRouter) GetStats(w http.ResponseWriter, r *http.Request) {
 	hostname := mux.Vars(r)["hostname"]
 
 	pagination, err := hr.getSkipAndLimit(r)
@@ -98,8 +99,12 @@ func (hr *HostsRouter) getStats(w http.ResponseWriter, r *http.Request) {
 
 	stats, err := hr.db.GetStatsByHostname(hostname, pagination)
 	if err != nil {
-		if errors.Is(err, db.ErrHostNotFound) || errors.Is(err, db.ErrAllEntriesSkipped) {
-			http.Error(w, err.Error(), http.StatusNotFound)
+		if errors.Is(err, db.ErrHostNotFound) {
+			http.Error(w, fmt.Sprintf("No host with the name '%s' found", hostname), http.StatusNotFound)
+			logNotFound.Error(err)
+			return
+		} else if errors.Is(err, db.ErrAllEntriesSkipped) {
+			http.Error(w, err.Error(), http.StatusBadRequest)
 			logNotFound.Error(err)
 			return
 		}
diff --git a/controller/hosts_test.go b/controller/hosts_test.go
index 224c3e6..cc19f3d 100644
--- a/controller/hosts_test.go
+++ b/controller/hosts_test.go
@@ -254,7 +254,7 @@ func TestGetHost(t *testing.T) {
 		hostDB.SetHost(hostInfo)
 		hostsRouter := controller.NewHostsRouter(hostDB)
 
-		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		req, err := http.NewRequest("GET", "/"+hostname, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -274,7 +274,7 @@ func TestGetHost(t *testing.T) {
 		hostDB.SetHost(hostInfo)
 		hostsRouter := controller.NewHostsRouter(hostDB)
 
-		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		req, err := http.NewRequest("GET", "/"+hostname, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -285,6 +285,7 @@ func TestGetHost(t *testing.T) {
 		handler.ServeHTTP(rr, req)
 
 		assert.Equal(t, http.StatusOK, rr.Code)
+		assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
 
 		var gotBody db.HostInfo
 		err = json.NewDecoder(rr.Body).Decode(&gotBody)
@@ -301,7 +302,7 @@ func TestGetHost(t *testing.T) {
 		hostDB.SetHostError(db.ErrHostNotFound)
 		hostsRouter := controller.NewHostsRouter(hostDB)
 
-		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		req, err := http.NewRequest("GET", "/"+hostname, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -324,7 +325,7 @@ func TestGetHost(t *testing.T) {
 		hostDB.SetHostError(unknownErr)
 		hostsRouter := controller.NewHostsRouter(hostDB)
 
-		req, err := http.NewRequest("GET", "/host/"+hostname, nil)
+		req, err := http.NewRequest("GET", "/"+hostname, nil)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -341,6 +342,254 @@ func TestGetHost(t *testing.T) {
 	})
 }
 
+func TestGetStat(t *testing.T) {
+	t.Run("pagination", func(t *testing.T) {
+		t.Run("default pagination has a limit of 10", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 10, hostDB.GetPagination().Limit)
+		})
+
+		t.Run("default pagination has a skip of 0", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 0, hostDB.GetPagination().Skip)
+		})
+
+		t.Run("sets custom limit", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats?limit=2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusOK, rr.Code)
+
+			assert.NotEqual(t, db.Pagination{}, hostDB.GetPagination())
+			assert.Equal(t, 2, hostDB.GetPagination().Limit)
+		})
+
+		t.Run("sets negative limit", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats?limit=-2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "No negative number allowed for the query param 'limit'\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets negative skip", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats?skip=-2", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "No negative number allowed for the query param 'skip'\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets skip to not a number", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats?skip=a", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "Query param 'skip' expected to be a number: a is not a number\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+
+		t.Run("sets limit to not a number", func(t *testing.T) {
+			hostname := "foo"
+			hostDB := &MockHostDB{}
+			hostsRouter := controller.NewHostsRouter(hostDB)
+
+			req, err := http.NewRequest("GET", "/"+hostname+"/stats?limit=a", nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			rr := httptest.NewRecorder()
+			handler := http.HandlerFunc(hostsRouter.GetStats)
+			handler.ServeHTTP(rr, req)
+
+			assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+			wantErr := "Query param 'limit' expected to be a number: a is not a number\n"
+			assert.Equal(t, wantErr, rr.Body.String())
+		})
+	})
+
+	t.Run("search with hostname from url", func(t *testing.T) {
+		hostname := "foo"
+		hostInfo := db.HostInfo{Hostname: hostname, DataPoints: 1}
+		hostDB := &MockHostDB{}
+		hostDB.SetHost(hostInfo)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, hostname, hostDB.GetStatsByHostnameHostname())
+	})
+
+	t.Run("db has an item", func(t *testing.T) {
+		hostname := "foo"
+		stats := []db.Stats{{Hostname: hostname}}
+		hostDB := &MockHostDB{}
+		hostDB.SetStatsByHostname(stats)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusOK, rr.Code)
+
+		var gotBody []db.Stats
+		err = json.NewDecoder(rr.Body).Decode(&gotBody)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, stats, gotBody)
+	})
+
+	t.Run("db returns not found error", func(t *testing.T) {
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostDB.SetStatsByHostnameError(db.ErrHostNotFound)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusNotFound, rr.Code)
+
+		wantErr := fmt.Sprintf("No host with the name '%s' found\n", hostname)
+		assert.Equal(t, wantErr, rr.Body.String())
+	})
+
+	t.Run("db returns all entries skipped error", func(t *testing.T) {
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostDB.SetStatsByHostnameError(db.ErrAllEntriesSkipped)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusBadRequest, rr.Code)
+
+		wantErr := "db: All entries skipped\n"
+		assert.Equal(t, wantErr, rr.Body.String())
+	})
+
+	t.Run("db returns unknown error", func(t *testing.T) {
+		unknownErr := errors.New("unknown error")
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostDB.SetStatsByHostnameError(unknownErr)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("GET", "/"+hostname+"/stats", nil)
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.GetStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusInternalServerError, rr.Code)
+
+		wantErr := fmt.Sprintf("%s\n", unknownErr.Error())
+		assert.Equal(t, wantErr, rr.Body.String())
+	})
+}
+
 type MockHostDB struct {
 	hosts      []db.HostInfo
 	hostsError error
@@ -349,8 +598,9 @@ type MockHostDB struct {
 	hostError error
 	hostname  string
 
-	stats      []db.Stats
-	statsError error
+	stats         []db.Stats
+	statsError    error
+	statsHostname string
 
 	insertedStat      db.Stats
 	insertedStatError error
@@ -398,9 +648,14 @@ func (m *MockHostDB) SetStatsByHostname(stats []db.Stats) {
 func (m *MockHostDB) SetStatsByHostnameError(err error) {
 	m.statsError = err
 }
+func (m *MockHostDB) GetStatsByHostnameHostname() string {
+	return m.statsHostname
+}
 func (m *MockHostDB) GetStatsByHostname(hostname string, pagination db.Pagination) ([]db.Stats, error) {
+	m.statsHostname = hostname
+	m.pagination = pagination
 	if m.statsError != nil {
-		return []db.Stats{}, m.hostError
+		return []db.Stats{}, m.statsError
 	}
 	return m.stats, nil
 }

From 445055c33294e8ee2b8dc30828e97381b87a836c Mon Sep 17 00:00:00 2001
From: Augusto Dwenger <hamburghammer@gmail.com>
Date: Mon, 26 Oct 2020 22:29:57 +0100
Subject: [PATCH 11/11] Add error better handling for PostStats func

---
 controller/hosts.go      | 10 ++---
 controller/hosts_test.go | 90 ++++++++++++++++++++++++++++++++++++----
 2 files changed, 87 insertions(+), 13 deletions(-)

diff --git a/controller/hosts.go b/controller/hosts.go
index a1e7eba..a0299a8 100644
--- a/controller/hosts.go
+++ b/controller/hosts.go
@@ -28,7 +28,7 @@ func (hr *HostsRouter) Register(subrouter *mux.Router) {
 	subrouter.HandleFunc("", hr.GetHosts).Methods(http.MethodGet).Name("GetHosts")
 	subrouter.HandleFunc("/{hostname}", hr.GetHost).Methods(http.MethodGet).Name("GetHost")
 	subrouter.HandleFunc("/{hostname}/stats", hr.GetStats).Methods(http.MethodGet).Name("GetStats")
-	subrouter.HandleFunc("/{hostname}/stats", hr.postStats).Methods(http.MethodPost).Name("PostStats")
+	subrouter.HandleFunc("/{hostname}/stats", hr.PostStats).Methods(http.MethodPost).Name("PostStats")
 }
 
 // GetPrefix returns the the pre route for this controller.
@@ -117,17 +117,17 @@ func (hr *HostsRouter) GetStats(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(stats)
 }
 
-func (hr *HostsRouter) postStats(w http.ResponseWriter, r *http.Request) {
+// PostStats is a HandleFunc to insert a new data point into the db.
+func (hr *HostsRouter) PostStats(w http.ResponseWriter, r *http.Request) {
 	hostname := mux.Vars(r)["hostname"]
 
 	var stats db.Stats
 	err := json.NewDecoder(r.Body).Decode(&stats)
 	if err != nil {
-		http.Error(w, err.Error(), http.StatusBadRequest)
-		logBadRequest.Error(err)
+		http.Error(w, "Could not read the body", http.StatusBadRequest)
+		logBadRequest.Error(fmt.Sprintf("JSON error decoding new stat: %v", err))
 		return
 	}
-	logPackage.Debugf("Received stat: %+v", stats)
 
 	err = hr.db.InsertStats(hostname, stats)
 	if err != nil {
diff --git a/controller/hosts_test.go b/controller/hosts_test.go
index cc19f3d..a029958 100644
--- a/controller/hosts_test.go
+++ b/controller/hosts_test.go
@@ -1,6 +1,7 @@
 package controller_test
 
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -512,6 +513,7 @@ func TestGetStat(t *testing.T) {
 		handler.ServeHTTP(rr, req)
 
 		assert.Equal(t, http.StatusOK, rr.Code)
+		assert.Equal(t, hostname, hostDB.GetStatsByHostnameHostname())
 
 		var gotBody []db.Stats
 		err = json.NewDecoder(rr.Body).Decode(&gotBody)
@@ -590,22 +592,89 @@ func TestGetStat(t *testing.T) {
 	})
 }
 
+func TestInsertStat(t *testing.T) {
+	t.Run("insert new stat into the db", func(t *testing.T) {
+		hostname := "foo"
+		stat := db.Stats{Hostname: hostname}
+		hostDB := &MockHostDB{}
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		requestBody, _ := json.Marshal(stat)
+		req, err := http.NewRequest("POST", "/"+hostname+"/stats", bytes.NewBuffer(requestBody))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.PostStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusCreated, rr.Code)
+		assert.Equal(t, hostname, hostDB.GetInsertStatsHostname())
+
+		assert.Equal(t, stat, hostDB.GetInsertedStats())
+	})
+
+	t.Run("missing body", func(t *testing.T) {
+		hostname := "foo"
+		hostDB := &MockHostDB{}
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		req, err := http.NewRequest("POST", "/"+hostname+"/stats", bytes.NewBuffer([]byte{}))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.PostStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusBadRequest, rr.Code)
+		assert.Equal(t, "Could not read the body\n", rr.Body.String())
+	})
+
+	t.Run("db returns an unknown error", func(t *testing.T) {
+		hostname := "foo"
+		unknownErr := errors.New("unknown error")
+		stat := db.Stats{Hostname: hostname}
+		hostDB := &MockHostDB{}
+		hostDB.SetInsertStatsError(unknownErr)
+		hostsRouter := controller.NewHostsRouter(hostDB)
+
+		requestBody, _ := json.Marshal(stat)
+		req, err := http.NewRequest("POST", "/"+hostname+"/stats", bytes.NewBuffer(requestBody))
+		if err != nil {
+			t.Fatal(err)
+		}
+		req = mux.SetURLVars(req, map[string]string{"hostname": hostname})
+
+		rr := httptest.NewRecorder()
+		handler := http.HandlerFunc(hostsRouter.PostStats)
+		handler.ServeHTTP(rr, req)
+
+		assert.Equal(t, http.StatusInternalServerError, rr.Code)
+		assert.Equal(t, "Something with the DB went wrong.\n", rr.Body.String())
+	})
+
+}
+
 type MockHostDB struct {
 	hosts      []db.HostInfo
 	hostsError error
 
 	host      db.HostInfo
 	hostError error
-	hostname  string
 
-	stats         []db.Stats
-	statsError    error
-	statsHostname string
+	stats      []db.Stats
+	statsError error
 
 	insertedStat      db.Stats
 	insertedStatError error
 
 	pagination db.Pagination
+	hostname   string
 }
 
 // GetHosts
@@ -649,10 +718,10 @@ func (m *MockHostDB) SetStatsByHostnameError(err error) {
 	m.statsError = err
 }
 func (m *MockHostDB) GetStatsByHostnameHostname() string {
-	return m.statsHostname
+	return m.hostname
 }
 func (m *MockHostDB) GetStatsByHostname(hostname string, pagination db.Pagination) ([]db.Stats, error) {
-	m.statsHostname = hostname
+	m.hostname = hostname
 	m.pagination = pagination
 	if m.statsError != nil {
 		return []db.Stats{}, m.statsError
@@ -667,9 +736,14 @@ func (m *MockHostDB) GetInsertedStats() db.Stats {
 func (m *MockHostDB) SetInsertStatsError(err error) {
 	m.insertedStatError = err
 }
+func (m *MockHostDB) GetInsertStatsHostname() string {
+	return m.hostname
+}
 func (m *MockHostDB) InsertStats(hostname string, stats db.Stats) error {
-	if m.statsError != nil {
-		return m.hostError
+	m.hostname = hostname
+	m.insertedStat = stats
+	if m.insertedStatError != nil {
+		return m.insertedStatError
 	}
 	return nil
 }