package server_test import ( "net" "net/http" "strconv" "testing" "time" "sneak.berlin/go/neoirc/internal/server" ) const ( testTimeout = 5 * time.Second // listenCheckStep is how often waitUntilListening tries // to connect. listenCheckStep = 10 * time.Millisecond // stopHoldTime is how long a test waits to see that Stop // has not returned. stopHoldTime = 200 * time.Millisecond ) // TestStopWaitsForRequestInProgress checks that stopping the // server waits for a request it is serving to finish, and // that the request gets its response. func TestStopWaitsForRequestInProgress(t *testing.T) { t.Parallel() started := make(chan struct{}) release := make(chan struct{}) port := freePort(t) addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port)) srv := server.NewTestHTTPServer( port, func(writer http.ResponseWriter, _ *http.Request) { close(started) <-release writer.WriteHeader(http.StatusNoContent) }, ) srv.Start() waitUntilListening(t, addr) statuses := make(chan int, 1) go func() { statuses <- get(t, "http://"+addr+"/") }() <-started stopped := make(chan struct{}) go func() { srv.Stop(t.Context()) close(stopped) }() select { case <-stopped: t.Error("Stop returned while a request was in progress") case <-time.After(stopHoldTime): } close(release) status := <-statuses if status != http.StatusNoContent { t.Errorf("status %d, want %d", status, http.StatusNoContent) } select { case <-stopped: case <-time.After(testTimeout): t.Fatal("Stop did not return after the request finished") } } // freePort returns a TCP port that nothing was listening on a // moment ago. func freePort(t *testing.T) int { t.Helper() var listenConfig net.ListenConfig listener, err := listenConfig.Listen( t.Context(), "tcp", "127.0.0.1:0", ) if err != nil { t.Fatalf("listen: %v", err) } tcpAddr, ok := listener.Addr().(*net.TCPAddr) if !ok { t.Fatalf("listener address %v is not TCP", listener.Addr()) } err = listener.Close() if err != nil { t.Fatalf("close listener: %v", err) } return tcpAddr.Port } // waitUntilListening waits until addr accepts connections, // and fails the test if it does not within testTimeout. func waitUntilListening(t *testing.T, addr string) { t.Helper() var dialer net.Dialer deadline := time.Now().Add(testTimeout) for { conn, err := dialer.DialContext(t.Context(), "tcp", addr) if err == nil { _ = conn.Close() return } if time.Now().After(deadline) { t.Fatalf("nothing listening on %s: %v", addr, err) } time.Sleep(listenCheckStep) } } // get sends a GET request to url and returns the response // status, or 0 if there was no response. func get(t *testing.T, url string) int { t.Helper() request, err := http.NewRequestWithContext( t.Context(), http.MethodGet, url, nil, ) if err != nil { t.Errorf("new request: %v", err) return 0 } client := http.Client{Timeout: testTimeout} response, err := client.Do(request) if err != nil { t.Errorf("get %s: %v", url, err) return 0 } _ = response.Body.Close() return response.StatusCode }