check / check (push) Waiting to run
The config tests unset variables without restoring them and read whatever the developer's shell exported, so results could differ from one machine to the next. config.ClearEnvForTest, in internal/config/testing.go, unsets every variable in the process environment and, when the test ends, leaves it exactly as it found it; every config test and the first-boot test call it before setting their own. TestEnvPositiveInt and TestEnvPort share one table runner, and the port errors name the bad value. A PORT of zero, below zero, or too large to parse now matches ErrInvalidPort, as one above 65535 already did. The README and the Settings page say an unparseable or non-positive RETENTION_SWEEP_INTERVAL stops startup. Model: opus-5-5
148 lines
4.7 KiB
Go
148 lines
4.7 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"
|
|
|
|
// 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 // ClearEnvForTest uses t.Setenv.
|
|
func TestLoadDotEnv_MissingFileIsFine(t *testing.T) {
|
|
config.ClearEnvForTest(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 // ClearEnvForTest uses t.Setenv.
|
|
func TestLoadDotEnv_AppliesValues(t *testing.T) {
|
|
config.ClearEnvForTest(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 // ClearEnvForTest uses t.Setenv.
|
|
func TestLoadDotEnv_MalformedFileAborts(t *testing.T) {
|
|
config.ClearEnvForTest(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) {
|
|
config.ClearEnvForTest(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))
|
|
}
|