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