Skip to content
quarry Artifact [e336e4d36d]
Login

Artifact [e336e4d36d]

Artifact e336e4d36dbf8a60b34a2944a20287ea40f0fcde7c558911f7ddd605f80f85d3:


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