Ctrl K
ComunidadGuías…

internal/reach/reach_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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")
	}
}