check / check (push) Waiting to run
The health check answered 200 "status":"ok" whatever happened, so a container whose report files could not be written stayed healthy while every report was refused with 500. The report buffer now says whether its last write of a report file failed. While it has, the health check's status is "error" and the handler answers 503 with the same fields, which fails the image's HEALTHCHECK. At start no write has failed, and the first write that succeeds brings back 200. A test removes DATA_DIR to make a write fail and checks the 503, then the 200 once a write succeeds again. Model: opus-5-5
134 lines
3.7 KiB
Go
134 lines
3.7 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/netwatch/internal/config"
|
|
"sneak.berlin/go/netwatch/internal/globals"
|
|
"sneak.berlin/go/netwatch/internal/handlers"
|
|
"sneak.berlin/go/netwatch/internal/healthcheck"
|
|
"sneak.berlin/go/netwatch/internal/logger"
|
|
"sneak.berlin/go/netwatch/internal/reportbuf"
|
|
|
|
"go.uber.org/fx/fxtest"
|
|
)
|
|
|
|
// newStartedHandlers builds Handlers with a real health check for the
|
|
// server named in g, and a report buffer in a temporary directory, 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)
|
|
}
|
|
|
|
buf, err := reportbuf.New(lc, reportbuf.Params{
|
|
Config: &config.Config{DataDir: t.TempDir(), DataDirMaxBytes: 1 << 20},
|
|
Logger: log,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("report buffer: %v", err)
|
|
}
|
|
|
|
hc, err := healthcheck.New(lc,
|
|
healthcheck.Params{Buffer: buf, Globals: g, Logger: log})
|
|
if err != nil {
|
|
t.Fatalf("health check: %v", err)
|
|
}
|
|
|
|
h, err := handlers.New(lc, handlers.Params{
|
|
Buffer: buf, 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 at start, when
|
|
// no write of a report file has failed: 200, a JSON content type, and a
|
|
// JSON object with exactly the fields of healthcheck.HealthcheckResponse,
|
|
// carrying "status":"ok", this server's name and version and an uptime
|
|
// counted from its start. The answer while writes fail is checked in
|
|
// internal/reportbuf, whose tests can make a write fail.
|
|
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"])
|
|
}
|
|
}
|