test: add tests for globals, healthcheck and logger (closes #110)
check / check (push) Failing after 0s
check / check (push) Failing after 0s
Tests for the three packages that had none, written from outside each package, each able to fail on a plausible break: - globals: values set are read back through New, and New returns an independent copy. One sequential test function with a disclosed paralleltest suppression, because it changes shared package variables. - healthcheck: Check returns status "ok", the documented JSON fields, an RFC3339Nano timestamp, the maintenance flag from config in both states, and version and appname from globals. - logger: New gives a usable *slog.Logger, debug output is off by default and EnableDebugLogging turns it on. No production code changed. The terminal output format is not asserted. Model: opus-4-8
This commit was merged in pull request #154.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package healthcheck_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"sneak.berlin/go/dnswatcher/internal/config"
|
||||
"sneak.berlin/go/dnswatcher/internal/globals"
|
||||
"sneak.berlin/go/dnswatcher/internal/healthcheck"
|
||||
"sneak.berlin/go/dnswatcher/internal/logger"
|
||||
)
|
||||
|
||||
// recordingLifecycle is a minimal fx.Lifecycle that records the hooks
|
||||
// appended to it, so healthcheck.New can be exercised through its real
|
||||
// constructor without standing up a whole fx application.
|
||||
type recordingLifecycle struct {
|
||||
hooks []fx.Hook
|
||||
}
|
||||
|
||||
func (l *recordingLifecycle) Append(hook fx.Hook) {
|
||||
l.hooks = append(l.hooks, hook)
|
||||
}
|
||||
|
||||
// newHealthcheck builds a Healthcheck through the real constructor and
|
||||
// runs the registered OnStart hook so StartupTime is set the same way
|
||||
// the fx lifecycle would set it.
|
||||
func newHealthcheck(
|
||||
t *testing.T,
|
||||
maintenance bool,
|
||||
version string,
|
||||
) *healthcheck.Healthcheck {
|
||||
t.Helper()
|
||||
|
||||
g := &globals.Globals{Appname: "dnswatcher", Version: version}
|
||||
|
||||
log, err := logger.New(nil, logger.Params{Globals: g})
|
||||
require.NoError(t, err)
|
||||
|
||||
lifecycle := &recordingLifecycle{}
|
||||
|
||||
hc, err := healthcheck.New(lifecycle, healthcheck.Params{
|
||||
Globals: g,
|
||||
Config: &config.Config{MaintenanceMode: maintenance},
|
||||
Logger: log,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, lifecycle.hooks, 1,
|
||||
"New must register exactly one lifecycle hook")
|
||||
require.NotNil(t, lifecycle.hooks[0].OnStart)
|
||||
require.NoError(t, lifecycle.hooks[0].OnStart(context.Background()))
|
||||
|
||||
return hc
|
||||
}
|
||||
|
||||
func TestCheckStatusAndPayloadShape(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hc := newHealthcheck(t, false, "v9.9.9")
|
||||
resp := hc.Check()
|
||||
|
||||
assert.Equal(t, "ok", resp.Status)
|
||||
|
||||
// The JSON shape and field names are part of the contract for the
|
||||
// /health and /.well-known/healthcheck routes, so assert on the
|
||||
// exact set of keys the response marshals to.
|
||||
raw, err := json.Marshal(resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
var fields map[string]json.RawMessage
|
||||
require.NoError(t, json.Unmarshal(raw, &fields))
|
||||
|
||||
wantKeys := []string{
|
||||
"status",
|
||||
"now",
|
||||
"uptimeSeconds",
|
||||
"uptimeHuman",
|
||||
"version",
|
||||
"appname",
|
||||
"maintenanceMode",
|
||||
}
|
||||
assert.Len(t, fields, len(wantKeys),
|
||||
"response must marshal to exactly the documented fields")
|
||||
|
||||
for _, key := range wantKeys {
|
||||
assert.Contains(t, fields, key, "missing JSON field %q", key)
|
||||
}
|
||||
|
||||
// The Now field is documented as RFC3339Nano; a change to the
|
||||
// format constant should turn this red.
|
||||
_, err = time.Parse(time.RFC3339Nano, resp.Now)
|
||||
assert.NoError(t, err, "Now must be RFC3339Nano")
|
||||
}
|
||||
|
||||
func TestCheckMaintenanceModeReflectsConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
maintenance bool
|
||||
}{
|
||||
{"maintenance off", false},
|
||||
{"maintenance on", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hc := newHealthcheck(t, tt.maintenance, "test")
|
||||
resp := hc.Check()
|
||||
assert.Equal(t, tt.maintenance, resp.Maintenance,
|
||||
"maintenanceMode must mirror Config.MaintenanceMode")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSurfacesVersionAndAppname(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hc := newHealthcheck(t, false, "surfaced-version-123")
|
||||
resp := hc.Check()
|
||||
|
||||
assert.Equal(t, "surfaced-version-123", resp.Version,
|
||||
"version from globals must appear in the payload")
|
||||
assert.Equal(t, "dnswatcher", resp.Appname)
|
||||
}
|
||||
Reference in New Issue
Block a user