a73x

internal/server/health/health_test.go

Ref:   Size: 3.2 KiB   History

package health

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestLive(t *testing.T) {
	rec := httptest.NewRecorder()
	Live(rec, httptest.NewRequest(http.MethodGet, "/livez", nil))
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", rec.Code)
	}
	if body := rec.Body.String(); body != "ok\n" {
		t.Fatalf("body = %q, want %q", body, "ok\n")
	}
}

func TestReadyAllPass(t *testing.T) {
	h := Ready(time.Second,
		Check{Name: "db", Probe: func(context.Context) error { return nil }},
		Check{Name: "cache", Probe: func(context.Context) error { return nil }},
	)
	rec := httptest.NewRecorder()
	h(rec, httptest.NewRequest(http.MethodGet, "/readyz", nil))
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d, want 200", rec.Code)
	}
	var got struct {
		Status string            `json:"status"`
		Checks map[string]string `json:"checks"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if got.Status != "ready" {
		t.Fatalf("status = %q, want ready", got.Status)
	}
	if got.Checks["db"] != "ok" || got.Checks["cache"] != "ok" {
		t.Fatalf("checks = %v, want all ok", got.Checks)
	}
}

func TestReadyOneFails(t *testing.T) {
	h := Ready(time.Second,
		Check{Name: "db", Probe: func(context.Context) error { return nil }},
		Check{Name: "cache", Probe: func(context.Context) error { return errors.New("dial /run/cache.sock: connection refused") }},
	)
	rec := httptest.NewRecorder()
	h(rec, httptest.NewRequest(http.MethodGet, "/readyz", nil))
	if rec.Code != http.StatusServiceUnavailable {
		t.Fatalf("status = %d, want 503", rec.Code)
	}
	var got struct {
		Status string            `json:"status"`
		Checks map[string]string `json:"checks"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if got.Status != "unready" {
		t.Fatalf("status = %q, want unready", got.Status)
	}
	if got.Checks["db"] != "ok" {
		t.Fatalf("db check = %q, want ok", got.Checks["db"])
	}
	// The body reports the failing check as unavailable WITHOUT leaking the raw
	// error (which may carry socket paths / internal detail) to an
	// unauthenticated endpoint.
	if got.Checks["cache"] != "unavailable" {
		t.Fatalf("cache check = %q, want unavailable", got.Checks["cache"])
	}
	if raw := rec.Body.String(); containsAny(raw, "connection refused", "/run/cache.sock") {
		t.Fatalf("body leaked internal error detail: %q", raw)
	}
}

func TestReadyProbeSeesTimeout(t *testing.T) {
	// A probe that outlives the readiness budget must see a cancelled context —
	// Ready bounds each probe so a wedged dependency cannot hang the endpoint.
	var deadlineOK bool
	h := Ready(10*time.Millisecond, Check{Name: "slow", Probe: func(ctx context.Context) error {
		_, ok := ctx.Deadline()
		deadlineOK = ok
		return ctx.Err()
	}})
	rec := httptest.NewRecorder()
	h(rec, httptest.NewRequest(http.MethodGet, "/readyz", nil))
	if !deadlineOK {
		t.Fatal("probe did not receive a deadline-bounded context")
	}
}

func containsAny(s string, subs ...string) bool {
	for _, sub := range subs {
		for i := 0; i+len(sub) <= len(s); i++ {
			if s[i:i+len(sub)] == sub {
				return true
			}
		}
	}
	return false
}