diff --git a/main.go b/main.go index 32b4971..0cf09d0 100644 --- a/main.go +++ b/main.go @@ -136,11 +136,7 @@ func Setup() (*fiber.App, *webhooks.Debouncer, func(), *gorm.DB) { // Automatically recover panics in handlers app.Use(recover.New()) - app.Use(healthcheck.New(healthcheck.Config{ - ReadinessProbe: func(c *fiber.Ctx) bool { - return gormDB.Exec("SELECT 1").Error == nil - }, - })) + app.Use(newHealthcheck(gormDB)) // Use Fiber Rate API Limiter if !common.EnvironmentConfig.IsTest() && common.EnvironmentConfig.MaxRequests != 0 { @@ -172,6 +168,14 @@ func Setup() (*fiber.App, *webhooks.Debouncer, func(), *gorm.DB) { return app, debouncer, stopEventPurge, gormDB } +func newHealthcheck(gormDB *gorm.DB) fiber.Handler { + return healthcheck.New(healthcheck.Config{ + ReadinessProbe: func(c *fiber.Ctx) bool { + return gormDB.Exec("SELECT 1").Error == nil + }, + }) +} + // requiresAuth reports whether a request must carry a valid token. Reads // are public except for webhook configuration and the audit trail, whose // GET operations are marked as authenticated in the OpenAPI contract. diff --git a/status_test.go b/status_test.go index 2e35575..29b73af 100644 --- a/status_test.go +++ b/status_test.go @@ -1,10 +1,14 @@ package main import ( + "io" "testing" + "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestHealthEndpoints(t *testing.T) { @@ -44,3 +48,52 @@ func TestStatusCacheHeaders(t *testing.T) { assert.Equal(t, 204, res.StatusCode) assert.Equal(t, "no-cache", res.Header.Get("Cache-Control")) } + +func TestHealthEndpointsWhenDatabaseUnavailable(t *testing.T) { + unavailableDB, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + require.NoError(t, err) + + pool, err := unavailableDB.DB() + require.NoError(t, err) + require.NoError(t, pool.Close()) + + healthApp := fiber.New() + healthApp.Use(newHealthcheck(unavailableDB)) + t.Cleanup(func() { require.NoError(t, healthApp.Shutdown()) }) + + tests := []struct { + path string + expectedCode int + expectedBody string + expectedContentType string + }{ + { + path: "/livez", + expectedCode: 200, + expectedBody: "OK", + expectedContentType: "text/plain; charset=utf-8", + }, + { + path: "/readyz", + expectedCode: 503, + expectedBody: "Service Unavailable", + expectedContentType: "text/plain; charset=utf-8", + }, + } + + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + req, err := newTestRequest("GET", test.path, nil) + require.NoError(t, err) + + res, err := healthApp.Test(req, -1) + require.NoError(t, err) + + assert.Equal(t, test.expectedCode, res.StatusCode) + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, test.expectedBody, string(body)) + assert.Equal(t, test.expectedContentType, res.Header.Get("Content-Type")) + }) + } +}