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 +[](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 }