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:
107
cmd/webhooker/dotenv_test.go
Normal file
107
cmd/webhooker/dotenv_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
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())
|
||||
}
|
||||
@@ -54,6 +54,11 @@ const stopTimeout = 5 * time.Second
|
||||
// caller can tell "called wrong" from "declined".
|
||||
const exitUsage = 2
|
||||
|
||||
// helpCommand is the subcommand that prints usage. The flag spellings
|
||||
// beside it in the switch are aliases; this is the name the usage text
|
||||
// documents and the one tests invoke.
|
||||
const helpCommand = "help"
|
||||
|
||||
// Build-time variables set via -ldflags.
|
||||
//
|
||||
//nolint:gochecknoglobals // Build-time variables injected by the linker.
|
||||
@@ -75,11 +80,27 @@ func main() {
|
||||
// every existing deployment invoke; that path is unchanged, including
|
||||
// where the DATA_DIR lock is taken relative to building the fx graph
|
||||
// and how fx propagates a non-zero exit itself.
|
||||
//
|
||||
// The optional .env file is read here, before any subcommand and so
|
||||
// before anything reads the environment — config.DataDir, which both
|
||||
// the DATA_DIR lock and resetpw call outside the fx graph, above all.
|
||||
// It used to be read from an init() in internal/config, which put it
|
||||
// earlier still but threw the error away: a single malformed line
|
||||
// applied none of the file and said nothing about it. A file that is
|
||||
// not there stays fine, since .env is optional and most deployments
|
||||
// do not have one.
|
||||
func dispatch(
|
||||
args []string,
|
||||
stdin io.Reader,
|
||||
stdout, stderr io.Writer,
|
||||
) int {
|
||||
err := config.LoadDotEnv()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "%s: %v\n", appname, err)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return run(stderr)
|
||||
}
|
||||
@@ -87,7 +108,7 @@ func dispatch(
|
||||
switch args[0] {
|
||||
case resetpw.Name:
|
||||
return resetpw.Run(args[1:], stdin, stdout, stderr)
|
||||
case "help", "-h", "-help", "--help":
|
||||
case helpCommand, "-h", "-help", "--help":
|
||||
usage(stdout)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -121,7 +121,7 @@ func TestDispatch_Help(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := dispatch(
|
||||
[]string{"help"}, strings.NewReader(""), &stdout, &stderr,
|
||||
[]string{helpCommand}, strings.NewReader(""), &stdout, &stderr,
|
||||
)
|
||||
|
||||
require.Equal(t, 0, code)
|
||||
|
||||
Reference in New Issue
Block a user