internal/reach/reach_test.go
Rama main · réplica de lectura del repositorio original.
package reach
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
type roundTripper struct {
resp *http.Response
err error
}
func (r *roundTripper) RoundTrip(*http.Request) (*http.Response, error) {
return r.resp, r.err
}
func through(sink *Sink, resp *http.Response, err error) (*http.Response, error) {
wrapped := sink.Wrap(&roundTripper{resp: resp, err: err})
request, _ := http.NewRequest(http.MethodGet, "https://cluster.test/api", http.NoBody)
return wrapped.RoundTrip(request)
}
func fired(t *testing.T, sink *Sink) bool {
t.Helper()
select {
case <-sink.Changed():
return true
case <-time.After(time.Second):
return false
}
}
func quiet(t *testing.T, sink *Sink) bool {
t.Helper()
select {
case <-sink.Changed():
return false
case <-time.After(50 * time.Millisecond):
return true
}
}
func TestARequestThatCameBackWithNothingSaysTheClusterIsGone(t *testing.T) {
sink := New()
_, _ = through(sink, nil, errors.New("dial tcp 10.0.0.1:6443: connect: connection refused"))
answering, reason := sink.State()
if answering {
t.Fatal("a request that never got a reply left the cluster looking fine")
}
if reason != "dial tcp 10.0.0.1:6443: connect: connection refused" {
t.Fatalf("reason = %q, want what the transport said", reason)
}
if !fired(t, sink) {
t.Fatal("nobody was told the answer changed")
}
}
func TestAReplyOfAnyKindSaysTheClusterIsThere(t *testing.T) {
sink := New()
_, _ = through(sink, nil, errors.New("connection refused"))
<-sink.Changed()
_, _ = through(sink, &http.Response{StatusCode: http.StatusForbidden}, nil)
answering, reason := sink.State()
if !answering {
t.Fatal("a cluster that answered 403 was still counted as gone")
}
if reason != "" {
t.Fatalf("reason = %q, want it forgotten", reason)
}
if !fired(t, sink) {
t.Fatal("nobody was told the cluster came back")
}
}
func TestARequestTheCallerGaveUpOnIsIgnored(t *testing.T) {
sink := New()
_, _ = through(sink, nil, fmt.Errorf("reading body: %w", context.Canceled))
answering, _ := sink.State()
if !answering {
t.Fatal("a request the caller dropped was read as an outage")
}
if !quiet(t, sink) {
t.Fatal("a request the caller dropped was announced")
}
}
func TestARequestThatRanOutOfTimeIsReported(t *testing.T) {
sink := New()
_, _ = through(sink, nil, fmt.Errorf("awaiting headers: %w", context.DeadlineExceeded))
answering, reason := sink.State()
if answering {
t.Fatal("a request that timed out left the cluster looking fine")
}
if reason == "" {
t.Fatal("the timeout was reported without saying what happened")
}
}
func TestTheSameAnswerTwiceIsSaidOnce(t *testing.T) {
sink := New()
_, _ = through(sink, nil, errors.New("connection refused"))
<-sink.Changed()
_, _ = through(sink, nil, errors.New("connection refused"))
if !quiet(t, sink) {
t.Fatal("the same answer was announced twice")
}
}
func TestADifferentReasonIsSaidAgain(t *testing.T) {
sink := New()
_, _ = through(sink, nil, errors.New("connection refused"))
<-sink.Changed()
_, _ = through(sink, nil, errors.New("no such host"))
if !fired(t, sink) {
t.Fatal("a cluster failing differently was not mentioned")
}
_, reason := sink.State()
if reason != "no such host" {
t.Fatalf("reason = %q, want the newer one", reason)
}
}
func TestChangesCoalesceWithoutLosingTheNewestState(t *testing.T) {
sink := New()
sink.Saw(errors.New("connection refused"))
sink.Saw(nil)
sink.Saw(errors.New("no route to host"))
select {
case <-sink.Changed():
default:
t.Fatal("the accumulated changes were not announced")
}
select {
case <-sink.Changed():
t.Fatal("changes queued without bound while nobody listened")
default:
}
answering, reason := sink.State()
if answering || reason != "no route to host" {
t.Fatalf("state = (%v, %q), want the newest observation", answering, reason)
}
}
func TestACancelledRequestDoesNotEraseAnExistingOutage(t *testing.T) {
sink := New()
sink.Saw(errors.New("connection refused"))
<-sink.Changed()
sink.Saw(fmt.Errorf("reading body: %w", context.Canceled))
answering, reason := sink.State()
if answering || reason != "connection refused" {
t.Fatalf("state = (%v, %q), want the existing outage", answering, reason)
}
select {
case <-sink.Changed():
t.Fatal("a canceled request announced a health change")
default:
}
}
func TestTheResponseIsHandedOnUntouched(t *testing.T) {
sink := New()
want := &http.Response{StatusCode: http.StatusTeapot}
got, err := through(sink, want, nil)
if err != nil {
t.Fatalf("err = %v", err)
}
if got != want {
t.Fatalf("response = %v, want the one the transport made", got)
}
}
func TestTheErrorIsHandedOnUntouched(t *testing.T) {
sink := New()
want := errors.New("no route to host")
_, err := through(sink, nil, want)
if !errors.Is(err, want) {
t.Fatalf("err = %v, want the transport's own", err)
}
}
func TestNobodyListeningDoesNotHoldUpTheRequest(t *testing.T) {
sink := New()
done := make(chan struct{})
go func() {
for i := range 100 {
_, _ = through(sink, nil, fmt.Errorf("failure %d", i))
}
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("reporting held up the requests")
}
}
func TestManyRequestsAtOnceAgreeOnOneAnswer(t *testing.T) {
sink := New()
var group sync.WaitGroup
for range 50 {
group.Go(func() {
_, _ = through(sink, &http.Response{StatusCode: http.StatusOK}, nil)
})
group.Go(func() {
_, _ = through(sink, nil, errors.New("connection refused"))
})
}
group.Wait()
answering, reason := sink.State()
if answering && reason != "" {
t.Fatalf("state = (%v, %q), want the two to agree", answering, reason)
}
}
func TestASinkThatIsNotThereIsHarmless(t *testing.T) {
var sink *Sink
sink.Saw(errors.New("connection refused"))
answering, reason := sink.State()
if !answering || reason != "" {
t.Fatalf("state = (%v, %q), want the benefit of the doubt", answering, reason)
}
if sink.Changed() != nil {
t.Fatal("a sink that is not there offered something to listen to")
}
}
func TestARealRequestThroughARealClientIsSeen(t *testing.T) {
sink := New()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
client := &http.Client{Transport: sink.Wrap(http.DefaultTransport)}
resp, err := client.Get(server.URL)
if err != nil {
t.Fatalf("get: %v", err)
}
_ = resp.Body.Close()
if answering, _ := sink.State(); !answering {
t.Fatal("a request that worked left the cluster looking gone")
}
server.Close()
_, gone := client.Get(server.URL)
if gone == nil {
t.Fatal("a request to a server that had gone came back fine")
}
answering, reason := sink.State()
if answering {
t.Fatal("a server that had gone was still counted as answering")
}
if reason == "" {
t.Fatal("nothing was said about why")
}
}