Files
job-search/internal/config/load_test.go
T

170 lines
5.0 KiB
Go

package config
import (
"log/slog"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// initDir makes a temp working directory holding a .env file (if env is not
// nil) and a config.yaml (if config is not empty), and returns its path.
func initDir(t *testing.T, env []string, config string) string {
t.Helper()
resetViper(t)
dir := t.TempDir()
t.Chdir(dir)
if env != nil {
// godotenv sets variables without cleaning up after the test.
for _, kv := range env {
key, _, _ := strings.Cut(kv, "=")
t.Cleanup(func() { os.Unsetenv(key) })
}
writeFile(t, filepath.Join(dir, ".env"), strings.Join(env, "\n")+"\n")
}
if config != "" {
writeFile(t, filepath.Join(dir, "config.yaml"), config)
}
return dir
}
func writeFile(t *testing.T, path, contents string) {
t.Helper()
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
}
const testConfigYAML = `logging:
level: info
sources:
fts_mock:
enabled: true
interval: 30s
`
func TestInitLoadsEnvAndConfigFile(t *testing.T) {
initDir(t, []string{"JOBSEARCH_TEST_ENV=from-dotenv"}, testConfigYAML)
captureLogs(t, slog.LevelError)
var conf JobSearchConfig
if err := Init("", &conf); err != nil {
t.Fatal(err)
}
if got := os.Getenv("JOBSEARCH_TEST_ENV"); got != "from-dotenv" {
t.Errorf("JOBSEARCH_TEST_ENV = %q; want it loaded from .env", got)
}
if conf.Logging.Level != "info" {
t.Errorf("Logging.Level = %q; want info", conf.Logging.Level)
}
if !conf.Sources.FTSMock.Enabled || conf.Sources.FTSMock.Interval != 30*time.Second {
t.Errorf("FTSMock = %+v; want enabled every 30s", conf.Sources.FTSMock)
}
// Keys missing from the file keep their defaults.
if conf.Sources.FindATender.Interval != 10*time.Minute {
t.Errorf("FindATender.Interval = %v; want default 10m", conf.Sources.FindATender.Interval)
}
}
func TestInitExplicitConfigFile(t *testing.T) {
dir := initDir(t, []string{}, "logging:\n level: info\n")
other := filepath.Join(dir, "other.yaml")
writeFile(t, other, "logging:\n level: error\n")
captureLogs(t, slog.LevelError)
var conf JobSearchConfig
if err := Init(other, &conf); err != nil {
t.Fatal(err)
}
if conf.Logging.Level != "error" {
t.Errorf("Logging.Level = %q; want error from %s", conf.Logging.Level, other)
}
}
func TestInitNoConfigFile(t *testing.T) {
initDir(t, []string{}, "")
logs := captureLogs(t, slog.LevelWarn)
var conf JobSearchConfig
if err := Init("", &conf); err != nil {
t.Fatal(err)
}
if !strings.Contains(logs.String(), "No config file found") {
t.Errorf("no warning logged: %s", logs)
}
if conf.Logging.Level != "warn" {
t.Errorf("Logging.Level = %q; want default warn", conf.Logging.Level)
}
}
// The -debug and -quiet levels set before Init win over the config file.
func TestInitKeepsCommandLineLevel(t *testing.T) {
for _, level := range []string{"debug", "error"} {
t.Run(level, func(t *testing.T) {
initDir(t, []string{}, "logging:\n level: info\n")
logs := captureLogs(t, slog.LevelDebug)
conf := JobSearchConfig{Logging: JobSearchConfigLogging{Level: level}}
if err := Init("", &conf); err != nil {
t.Fatal(err)
}
if conf.Logging.Level != level {
t.Errorf("Logging.Level = %q; want %s kept", conf.Logging.Level, level)
}
// At debug, the environment and loaded config are logged.
gotDebug := strings.Contains(logs.String(), "Loaded config:")
if gotDebug != (level == "debug") {
t.Errorf("debug output logged = %v at level %s: %s", gotDebug, level, logs)
}
})
}
}
func TestInitErrors(t *testing.T) {
t.Run("no .env file", func(t *testing.T) {
initDir(t, nil, testConfigYAML)
var conf JobSearchConfig
if err := Init("", &conf); err == nil || !strings.Contains(err.Error(), ".env") {
t.Fatalf("err = %v; want an invalid .env file error", err)
}
})
t.Run("invalid config file", func(t *testing.T) {
initDir(t, []string{}, "logging: [unclosed\n")
captureLogs(t, slog.LevelError)
var conf JobSearchConfig
if err := Init("", &conf); err == nil {
t.Fatal("expected an error for invalid YAML")
}
})
t.Run("missing explicit config file", func(t *testing.T) {
dir := initDir(t, []string{}, "")
captureLogs(t, slog.LevelError)
var conf JobSearchConfig
if err := Init(filepath.Join(dir, "missing.yaml"), &conf); err == nil {
t.Fatal("expected an error for a missing config file")
}
})
}
// Environment variables override the config file, with nested keys joined
// by underscores.
func TestInitEnvOverride(t *testing.T) {
initDir(t, []string{}, testConfigYAML)
captureLogs(t, slog.LevelError)
t.Setenv("JOBSEARCH_LOGGING_LEVEL", "error")
t.Setenv("JOBSEARCH_SOURCES_FTS_MOCK_INTERVAL", "45s")
var conf JobSearchConfig
if err := Init("", &conf); err != nil {
t.Fatal(err)
}
if conf.Logging.Level != "error" {
t.Errorf("Logging.Level = %q; want error from the environment", conf.Logging.Level)
}
if conf.Sources.FTSMock.Interval != 45*time.Second {
t.Errorf("FTSMock.Interval = %v; want 45s from the environment", conf.Sources.FTSMock.Interval)
}
}