package httpserver import ( "context" "errors" "io" "net" "net/http" "testing" "time" ) func TestServeRunsIndependentEndpointsAndShutsDownTogether(t *testing.T) { t.Parallel() first := mustListen(t) second := mustListen(t) ctx, cancel := context.WithCancel(context.Background()) result := make(chan error, 1) go func() { result <- Serve(ctx, testOptions(), Endpoint{Name: "distribution", Listener: first, Handler: textHandler("distribution")}, Endpoint{Name: "admin", Listener: second, Handler: textHandler("admin")}, ) }() assertBody(t, first.Addr().String(), "distribution") assertBody(t, second.Addr().String(), "admin") cancel() select { case err := <-result: if err != nil { t.Fatalf("Serve() error = %v", err) } case <-time.After(2 * time.Second): t.Fatal("Serve() did not stop after cancellation") } } func TestServeRejectsInvalidEndpointDefinitions(t *testing.T) { t.Parallel() listener := mustListen(t) defer listener.Close() tests := []struct { name string endpoints []Endpoint }{ {name: "empty"}, {name: "missing name", endpoints: []Endpoint{{Listener: listener, Handler: textHandler("ok")}}}, {name: "missing listener", endpoints: []Endpoint{{Name: "api", Handler: textHandler("ok")}}}, {name: "missing handler", endpoints: []Endpoint{{Name: "api", Listener: listener}}}, {name: "duplicate name", endpoints: []Endpoint{ {Name: "api", Listener: listener, Handler: textHandler("one")}, {Name: "api", Listener: listener, Handler: textHandler("two")}, }}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { err := Serve(context.Background(), testOptions(), test.endpoints...) if !errors.Is(err, ErrInvalidEndpoint) { t.Fatalf("Serve() error = %v, want %v", err, ErrInvalidEndpoint) } }) } } func TestServeStopsSiblingWhenEndpointFails(t *testing.T) { t.Parallel() failed := mustListen(t) sibling := mustListen(t) failedAddress := failed.Addr().String() if err := failed.Close(); err != nil { t.Fatalf("Close(%s): %v", failedAddress, err) } err := Serve(context.Background(), testOptions(), Endpoint{Name: "failed", Listener: failed, Handler: textHandler("failed")}, Endpoint{Name: "sibling", Listener: sibling, Handler: textHandler("sibling")}, ) if err == nil { t.Fatal("Serve() error = nil, want endpoint failure") } connection, dialErr := net.DialTimeout("tcp", sibling.Addr().String(), 100*time.Millisecond) if dialErr == nil { connection.Close() t.Fatal("sibling listener remained open after endpoint failure") } } func TestListenAndServeValidatesAllBindingsBeforeOpeningSockets(t *testing.T) { t.Parallel() err := ListenAndServe(context.Background(), testOptions(), Binding{Name: "api", Address: "\x00", Handler: textHandler("one")}, Binding{Name: "api", Address: "127.0.0.1:0", Handler: textHandler("two")}, ) if !errors.Is(err, ErrInvalidEndpoint) { t.Fatalf("ListenAndServe() error = %v, want %v", err, ErrInvalidEndpoint) } } func TestListenAndServeTreatsCancellationBeforeBindingAsGracefulStop(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) cancel() err := ListenAndServe(ctx, testOptions(), Binding{ Name: "api", Address: "127.0.0.1:0", Handler: textHandler("ok"), }) if err != nil { t.Fatalf("ListenAndServe() error = %v, want nil", err) } } func testOptions() Options { return Options{ ReadHeaderTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second, IdleTimeout: time.Second, ShutdownTimeout: time.Second, MaxHeaderBytes: 16 << 10, } } func mustListen(t *testing.T) net.Listener { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("Listen(): %v", err) } return listener } func textHandler(body string) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(writer, body) }) } func assertBody(t *testing.T, address, want string) { t.Helper() client := &http.Client{Timeout: time.Second} response, err := client.Get("http://" + address) if err != nil { t.Fatalf("GET %s: %v", address, err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { t.Fatalf("ReadAll(): %v", err) } if string(body) != want { t.Fatalf("body = %q, want %q", body, want) } }