package config_test import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/config" ) // envKeySentryDSN is the variable envSentryDSN reads in production. const envKeySentryDSN = "SENTRY_DSN" // validSentryDSN is a syntactically complete DSN. The host is under // .invalid (RFC 2606), so nothing a test builds around it can reach a // real Sentry installation. const validSentryDSN = "https://abc123@sentry.invalid/42" // envSentryDSNCase is one row of the envSentryDSN table. type envSentryDSNCase struct { name string set bool value string expectError bool expected string } // envSentryDSNCases is the envSentryDSN table. The three invalid // values are the ones measured on the defect: each initialised the SDK // with an error and left the process serving with reporting off. func envSentryDSNCases() []envSentryDSNCase { return []envSentryDSNCase{ { name: "unset means reporting off", expected: "", }, { name: "empty means reporting off", set: true, value: "", expected: "", }, { name: "whitespace means reporting off", set: true, value: " ", expected: "", }, { name: "a valid DSN is kept", set: true, value: validSentryDSN, expected: validSentryDSN, }, { name: "surrounding whitespace is trimmed", set: true, value: " " + validSentryDSN + "\t", expected: validSentryDSN, }, { name: "a value that is not a URL is rejected", set: true, value: "not-a-dsn", expectError: true, }, { name: "an unparseable URL is rejected", set: true, value: "%%%", expectError: true, }, { name: "a DSN without a public key is rejected", set: true, value: "https://example.invalid/1", expectError: true, }, { name: "a DSN without a project id is rejected", set: true, value: "https://abc123@sentry.invalid/", expectError: true, }, { name: "a non-HTTP scheme is rejected", set: true, value: "ftp://abc123@sentry.invalid/42", expectError: true, }, } } // TestEnvSentryDSN covers the helper directly. What it pins beyond the // value is the failure shape: a set-but-unparseable DSN names the // variable and the value, exactly as the other fail-loud helpers do, // so an operator reads the fix off the message. func TestEnvSentryDSN(t *testing.T) { for _, tt := range envSentryDSNCases() { t.Run(tt.name, func(t *testing.T) { // Cannot use t.Parallel() here because t.Setenv // is incompatible with parallel subtests. if tt.set { t.Setenv(envKeySentryDSN, tt.value) } else { require.NoError(t, os.Unsetenv(envKeySentryDSN)) } got, err := config.EnvSentryDSNForTest(envKeySentryDSN) if tt.expectError { require.Error(t, err) require.ErrorIs(t, err, config.ErrInvalidSentryDSN) assert.Contains(t, err.Error(), envKeySentryDSN) assert.Contains(t, err.Error(), tt.value) assert.Empty(t, got) return } require.NoError(t, err) assert.Equal(t, tt.expected, got) }) } } // TestSentryEnabled_TracksTheDSN pins that the one method answering // "is anything being reported" agrees with the DSN in every state. The // startup log, the SDK initialisation and the sentryhttp middleware // all read it, so a log field cannot report reporting as on while // nothing is sending. func TestSentryEnabled_TracksTheDSN(t *testing.T) { t.Parallel() assert.False(t, (&config.Config{}).SentryEnabled()) assert.True( t, (&config.Config{SentryDSN: validSentryDSN}).SentryEnabled(), ) }