package handlers_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" "github.com/Wikid82/charon/backend/internal/api/handlers" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/services" ) func setupUptimeHandlerTest(t *testing.T) (*gin.Engine, *gorm.DB) { t.Helper() db := handlers.OpenTestDB(t) require.NoError(t, db.AutoMigrate(&models.UptimeMonitor{}, &models.UptimeHeartbeat{}, &models.UptimeHost{}, &models.RemoteServer{}, &models.NotificationProvider{}, &models.Notification{}, &models.ProxyHost{})) ns := services.NewNotificationService(db) service := services.NewUptimeService(db, ns) handler := handlers.NewUptimeHandler(service) r := gin.Default() api := r.Group("/api/v1") uptime := api.Group("/uptime") uptime.GET("", handler.List) uptime.GET(":id/history", handler.GetHistory) uptime.PUT(":id", handler.Update) uptime.DELETE(":id", handler.Delete) uptime.POST(":id/check", handler.CheckMonitor) uptime.POST("/sync", handler.Sync) return r, db } func TestUptimeHandler_List(t *testing.T) { r, db := setupUptimeHandlerTest(t) // Seed Monitor monitor := models.UptimeMonitor{ ID: "monitor-1", Name: "Test Monitor", Type: "http", URL: "http://example.com", } db.Create(&monitor) req, _ := http.NewRequest("GET", "/api/v1/uptime", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var list []models.UptimeMonitor err := json.Unmarshal(w.Body.Bytes(), &list) require.NoError(t, err) assert.Len(t, list, 1) assert.Equal(t, "Test Monitor", list[0].Name) } func TestUptimeHandler_GetHistory(t *testing.T) { r, db := setupUptimeHandlerTest(t) // Seed Monitor and Heartbeats monitorID := "monitor-1" monitor := models.UptimeMonitor{ ID: monitorID, Name: "Test Monitor", } db.Create(&monitor) db.Create(&models.UptimeHeartbeat{ MonitorID: monitorID, Status: "up", Latency: 10, CreatedAt: time.Now().Add(-1 * time.Minute), }) db.Create(&models.UptimeHeartbeat{ MonitorID: monitorID, Status: "down", Latency: 0, CreatedAt: time.Now(), }) req, _ := http.NewRequest("GET", "/api/v1/uptime/"+monitorID+"/history", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var history []models.UptimeHeartbeat err := json.Unmarshal(w.Body.Bytes(), &history) require.NoError(t, err) assert.Len(t, history, 2) // Should be ordered by created_at desc assert.Equal(t, "down", history[0].Status) } func TestUptimeHandler_CheckMonitor(t *testing.T) { r, db := setupUptimeHandlerTest(t) // Create monitor monitor := models.UptimeMonitor{ID: "check-mon-1", Name: "Check Monitor", Type: "http", URL: "http://example.com"} db.Create(&monitor) req, _ := http.NewRequest("POST", "/api/v1/uptime/check-mon-1/check", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestUptimeHandler_CheckMonitor_NotFound(t *testing.T) { r, _ := setupUptimeHandlerTest(t) req, _ := http.NewRequest("POST", "/api/v1/uptime/nonexistent/check", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestUptimeHandler_Update(t *testing.T) { t.Run("success", func(t *testing.T) { r, db := setupUptimeHandlerTest(t) monitorID := "monitor-update" monitor := models.UptimeMonitor{ ID: monitorID, Name: "Original Name", Interval: 30, MaxRetries: 3, } db.Create(&monitor) updates := map[string]any{ "interval": 60, "max_retries": 5, } body, _ := json.Marshal(updates) req, _ := http.NewRequest("PUT", "/api/v1/uptime/"+monitorID, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var result models.UptimeMonitor err := json.Unmarshal(w.Body.Bytes(), &result) require.NoError(t, err) assert.Equal(t, 60, result.Interval) assert.Equal(t, 5, result.MaxRetries) }) t.Run("invalid_json", func(t *testing.T) { r, _ := setupUptimeHandlerTest(t) req, _ := http.NewRequest("PUT", "/api/v1/uptime/monitor-1", bytes.NewBuffer([]byte("invalid"))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) }) t.Run("not_found", func(t *testing.T) { r, _ := setupUptimeHandlerTest(t) updates := map[string]any{ "interval": 60, } body, _ := json.Marshal(updates) req, _ := http.NewRequest("PUT", "/api/v1/uptime/nonexistent", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) }) } func TestUptimeHandler_DeleteAndSync(t *testing.T) { t.Run("delete monitor", func(t *testing.T) { r, db := setupUptimeHandlerTest(t) monitor := models.UptimeMonitor{ID: "mon-delete", Name: "ToDelete", Type: "http", URL: "http://example.com"} db.Create(&monitor) req, _ := http.NewRequest("DELETE", "/api/v1/uptime/mon-delete", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var m models.UptimeMonitor require.Error(t, db.First(&m, "id = ?", "mon-delete").Error) }) t.Run("sync creates monitor for proxy host", func(t *testing.T) { r, db := setupUptimeHandlerTest(t) // Create a proxy host to be synced to an uptime monitor host := models.ProxyHost{UUID: "ph-up-1", Name: "Test Host", DomainNames: "sync.example.com", ForwardHost: "127.0.0.1", ForwardPort: 80, Enabled: true} db.Create(&host) req, _ := http.NewRequest("POST", "/api/v1/uptime/sync", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var monitors []models.UptimeMonitor db.Where("proxy_host_id = ?", host.ID).Find(&monitors) assert.Len(t, monitors, 1) assert.Equal(t, "Test Host", monitors[0].Name) }) t.Run("update enabled via PUT", func(t *testing.T) { r, db := setupUptimeHandlerTest(t) monitor := models.UptimeMonitor{ID: "mon-enable", Name: "ToToggle", Type: "http", URL: "http://example.com", Enabled: true} db.Create(&monitor) updates := map[string]any{"enabled": false} body, _ := json.Marshal(updates) req, _ := http.NewRequest("PUT", "/api/v1/uptime/mon-enable", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var result models.UptimeMonitor err := json.Unmarshal(w.Body.Bytes(), &result) require.NoError(t, err) assert.False(t, result.Enabled) }) } func TestUptimeHandler_Sync_Success(t *testing.T) { r, _ := setupUptimeHandlerTest(t) req, _ := http.NewRequest("POST", "/api/v1/uptime/sync", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var result map[string]string err := json.Unmarshal(w.Body.Bytes(), &result) require.NoError(t, err) assert.Equal(t, "Sync started", result["message"]) } func TestUptimeHandler_Delete_Error(t *testing.T) { r, db := setupUptimeHandlerTest(t) db.Exec("DROP TABLE IF EXISTS uptime_monitors") req, _ := http.NewRequest("DELETE", "/api/v1/uptime/nonexistent", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestUptimeHandler_List_Error(t *testing.T) { r, db := setupUptimeHandlerTest(t) db.Exec("DROP TABLE IF EXISTS uptime_monitors") req, _ := http.NewRequest("GET", "/api/v1/uptime", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) } func TestUptimeHandler_GetHistory_Error(t *testing.T) { r, db := setupUptimeHandlerTest(t) db.Exec("DROP TABLE IF EXISTS uptime_heartbeats") req, _ := http.NewRequest("GET", "/api/v1/uptime/monitor-1/history", http.NoBody) w := httptest.NewRecorder() r.ServeHTTP(w, req) assert.Equal(t, http.StatusInternalServerError, w.Code) }