diff --git a/README.md b/README.md index 78c0ba6..1d7ff34 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ Configuration is read from, in increasing priority: given with `--config` 3. `JOBSEARCH_*` environment variables, e.g. `JOBSEARCH_LOGGING_LEVEL=debug` -A `.env` file in the working directory is loaded into the environment first, -and must exist. +A `.env` file in the working directory, if there is one, is loaded into the +environment first. Example `config.yaml`: ```yaml diff --git a/internal/cli/flags_test.go b/internal/cli/flags_test.go index 22c8b2d..3ede77d 100644 --- a/internal/cli/flags_test.go +++ b/internal/cli/flags_test.go @@ -73,11 +73,17 @@ func TestConfigFlag(t *testing.T) { } } -func TestMissingEnvFile(t *testing.T) { +// A .env file is optional, but one that can't be parsed is an error. +func TestEnvFile(t *testing.T) { cmdtest.Setup(t, appConfig) if err := os.Remove(".env"); err != nil { t.Fatal(err) } + if _, err := cmdtest.Run(t, newRoot, "", "level"); err != nil { + t.Errorf("without a .env file: %v; want no error", err) + } + + cmdtest.WriteFile(t, ".env", "NOT VALID\n") _, err := cmdtest.Run(t, newRoot, "", "level") cmdtest.ErrContains(t, err, ".env") } diff --git a/internal/config/README.md b/internal/config/README.md index 2ed436c..ba4b73d 100644 --- a/internal/config/README.md +++ b/internal/config/README.md @@ -8,8 +8,8 @@ environment variables, merged through [Viper](https://github.com/spf13/viper). ## Loading order `Init(cfgFile, &conf)` builds the configuration in this order: -1. Loads `.env` from the working directory into the environment. A missing - `.env` file is an error. +1. Loads `.env` from the working directory into the environment, if there + is one. A `.env` file that can't be read or parsed is an error. 2. Sets the defaults from the `default` struct tags as Viper defaults. 3. Reads the config file: `cfgFile` if given, otherwise the first `config.yaml` found in `/etc/job-search` then the working directory. If no diff --git a/internal/config/load.go b/internal/config/load.go index fa47f70..9b8a78c 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -3,6 +3,7 @@ package config import ( "errors" "fmt" + "io/fs" "log/slog" "os" "strings" @@ -34,9 +35,13 @@ func isExplicitlySet(key string) bool { } // loadEnv loads environment variables from a .env file in the working -// directory. +// directory, if there is one. One that can't be read or parsed is an error. func loadEnv() error { - if err := godotenv.Load(); err != nil { + err := godotenv.Load() + if errors.Is(err, fs.ErrNotExist) { + return nil + } + if err != nil { return fmt.Errorf("invalid .env file: %w", err) } return nil diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 87a51e8..5c393e1 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -141,9 +141,19 @@ func TestInitKeepsCommandLineLevel(t *testing.T) { } } +// A .env file is optional. +func TestInitWithoutEnvFile(t *testing.T) { + initDir(t, nil, testConfigYAML) + var conf JobSearchConfig + if err := Init("", &conf); err != nil { + t.Fatalf("err = %v; want nil without a .env file", err) + } +} + func TestInitErrors(t *testing.T) { - t.Run("no .env file", func(t *testing.T) { - initDir(t, nil, testConfigYAML) + t.Run("invalid .env file", func(t *testing.T) { + dir := initDir(t, nil, testConfigYAML) + writeFile(t, filepath.Join(dir, ".env"), "NOT VALID\n") var conf JobSearchConfig if err := Init("", &conf); err == nil || !strings.Contains(err.Error(), ".env") {