Fail startup when the HTTP listener cannot bind (closes #167)
check / check (push) Waiting to run
check / check (push) Waiting to run
The HTTP server ran ListenAndServe in a goroutine, so a failed bind was only logged and neoircd kept running without its HTTP API. The start hook now binds the listen address and returns the bind error, so neoircd exits 1 with an error naming the address. If serving the bound listener later ends with anything other than a normal shutdown, the server stops the app through fx.Shutdowner with exit code 1; the handler tests now pass it that Shutdowner. Tests check that a taken port fails startup, that a serve failure stops the app with exit code 1, and that a normal stop does not. The server tests no longer run in parallel: building the app writes the global viper instance. Model: opus-5-5
This commit is contained in:
+127
-16
@@ -1,3 +1,7 @@
|
||||
// Tests use a global viper instance for configuration,
|
||||
// making parallel execution unsafe.
|
||||
//
|
||||
//nolint:paralleltest
|
||||
package server_test
|
||||
|
||||
import (
|
||||
@@ -6,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -40,8 +45,6 @@ const (
|
||||
// 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))
|
||||
|
||||
@@ -103,12 +106,114 @@ func TestStopWaitsForRequestInProgress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// startApp starts the server with the dependencies
|
||||
// cmd/neoircd gives it, on a fresh in-memory database, with
|
||||
// the HTTP server listening on port.
|
||||
// TestStartFailsWhenHTTPPortIsTaken checks that starting the
|
||||
// app, which runs the start hook server.New registers,
|
||||
// returns an error naming the HTTP listen address when
|
||||
// something else is already listening on its port.
|
||||
func TestStartFailsWhenHTTPPortIsTaken(t *testing.T) {
|
||||
var listenConfig net.ListenConfig
|
||||
|
||||
taken, err := listenConfig.Listen(t.Context(), "tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() { _ = taken.Close() })
|
||||
|
||||
tcpAddr, ok := taken.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("listener address %v is not TCP", taken.Addr())
|
||||
}
|
||||
|
||||
app, _, _ := newApp(t, tcpAddr.Port)
|
||||
|
||||
err = app.Start(t.Context())
|
||||
if err == nil {
|
||||
app.RequireStop()
|
||||
t.Fatal("the app started while its HTTP port was taken")
|
||||
}
|
||||
|
||||
listenAddr := fmt.Sprintf(":%d", tcpAddr.Port)
|
||||
if !strings.Contains(err.Error(), listenAddr) {
|
||||
t.Errorf("start error %q does not name %s", err, listenAddr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeFailureStopsApp checks that when serving HTTP fails
|
||||
// after the start hook has bound the listen address, the
|
||||
// server stops the app with exit code 1.
|
||||
func TestServeFailureStopsApp(t *testing.T) {
|
||||
var srv *server.Server
|
||||
|
||||
app, _, _ := startApp(t, freePort(t), fx.Populate(&srv))
|
||||
|
||||
shutdowns := app.Wait()
|
||||
|
||||
srv.ServeUntilShutdown(closedListener(t))
|
||||
|
||||
select {
|
||||
case shutdown := <-shutdowns:
|
||||
if shutdown.ExitCode != 1 {
|
||||
t.Errorf("exit code %d, want 1", shutdown.ExitCode)
|
||||
}
|
||||
default:
|
||||
t.Error("the server did not stop the app")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeAfterStopDoesNotStopApp checks that serving HTTP
|
||||
// after the app has stopped, which ends at once because the
|
||||
// stop hook has shut the HTTP server down, does not stop the
|
||||
// app. Serving ends the same way on every normal stop.
|
||||
func TestServeAfterStopDoesNotStopApp(t *testing.T) {
|
||||
var srv *server.Server
|
||||
|
||||
app, _, _ := startApp(t, freePort(t), fx.Populate(&srv))
|
||||
|
||||
shutdowns := app.Wait()
|
||||
|
||||
app.RequireStop()
|
||||
|
||||
srv.ServeUntilShutdown(closedListener(t))
|
||||
|
||||
select {
|
||||
case shutdown := <-shutdowns:
|
||||
t.Errorf(
|
||||
"the server stopped the app with exit code %d",
|
||||
shutdown.ExitCode,
|
||||
)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// startApp starts the app newApp builds, with the HTTP server
|
||||
// listening on port.
|
||||
func startApp(
|
||||
t *testing.T,
|
||||
port int,
|
||||
options ...fx.Option,
|
||||
) (*fxtest.App, *broker.Broker, *db.Database) {
|
||||
t.Helper()
|
||||
|
||||
app, brk, database := newApp(t, port, options...)
|
||||
|
||||
app.RequireStart()
|
||||
|
||||
// Stops the app if the test ends before stopping it.
|
||||
t.Cleanup(app.RequireStop)
|
||||
|
||||
return app, brk, database
|
||||
}
|
||||
|
||||
// newApp builds, without starting it, an app that runs the
|
||||
// server with the dependencies cmd/neoircd gives it, on a
|
||||
// fresh in-memory database, with the HTTP port set to port.
|
||||
// options are added to the app, for a test that needs more
|
||||
// from it than the broker and the database.
|
||||
func newApp(
|
||||
t *testing.T,
|
||||
port int,
|
||||
options ...fx.Option,
|
||||
) (*fxtest.App, *broker.Broker, *db.Database) {
|
||||
t.Helper()
|
||||
|
||||
@@ -146,13 +251,9 @@ func startApp(
|
||||
}),
|
||||
fx.Invoke(func(*server.Server) {}),
|
||||
fx.Populate(&brk, &database),
|
||||
fx.Options(options...),
|
||||
)
|
||||
|
||||
app.RequireStart()
|
||||
|
||||
// Stops the app if the test ends before stopping it.
|
||||
t.Cleanup(app.RequireStop)
|
||||
|
||||
return app, brk, database
|
||||
}
|
||||
|
||||
@@ -161,6 +262,21 @@ func startApp(
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
|
||||
listener := closedListener(t)
|
||||
|
||||
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
t.Fatalf("listener address %v is not TCP", listener.Addr())
|
||||
}
|
||||
|
||||
return tcpAddr.Port
|
||||
}
|
||||
|
||||
// closedListener returns a TCP listener that has already been
|
||||
// closed, so that accepting a connection on it fails.
|
||||
func closedListener(t *testing.T) net.Listener {
|
||||
t.Helper()
|
||||
|
||||
var listenConfig net.ListenConfig
|
||||
|
||||
listener, err := listenConfig.Listen(
|
||||
@@ -170,17 +286,12 @@ func freePort(t *testing.T) int {
|
||||
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
|
||||
return listener
|
||||
}
|
||||
|
||||
// canConnect reports whether addr accepts a TCP connection.
|
||||
|
||||
Reference in New Issue
Block a user