diff --git a/api/health.go b/api/health.go new file mode 100644 index 0000000..6dd5c25 --- /dev/null +++ b/api/health.go @@ -0,0 +1,46 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/gotify/server/model" +) + +// The HealthDatabase interface for encapsulating database access. +type HealthDatabase interface { + Ping() error +} + +// The HealthAPI provides handlers for the health information. +type HealthAPI struct { + DB HealthDatabase +} + +// Health returns health information. +// swagger:operation GET /health health getHealth +// +// Get health information. +// +// --- +// produces: [application/json] +// responses: +// 200: +// description: Ok +// schema: +// $ref: "#/definitions/Health" +// 500: +// description: Ok +// schema: +// $ref: "#/definitions/Health" +func (a *HealthAPI) Health(ctx *gin.Context) { + if err := a.DB.Ping(); err != nil { + ctx.JSON(500, model.Health{ + Health: model.StatusOrange, + Database: model.StatusRed, + }) + return + } + ctx.JSON(200, model.Health{ + Health: model.StatusGreen, + Database: model.StatusGreen, + }) +} diff --git a/api/health_test.go b/api/health_test.go new file mode 100644 index 0000000..40ad30e --- /dev/null +++ b/api/health_test.go @@ -0,0 +1,49 @@ +package api + +import ( + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/gotify/server/mode" + "github.com/gotify/server/model" + "github.com/gotify/server/test" + "github.com/gotify/server/test/testdb" + "github.com/stretchr/testify/suite" +) + +func TestHealthSuite(t *testing.T) { + suite.Run(t, new(HealthSuite)) +} + +type HealthSuite struct { + suite.Suite + db *testdb.Database + a *HealthAPI + ctx *gin.Context + recorder *httptest.ResponseRecorder +} + +func (s *HealthSuite) BeforeTest(suiteName, testName string) { + mode.Set(mode.TestDev) + s.recorder = httptest.NewRecorder() + s.db = testdb.NewDB(s.T()) + s.ctx, _ = gin.CreateTestContext(s.recorder) + withURL(s.ctx, "http", "example.com") + s.a = &HealthAPI{DB: s.db} +} + +func (s *HealthSuite) AfterTest(suiteName, testName string) { + s.db.Close() +} + +func (s *HealthSuite) TestHealthSuccess() { + s.a.Health(s.ctx) + test.BodyEquals(s.T(), model.Health{Health: model.StatusGreen, Database: model.StatusGreen}, s.recorder) +} + +func (s *HealthSuite) TestDatabaseFailure() { + s.db.Close() + s.a.Health(s.ctx) + test.BodyEquals(s.T(), model.Health{Health: model.StatusOrange, Database: model.StatusRed}, s.recorder) +} diff --git a/database/ping.go b/database/ping.go new file mode 100644 index 0000000..181bc4a --- /dev/null +++ b/database/ping.go @@ -0,0 +1,6 @@ +package database + +// Ping pings the database to verify the connection. +func (d *GormDatabase) Ping() error { + return d.DB.DB().Ping() +} diff --git a/database/ping_test.go b/database/ping_test.go new file mode 100644 index 0000000..284ee31 --- /dev/null +++ b/database/ping_test.go @@ -0,0 +1,16 @@ +package database + +import ( + "github.com/stretchr/testify/assert" +) + +func (s *DatabaseSuite) TestPing_onValidDB() { + err := s.db.Ping() + assert.NoError(s.T(), err) +} + +func (s *DatabaseSuite) TestPing_onClosedDB() { + s.db.Close() + err := s.db.Ping() + assert.Error(s.T(), err) +} diff --git a/docs/spec.json b/docs/spec.json index 5f54396..54c8076 100644 --- a/docs/spec.json +++ b/docs/spec.json @@ -839,6 +839,32 @@ } } }, + "/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Get health information.", + "operationId": "getHealth", + "responses": { + "200": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/Health" + } + }, + "500": { + "description": "Ok", + "schema": { + "$ref": "#/definitions/Health" + } + } + } + } + }, "/message": { "get": { "security": [ @@ -1969,6 +1995,30 @@ }, "x-go-package": "github.com/gotify/server/model" }, + "Health": { + "description": "Health represents how healthy the application is.", + "type": "object", + "title": "Health Model", + "required": [ + "health", + "database" + ], + "properties": { + "database": { + "description": "The health of the database connection.", + "type": "string", + "x-go-name": "Database", + "example": "green" + }, + "health": { + "description": "The health of the overall application.", + "type": "string", + "x-go-name": "Health", + "example": "green" + } + }, + "x-go-package": "github.com/gotify/server/model" + }, "Message": { "description": "The MessageExternal holds information about a message which was sent by an Application.", "type": "object", diff --git a/model/health.go b/model/health.go new file mode 100644 index 0000000..1cc5101 --- /dev/null +++ b/model/health.go @@ -0,0 +1,28 @@ +package model + +// Health Model +// +// Health represents how healthy the application is. +// +// swagger:model Health +type Health struct { + // The health of the overall application. + // + // required: true + // example: green + Health string `json:"health"` + // The health of the database connection. + // + // required: true + // example: green + Database string `json:"database"` +} + +const ( + // StatusGreen everything is alright. + StatusGreen = "green" + // StatusOrange some things are alright. + StatusOrange = "orange" + // StatusRed nothing is alright. + StatusRed = "red" +) diff --git a/router/router.go b/router/router.go index 79817a7..520d8a5 100644 --- a/router/router.go +++ b/router/router.go @@ -29,6 +29,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co streamHandler := stream.New(200*time.Second, 15*time.Second, conf.Server.Stream.AllowedOrigins) authentication := auth.Auth{DB: db} messageHandler := api.MessageAPI{Notifier: streamHandler, DB: db} + healthHandler := api.HealthAPI{DB: db} clientHandler := api.ClientAPI{ DB: db, ImageDir: conf.UploadedImagesDir, @@ -57,6 +58,7 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co ui.Register(g) + g.GET("/health", healthHandler.Health) g.GET("/swagger", docs.Serve) g.Static("/image", conf.UploadedImagesDir) g.GET("/docs", docs.UI)