All checks were successful
check / check (push) Successful in 3m32s
Two configuration paths still failed silently, against the rule every other variable follows: a value that is set but cannot be parsed must abort startup rather than substitute a default. SENTRY_DSN is now parsed in loadFromEnv, with sentry.NewDsn — the same call sentry.Init makes on the DSN it is handed, so configuration and initialisation cannot disagree about what a valid DSN is. That costs internal/config an import of the Sentry SDK, which is already a module dependency already linked into the binary, and buys a single definition of validity rather than a hand-rolled second one free to drift. A typo in a DSN used to log one error line and leave the process serving with error reporting off forever, which nothing downstream can notice: the variable is still set, so every later signal reports it as on. hasSentryDSN is replaced by Config.SentryEnabled(), following MetricsAuthEnabled(): one method read by the startup log field, by the SDK initialisation and by the sentryhttp middleware, so the log cannot report reporting as on while nothing is sending. enableSentry's error branch is now fatal, and Run gives up before it listens rather than binding a port it is about to release. Fatal there means what a listen failure already meant — Shutdowner.Shutdown(fx.ExitCode(1)), through fx's normal stop sequence — so shutdownOnListenFailure is now shutdownWithFailure and ListenFailureExitCode is StartupFailureExitCode. The godotenv/autoload blank import is replaced by config.LoadDotEnv, called at the top of dispatch. autoload discarded Load's error, and godotenv applies nothing at all when a file will not parse, so one mistyped line reverted every variable in the file to its default and started the server with no log line naming the file. A missing file stays fine — it is optional and most deployments have none. The call sits in dispatch rather than in loadFromEnv because autoload ran in an init(), ahead of config.DataDir(), which both the DATA_DIR lock and resetpw call outside the fx graph; loading any later would let a .env that sets DATA_DIR lock one directory while the config opened databases in another. Both defects were reproduced against the previous build first: an unparseable DSN served traffic while logging "hasSentryDSN":true, and a malformed .env started on the default port with the file unmentioned.
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
package config_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
)
|
|
|
|
// dotEnvKey is a throwaway variable name the .env tests write and
|
|
// read, so they cannot disturb real configuration.
|
|
const dotEnvKey = "WEBHOOKER_TEST_DOTENV_VALUE"
|
|
|
|
// malformedDotEnv is a file godotenv cannot parse. The first line is
|
|
// the realistic typo — a space where the `=` belongs — and the rest
|
|
// make sure nothing downstream treats the file as salvageable line by
|
|
// line.
|
|
const malformedDotEnv = "PORT 19615\n" +
|
|
"this is not = valid ! syntax\n" +
|
|
"\"unclosed\n"
|
|
|
|
// unsetDotEnvKey makes dotEnvKey genuinely absent for the duration of
|
|
// the test and restores it afterwards. t.Setenv registers the restore;
|
|
// the Unsetenv that follows is what the test actually needs, because a
|
|
// variable set to the empty string is still present in os.Environ and
|
|
// godotenv would refuse to overwrite it.
|
|
func unsetDotEnvKey(t *testing.T) {
|
|
t.Helper()
|
|
t.Setenv(dotEnvKey, "placeholder")
|
|
require.NoError(t, os.Unsetenv(dotEnvKey))
|
|
}
|
|
|
|
// writeDotEnv writes contents to a .env file in a fresh temporary
|
|
// directory and returns its path.
|
|
func writeDotEnv(t *testing.T, contents string) string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), config.DotEnvPath)
|
|
require.NoError(t, os.WriteFile(path, []byte(contents), 0o600))
|
|
|
|
return path
|
|
}
|
|
|
|
// TestLoadDotEnv_MissingFileIsFine pins the case most deployments are
|
|
// in. The file is optional: it is a development convenience, and a
|
|
// deployment that configures the environment directly must start
|
|
// normally rather than be refused for a file it was never meant to
|
|
// have.
|
|
//
|
|
//nolint:paralleltest // unsetDotEnvKey uses t.Setenv.
|
|
func TestLoadDotEnv_MissingFileIsFine(t *testing.T) {
|
|
unsetDotEnvKey(t)
|
|
|
|
absent := filepath.Join(t.TempDir(), config.DotEnvPath)
|
|
require.NoError(t, config.LoadDotEnvFileForTest(absent))
|
|
|
|
_, present := os.LookupEnv(dotEnvKey)
|
|
assert.False(t, present, "nothing may be set from an absent file")
|
|
}
|
|
|
|
// TestLoadDotEnv_AppliesValues pins that a well-formed file still
|
|
// reaches the environment, which is the whole reason the file is read
|
|
// at all.
|
|
//
|
|
//nolint:paralleltest // unsetDotEnvKey uses t.Setenv.
|
|
func TestLoadDotEnv_AppliesValues(t *testing.T) {
|
|
unsetDotEnvKey(t)
|
|
|
|
path := writeDotEnv(t, "# a comment\n"+dotEnvKey+"=from-dot-env\n")
|
|
|
|
require.NoError(t, config.LoadDotEnvFileForTest(path))
|
|
assert.Equal(t, "from-dot-env", os.Getenv(dotEnvKey))
|
|
}
|
|
|
|
// TestLoadDotEnv_RealEnvironmentWins pins that the file cannot
|
|
// override a variable the process was actually started with. A
|
|
// deployment that sets DATA_DIR in its unit file must not have it
|
|
// silently replaced by a stale .env left in the working directory.
|
|
func TestLoadDotEnv_RealEnvironmentWins(t *testing.T) {
|
|
t.Setenv(dotEnvKey, "from-environment")
|
|
|
|
path := writeDotEnv(t, dotEnvKey+"=from-dot-env\n")
|
|
|
|
require.NoError(t, config.LoadDotEnvFileForTest(path))
|
|
assert.Equal(t, "from-environment", os.Getenv(dotEnvKey))
|
|
}
|
|
|
|
// TestLoadDotEnv_MalformedFileAborts is the defect this fixes. One bad
|
|
// line makes godotenv apply none of the file, so every variable in it
|
|
// reverts to its default; the process used to start that way with no
|
|
// log line naming the file at all.
|
|
//
|
|
//nolint:paralleltest // unsetDotEnvKey uses t.Setenv.
|
|
func TestLoadDotEnv_MalformedFileAborts(t *testing.T) {
|
|
unsetDotEnvKey(t)
|
|
|
|
path := writeDotEnv(
|
|
t, malformedDotEnv+dotEnvKey+"=from-dot-env\n",
|
|
)
|
|
|
|
err := config.LoadDotEnvFileForTest(path)
|
|
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, config.ErrDotEnvUnreadable)
|
|
assert.Contains(
|
|
t, err.Error(), config.DotEnvPath,
|
|
"the failure must name the file it could not read",
|
|
)
|
|
|
|
_, present := os.LookupEnv(dotEnvKey)
|
|
assert.False(
|
|
t, present,
|
|
"a rejected file must apply nothing, not part of itself",
|
|
)
|
|
}
|
|
|
|
// TestLoadDotEnv_UnreadableFileAborts pins that only absence is
|
|
// tolerated. A .env that exists but cannot be read is a file the
|
|
// operator meant to be applied, so it fails like a malformed one
|
|
// rather than being treated as though it were not there.
|
|
func TestLoadDotEnv_UnreadableFileAborts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A directory in the file's place: open succeeds and the read
|
|
// fails, which no umask or root-ness can turn back into success
|
|
// the way a chmod could.
|
|
path := filepath.Join(t.TempDir(), config.DotEnvPath)
|
|
require.NoError(t, os.Mkdir(path, 0o750))
|
|
|
|
err := config.LoadDotEnvFileForTest(path)
|
|
|
|
require.Error(t, err)
|
|
require.ErrorIs(t, err, config.ErrDotEnvUnreadable)
|
|
}
|
|
|
|
// TestLoadDotEnv_ReadsTheWorkingDirectory pins the path LoadDotEnv
|
|
// itself opens, which the tests above bypass. It is relative to the
|
|
// process working directory, as it was under godotenv/autoload and as
|
|
// the README documents.
|
|
//
|
|
//nolint:paralleltest // t.Chdir moves the whole process.
|
|
func TestLoadDotEnv_ReadsTheWorkingDirectory(t *testing.T) {
|
|
unsetDotEnvKey(t)
|
|
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, config.DotEnvPath),
|
|
[]byte(dotEnvKey+"=from-working-directory\n"),
|
|
0o600,
|
|
))
|
|
t.Chdir(dir)
|
|
|
|
require.NoError(t, config.LoadDotEnv())
|
|
assert.Equal(t, "from-working-directory", os.Getenv(dotEnvKey))
|
|
}
|