feat(server): add GET /api/v1/uptime endpoint

Add new endpoint that returns server start time and uptime duration in seconds.
The endpoint follows the same pattern as handleHealth and handleInfo handlers,
using the existing startedAt field from the Server struct.

Response format:
{
  "start_time": "2026-05-05T19:30:00Z",
  "uptime_seconds": 1234
}

Includes unit tests with httptest.NewRecorder() to verify JSON response
structure and content type headers.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-05-05 19:17:59 +02:00
parent 9072b3e246
commit 3f2f845ef2
2 changed files with 108 additions and 0 deletions

View File

@@ -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

81
pkg/server/uptime_test.go Normal file
View File

@@ -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)
}