Files
neoirc/internal/server/server_test.go
T
sneak 3f936925b9
check / check (push) Waiting to run
Wait for IRC listener cleanup and HTTP shutdown on stop (closes #159)
Stopping the server returned before the IRC listener's connections had
run their cleanup, and the database closed under them, so their
sessions, clients and channel memberships stayed and their nicks stayed
taken after a restart.

The IRC listener's stop now waits up to five seconds for every
connection's cleanup, which now runs on a context that stopping does
not cancel. The HTTP server's stop hook now runs Shutdown itself and
waits for it; httpServer is set before serving starts, and the server's
own signal watcher, which raced fx's stop hooks, is gone.

Model: opus-5-5
2026-10-20 19:46:40 +00:00

163 lines
3.1 KiB
Go

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
}