package main import ( "bytes" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/config" ) // dotEnvKey is a throwaway variable name these tests write and read, // so they cannot disturb real configuration. const dotEnvKey = "WEBHOOKER_TEST_DISPATCH_VALUE" // writeDotEnvInWorkingDir puts contents in a .env file in a fresh // temporary directory and moves the process there. // // The callers are deliberately not parallel and must stay that way: // t.Chdir moves the whole process. Go releases parallel tests only // after every sequential test in the package has finished, so nothing // else runs while these do. func writeDotEnvInWorkingDir(t *testing.T, contents string) { t.Helper() dir := t.TempDir() require.NoError(t, os.WriteFile( filepath.Join(dir, config.DotEnvPath), []byte(contents), 0o600, )) t.Chdir(dir) } // TestDispatch_MalformedDotEnvRefuses pins the second half of the // defect. godotenv applies nothing at all when a file will not parse, // so one mistyped line used to revert every variable in it to its // default and start the server anyway, with no log line naming the // file. The refusal has to arrive before any subcommand runs, which // is why `help` — the one subcommand that touches nothing — is still // refused here. // //nolint:paralleltest // t.Chdir moves the whole process. func TestDispatch_MalformedDotEnvRefuses(t *testing.T) { writeDotEnvInWorkingDir(t, "PORT 19615\n") var stdout, stderr bytes.Buffer code := dispatch( []string{helpCommand}, strings.NewReader(""), &stdout, &stderr, ) require.Equal(t, 1, code, "a broken .env must exit non-zero") assert.Contains( t, stderr.String(), config.DotEnvPath, "the refusal must name the file", ) assert.Empty( t, stdout.String(), "the subcommand must not have run", ) } // TestDispatch_LoadsDotEnvBeforeSubcommands pins the ordering the // godotenv/autoload import used to provide for free. It ran in an // init(), so .env was in the environment before anything read it — // including 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. func TestDispatch_LoadsDotEnvBeforeSubcommands(t *testing.T) { t.Setenv(dotEnvKey, "placeholder") require.NoError(t, os.Unsetenv(dotEnvKey)) writeDotEnvInWorkingDir(t, dotEnvKey+"=from-dot-env\n") var stdout, stderr bytes.Buffer code := dispatch( []string{helpCommand}, strings.NewReader(""), &stdout, &stderr, ) require.Equal(t, 0, code) assert.Equal( t, "from-dot-env", os.Getenv(dotEnvKey), "the file must be applied before the subcommand runs", ) } // TestDispatch_MissingDotEnvIsFine pins the case most deployments are // in: no .env at all, which must stay a normal start. // //nolint:paralleltest // t.Chdir moves the whole process. func TestDispatch_MissingDotEnvIsFine(t *testing.T) { t.Chdir(t.TempDir()) var stdout, stderr bytes.Buffer code := dispatch( []string{helpCommand}, strings.NewReader(""), &stdout, &stderr, ) require.Equal(t, 0, code) assert.Empty(t, stderr.String()) }