check / check (push) Successful in 3m40s
fx wrote its own steps of starting and stopping as plain text to stderr. It now logs them with its own slog event logger through the backend's logger, so off a terminal every line from start to stop is JSON. A config file that is found but cannot be read now makes config.New return the error instead of panicking. A test runs the server as a child process and checks that all it writes is JSON, on a normal start and stop and with such a config file. The backend logs its name, version and architecture once at start. The health check's uptime keys are now uptime_seconds and uptime_human, and its type and method take the names GO_HTTP_SERVER_CONVENTIONS.md gives. Its path, content type, status and code are unchanged. Model: opus-5-5
121 lines
3.2 KiB
Go
121 lines
3.2 KiB
Go
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"])
|
|
}
|
|
}
|