Merge branch 'feature/hosts-testing' into develop

Add CI/CD pipeline with drone ci
Add application logging
Add flags to configure the server
This commit is contained in:
Augusto Dwenger 2020-10-26 22:37:15 +01:00
commit 96f1638564
8 changed files with 897 additions and 38 deletions

37
.drone.yml Normal file
View file

@ -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: {}

View file

@ -1,12 +1,14 @@
# 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.
## TODO
- [] Configure CI/CD pipeline
- [] HTTP testing
- [] Auth Middleware with Basic Auth
- [] Flag configurable
- [] Logging
- [] SQLite DB implementation
- [] Env Variable configurable
- [x] Configure CI/CD pipeline
- [x] HTTP testing
- [ ] Auth Middleware with Basic Auth
- [x] Flag configurable
- [x] Logging
- [ ] SQLite DB implementation
- [ ] Env Variable configurable

View file

@ -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.

View file

@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"strconv"
@ -26,10 +25,10 @@ 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("/{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("", 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")
}
// GetPrefix returns the the pre route for this controller.
@ -42,10 +41,12 @@ func (hr *HostsRouter) GetRouteName() string {
return "Hosts"
}
func (hr *HostsRouter) getHosts(w http.ResponseWriter, r *http.Request) {
// 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 {
http.Error(w, err.Error(), http.StatusBadRequest)
logBadRequest.Error(err)
return
}
@ -53,9 +54,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
}
@ -63,41 +66,50 @@ 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"]
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)
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")
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)
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) {
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
}
http.Error(w, err.Error(), http.StatusInternalServerError)
logInternalServerError.Error(err)
return
}
@ -105,23 +117,25 @@ 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)
log.Println(err)
http.Error(w, "Could not read the body", http.StatusBadRequest)
logBadRequest.Error(fmt.Sprintf("JSON error decoding new stat: %v", err))
return
}
log.Printf("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)
}
// getSkipAndLimit from the query of the request.
@ -137,6 +151,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 == "" {
@ -146,6 +163,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
}

753
controller/hosts_test.go Normal file
View file

@ -0,0 +1,753 @@
package controller_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/hamburghammer/gsave/controller"
"github.com/hamburghammer/gsave/db"
"github.com/stretchr/testify/assert"
)
func TestHostsRouter_GetHosts(t *testing.T) {
t.Run("db has one item", 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("db is empty", func(t *testing.T) {
stats := []db.HostInfo{}
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)
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())
})
})
}
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", "/"+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", "/"+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)
assert.Equal(t, "application/json", rr.Header().Get("Content-Type"))
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", "/"+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", "/"+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())
})
}
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)
assert.Equal(t, hostname, hostDB.GetStatsByHostnameHostname())
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())
})
}
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
stats []db.Stats
statsError error
insertedStat db.Stats
insertedStatError error
pagination db.Pagination
hostname string
}
// 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) {
m.pagination = pagination
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) 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
}
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) GetStatsByHostnameHostname() string {
return m.hostname
}
func (m *MockHostDB) GetStatsByHostname(hostname string, pagination db.Pagination) ([]db.Stats, error) {
m.hostname = hostname
m.pagination = pagination
if m.statsError != nil {
return []db.Stats{}, m.statsError
}
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) GetInsertStatsHostname() string {
return m.hostname
}
func (m *MockHostDB) InsertStats(hostname string, stats db.Stats) error {
m.hostname = hostname
m.insertedStat = stats
if m.insertedStatError != nil {
return m.insertedStatError
}
return nil
}
func (m *MockHostDB) GetPagination() db.Pagination {
return m.pagination
}

1
go.mod
View file

@ -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
)

7
go.sum
View file

@ -1,12 +1,19 @@
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=
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=

48
main.go
View file

@ -3,8 +3,8 @@ package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
@ -14,12 +14,38 @@ import (
"github.com/gorilla/mux"
"github.com/hamburghammer/gsave/controller"
"github.com/hamburghammer/gsave/db"
log "github.com/sirupsen/logrus"
)
const serveAddr = "127.0.0.1:8080"
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},
@ -27,19 +53,19 @@ 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: serveAddr,
Addr: fmt.Sprintf(":%d", servePort),
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
@ -75,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://%s\n", serveAddr)
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)
}
}
@ -96,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)
}
}