Files
job-search/internal/cli/config/config_test.go
T
bwilliams e19c8c2b69 Imports split into three groups: standard library, third-party, then this project's own packages
gci is configured with the sections standard, default and localmodule, and
goimports with the module path as its local prefix, so both formatters
agree.
2026-10-14 17:40:06 +01:00

209 lines
6.2 KiB
Go

package config
import (
"strings"
"testing"
"github.com/spf13/cobra"
"go.yaml.in/yaml/v3"
"git.avsdev.uk/Go/job-search/internal/cli"
"git.avsdev.uk/Go/job-search/internal/cli/cmdtest"
)
// These tests run the config commands as a user would, through a root
// command like job-search's (see cmdtest).
const appConfig = `logging:
level: info
sources:
sleeper:
interval: 2m
`
func newRoot() *cobra.Command {
opts := &cli.Options{}
return cmdtest.NewRoot(opts, NewCommand(opts))
}
func run(t *testing.T, args ...string) (string, error) {
t.Helper()
return cmdtest.Run(t, newRoot, "", args...)
}
func mustRun(t *testing.T, args ...string) string {
t.Helper()
return cmdtest.MustRun(t, newRoot, args...)
}
// fileSettings returns the settings in the YAML file at path, flattened to
// dotted keys.
func fileSettings(t *testing.T, path string) map[string]any {
t.Helper()
var nested map[string]any
if err := yaml.Unmarshal([]byte(cmdtest.ReadFile(t, path)), &nested); err != nil {
t.Fatal(err)
}
flat := map[string]any{}
var walk func(prefix string, m map[string]any)
walk = func(prefix string, m map[string]any) {
for k, v := range m {
if sub, ok := v.(map[string]any); ok {
walk(prefix+k+".", sub)
} else {
flat[prefix+k] = v
}
}
}
walk("", nested)
return flat
}
func TestConfigShow(t *testing.T) {
cmdtest.Setup(t, appConfig)
out := mustRun(t, "config", "show")
cmdtest.Contains(t, out,
"logging.level = info <string>", "sources.sleeper.interval = 2m0s <string>")
// Keys are listed in order, the same every time.
if again := mustRun(t, "config", "show"); again != out {
t.Errorf("config show output changed between runs:\n%s\n---\n%s", out, again)
}
if i, j := strings.Index(out, "database.file"), strings.Index(out, "logging.level"); i > j {
t.Errorf("keys not sorted:\n%s", out)
}
out = mustRun(t, "config", "show", "--json")
cmdtest.Contains(t, out, `"level": "info"`, `"interval": "2m0s"`)
out = mustRun(t, "config", "show", "--yaml")
cmdtest.Contains(t, out, "level: info", "interval: 2m0s")
}
func TestConfigGet(t *testing.T) {
cmdtest.Setup(t, appConfig)
cmdtest.Contains(t, mustRun(t, "config", "get", "logging.level"), "info <string>")
cmdtest.Contains(t, mustRun(t, "config", "get", "sources.sleeper.interval"), "2m0s <string>")
cmdtest.Contains(t, mustRun(t, "config", "get", "sources.sleeper.enabled"), "false <bool>")
if _, err := run(t, "config", "get", "no.such.key"); err == nil {
t.Error("get of an unknown key succeeded")
}
if _, err := run(t, "config", "get"); err == nil {
t.Error("get without a key succeeded")
}
}
func TestConfigSet(t *testing.T) {
cmdtest.Setup(t, appConfig)
out := mustRun(t, "config", "set", "logging.level", "debug")
cmdtest.Contains(t, out, "logging.level = debug <string>")
cmdtest.Contains(t, mustRun(t, "config", "get", "logging.level"), "debug <string>")
out = mustRun(t, "config", "set", "sources.sleeper.enabled", "true")
cmdtest.Contains(t, out, "sources.sleeper.enabled = true <bool>")
// Durations are stored as given out, e.g. 5m0s.
out = mustRun(t, "config", "set", "sources.sleeper.interval", "5m")
cmdtest.Contains(t, out, "sources.sleeper.interval = 5m0s <string>")
// Only the settings the file had, plus those set, are written: no
// defaults.
want := map[string]any{
"logging.level": "debug",
"sources.sleeper.enabled": true,
"sources.sleeper.interval": "5m0s",
}
if got := fileSettings(t, "config.yaml"); !equalSettings(got, want) {
t.Errorf("config.yaml settings = %v; want %v", got, want)
}
for _, args := range [][]string{
{"config", "set", "logging.level", "loud"}, // rejected by validation
{"config", "set", "sources.sleeper.enabled", "yes"}, // not a bool
{"config", "set", "sources.sleeper.interval", "soon"},
{"config", "set", "no.such.key", "1"},
{"config", "set", "logging.level"}, // no value, no --unset
} {
if _, err := run(t, args...); err == nil {
t.Errorf("%v succeeded", args)
}
}
if got := fileSettings(t, "config.yaml"); !equalSettings(got, want) {
t.Errorf("failed sets changed config.yaml: %v", got)
}
}
func equalSettings(a, b map[string]any) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}
func TestConfigSetUnset(t *testing.T) {
cmdtest.Setup(t, appConfig)
mustRun(t, "config", "set", "--unset", "sources.sleeper.interval")
// Only the unset key is gone: no defaults are written in its place.
want := map[string]any{"logging.level": "info"}
if got := fileSettings(t, "config.yaml"); !equalSettings(got, want) {
t.Errorf("config.yaml settings = %v; want %v", got, want)
}
// Back to the default.
cmdtest.Contains(t, mustRun(t, "config", "get", "sources.sleeper.interval"),
"10m0s <string>")
}
func TestConfigSetWithoutConfigFile(t *testing.T) {
cmdtest.Setup(t, "")
_, err := run(t, "config", "set", "logging.level", "debug")
cmdtest.ErrContains(t, err, "no configuration file exists")
}
func TestConfigInit(t *testing.T) {
cmdtest.Setup(t, "")
out := mustRun(t, "config", "init", "./new.yaml")
cmdtest.Contains(t, out, "writing default configuration to file: ./new.yaml")
cmdtest.Contains(t, cmdtest.ReadFile(t, "new.yaml"), "level: warn", "interval: 10m0s")
_, err := run(t, "config", "init", "./new.yaml")
cmdtest.ErrContains(t, err, "already exists")
if _, err := run(t, "config", "init", "a.yaml", "b.yaml"); err == nil {
t.Error("init with two paths succeeded")
}
}
// With no path (or a blank one), init searches the default locations. The
// working directory is one, so its config.yaml is found. (Not finding one
// would write to /etc/job-search, which a test can't do.)
func TestConfigInitDefaultLocations(t *testing.T) {
cmdtest.Setup(t, appConfig)
for _, args := range [][]string{
{"config", "init"},
{"config", "init", " "},
} {
_, err := run(t, args...)
cmdtest.ErrContains(t, err, "already exists")
}
}
// init takes the path as an argument, so --config isn't shown in its help.
func TestConfigInitHelpHidesConfigFlag(t *testing.T) {
cmdtest.Setup(t, "")
out := mustRun(t, "config", "init", "--help")
cmdtest.Contains(t, out, "init [config_path]", "--debug")
if strings.Contains(out, "--config") {
t.Errorf("init help shows --config:\n%s", out)
}
}