Fail loudly on an unparseable SENTRY_DSN and a malformed .env (closes #283)
All checks were successful
check / check (push) Successful in 2m54s
All checks were successful
check / check (push) Successful in 2m54s
This commit was merged in pull request #289.
This commit is contained in:
158
internal/config/dotenv_test.go
Normal file
158
internal/config/dotenv_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user