package handlers_test import ( "encoding/json" "maps" "net/http" "net/http/httptest" "slices" "testing" "time" "sneak.berlin/go/netwatch/internal/globals" "sneak.berlin/go/netwatch/internal/handlers" "sneak.berlin/go/netwatch/internal/healthcheck" "sneak.berlin/go/netwatch/internal/logger" "go.uber.org/fx/fxtest" ) // newStartedHandlers builds Handlers with a real health check for the // server named in g, and starts them, which records the time the // uptime counts from. func newStartedHandlers(t *testing.T, g *globals.Globals) *handlers.Handlers { t.Helper() lc := fxtest.NewLifecycle(t) log, err := logger.New(lc, logger.Params{Globals: g}) if err != nil { t.Fatalf("logger: %v", err) } hc, err := healthcheck.New(lc, healthcheck.Params{Globals: g, Logger: log}) if err != nil { t.Fatalf("health check: %v", err) } h, err := handlers.New(lc, handlers.Params{Globals: g, Healthcheck: hc, Logger: log}) if err != nil { t.Fatalf("handlers: %v", err) } lc.RequireStart() t.Cleanup(lc.RequireStop) return h } // TestHandleHealthCheck checks the health check's answer: 200, a JSON // content type, and a JSON object with exactly the fields of // healthcheck.HealthcheckResponse, carrying this server's name and // version and an uptime counted from its start. func TestHandleHealthCheck(t *testing.T) { t.Parallel() g := &globals.Globals{Appname: "netwatch-server", Version: "v1.2.3"} h := newStartedHandlers(t, g) rec := httptest.NewRecorder() req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/.well-known/healthcheck", http.NoBody) h.HandleHealthCheck().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } contentType := rec.Header().Get("Content-Type") if contentType != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q, want %q", contentType, "application/json; charset=utf-8") } var body map[string]any err := json.Unmarshal(rec.Body.Bytes(), &body) if err != nil { t.Fatalf("body not a JSON object: %v (%q)", err, rec.Body.String()) } fields := []string{ "appname", "now", "status", "uptime_human", "uptime_seconds", "version", } if got := slices.Sorted(maps.Keys(body)); !slices.Equal(got, fields) { t.Fatalf("fields = %v, want %v", got, fields) } for field, want := range map[string]string{ "appname": g.Appname, "status": "ok", "version": g.Version, } { if body[field] != want { t.Errorf("%s = %v, want %q", field, body[field], want) } } now, _ := body["now"].(string) at, err := time.Parse(time.RFC3339Nano, now) if err != nil || time.Since(at).Abs() > time.Minute { t.Errorf("now = %q, want the current time in RFC 3339 (%v)", now, err) } // Started just now, so the uptime is well under a minute. human, _ := body["uptime_human"].(string) uptime, err := time.ParseDuration(human) if err != nil || uptime > time.Minute { t.Errorf("uptime_human = %q, want a duration under a minute (%v)", human, err) } seconds, ok := body["uptime_seconds"].(float64) if !ok || seconds < 0 || seconds > time.Minute.Seconds() { t.Errorf("uptime_seconds = %v, want a number of seconds under a minute", body["uptime_seconds"]) } }