check / check (push) Waiting to run
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
248 lines
5.1 KiB
Go
248 lines
5.1 KiB
Go
package server_test
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
"sneak.berlin/go/neoirc/internal/broker"
|
|
"sneak.berlin/go/neoirc/internal/config"
|
|
"sneak.berlin/go/neoirc/internal/db"
|
|
"sneak.berlin/go/neoirc/internal/globals"
|
|
"sneak.berlin/go/neoirc/internal/handlers"
|
|
"sneak.berlin/go/neoirc/internal/healthcheck"
|
|
"sneak.berlin/go/neoirc/internal/logger"
|
|
"sneak.berlin/go/neoirc/internal/middleware"
|
|
"sneak.berlin/go/neoirc/internal/server"
|
|
"sneak.berlin/go/neoirc/internal/service"
|
|
"sneak.berlin/go/neoirc/internal/stats"
|
|
)
|
|
|
|
const (
|
|
testTimeout = 5 * time.Second
|
|
|
|
// checkStep is how often waitUntil checks again.
|
|
checkStep = 10 * time.Millisecond
|
|
|
|
// stopHoldTime is how long a test waits to see that the
|
|
// app has not stopped.
|
|
stopHoldTime = 200 * time.Millisecond
|
|
)
|
|
|
|
// TestStopWaitsForRequestInProgress checks that stopping the
|
|
// app, which runs the stop hook server.New registers, waits
|
|
// for a long poll the HTTP server is serving to finish, and
|
|
// that the poll gets its response.
|
|
func TestStopWaitsForRequestInProgress(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
port := freePort(t)
|
|
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
|
|
|
app, brk, database := startApp(t, port)
|
|
|
|
sessionID, _, token, err := database.CreateSession(
|
|
t.Context(), "poller", "poller", "", "",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("create session: %v", err)
|
|
}
|
|
|
|
waitUntil(t, "the HTTP server to listen", func() bool {
|
|
return canConnect(t, addr)
|
|
})
|
|
|
|
statuses := make(chan int, 1)
|
|
|
|
go func() {
|
|
statuses <- getAuthenticated(
|
|
t, "http://"+addr+"/api/v1/messages?timeout=30", token,
|
|
)
|
|
}()
|
|
|
|
waitUntil(t, "the long poll to wait for a message", func() bool {
|
|
return brk.ListenerCount(sessionID) == 1
|
|
})
|
|
|
|
var stopErr error
|
|
|
|
stopped := make(chan struct{})
|
|
|
|
go func() {
|
|
stopErr = app.Stop(t.Context())
|
|
|
|
close(stopped)
|
|
}()
|
|
|
|
select {
|
|
case <-stopped:
|
|
t.Error("the app stopped while a request was in progress")
|
|
case <-time.After(stopHoldTime):
|
|
}
|
|
|
|
brk.Notify(sessionID)
|
|
|
|
status := <-statuses
|
|
if status != http.StatusOK {
|
|
t.Errorf("status %d, want %d", status, http.StatusOK)
|
|
}
|
|
|
|
select {
|
|
case <-stopped:
|
|
if stopErr != nil {
|
|
t.Errorf("stop: %v", stopErr)
|
|
}
|
|
case <-time.After(testTimeout):
|
|
t.Fatal("the app did not stop after the request finished")
|
|
}
|
|
}
|
|
|
|
// startApp starts the server with the dependencies
|
|
// cmd/neoircd gives it, on a fresh in-memory database, with
|
|
// the HTTP server listening on port.
|
|
func startApp(
|
|
t *testing.T,
|
|
port int,
|
|
) (*fxtest.App, *broker.Broker, *db.Database) {
|
|
t.Helper()
|
|
|
|
var (
|
|
brk *broker.Broker
|
|
database *db.Database
|
|
)
|
|
|
|
app := fxtest.New(t,
|
|
fx.Supply(&globals.Globals{
|
|
Appname: "neoirc-test",
|
|
Version: "test",
|
|
StartTime: time.Now(),
|
|
}),
|
|
fx.Provide(
|
|
broker.New,
|
|
config.New,
|
|
db.New,
|
|
handlers.New,
|
|
healthcheck.New,
|
|
logger.New,
|
|
middleware.New,
|
|
server.New,
|
|
service.New,
|
|
stats.New,
|
|
),
|
|
fx.Decorate(func(cfg *config.Config) *config.Config {
|
|
// A random name, so that no other test can share it.
|
|
cfg.DBURL = fmt.Sprintf(
|
|
"file:%s?mode=memory&cache=shared", rand.Text(),
|
|
)
|
|
cfg.Port = port
|
|
|
|
return cfg
|
|
}),
|
|
fx.Invoke(func(*server.Server) {}),
|
|
fx.Populate(&brk, &database),
|
|
)
|
|
|
|
app.RequireStart()
|
|
|
|
// Stops the app if the test ends before stopping it.
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
return app, brk, database
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// canConnect reports whether addr accepts a TCP connection.
|
|
func canConnect(t *testing.T, addr string) bool {
|
|
t.Helper()
|
|
|
|
var dialer net.Dialer
|
|
|
|
conn, err := dialer.DialContext(t.Context(), "tcp", addr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
_ = conn.Close()
|
|
|
|
return true
|
|
}
|
|
|
|
// waitUntil calls done every checkStep until it returns true,
|
|
// and fails the test if it does not within testTimeout.
|
|
func waitUntil(t *testing.T, what string, done func() bool) {
|
|
t.Helper()
|
|
|
|
deadline := time.Now().Add(testTimeout)
|
|
|
|
for !done() {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("timed out waiting for %s", what)
|
|
}
|
|
|
|
time.Sleep(checkStep)
|
|
}
|
|
}
|
|
|
|
// getAuthenticated sends a GET request to url as the session
|
|
// whose auth token is token, and returns the response status,
|
|
// or 0 if there was no response.
|
|
func getAuthenticated(t *testing.T, url, token 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
|
|
}
|
|
|
|
request.Header.Set("Cookie", "neoirc_auth="+token)
|
|
|
|
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
|
|
}
|