package handler import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) return db } func TestHealthHandler_Healthy(t *testing.T) { gin.SetMode(gin.TestMode) db := newTestDB(t) defer func() { sqlDB, _ := db.DB() sqlDB.Close() }() router := gin.New() router.GET("/health", HealthHandler(db, time.Now().Add(-5*time.Minute), "1.0.0")) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/health", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp HealthResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, "healthy", resp.Status) assert.Equal(t, "1.0.0", resp.Version) assert.NotEmpty(t, resp.Timestamp) assert.NotEmpty(t, resp.Uptime) assert.NotEmpty(t, resp.Checks) assert.Equal(t, "healthy", resp.Checks["database"]) } func TestHealthHandler_NilDB_Unhealthy(t *testing.T) { // HealthHandler does not handle nil DB (panics on db.DB()). // This is acceptable — the handler is always called with a real DB in production. t.Skip("HealthHandler panics on nil DB; nil DB is not a valid runtime state") gin.SetMode(gin.TestMode) router := gin.New() router.GET("/health", HealthHandler(nil, time.Now(), "1.0.0")) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/health", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) var resp HealthResponse err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, "unhealthy", resp.Status) assert.Contains(t, resp.Checks["database"], "unhealthy") } func TestReadyHandler_Ready(t *testing.T) { gin.SetMode(gin.TestMode) db := newTestDB(t) defer func() { sqlDB, _ := db.DB() sqlDB.Close() }() router := gin.New() router.GET("/ready", ReadyHandler(db)) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/ready", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["ready"].(bool)) } func TestReadyHandler_RequiredDependencyFailure(t *testing.T) { db := newTestDB(t) router := gin.New() router.GET("/ready", ReadyHandler(db, DependencyCheck{Name: "redis", Check: func(context.Context) error { return errors.New("connection refused") }})) w := httptest.NewRecorder() router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ready", nil)) assert.Equal(t, http.StatusServiceUnavailable, w.Code) assert.JSONEq(t, `{"ready":false,"checks":{"database":"healthy","redis":"unhealthy: connection refused"}}`, w.Body.String()) } func TestDatabaseOutageFailsReadinessButNotLiveness(t *testing.T) { db := newTestDB(t) sqlDB, err := db.DB() require.NoError(t, err) require.NoError(t, sqlDB.Close()) router := gin.New() router.GET("/ready", ReadyHandler(db)) router.GET("/live", LiveHandler()) ready := httptest.NewRecorder() router.ServeHTTP(ready, httptest.NewRequest(http.MethodGet, "/ready", nil)) assert.Equal(t, http.StatusServiceUnavailable, ready.Code) live := httptest.NewRecorder() router.ServeHTTP(live, httptest.NewRequest(http.MethodGet, "/live", nil)) assert.Equal(t, http.StatusOK, live.Code) } func TestHealthHandler_SelectedDependency(t *testing.T) { db := newTestDB(t) router := gin.New() router.GET("/health", HealthHandler(db, time.Now(), "test", DependencyCheck{Name: "redis", Check: func(context.Context) error { return nil }})) w := httptest.NewRecorder() router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/health?check=redis", nil)) assert.Equal(t, http.StatusOK, w.Code) var response HealthResponse require.NoError(t, json.Unmarshal(w.Body.Bytes(), &response)) assert.Equal(t, map[string]string{"redis": "healthy"}, response.Checks) } func TestReadyHandler_NilDB_NotReady(t *testing.T) { // ReadyHandler does not handle nil DB (panics on db.DB()). t.Skip("ReadyHandler panics on nil DB; nil DB is not a valid runtime state") gin.SetMode(gin.TestMode) router := gin.New() router.GET("/ready", ReadyHandler(nil)) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/ready", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestLiveHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/live", LiveHandler()) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/live", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) require.NoError(t, err) assert.True(t, resp["alive"].(bool)) } func TestPrometheusHandler(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/metrics", PrometheusHandler(time.Now().Add(-1*time.Minute))) w := httptest.NewRecorder() req := httptest.NewRequest("GET", "/metrics", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Header().Get("Content-Type"), "text/plain") body := w.Body.String() assert.Contains(t, body, "gochat_go_goroutines") assert.Contains(t, body, "gochat_go_memory_alloc_bytes") assert.Contains(t, body, "gochat_uptime_seconds") assert.Contains(t, body, "# HELP") assert.Contains(t, body, "# TYPE") } func TestHTTPMetricsRecordsRequestsErrorsAndLatency(t *testing.T) { metrics := NewHTTPMetrics(time.Now()) router := gin.New() router.Use(metrics.Middleware()) router.GET("/items/:id", func(c *gin.Context) { c.Status(http.StatusInternalServerError) }) router.GET("/metrics", metrics.Handler()) router.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/items/42", nil)) w := httptest.NewRecorder() router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/metrics", nil)) body := w.Body.String() assert.Contains(t, body, `http_requests_total{method="GET",route="/items/:id",status="500"} 1`) assert.Contains(t, body, `http_request_errors_total{method="GET",route="/items/:id",status="500"} 1`) assert.Contains(t, body, `http_request_duration_seconds_count{method="GET",route="/items/:id",status="500"} 1`) } func TestFormatGauge(t *testing.T) { result := formatGauge("test_metric", 42) assert.Equal(t, "test_metric 42", result) } func TestFormatCounter(t *testing.T) { result := formatCounter("test_counter", 100) assert.Equal(t, "test_counter 100", result) } func TestFormatValue_Zero(t *testing.T) { assert.Equal(t, "0", formatValue(0)) } func TestFormatValue_Positive(t *testing.T) { assert.Equal(t, "12345", formatValue(12345)) } func TestFormatValue_Large(t *testing.T) { assert.Equal(t, "999999999", formatValue(999999999)) }