package health
import (
"context"
"errors"
"testing"
"time"
)
// fakeChecker is a hand-built Checker for unit tests. Tests construct Set
// directly via &Set{Checks: []Checker{...}} — no steward involvement.
type fakeChecker struct {
name string
err error
delay time.Duration
}
func (f *fakeChecker) Name() string { return f.name }
func (f *fakeChecker) Check(ctx context.Context) error {
if f.delay > 0 {
select {
case <-time.After(f.delay):
case <-ctx.Done():
return ctx.Err()
}
}
return f.err
}
func TestSetRunAllOK(t *testing.T) {
s := &Set{Checks: []Checker{
&fakeChecker{name: "a"},
&fakeChecker{name: "b"},
}}
results, allOK := s.Run(context.Background())
if !allOK {
t.Fatalf("allOK = false; want true")
}
if len(results) != 2 {
t.Fatalf("len(results) = %d; want 2", len(results))
}
for name, err := range results {
if err != nil {
t.Errorf("results[%q] = %v; want nil", name, err)
}
}
}
func TestSetRunAggregatesFailures(t *testing.T) {
boom := errors.New("boom")
s := &Set{Checks: []Checker{
&fakeChecker{name: "a"},
&fakeChecker{name: "b", err: boom},
&fakeChecker{name: "c"},
}}
results, allOK := s.Run(context.Background())
if allOK {
t.Fatalf("allOK = true; want false (one check failed)")
}
if got := results["a"]; got != nil {
t.Errorf("results[a] = %v; want nil", got)
}
if got := results["b"]; !errors.Is(got, boom) {
t.Errorf("results[b] = %v; want errors.Is boom", got)
}
if got := results["c"]; got != nil {
t.Errorf("results[c] = %v; want nil", got)
}
}
func TestSetRunEmptyChecksReturnsAllOK(t *testing.T) {
s := &Set{}
results, allOK := s.Run(context.Background())
if !allOK {
t.Fatalf("allOK = false; want true (empty Checks is intentional)")
}
if len(results) != 0 {
t.Fatalf("len(results) = %d; want 0", len(results))
}
}
func TestSetRunEnforcesPerCheckTimeout(t *testing.T) {
// fakeChecker with a delay much larger than the 2s per-check budget.
// Use 10s delay; if the timeout works the test returns in ~2s.
s := &Set{Checks: []Checker{
&fakeChecker{name: "slow", delay: 10 * time.Second},
}}
start := time.Now()
results, allOK := s.Run(context.Background())
elapsed := time.Since(start)
// The 2s per-check timeout must fire. Allow generous slack for CI but
// firmly less than the 10s delay we set.
if elapsed >= 5*time.Second {
t.Fatalf("Run took %s; per-check timeout did not fire", elapsed)
}
if allOK {
t.Fatalf("allOK = true; want false (timeout should surface as error)")
}
got := results["slow"]
if got == nil {
t.Fatalf("results[slow] = nil; want timeout error")
}
if !errors.Is(got, context.DeadlineExceeded) {
t.Errorf("results[slow] = %v; want errors.Is(context.DeadlineExceeded)", got)
}
}