package shared import ( "os" "path/filepath" "testing" ) // withConfigDir points XDG_CONFIG_HOME at a temp dir and writes the given // config file into /vcm/, returning the temp base. func withConfigDir(t *testing.T, name, content string) string { t.Helper() base := t.TempDir() t.Setenv("XDG_CONFIG_HOME", base) dir := filepath.Join(base, appDir) if err := os.MkdirAll(dir, 0o700); err != nil { t.Fatal(err) } if content != "" { if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil { t.Fatal(err) } } return base } const sampleConfig = ` defaults: method: ldap user: ben contexts: sydney: address: https://vault.syd1.au.unkin.net staging/sydney: address: https://vault-staging.syd1.au.unkin.net namespace: staging user: svc-ben legacy: address: https://vault-legacy.example.net method: userpass path: userpass2 ` func TestLoadAndContextNames(t *testing.T) { withConfigDir(t, "config.yaml", sampleConfig) cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } got := cfg.ContextNames() want := []string{"legacy", "staging/sydney", "sydney"} if len(got) != len(want) { t.Fatalf("ContextNames = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("ContextNames[%d] = %q, want %q (%v)", i, got[i], want[i], got) } } if cfg.Path() == "" { t.Error("Path() is empty after loading a config") } } func TestConfigPath(t *testing.T) { base := withConfigDir(t, "config.yaml", sampleConfig) if got, want := ConfigPath(), filepath.Join(base, appDir, "config.yaml"); got != want { t.Errorf("ConfigPath = %q, want %q", got, want) } } func TestLoadMissingConfigIsNotError(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) cfg, err := Load() if err != nil { t.Fatalf("Load with no file: %v", err) } if len(cfg.ContextNames()) != 0 { t.Errorf("expected no contexts, got %v", cfg.ContextNames()) } } func TestResolveAppliesDefaults(t *testing.T) { withConfigDir(t, "config.yaml", sampleConfig) cfg, err := Load() if err != nil { t.Fatal(err) } // sydney: inherits method+user from defaults, no namespace, path == method. rc, err := cfg.Resolve("sydney") if err != nil { t.Fatal(err) } if rc.Method != "ldap" || rc.User != "ben" || rc.Namespace != "" || rc.Path != "ldap" { t.Errorf("sydney resolved = %+v", rc) } // staging/sydney: overrides user + namespace, inherits method. rc, err = cfg.Resolve("staging/sydney") if err != nil { t.Fatal(err) } if rc.Method != "ldap" || rc.User != "svc-ben" || rc.Namespace != "staging" { t.Errorf("staging/sydney resolved = %+v", rc) } // legacy: explicit method + custom auth path. rc, err = cfg.Resolve("legacy") if err != nil { t.Fatal(err) } if rc.Method != "userpass" || rc.Path != "userpass2" { t.Errorf("legacy resolved = %+v", rc) } } func TestResolveUnknownContext(t *testing.T) { withConfigDir(t, "config.yaml", sampleConfig) cfg, _ := Load() if _, err := cfg.Resolve("nope"); err == nil { t.Error("expected error for unknown context") } } func TestResolveMethodDefaultWhenUnset(t *testing.T) { withConfigDir(t, "config.yaml", ` contexts: bare: address: https://vault.example.net `) t.Setenv("USER", "alice") cfg, _ := Load() rc, err := cfg.Resolve("bare") if err != nil { t.Fatal(err) } if rc.Method != DefaultMethod { t.Errorf("method = %q, want %q", rc.Method, DefaultMethod) } if rc.User != "alice" { t.Errorf("user = %q, want alice ($USER fallback)", rc.User) } } func TestResolveWithOverrides(t *testing.T) { withConfigDir(t, "config.yaml", sampleConfig) cfg, _ := Load() // Overriding the method also moves the auth path (context did not pin one). rc, err := cfg.ResolveWithOverrides("sydney", Overrides{Method: "okta", User: "otheruser"}) if err != nil { t.Fatal(err) } if rc.Method != "okta" || rc.Path != "okta" || rc.User != "otheruser" { t.Errorf("override resolved = %+v", rc) } // legacy pins path=userpass2, so a method override must NOT change the path. rc, err = cfg.ResolveWithOverrides("legacy", Overrides{Method: "okta"}) if err != nil { t.Fatal(err) } if rc.Method != "okta" || rc.Path != "userpass2" { t.Errorf("pinned-path override resolved = %+v", rc) } // Empty overrides leave resolved values untouched. rc, err = cfg.ResolveWithOverrides("sydney", Overrides{}) if err != nil { t.Fatal(err) } if rc.Method != "ldap" || rc.User != "ben" { t.Errorf("no-op override resolved = %+v", rc) } } func TestResolveOIDC(t *testing.T) { withConfigDir(t, "config.yaml", ` contexts: admin: address: https://vault.example method: oidc role: admins dev: address: https://vault.example method: oidc path: oidc-dev oidc_listen: localhost:8251 `) cfg, _ := Load() rc, err := cfg.Resolve("admin") if err != nil { t.Fatal(err) } if rc.Path != "oidc" || rc.Role != "admins" || rc.OIDCListen != DefaultOIDCListen { t.Errorf("admin resolved = %+v", rc) } rc, err = cfg.ResolveWithOverrides("dev", Overrides{Role: "readers"}) if err != nil { t.Fatal(err) } if rc.Path != "oidc-dev" || rc.Role != "readers" || rc.OIDCListen != "localhost:8251" { t.Errorf("dev resolved = %+v", rc) } rc, _ = cfg.ResolveWithOverrides("admin", Overrides{Listen: "127.0.0.1:9999"}) if rc.OIDCListen != "127.0.0.1:9999" || rc.Role != "admins" { t.Errorf("listen override resolved = %+v", rc) } }