From b17b72715747ac7e974a77d5cf462c1392b07551 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 5 May 2026 19:18:24 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(server):=20add=20GET=20/api/v1?= =?UTF-8?q?/uptime=20endpoint=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gabriel Radureau Co-committed-by: Gabriel Radureau --- pkg/server/server.go | 27 +++++++++++++ pkg/server/uptime_test.go | 81 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 pkg/server/uptime_test.go diff --git a/pkg/server/server.go b/pkg/server/server.go index 49666ee..b82a516 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) +}