diff --git a/pkg/service/handlers/handlers_health.go b/pkg/service/handlers/handlers_health.go index 5eda391..a9bb0f3 100644 --- a/pkg/service/handlers/handlers_health.go +++ b/pkg/service/handlers/handlers_health.go @@ -7,45 +7,53 @@ import ( "time" ) -// HandleHealth returns the health status of the service. -func (s *Server) HandleHealth(w http.ResponseWriter, _ *http.Request) { - version := "0.0.1" - vcsRevision := "" - vcsTime := "" - vcsModified := "" +// buildVersionInfo extracts module version + VCS metadata from the +// runtime/debug build info, falling back to "0.0.1" when the binary +// wasn't built with module info (e.g. local `go run`). Shared between +// HandleHealth and HandleRoot so both endpoints report identical +// release context; keys present-when-non-empty so a `go run` response +// stays minimal instead of carrying empty strings. +func buildVersionInfo() map[string]string { + info := map[string]string{"version": "0.0.1"} - if info, ok := debug.ReadBuildInfo(); ok { - if info.Main.Version != "" && info.Main.Version != "(devel)" { - version = info.Main.Version - } + build, ok := debug.ReadBuildInfo() + if !ok { + return info + } - for _, setting := range info.Settings { - switch setting.Key { - case "vcs.revision": - vcsRevision = setting.Value - case "vcs.time": - vcsTime = setting.Value - case "vcs.modified": - vcsModified = setting.Value + if build.Main.Version != "" && build.Main.Version != "(devel)" { + info["version"] = build.Main.Version + } + + for _, setting := range build.Settings { + switch setting.Key { + case "vcs.revision": + if setting.Value != "" { + info["vcs_revision"] = setting.Value + } + case "vcs.time": + if setting.Value != "" { + info["vcs_time"] = setting.Value + } + case "vcs.modified": + if setting.Value != "" { + info["vcs_modified"] = setting.Value } } } + return info +} + +// HandleHealth returns the health status of the service. +func (s *Server) HandleHealth(w http.ResponseWriter, _ *http.Request) { status := map[string]interface{}{ "status": "up", "timestamp": time.Now().Format(time.RFC3339), - "version": version, - } - if vcsRevision != "" { - status["vcs_revision"] = vcsRevision } - if vcsTime != "" { - status["vcs_time"] = vcsTime - } - - if vcsModified != "" { - status["vcs_modified"] = vcsModified + for k, v := range buildVersionInfo() { + status[k] = v } w.Header().Set("Content-Type", "application/json") diff --git a/pkg/service/handlers/handlers_media.go b/pkg/service/handlers/handlers_media.go index 1377834..d749e47 100644 --- a/pkg/service/handlers/handlers_media.go +++ b/pkg/service/handlers/handlers_media.go @@ -2,7 +2,7 @@ package handlers import ( "embed" - "fmt" + "encoding/json" "io/fs" "net/http" "strings" @@ -36,8 +36,25 @@ var swUpdateXML []byte func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) { accept := r.Header.Get("Accept") if !strings.Contains(accept, "text/html") && (strings.Contains(accept, "application/json") || accept == "*/*" || accept == "") { + // Mirror the version + VCS metadata exposed by /health so any + // caller hitting "/" gets identical release context. Keys that + // would carry empty strings are omitted by buildVersionInfo. + payload := map[string]string{ + "Bose": "AfterTouch", + "service": "Go/Chi", + "docs": "https://gesellix.github.io/Bose-SoundTouch/", + } + + for k, v := range buildVersionInfo() { + payload[k] = v + } + w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprintf(w, `{"Bose": "AfterTouch", "service": "Go/Chi", "docs": "https://gesellix.github.io/Bose-SoundTouch/"}`) + + if err := json.NewEncoder(w).Encode(payload); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } return } diff --git a/pkg/service/handlers/handlers_media_test.go b/pkg/service/handlers/handlers_media_test.go index 129da74..ed4bdc5 100644 --- a/pkg/service/handlers/handlers_media_test.go +++ b/pkg/service/handlers/handlers_media_test.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "io" "net/http" "net/http/httptest" @@ -66,10 +67,26 @@ func TestRootEndpointJSON(t *testing.T) { t.Errorf("Expected application/json content type, got %s", contentType) } - body, _ := io.ReadAll(res.Body) - expected := `{"Bose": "AfterTouch", "service": "Go/Chi", "docs": "https://gesellix.github.io/Bose-SoundTouch/"}` - if strings.TrimSpace(string(body)) != expected { - t.Errorf("Expected body %s, got %s", expected, string(body)) + var got map[string]string + if err := json.NewDecoder(res.Body).Decode(&got); err != nil { + t.Fatalf("failed to decode root response: %v", err) + } + + for _, want := range []struct{ key, value string }{ + {"Bose", "AfterTouch"}, + {"service", "Go/Chi"}, + {"docs", "https://gesellix.github.io/Bose-SoundTouch/"}, + } { + if got[want.key] != want.value { + t.Errorf("payload[%q] = %q, want %q", want.key, got[want.key], want.value) + } + } + + // Version mirrors /health — falls back to "0.0.1" under `go test` + // (debug.ReadBuildInfo reports Main.Version="(devel)") but must + // always be present so monitoring tools can pin a release. + if got["version"] == "" { + t.Error("expected version field to be present") } }