Find the data root via the XDG config directory
Running the tool from anywhere required either a --root flag or an exported MONEY_ROOT. Read the data root from $XDG_CONFIG_HOME/money/config.toml (~/.config/money/config.toml) instead, so it can be configured once. Resolution order is --root, then $MONEY_ROOT, then the config file, then ~/money. A leading tilde in the config file is expanded, since no shell has done it for us, and relative paths are made absolute. The new "money config" subcommand prints the resolved root and which rule chose it, which is the first thing to check when the tool reads the wrong directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// writeUserConfig points XDG_CONFIG_HOME at a temporary directory and puts a
|
||||
// config file in it, so tests never touch the real one.
|
||||
func writeUserConfig(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", dir)
|
||||
if body == "" {
|
||||
return dir
|
||||
}
|
||||
cfgDir := filepath.Join(dir, "money")
|
||||
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "config.toml"), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestResolveRootPrecedence(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("no home directory in this environment")
|
||||
}
|
||||
|
||||
t.Run("flag beats everything", func(t *testing.T) {
|
||||
writeUserConfig(t, "root = \"/from/config\"\n")
|
||||
t.Setenv("MONEY_ROOT", "/from/env")
|
||||
|
||||
root, source, err := ResolveRoot("/from/flag")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root != "/from/flag" || source != RootFromFlag {
|
||||
t.Errorf("got %q from %q, want /from/flag from the flag", root, source)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("env beats config", func(t *testing.T) {
|
||||
writeUserConfig(t, "root = \"/from/config\"\n")
|
||||
t.Setenv("MONEY_ROOT", "/from/env")
|
||||
|
||||
root, source, err := ResolveRoot("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root != "/from/env" || source != RootFromEnv {
|
||||
t.Errorf("got %q from %q, want /from/env from the environment", root, source)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("config beats the default", func(t *testing.T) {
|
||||
writeUserConfig(t, "root = \"/from/config\"\n")
|
||||
t.Setenv("MONEY_ROOT", "")
|
||||
|
||||
root, source, err := ResolveRoot("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root != "/from/config" || source != RootFromConfig {
|
||||
t.Errorf("got %q from %q, want /from/config from the config file", root, source)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default when nothing is set", func(t *testing.T) {
|
||||
writeUserConfig(t, "") // XDG dir exists but holds no config file
|
||||
t.Setenv("MONEY_ROOT", "")
|
||||
|
||||
root, source, err := ResolveRoot("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join(home, "money")
|
||||
if root != want || source != RootFromDefault {
|
||||
t.Errorf("got %q from %q, want %q from the default", root, source, want)
|
||||
}
|
||||
})
|
||||
|
||||
// A hand-written config file is likely to contain a tilde, which no shell
|
||||
// has expanded for us.
|
||||
t.Run("tilde in the config file is expanded", func(t *testing.T) {
|
||||
writeUserConfig(t, "root = \"~/finances\"\n")
|
||||
t.Setenv("MONEY_ROOT", "")
|
||||
|
||||
root, _, err := ResolveRoot("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join(home, "finances")
|
||||
if root != want {
|
||||
t.Errorf("root = %q, want %q", root, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("relative paths become absolute", func(t *testing.T) {
|
||||
writeUserConfig(t, "")
|
||||
t.Setenv("MONEY_ROOT", "some/relative/dir")
|
||||
|
||||
root, _, err := ResolveRoot("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !filepath.IsAbs(root) {
|
||||
t.Errorf("root = %q, want an absolute path", root)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserConfigPathFollowsXDG(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
|
||||
path, err := UserConfigPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := "/custom/config/money/config.toml"; path != want {
|
||||
t.Errorf("path = %q, want %q", path, want)
|
||||
}
|
||||
|
||||
// Unset, it falls back to ~/.config.
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("no home directory in this environment")
|
||||
}
|
||||
path, err = UserConfigPath()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := filepath.Join(home, ".config", "money", "config.toml"); path != want {
|
||||
t.Errorf("path = %q, want %q", path, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A missing config file is normal, not an error.
|
||||
func TestLoadUserConfigMissingFile(t *testing.T) {
|
||||
writeUserConfig(t, "")
|
||||
c, path, err := LoadUserConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("a missing config file should not be an error: %v", err)
|
||||
}
|
||||
if c.Root != "" {
|
||||
t.Errorf("root = %q, want empty", c.Root)
|
||||
}
|
||||
if path == "" {
|
||||
t.Error("expected the path to be reported even when the file is absent")
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed config file is an error, so a typo does not silently send the
|
||||
// tool to the wrong directory.
|
||||
func TestLoadUserConfigMalformed(t *testing.T) {
|
||||
writeUserConfig(t, "root = this is not valid toml\n")
|
||||
if _, _, err := LoadUserConfig(); err == nil {
|
||||
t.Error("expected an error for a malformed config file")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user