mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
Add several tests
This commit is contained in:
parent
1e0d9e184c
commit
9196f57487
5
Makefile
5
Makefile
@ -8,4 +8,7 @@ build-frontend:
|
|||||||
npm --prefix web/app run build
|
npm --prefix web/app run build
|
||||||
|
|
||||||
run-frontend:
|
run-frontend:
|
||||||
npm --prefix web/app run serve
|
npm --prefix web/app run serve
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -mod=vendor ./... -cover
|
@ -72,7 +72,7 @@ type Config struct {
|
|||||||
Kubernetes *k8s.Config `yaml:"kubernetes"`
|
Kubernetes *k8s.Config `yaml:"kubernetes"`
|
||||||
|
|
||||||
// Web is the configuration for the web listener
|
// Web is the configuration for the web listener
|
||||||
Web *webConfig `yaml:"web"`
|
Web *WebConfig `yaml:"web"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the configuration, or panics if the configuration hasn't loaded yet
|
// Get returns the configuration, or panics if the configuration hasn't loaded yet
|
||||||
@ -150,7 +150,7 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
|
|
||||||
func validateWebConfig(config *Config) {
|
func validateWebConfig(config *Config) {
|
||||||
if config.Web == nil {
|
if config.Web == nil {
|
||||||
config.Web = &webConfig{Address: DefaultAddress, Port: DefaultPort}
|
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
||||||
} else {
|
} else {
|
||||||
config.Web.validateAndSetDefaults()
|
config.Web.validateAndSetDefaults()
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
// webConfig is the structure which supports the configuration of the endpoint
|
// WebConfig is the structure which supports the configuration of the endpoint
|
||||||
// which provides access to the web frontend
|
// which provides access to the web frontend
|
||||||
type webConfig struct {
|
type WebConfig struct {
|
||||||
// Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress)
|
// Address to listen on (defaults to 0.0.0.0 specified by DefaultAddress)
|
||||||
Address string `yaml:"address"`
|
Address string `yaml:"address"`
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ type webConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
||||||
func (web *webConfig) validateAndSetDefaults() {
|
func (web *WebConfig) validateAndSetDefaults() {
|
||||||
// Validate the Address
|
// Validate the Address
|
||||||
if len(web.Address) == 0 {
|
if len(web.Address) == 0 {
|
||||||
web.Address = DefaultAddress
|
web.Address = DefaultAddress
|
||||||
@ -30,6 +30,6 @@ func (web *webConfig) validateAndSetDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SocketAddress returns the combination of the Address and the Port
|
// SocketAddress returns the combination of the Address and the Port
|
||||||
func (web *webConfig) SocketAddress() string {
|
func (web *WebConfig) SocketAddress() string {
|
||||||
return fmt.Sprintf("%s:%d", web.Address, web.Port)
|
return fmt.Sprintf("%s:%d", web.Address, web.Port)
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestWebConfig_SocketAddress(t *testing.T) {
|
func TestWebConfig_SocketAddress(t *testing.T) {
|
||||||
web := &webConfig{
|
web := &WebConfig{
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
Port: 8081,
|
Port: 8081,
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,14 @@ const (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.LeastRecentlyUsed)
|
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.LeastRecentlyUsed)
|
||||||
|
|
||||||
|
// staticFolder is the path to the location of the static folder from the root path of the project
|
||||||
|
// The only reason this is exposed is to allow running tests from a different path than the root path of the project
|
||||||
|
staticFolder = "./web/static"
|
||||||
|
|
||||||
|
// server is the http.Server created by Handle.
|
||||||
|
// The only reason it exists is for testing purposes.
|
||||||
|
server *http.Server
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -40,7 +48,7 @@ func Handle() {
|
|||||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||||
router = developmentCorsHandler(router)
|
router = developmentCorsHandler(router)
|
||||||
}
|
}
|
||||||
server := &http.Server{
|
server = &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
Addr: fmt.Sprintf("%s:%d", cfg.Web.Address, cfg.Web.Port),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
@ -48,22 +56,27 @@ func Handle() {
|
|||||||
IdleTimeout: 15 * time.Second,
|
IdleTimeout: 15 * time.Second,
|
||||||
}
|
}
|
||||||
log.Println("[controller][Handle] Listening on" + cfg.Web.SocketAddress())
|
log.Println("[controller][Handle] Listening on" + cfg.Web.SocketAddress())
|
||||||
|
if os.Getenv("ROUTER_TEST") == "true" {
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Fatal(server.ListenAndServe())
|
log.Fatal(server.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRouter creates the router for the http server
|
// CreateRouter creates the router for the http server
|
||||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
|
|
||||||
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
|
||||||
router.HandleFunc("/api/v1/statuses", secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET")
|
|
||||||
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
|
||||||
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
|
||||||
router.HandleFunc("/health", healthHandler).Methods("GET")
|
|
||||||
if cfg.Metrics {
|
if cfg.Metrics {
|
||||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||||
}
|
}
|
||||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir("./web/static"))))
|
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
||||||
|
router.HandleFunc("/health", healthHandler).Methods("GET")
|
||||||
|
router.HandleFunc("/api/v1/statuses", secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content
|
||||||
|
router.HandleFunc("/api/v1/statuses/{key}", secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
|
router.HandleFunc("/api/v1/badges/uptime/{duration}/{identifier}", badgeHandler).Methods("GET")
|
||||||
|
// SPA
|
||||||
|
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||||
|
// Everything else falls back on static content
|
||||||
|
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,10 +165,10 @@ func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
|||||||
|
|
||||||
// favIconHandler handles requests for /favicon.ico
|
// favIconHandler handles requests for /favicon.ico
|
||||||
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
http.ServeFile(writer, request, "./web/static/favicon.ico")
|
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||||
}
|
}
|
||||||
|
|
||||||
// spaHandler handles requests for /favicon.ico
|
// spaHandler handles requests for /favicon.ico
|
||||||
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
http.ServeFile(writer, request, "./web/static/index.html")
|
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||||
}
|
}
|
||||||
|
169
controller/controller_test.go
Normal file
169
controller/controller_test.go
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TwinProduction/gatus/config"
|
||||||
|
"github.com/TwinProduction/gatus/core"
|
||||||
|
"github.com/TwinProduction/gatus/watchdog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRouter(t *testing.T) {
|
||||||
|
staticFolder = "../web/static"
|
||||||
|
cfg := &config.Config{
|
||||||
|
Metrics: true,
|
||||||
|
Services: []*core.Service{
|
||||||
|
{
|
||||||
|
Name: "frontend",
|
||||||
|
Group: "core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "backend",
|
||||||
|
Group: "core",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
watchdog.UpdateServiceStatuses(cfg.Services[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||||
|
watchdog.UpdateServiceStatuses(cfg.Services[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||||
|
router := CreateRouter(cfg)
|
||||||
|
type Scenario struct {
|
||||||
|
Description string
|
||||||
|
Path string
|
||||||
|
ExpectedCode int
|
||||||
|
Gzip bool
|
||||||
|
}
|
||||||
|
scenarios := []Scenario{
|
||||||
|
{
|
||||||
|
Description: "health",
|
||||||
|
Path: "/health",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "metrics",
|
||||||
|
Path: "/metrics",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "badges-1h",
|
||||||
|
Path: "/api/v1/badges/uptime/1h/core_frontend.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "badges-24h",
|
||||||
|
Path: "/api/v1/badges/uptime/24h/core_backend.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "badges-7d",
|
||||||
|
Path: "/api/v1/badges/uptime/7d/core_frontend.svg",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "badges-with-invalid-duration",
|
||||||
|
Path: "/api/v1/badges/uptime/3d/core_backend.svg",
|
||||||
|
ExpectedCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "badges-for-invalid-key",
|
||||||
|
Path: "/api/v1/badges/uptime/7d/invalid_key.svg",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "service-statuses",
|
||||||
|
Path: "/api/v1/statuses",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "service-statuses-gzip",
|
||||||
|
Path: "/api/v1/statuses",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
Gzip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "service-status",
|
||||||
|
Path: "/api/v1/statuses/core_frontend",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "service-status-gzip",
|
||||||
|
Path: "/api/v1/statuses/core_frontend",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
Gzip: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "service-status-for-invalid-key",
|
||||||
|
Path: "/api/v1/statuses/invalid_key",
|
||||||
|
ExpectedCode: http.StatusNotFound,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "favicon",
|
||||||
|
Path: "/favicon.ico",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "frontend-home",
|
||||||
|
Path: "/",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "frontend-assets",
|
||||||
|
Path: "/js/app.js",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Description: "frontend-service",
|
||||||
|
Path: "/services/core_frontend",
|
||||||
|
ExpectedCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Description, func(t *testing.T) {
|
||||||
|
request, _ := http.NewRequest("GET", scenario.Path, nil)
|
||||||
|
if scenario.Gzip {
|
||||||
|
request.Header.Set("Accept-Encoding", "gzip")
|
||||||
|
}
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != scenario.ExpectedCode {
|
||||||
|
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, scenario.ExpectedCode, responseRecorder.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
Web: &config.WebConfig{
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
Port: rand.Intn(65534),
|
||||||
|
},
|
||||||
|
Services: []*core.Service{
|
||||||
|
{
|
||||||
|
Name: "frontend",
|
||||||
|
Group: "core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "backend",
|
||||||
|
Group: "core",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.Set(cfg)
|
||||||
|
_ = os.Setenv("ROUTER_TEST", "true")
|
||||||
|
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||||
|
Handle()
|
||||||
|
request, _ := http.NewRequest("GET", "/health", nil)
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
server.Handler.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusOK {
|
||||||
|
t.Error("expected GET /health to return status code 200")
|
||||||
|
}
|
||||||
|
if server == nil {
|
||||||
|
t.Fatal("server should've been set (but because we set ROUTER_TEST, it shouldn't have been started)")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user