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) }