package health
import (
"context"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
type recorder struct {
mu sync.Mutex
rows []string
}
func (r *recorder) fn(d Dep, up bool, err error) {
r.mu.Lock()
defer r.mu.Unlock()
state := "down"
if up {
state = "up"
}
r.rows = append(r.rows, d.Name+" "+state)
}
func (r *recorder) all() []string {
r.mu.Lock()
defer r.mu.Unlock()
return append([]string(nil), r.rows...)
}
// The event is the TRANSITION. A service that is down stays down
// without filing a report every thirty seconds — that is the difference
// between an alert and a flood.
func TestOnlyTransitionsAreReported(t *testing.T) {
var down bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if down {
w.WriteHeader(http.StatusBadGateway)
return
}
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
var rec recorder
w := New([]Dep{{Name: "notify", URL: srv.URL, Why: "no alerts"}}, rec.fn)
ctx := context.Background()
w.Check(ctx) // healthy: nothing to say
w.Check(ctx)
if got := rec.all(); len(got) != 0 {
t.Fatalf("a healthy service reported %v, want silence", got)
}
down = true
w.Check(ctx)
w.Check(ctx)
w.Check(ctx)
if got := rec.all(); len(got) != 1 || got[0] != "notify down" {
t.Fatalf("reports = %v, want exactly one 'notify down'", got)
}
if d := w.Down(); len(d) != 1 || d[0].Up {
t.Errorf("Down() = %+v, want the failing dep", d)
}
down = false
w.Check(ctx)
if got := rec.all(); len(got) != 2 || got[1] != "notify up" {
t.Fatalf("reports = %v, want a recovery", got)
}
if d := w.Down(); len(d) != 0 {
t.Errorf("Down() = %+v after recovery, want empty", d)
}
}
// A dependency answering 404 is a dependency that is RUNNING. Only a
// transport failure, or a server saying it is broken, counts as down.
func TestAnyAnswerBelow500IsUp(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.NotFound(w, nil)
}))
defer srv.Close()
var rec recorder
w := New([]Dep{{Name: "dagnats", URL: srv.URL}}, rec.fn)
w.Check(context.Background())
if d := w.Down(); len(d) != 0 {
t.Errorf("a 404 was treated as down: %+v", d)
}
}
func TestUnreachableIsDown(t *testing.T) {
var rec recorder
// Closed port: nothing is listening.
w := New([]Dep{{Name: "dagnats", URL: "http://127.0.0.1:1"}}, rec.fn)
w.Check(context.Background())
d := w.Down()
if len(d) != 1 {
t.Fatalf("Down() = %+v, want the unreachable dep", d)
}
if d[0].Err == "" {
t.Error("a down dependency carries no reason")
}
if d[0].Down() <= 0 {
t.Error("a down dependency reports no duration")
}
}
// Not configured is not broken.
func TestUnconfiguredDepsAreNotWatched(t *testing.T) {
w := New([]Dep{{Name: "notify", URL: ""}}, nil)
if got := w.States(); len(got) != 0 {
t.Errorf("watching %+v, want nothing", got)
}
// And Run must return rather than spin on an empty set.
done := make(chan struct{})
go func() { w.Run(context.Background(), time.Millisecond); close(done) }()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Error("Run did not return with no dependencies to watch")
}
}