check / check (push) Canceled after 0s
The health check answered ok while every DNS lookup failed, as on a network that intercepts DNS queries. A lookup whose root servers give no usable reply now fails with ErrNoRootServerAnswered. At the end of each round of DNS checks the watcher records whether every lookup failed with it or with ErrIntercepted; the health check then gives status error, and /.well-known/healthcheck and /health answer 503. Until the first round finishes it stays ok. The health check now depends on the watcher, so fx builds the watcher first: its first round starts before the HTTP server listens, and the server tests' fx graph includes it. NewWithRootServers lets tests start a resolver's walks at real servers that are not root servers. Model: opus-5-5
199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
package server_test
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/config"
|
|
"sneak.berlin/go/dnswatcher/internal/globals"
|
|
"sneak.berlin/go/dnswatcher/internal/handlers"
|
|
"sneak.berlin/go/dnswatcher/internal/healthcheck"
|
|
"sneak.berlin/go/dnswatcher/internal/logger"
|
|
"sneak.berlin/go/dnswatcher/internal/middleware"
|
|
"sneak.berlin/go/dnswatcher/internal/notify"
|
|
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
|
"sneak.berlin/go/dnswatcher/internal/resolver"
|
|
"sneak.berlin/go/dnswatcher/internal/server"
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
"sneak.berlin/go/dnswatcher/internal/tlscheck"
|
|
"sneak.berlin/go/dnswatcher/internal/watcher"
|
|
)
|
|
|
|
// newServerApp builds an fx app holding a *server.Server wired exactly
|
|
// as cmd/dnswatcher wires it, plus the given option. The health check
|
|
// follows the watcher, so the watcher is in the app too. Starting the
|
|
// app starts the watcher's first round of checks, against live DNS,
|
|
// before the server; the tests that start it expect the server to fail
|
|
// to start, which stops the watcher again. config.New reads viper, so
|
|
// the caller must first configure it, which is also why the caller
|
|
// cannot run in parallel.
|
|
func newServerApp(option fx.Option) *fx.App {
|
|
return fx.New(
|
|
fx.NopLogger,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
state.New,
|
|
healthcheck.New,
|
|
resolver.New,
|
|
portcheck.New,
|
|
tlscheck.New,
|
|
notify.New,
|
|
watcher.New,
|
|
middleware.New,
|
|
handlers.New,
|
|
server.New,
|
|
),
|
|
fx.Provide(
|
|
func(r *resolver.Resolver) watcher.DNSResolver {
|
|
return r
|
|
},
|
|
func(p *portcheck.Checker) watcher.PortChecker {
|
|
return p
|
|
},
|
|
func(t *tlscheck.Checker) watcher.TLSChecker {
|
|
return t
|
|
},
|
|
func(n *notify.Service) watcher.Notifier {
|
|
return n
|
|
},
|
|
),
|
|
option,
|
|
)
|
|
}
|
|
|
|
// buildServer builds the server without starting the app's lifecycle,
|
|
// so no OnStart hook runs and nothing listens or resolves.
|
|
func buildServer(t *testing.T) *server.Server {
|
|
t.Helper()
|
|
|
|
var srv *server.Server
|
|
|
|
app := newServerApp(fx.Populate(&srv))
|
|
|
|
err := app.Err()
|
|
if err != nil {
|
|
t.Fatalf("building server graph: %v", err)
|
|
}
|
|
|
|
return srv
|
|
}
|
|
|
|
// TestRunWiresSocketTimeouts pins that the http.Server the running
|
|
// server actually serves — the one Run builds and serves on the port
|
|
// it binds — carries every socket-level timeout, plus the two
|
|
// relationships the values must satisfy.
|
|
//
|
|
// Run is given an unbindable port: it builds and stores s.httpServer,
|
|
// then the bind fails and Run returns the error without ever
|
|
// listening. Nothing here measures elapsed time.
|
|
//
|
|
// ReadTimeout must be at least ReadHeaderTimeout. net/http reads the
|
|
// headers under ReadHeaderTimeout, then sets the read deadline for the
|
|
// rest of the request to ReadTimeout, counted from when it started
|
|
// reading the request. If ReadTimeout were smaller, a request whose
|
|
// headers arrived after ReadTimeout but within ReadHeaderTimeout would
|
|
// get a read deadline that had already passed, so reading its body
|
|
// would fail at once.
|
|
func TestRunWiresSocketTimeouts(t *testing.T) {
|
|
// Sets an env var and touches viper global state, so like the
|
|
// config tests it cannot use t.Parallel.
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
|
|
srv := buildServer(t)
|
|
server.SetListenPort(srv, -1)
|
|
|
|
err := srv.Run(t.Context())
|
|
if err == nil {
|
|
t.Fatal("Run bound port -1")
|
|
}
|
|
|
|
hs := server.HTTPServerOf(srv)
|
|
if hs == nil {
|
|
t.Fatal("Run did not build an http.Server")
|
|
}
|
|
|
|
if hs.ReadTimeout <= 0 {
|
|
t.Errorf("ReadTimeout must be non-zero, got %v", hs.ReadTimeout)
|
|
}
|
|
|
|
if hs.ReadHeaderTimeout <= 0 {
|
|
t.Errorf(
|
|
"ReadHeaderTimeout must be non-zero, got %v",
|
|
hs.ReadHeaderTimeout,
|
|
)
|
|
}
|
|
|
|
if hs.WriteTimeout <= 0 {
|
|
t.Errorf("WriteTimeout must be non-zero, got %v", hs.WriteTimeout)
|
|
}
|
|
|
|
if hs.IdleTimeout <= 0 {
|
|
t.Errorf("IdleTimeout must be non-zero, got %v", hs.IdleTimeout)
|
|
}
|
|
|
|
if hs.WriteTimeout <= server.RequestTimeout {
|
|
t.Errorf(
|
|
"WriteTimeout (%v) must exceed handler budget (%v)",
|
|
hs.WriteTimeout,
|
|
server.RequestTimeout,
|
|
)
|
|
}
|
|
|
|
if hs.ReadTimeout < hs.ReadHeaderTimeout {
|
|
t.Errorf(
|
|
"ReadTimeout (%v) must be >= ReadHeaderTimeout (%v)",
|
|
hs.ReadTimeout,
|
|
hs.ReadHeaderTimeout,
|
|
)
|
|
}
|
|
|
|
if hs.Handler != srv {
|
|
t.Errorf(
|
|
"Run wired handler %T, want the *server.Server",
|
|
hs.Handler,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestPortInUseStopsStartup(t *testing.T) {
|
|
// Bound on every address, as Run binds, so that Run's bind fails.
|
|
lc := &net.ListenConfig{}
|
|
|
|
taken, err := lc.Listen(t.Context(), "tcp", ":0")
|
|
if err != nil {
|
|
t.Fatalf("binding a port for the test: %v", err)
|
|
}
|
|
|
|
defer func() { _ = taken.Close() }()
|
|
|
|
_, port, err := net.SplitHostPort(taken.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("reading the bound port: %v", err)
|
|
}
|
|
|
|
viper.Reset()
|
|
t.Setenv("DNSWATCHER_TARGETS", "example.com")
|
|
t.Setenv("DNSWATCHER_DATA_DIR", t.TempDir())
|
|
t.Setenv("PORT", port)
|
|
|
|
app := newServerApp(fx.Invoke(func(*server.Server) {}))
|
|
|
|
err = app.Start(t.Context())
|
|
if err == nil {
|
|
_ = app.Stop(t.Context())
|
|
|
|
t.Fatal("startup succeeded on a port that is already bound")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), ":"+port) {
|
|
t.Errorf("startup error does not name port %s: %v", port, err)
|
|
}
|
|
}
|