package compass import ( "os" "path/filepath" "strings" "sync" "testing" "testing/fstest" ) func TestLoadStringFromAppSection(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "app.yaml") if err := os.WriteFile(path, []byte("name: hello-app\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := Load(dir) if err != nil { t.Fatalf("Load: %v", err) } if got := cfg.String("app.name"); got != "hello-app" { t.Fatalf("String(app.name) = %q, want hello-app", got) } } func TestOpenDefaultsEnvToProduction(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: base\ndebug: false\n", "env/production/app.yaml": "debug: true\n", "env/development/app.yaml": "name: dev\n", }) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } if cfg.Environment() != "production" { t.Fatalf("Environment = %q, want production", cfg.Environment()) } if got := cfg.String("app.name"); got != "base" { t.Fatalf("name = %q, want base", got) } if !cfg.Bool("app.debug") { t.Fatal("production overlay should set app.debug") } } func TestExplicitEnvWinsOverSUMMER_ENV(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: base\n", "env/development/app.yaml": "name: from-dev\n", "env/staging/app.yaml": "name: from-staging\n", }) cfg, err := Open(Options{ Dir: dir, Env: "staging", Environ: []string{"SUMMER_ENV=development"}, }) if err != nil { t.Fatalf("Open: %v", err) } if cfg.Environment() != "staging" { t.Fatalf("Environment = %q, want staging", cfg.Environment()) } if got := cfg.String("app.name"); got != "from-staging" { t.Fatalf("name = %q, want from-staging", got) } } func TestDeepMergeKeepsUntouchedNestedKeys(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "database.yaml": ` host: localhost port: 5432 pool: max: 10 idle: 2 `, "env/development/database.yaml": ` host: dev-db pool: max: 20 `, }) cfg, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } if got := cfg.String("database.host"); got != "dev-db" { t.Fatalf("host = %q, want dev-db", got) } if got := cfg.Int("database.port"); got != 5432 { t.Fatalf("port = %d, want 5432", got) } if got := cfg.Int("database.pool.max"); got != 20 { t.Fatalf("pool.max = %d, want 20", got) } if got := cfg.Int("database.pool.idle"); got != 2 { t.Fatalf("pool.idle = %d, want 2", got) } } func TestMergePluginConfigAtBarePluginID(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: hello-app\n", }) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } fsys := fstest.MapFS{ "config/config.yaml": {Data: []byte("posts_per_page: 10\nshow_author: true\n")}, } if err := cfg.MergePlugin("golem15.hello", fsys); err != nil { t.Fatalf("MergePlugin: %v", err) } if got := cfg.Int("golem15.hello.posts_per_page"); got != 10 { t.Fatalf("posts_per_page = %d, want 10", got) } if !cfg.Bool("golem15.hello.show_author") { t.Fatal("show_author should be true") } if !cfg.Has("golem15.hello.posts_per_page") { t.Fatal("Has(golem15.hello.posts_per_page) = false") } got, ok := cfg.Lookup("golem15.hello.posts_per_page") if !ok { t.Fatal("Lookup missing posts_per_page") } switch v := got.(type) { case int: if v != 10 { t.Fatalf("Lookup = %v, want 10", got) } case int64: if v != 10 { t.Fatalf("Lookup = %v, want 10", got) } case uint64: if v != 10 { t.Fatalf("Lookup = %v, want 10", got) } default: // YAML may decode as int or the getter path may return the raw value. if cfg.Int("golem15.hello.posts_per_page") != 10 { t.Fatalf("Lookup type %T value %v", got, got) } } var section struct { PostsPerPage int `koanf:"posts_per_page"` ShowAuthor bool `koanf:"show_author"` } if err := cfg.LoadSection("golem15.hello", §ion); err != nil { t.Fatalf("LoadSection: %v", err) } if section.PostsPerPage != 10 || !section.ShowAuthor { t.Fatalf("LoadSection = %+v", section) } } func TestEnvVarsOverridePluginDefaults(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: hello-app\n", }) cfg, err := Open(Options{ Dir: dir, Environ: []string{ "SUMMER_GOLEM15__HELLO__POSTS_PER_PAGE=25", }, }) if err != nil { t.Fatalf("Open: %v", err) } fsys := fstest.MapFS{ "config/config.yaml": {Data: []byte("posts_per_page: 10\n")}, } if err := cfg.MergePlugin("golem15.hello", fsys); err != nil { t.Fatalf("MergePlugin: %v", err) } if got := cfg.Int("golem15.hello.posts_per_page"); got != 25 { t.Fatalf("posts_per_page = %d, want 25 (env over plugin default)", got) } } func TestPriorityRuntimeSetThenPersistThenEnvThenOverlayThenBaseThenPlugin(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: base\n", "env/development/app.yaml": "name: overlay\n", "env/development/overrides.yaml": "app:\n name: persisted\n", }) cfg, err := Open(Options{ Dir: dir, Env: "development", Environ: []string{"SUMMER_APP__NAME=from-env"}, }) if err != nil { t.Fatalf("Open: %v", err) } if got := cfg.String("app.name"); got != "persisted" { t.Fatalf("before Set, name = %q, want persisted", got) } if err := cfg.Set("app.name", "runtime"); err != nil { t.Fatalf("Set: %v", err) } if got := cfg.String("app.name"); got != "runtime" { t.Fatalf("after Set, name = %q, want runtime", got) } } func TestSortedSectionFilesLoadDeterministically(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "b.yaml": "value: b\n", "a.yaml": "value: a\nshared: 1\n", }) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } if got := cfg.String("a.value"); got != "a" { t.Fatalf("a.value = %q, want a", got) } if got := cfg.String("b.value"); got != "b" { t.Fatalf("b.value = %q, want b", got) } if got := cfg.Int("a.shared"); got != 1 { t.Fatalf("a.shared = %d, want 1", got) } } func writeConfigTree(t *testing.T, files map[string]string) string { t.Helper() dir := t.TempDir() for name, body := range files { path := filepath.Join(dir, name) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } } return dir } func TestPrecedenceIsolatesEachLayer(t *testing.T) { pluginFS := fstest.MapFS{ "config/config.yaml": {Data: []byte("name: plugin\n")}, } tests := []struct { name string plugin bool base bool overlay bool env bool persist bool set bool want string }{ {name: "plugin default", plugin: true, want: "plugin"}, {name: "base over plugin", plugin: true, base: true, want: "base"}, {name: "overlay over base", plugin: true, base: true, overlay: true, want: "overlay"}, {name: "env over overlay", plugin: true, base: true, overlay: true, env: true, want: "env"}, {name: "persist over env", plugin: true, base: true, overlay: true, env: true, persist: true, want: "persist"}, {name: "set over persist", plugin: true, base: true, overlay: true, env: true, persist: true, set: true, want: "set"}, {name: "env over base without overlay", plugin: true, base: true, env: true, want: "env"}, {name: "persist over overlay without env", plugin: true, base: true, overlay: true, persist: true, want: "persist"}, {name: "set only", set: true, want: "set"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { files := map[string]string{} if tt.base { files["app.yaml"] = "name: base\n" } else { files["app.yaml"] = "other: 1\n" } if tt.overlay { files["env/development/app.yaml"] = "name: overlay\n" } if tt.persist { files["env/development/overrides.yaml"] = "app:\n name: persist\n" } dir := writeConfigTree(t, files) environ := []string{} if tt.env { environ = []string{"SUMMER_APP__NAME=env"} } cfg, err := Open(Options{Dir: dir, Env: "development", Environ: environ}) if err != nil { t.Fatalf("Open: %v", err) } if tt.plugin { if err := cfg.MergePlugin("app", pluginFS); err != nil { t.Fatalf("MergePlugin: %v", err) } } if tt.set { if err := cfg.Set("app.name", "set"); err != nil { t.Fatalf("Set: %v", err) } } if got := cfg.String("app.name"); got != tt.want { t.Fatalf("app.name = %q, want %q", got, tt.want) } }) } } func TestMalformedYAMLIsRejectedByName(t *testing.T) { tests := []struct { name string file string body string want string }{ {name: "base section", file: "app.yaml", body: "name: [unterminated\n", want: "app.yaml"}, {name: "env overlay", file: "env/development/app.yaml", body: "name: : :\n[", want: "app.yaml"}, {name: "overrides", file: "env/development/overrides.yaml", body: "app: [\n", want: "overrides"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { files := map[string]string{"app.yaml": "name: base\n"} files[tt.file] = tt.body dir := writeConfigTree(t, files) _, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}}) if err == nil { t.Fatal("expected malformed YAML error") } if !strings.Contains(err.Error(), tt.want) { t.Fatalf("error %q should name %q", err, tt.want) } }) } } func TestMalformedPluginYAMLIsRejected(t *testing.T) { dir := writeConfigTree(t, map[string]string{"app.yaml": "name: base\n"}) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } fsys := fstest.MapFS{ "config/config.yaml": {Data: []byte("posts_per_page: [\n")}, } err = cfg.MergePlugin("golem15.hello", fsys) if err == nil { t.Fatal("expected plugin YAML parse error") } if !strings.Contains(err.Error(), "golem15.hello") { t.Fatalf("error should name plugin id, got %v", err) } } func TestSnakeCaseEnvKeysStayLiteral(t *testing.T) { dir := writeConfigTree(t, map[string]string{"app.yaml": "name: base\n"}) cfg, err := Open(Options{ Dir: dir, Environ: []string{ "SUMMER_APP__FEATURE_FLAG=on", "SUMMER_GOLEM15__HELLO__POSTS_PER_PAGE=7", }, }) if err != nil { t.Fatalf("Open: %v", err) } if got := cfg.String("app.feature_flag"); got != "on" { t.Fatalf("feature_flag = %q, want on", got) } if cfg.Has("app.feature.flag") { t.Fatal("single underscore must not split the leaf") } if got := cfg.Int("golem15.hello.posts_per_page"); got != 7 { t.Fatalf("posts_per_page = %d, want 7", got) } if cfg.Has("golem15.hello.posts.per.page") { t.Fatal("snake_case plugin key must stay one leaf") } } func TestDotEnvDoesNotOverrideRealEnviron(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "config") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: base\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, ".env"), []byte("SUMMER_APP__NAME=from-dotenv\nSUMMER_APP__TITLE=dotenv-title\n"), 0o600); err != nil { t.Fatal(err) } cfg, err := Open(Options{ Dir: dir, Environ: []string{"SUMMER_APP__NAME=from-real"}, }) if err != nil { t.Fatalf("Open: %v", err) } if got := cfg.String("app.name"); got != "from-real" { t.Fatalf("name = %q, want from-real", got) } if got := cfg.String("app.title"); got != "dotenv-title" { t.Fatalf("title = %q, want dotenv-title", got) } } func TestDottedPluginNamespaceAndTypedSection(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: hello-app\n", "golem15.hello.yaml": "from_app: true\n", }) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } fsys := fstest.MapFS{ "config/config.yaml": {Data: []byte("posts_per_page: 10\nshow_author: true\n")}, "config/settings.yaml": {Data: []byte("theme: dark\n")}, } if err := cfg.MergePlugin("golem15.hello", fsys); err != nil { t.Fatalf("MergePlugin: %v", err) } if got := cfg.Int("golem15.hello.posts_per_page"); got != 10 { t.Fatalf("posts_per_page = %d, want 10", got) } if got := cfg.String("golem15.hello.settings.theme"); got != "dark" { t.Fatalf("settings.theme = %q, want dark", got) } if !cfg.Bool("golem15.hello.from_app") { t.Fatal("app-level dotted plugin namespace file should merge at golem15.hello") } var section struct { PostsPerPage int `koanf:"posts_per_page"` ShowAuthor bool `koanf:"show_author"` FromApp bool `koanf:"from_app"` } if err := cfg.LoadSection("golem15.hello", §ion); err != nil { t.Fatalf("LoadSection: %v", err) } if section.PostsPerPage != 10 || !section.ShowAuthor || !section.FromApp { t.Fatalf("LoadSection = %+v", section) } } func TestSetPersistReloadRoundTrip(t *testing.T) { dir := writeConfigTree(t, map[string]string{"app.yaml": "name: base\n"}) cfg, err := Open(Options{Dir: dir, Env: "development", Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } if err := cfg.Set("app.name", "runtime"); err != nil { t.Fatalf("Set: %v", err) } if err := cfg.Persist(); err != nil { t.Fatalf("Persist: %v", err) } body, err := os.ReadFile(filepath.Join(dir, "env", "development", "overrides.yaml")) if err != nil { t.Fatal(err) } if !strings.Contains(string(body), "runtime") { t.Fatalf("overrides missing runtime value: %s", body) } if err := cfg.Set("app.name", "unsaved"); err != nil { t.Fatalf("Set unsaved: %v", err) } if err := cfg.Reload(); err != nil { t.Fatalf("Reload: %v", err) } if got := cfg.String("app.name"); got != "runtime" { t.Fatalf("after Reload, name = %q, want persisted runtime", got) } } func TestConcurrentReadsWhileSet(t *testing.T) { dir := writeConfigTree(t, map[string]string{"app.yaml": "name: base\ncount: 1\n"}) cfg, err := Open(Options{Dir: dir, Environ: []string{}}) if err != nil { t.Fatalf("Open: %v", err) } var wg sync.WaitGroup for i := 0; i < 16; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 80; j++ { _ = cfg.String("app.name") _ = cfg.Int("app.count") _ = cfg.Bool("app.debug") _ = cfg.Has("app.name") _, _ = cfg.Lookup("app.name") var section struct { Name string `koanf:"name"` } _ = cfg.LoadSection("app", §ion) } }() } wg.Add(1) go func() { defer wg.Done() for j := 0; j < 80; j++ { if err := cfg.Set("app.count", j); err != nil { t.Errorf("Set: %v", err) return } } }() wg.Wait() if !cfg.Has("app.name") { t.Fatal("app.name missing after concurrent access") } } func TestOpenRejectsEmptyDir(t *testing.T) { _, err := Open(Options{}) if err == nil { t.Fatal("expected empty dir error") } } func TestSUMMER_ENVDoesNotEnterConfigTree(t *testing.T) { dir := writeConfigTree(t, map[string]string{ "app.yaml": "name: base\n", "env/development/app.yaml": "name: dev\n", }) cfg, err := Open(Options{Dir: dir, Environ: []string{"SUMMER_ENV=development"}}) if err != nil { t.Fatalf("Open: %v", err) } if cfg.Has("env") || cfg.Has("SUMMER_ENV") { t.Fatal("SUMMER_ENV must not become a config key") } if cfg.String("app.name") != "dev" { t.Fatalf("name = %q, want dev", cfg.String("app.name")) } }