Files
member-console/internal/config/overlay_test.go
T
cgalo5758 296236a721 Add operator-managed integration settings
Persist non-secret ConfigSpec overrides in core and apply them at boot
ahead of environment values and defaults. Validate typed and enum
values,
show pending restart state, and remove Stripe's unused provider config
table.
2026-07-22 22:17:58 -05:00

195 lines
6.7 KiB
Go

package config
import (
"strings"
"testing"
"github.com/spf13/viper"
)
// overlaySpecs is a synthetic ConfigSpec covering every declared-default
// type the overlay coerces, mirroring how adapters declare keys without
// importing the real adapter trees.
var overlaySpecs = []ConfigKey{
{Name: "it-color", Default: "blue", Usage: "a string key"},
{Name: "it-domains", Default: []string(nil), Usage: "a list key"},
{Name: "it-enabled", Default: false, Usage: "a bool key"},
{Name: "it-limit", Default: 7, Usage: "an int key"},
{Name: "it-scheme", Default: "https", Enum: []string{"http", "https"}, Usage: "an enum key"},
{Name: "it-token", Secret: true, Usage: "a secret key"},
}
// resetOverlay clears the global viper and the boot snapshot, then
// registers each spec's declared default the way flag binding does in
// production (BindPFlags makes Get fall back to the flag default, which is
// itself registered from ConfigSpec).
func resetOverlay(t *testing.T) {
t.Helper()
viper.Reset()
bootEffective = map[string]Effective{}
for _, s := range overlaySpecs {
if s.Default != nil {
viper.SetDefault(s.Name, s.Default)
}
}
}
func TestCoerceOverride(t *testing.T) {
specFor := func(name string) ConfigKey {
for _, s := range overlaySpecs {
if s.Name == name {
return s
}
}
t.Fatalf("no spec %q", name)
return ConfigKey{}
}
cases := []struct {
name string
key string
raw string
want any
wantErr string
}{
{name: "string passthrough", key: "it-color", raw: "green", want: "green"},
{name: "list split and trimmed", key: "it-domains", raw: " a.example, b.example ,", want: []string{"a.example", "b.example"}},
{name: "bool parsed", key: "it-enabled", raw: "true", want: true},
{name: "bool invalid", key: "it-enabled", raw: "banana", wantErr: "not a boolean"},
{name: "int parsed", key: "it-limit", raw: "42", want: 42},
{name: "int invalid", key: "it-limit", raw: "many", wantErr: "not an integer"},
{name: "enum member", key: "it-scheme", raw: "http", want: "http"},
{name: "enum non-member", key: "it-scheme", raw: "gopher", wantErr: "not one of http|https"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := CoerceOverride(specFor(tc.key), tc.raw)
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("want error containing %q, got %v", tc.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
switch want := tc.want.(type) {
case []string:
gotSlice, ok := got.([]string)
if !ok || strings.Join(gotSlice, ",") != strings.Join(want, ",") {
t.Fatalf("want %v, got %v", want, got)
}
default:
if got != tc.want {
t.Fatalf("want %v, got %v", tc.want, got)
}
}
})
}
}
func TestApplyOverlayPrecedence(t *testing.T) {
t.Run("override wins over environment", func(t *testing.T) {
resetOverlay(t)
viper.Set("it-color", "green") // any non-default boot source
// A second ApplyOverlay viper.Set replaces it; assert via resolution.
if err := ApplyOverlay(overlaySpecs, []Override{{Key: "it-color", Value: "red"}}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := viper.GetString("it-color"); got != "red" {
t.Fatalf("want red, got %q", got)
}
if eff := bootEffective["it-color"]; eff.Source != SourceOverride || eff.Value != "red" {
t.Fatalf("want override/red, got %+v", eff)
}
})
t.Run("environment wins over default", func(t *testing.T) {
resetOverlay(t)
viper.Set("it-color", "green")
if err := ApplyOverlay(overlaySpecs, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if eff := bootEffective["it-color"]; eff.Source != SourceEnvironment || eff.Value != "green" {
t.Fatalf("want environment/green, got %+v", eff)
}
})
t.Run("default when nothing set", func(t *testing.T) {
resetOverlay(t)
if err := ApplyOverlay(overlaySpecs, nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if eff := bootEffective["it-color"]; eff.Source != SourceDefault || eff.Value != "blue" {
t.Fatalf("want default/blue, got %+v", eff)
}
})
}
func TestApplyOverlayListKeyReadsAsList(t *testing.T) {
resetOverlay(t)
if err := ApplyOverlay(overlaySpecs, []Override{{Key: "it-domains", Value: "a.example,b.example"}}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
got := viper.GetStringSlice("it-domains")
if len(got) != 2 || got[0] != "a.example" || got[1] != "b.example" {
t.Fatalf("want [a.example b.example], got %v", got)
}
if eff := bootEffective["it-domains"]; eff.Value != "a.example,b.example" {
t.Fatalf("snapshot value: want comma-joined form, got %+v", eff)
}
}
func TestApplyOverlayUnknownKeySkipped(t *testing.T) {
resetOverlay(t)
if err := ApplyOverlay(overlaySpecs, []Override{{Key: "it-retired", Value: "x"}}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if viper.IsSet("it-retired") {
t.Fatal("unknown override must not be applied")
}
if _, ok := bootEffective["it-retired"]; ok {
t.Fatal("unknown override must not enter the snapshot")
}
}
func TestApplyOverlaySecretRejected(t *testing.T) {
resetOverlay(t)
err := ApplyOverlay(overlaySpecs, []Override{{Key: "it-token", Value: "hunter2"}})
if err == nil || !strings.Contains(err.Error(), "declared secret") {
t.Fatalf("want declared-secret error, got %v", err)
}
if !strings.Contains(err.Error(), "DELETE FROM core.integration_config_overrides WHERE key = 'it-token'") {
t.Fatalf("want remediation in error, got %v", err)
}
if viper.IsSet("it-token") {
t.Fatal("secret override must not be applied")
}
}
func TestApplyOverlayCorruptValueFailsActionably(t *testing.T) {
resetOverlay(t)
err := ApplyOverlay(overlaySpecs, []Override{{Key: "it-enabled", Value: "banana"}})
if err == nil ||
!strings.Contains(err.Error(), `"it-enabled"`) ||
!strings.Contains(err.Error(), "DELETE FROM core.integration_config_overrides WHERE key = 'it-enabled'") {
t.Fatalf("want error naming key and remediation, got %v", err)
}
}
// A required key satisfied only by an override row still fails boot
// validation: ValidateStart runs before the database exists, so the
// bootstrap contract is environment-only. (cmd/start.go orders
// ValidateStart before ApplyOverlay; this pins that contract.)
func TestOverrideCannotSatisfyBootValidation(t *testing.T) {
validConfig()
bootEffective = map[string]Effective{}
groupSpecs := []ConfigKey{
{Name: "it-url", RequiredGroup: "IT"},
{Name: "it-key", RequiredGroup: "IT"},
}
viper.Set("it-url", "https://it.example.com")
pending := []Override{{Key: "it-key", Value: "abc"}} // not yet applied — the point
if err := ValidateStart(groupSpecs); err == nil || !strings.Contains(err.Error(), "it-key") {
t.Fatalf("want partial-group failure naming it-key, got %v", err)
}
_ = pending
}