100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package server_test
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/server"
|
|
)
|
|
|
|
// TestSentryInitFailure_ShutsDownTheApp pins that error reporting
|
|
// which is configured and cannot be started ends the application
|
|
// instead of serving without it.
|
|
//
|
|
// The measured defect logged `sentry init failure` and kept running,
|
|
// so the deployment served traffic with reporting off while every
|
|
// other signal — SENTRY_DSN still set, the startup summary's own
|
|
// field — said it was on. Nothing later in the process can notice
|
|
// that reports are going nowhere, which is why this exits rather than
|
|
// degrades.
|
|
//
|
|
// The DSN is placed on a hand-built Config, which is the only way to
|
|
// reach this branch at all: loadFromEnv now parses SENTRY_DSN with
|
|
// sentry.NewDsn, the same call sentry.Init makes, so a DSN that
|
|
// survives configuration cannot fail initialisation in the SDK
|
|
// version this pins. The branch stays because that is a property of
|
|
// the SDK's current implementation rather than of its contract.
|
|
func TestSentryInitFailure_ShutsDownTheApp(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
port := freePort(t)
|
|
|
|
env := newTestEnvWithConfig(t, &config.Config{
|
|
DataDir: t.TempDir(),
|
|
Environment: config.EnvironmentDev,
|
|
BindAddress: loopbackV4,
|
|
Port: port,
|
|
SentryDSN: "not-a-dsn",
|
|
})
|
|
|
|
app := fx.New(
|
|
fx.NopLogger,
|
|
fx.Supply(env.log, env.cfg, env.mw, env.hnd),
|
|
fx.Provide(globals.New, server.New),
|
|
fx.Invoke(func(*server.Server) {}),
|
|
)
|
|
|
|
startCtx, cancelStart := context.WithTimeout(
|
|
context.Background(), lifecycleTimeout,
|
|
)
|
|
defer cancelStart()
|
|
|
|
require.NoError(t, app.Start(startCtx))
|
|
|
|
select {
|
|
case sig := <-app.Wait():
|
|
require.Equal(
|
|
t, server.StartupFailureExitCode, sig.ExitCode,
|
|
"a sentry failure must exit non-zero",
|
|
)
|
|
case <-time.After(listenFailureDeadline):
|
|
t.Fatal("a sentry failure left the app running")
|
|
}
|
|
|
|
// The stop sequence still has to complete: the failure must reach
|
|
// shutdown through fx rather than around it.
|
|
stopCtx, cancelStop := context.WithTimeout(
|
|
context.Background(), lifecycleTimeout,
|
|
)
|
|
defer cancelStop()
|
|
|
|
require.NoError(t, app.Stop(stopCtx))
|
|
|
|
// And it must give up before it listens. A process that bound the
|
|
// port and then exited would have accepted requests it could not
|
|
// report on, which is the state under test in miniature.
|
|
requireBindable(t, port)
|
|
}
|
|
|
|
// requireBindable asserts that the port is free, which it is only if
|
|
// the server under test never claimed it.
|
|
func requireBindable(t *testing.T, port int) {
|
|
t.Helper()
|
|
|
|
var listenCfg net.ListenConfig
|
|
|
|
listener, err := listenCfg.Listen(
|
|
t.Context(), "tcp",
|
|
net.JoinHostPort(loopbackV4, strconv.Itoa(port)),
|
|
)
|
|
require.NoError(t, err, "the server bound a port it then gave up")
|
|
require.NoError(t, listener.Close())
|
|
}
|