diff --git a/pkg/server/server.go b/pkg/server/server.go index b54eb6b..5594c8e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -246,6 +246,9 @@ func (s *Server) registerApiV1Routes(r chi.Router) { r.Get("/{name}", s.handleGreetPath) }) + // Uptime endpoint + r.Get("/uptime", s.handleUptime) + // Register user authentication routes if s.userService != nil && s.userRepo != nil { // Use unified user service - much simpler! @@ -583,6 +586,30 @@ func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { w.Write(data) } +// UptimeResponse represents the JSON response for /api/v1/uptime +type UptimeResponse struct { + StartTime string `json:"start_time"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// handleUptime godoc +// +// @Summary Get server uptime +// @Description Returns server start time and uptime duration +// @Tags System/Info +// @Produce json +// @Success 200 {object} UptimeResponse +// @Router /v1/uptime [get] +func (s *Server) handleUptime(w http.ResponseWriter, r *http.Request) { + log.Trace().Msg("Uptime check requested") + resp := UptimeResponse{ + StartTime: s.startedAt.Format(time.RFC3339), + UptimeSeconds: int(time.Since(s.startedAt).Seconds()), + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} + // handleGreetQuery godoc // // @Summary Get greeting with cache diff --git a/pkg/server/uptime_test.go b/pkg/server/uptime_test.go new file mode 100644 index 0000000..265076d --- /dev/null +++ b/pkg/server/uptime_test.go @@ -0,0 +1,81 @@ +package server + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "dance-lessons-coach/pkg/config" + + "github.com/stretchr/testify/assert" +) + +func TestHandleUptime(t *testing.T) { + // Setup with a known start time + cfg := &config.Config{} + // We need to create a server and then set its startedAt to a known time + // Since NewServer sets startedAt to time.Now(), we'll create the server + // and then use reflection or we can use NewServerWithUserRepo which also sets startedAt + s := NewServer(cfg, context.Background()) + + // Set a fixed start time for deterministic testing + // We can't directly set s.startedAt since it's unexported, but we can test + // that the handler uses the server's startedAt + // The test will verify the structure and that uptime_seconds is >= 0 + + // Create request + req := httptest.NewRequest(http.MethodGet, "/api/v1/uptime", nil) + w := httptest.NewRecorder() + + // Call handler + s.handleUptime(w, req) + + // Check status code + assert.Equal(t, http.StatusOK, w.Code) + + // Check content type + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + // Decode response + var resp UptimeResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + + // Assert fields + assert.NotEmpty(t, resp.StartTime) + // Verify start_time is in RFC3339 format + _, err = time.Parse(time.RFC3339, resp.StartTime) + assert.NoError(t, err) + assert.GreaterOrEqual(t, resp.UptimeSeconds, 0) +} + +func TestHandleUptime_Deterministic(t *testing.T) { + // For a more deterministic test, we would need to be able to set startedAt + // Since startedAt is unexported, we test the behavior with a known server + // that was just created (uptime should be very small) + cfg := &config.Config{} + s := NewServer(cfg, context.Background()) + + // Small delay to ensure uptime is at least 0 seconds + time.Sleep(10 * time.Millisecond) + + req := httptest.NewRequest(http.MethodGet, "/api/v1/uptime", nil) + w := httptest.NewRecorder() + + s.handleUptime(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp UptimeResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + + // Uptime should be at least 0 (it's int() of seconds, so minimum is 0) + assert.GreaterOrEqual(t, resp.UptimeSeconds, 0) + // Start time should be parseable + _, err = time.Parse(time.RFC3339, resp.StartTime) + assert.NoError(t, err) +}