✨ 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:
@@ -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
81
pkg/server/uptime_test.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user