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()) }