// internal/appconfig/store_test.go package appconfig import ( "context" "errors" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestSetEnvThenGet(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } m, err := Get(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } if v := m.Vars["LOG_LEVEL"]; v.Kind != "env" || v.Value != "debug" { t.Fatalf("LOG_LEVEL = %+v, want kind env value debug", v) } } func TestGetOnMissingManifestReturnsEmpty(t *testing.T) { s := release.NewDirStore(t.TempDir()) m, err := Get(context.Background(), s, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } if len(m.Vars) != 0 { t.Fatalf("Vars = %v, want empty for a never-configured app", m.Vars) } } func TestUnsetRemovesVar(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil { t.Fatalf("SetEnv: %v", err) } removed, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"}) if err != nil { t.Fatalf("Unset: %v", err) } if removed != 1 { t.Fatalf("Unset removed %d, want 1", removed) } m, err := Get(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } if _, ok := m.Vars["LOG_LEVEL"]; ok { t.Fatalf("LOG_LEVEL still present after Unset") } } // Review CRITICAL 2: an unset against an app that has never been configured // must NOT create the manifest it was asked to remove from. A created-empty // config.json reads as "configured, then fully unset" — the state Sync // converges every one of that app's env files to EMPTY for, as root. func TestUnsetOnNeverConfiguredAppErrorsAndWritesNothing(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() removed, err := Unset(ctx, s, "bab", "keymail", []string{"OLDKEY"}) if err == nil { t.Fatalf("Unset on a never-configured app succeeded (removed %d), want a refusal", removed) } if !strings.Contains(err.Error(), "nothing to unset") { t.Errorf("error = %v, want it to say there is nothing to unset", err) } if _, found, gerr := getIfExists(ctx, s, "bab", "keymail"); gerr != nil || found { t.Fatalf("unset created a manifest object (found = %v, err = %v)", found, gerr) } } // Unsetting a key that is not set, on a bundle that DOES exist, stays a // success (heroku config:unset's idempotence) — it just removes nothing. func TestUnsetOfAnAbsentKeyOnAnExistingBundleIsANoopSuccess(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"KEEP": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } removed, err := Unset(ctx, s, "bab", "keymail", []string{"NEVER_SET"}) if err != nil { t.Fatalf("Unset of an absent key: %v", err) } if removed != 0 { t.Fatalf("Unset reported %d removals, want 0 — the count is of removals, not arguments", removed) } m, err := Get(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } if m.Vars["KEEP"].Value != "1" { t.Fatalf("KEEP = %+v, want it untouched", m.Vars["KEEP"]) } } func TestSetSecretStoresEnvelopeOnly(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetSecret(ctx, s, "bab", "keymail", map[string]string{"SES_SECRET": "opaque-envelope-bytes"}); err != nil { t.Fatalf("SetSecret: %v", err) } m, err := Get(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } v := m.Vars["SES_SECRET"] if v.Kind != "secret" || v.Envelope != "opaque-envelope-bytes" || v.Value != "" { t.Fatalf("SES_SECRET = %+v, want kind secret, envelope set, value empty", v) } } func TestSetRejectsInvalidVarName(t *testing.T) { s := release.NewDirStore(t.TempDir()) err := SetEnv(context.Background(), s, "bab", "keymail", map[string]string{"has-hyphen": "x"}) if err == nil { t.Fatal("SetEnv accepted an invalid var name, want an error") } } // racingStore wraps a release.Store and lets a competing write land // between a reader's Get and its subsequent Put — the interleaving apply's // read-modify-write must lose, visibly. type racingStore struct { release.Store betweenGetAndPut func() } func (r *racingStore) Get(ctx context.Context, key string) ([]byte, string, error) { raw, etag, err := r.Store.Get(ctx, key) if r.betweenGetAndPut != nil { f := r.betweenGetAndPut r.betweenGetAndPut = nil f() } return raw, etag, err } // TestConcurrentWriteSurfacesCASConflict proves SetEnv's real // read-modify-write path surfaces ErrCASConflict (rather than retrying or // clobbering) when a competing write lands between its read and its write // — the same discipline release.Promote already uses for channel pointers. func TestConcurrentWriteSurfacesCASConflict(t *testing.T) { base := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, base, "bab", "keymail", map[string]string{"A": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } rs := &racingStore{Store: base, betweenGetAndPut: func() { // A second operator's write lands first, moving the object on. if err := SetEnv(ctx, base, "bab", "keymail", map[string]string{"B": "2"}); err != nil { t.Fatalf("SetEnv (racing writer): %v", err) } }} err := SetEnv(ctx, rs, "bab", "keymail", map[string]string{"C": "3"}) if !errors.Is(err, release.ErrCASConflict) { t.Fatalf("SetEnv against a moved object = %v, want ErrCASConflict", err) } // The racing writer's value survived; the loser's never landed. m, err := Get(ctx, base, "bab", "keymail") if err != nil { t.Fatalf("Get: %v", err) } if _, ok := m.Vars["C"]; ok { t.Fatal("losing write landed anyway") } if v := m.Vars["B"]; v.Value != "2" { t.Fatalf("racing writer's var = %+v, want value 2", v) } } func TestSetEnvRejectsUnrepresentableValue(t *testing.T) { // systemd's EnvironmentFile parser mangles backslashes and quotes; // those must be refused at set time, where the operator can still see // the plaintext — not hours later in the adopt log. s := release.NewDirStore(t.TempDir()) ctx := context.Background() for _, bad := range []string{`back\slash`, `dou"ble`, `sin'gle`, "new\nline", "nul\x00byte"} { if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"V": bad}); err == nil { t.Fatalf("SetEnv accepted %q, want a rejection", bad) } } // The rejection stays narrow: the shapes real credentials take must pass. for _, good := range []string{"postgres://u:p@h/db?sslmode=require", "dGVzdA==", "100%", "a+b c"} { if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"V": good}); err != nil { t.Fatalf("SetEnv rejected %q: %v", good, err) } } } func TestSetExecDeliveryRoundTrips(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil { t.Fatalf("SetExecDelivery(file): %v", err) } m, err := Get(ctx, s, "bab", "titogo") if err != nil { t.Fatalf("Get: %v", err) } if m.ExecDelivery != ExecDeliveryFile { t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile) } // The var set earlier survives — SetExecDelivery is a field write, not // a bundle replace. if m.Vars["K"].Value != "1" { t.Fatalf("Vars[K] = %+v, want value 1 (SetExecDelivery must not clobber existing vars)", m.Vars["K"]) } if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err != nil { t.Fatalf("SetExecDelivery(off): %v", err) } m, err = Get(ctx, s, "bab", "titogo") if err != nil { t.Fatalf("Get: %v", err) } if m.ExecDelivery != "" { t.Fatalf("ExecDelivery after off = %q, want empty", m.ExecDelivery) } } func TestSetExecDeliveryRejectsUnknownValue(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil { t.Fatalf("SetEnv: %v", err) } for _, bad := range []string{"grpc", "FILE", "true", "yes"} { if err := SetExecDelivery(ctx, s, "bab", "titogo", bad); err == nil { t.Fatalf("SetExecDelivery(%q) succeeded, want a refusal", bad) } } // The refusal must not have touched the bucket object at all. m, err := Get(ctx, s, "bab", "titogo") if err != nil { t.Fatalf("Get: %v", err) } if m.ExecDelivery != "" { t.Fatalf("ExecDelivery = %q after a refused set, want untouched (empty)", m.ExecDelivery) } } func TestSetExecDeliveryOffOnNeverConfiguredAppIsRefused(t *testing.T) { // Mirrors TestUnsetOnNeverConfiguredAppErrorsAndWritesNothing: turning // the field off must not mint an empty default bundle just to hold an // absent flag. s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err == nil { t.Fatal("SetExecDelivery(off) on a never-configured app succeeded, want a refusal") } if _, found, gerr := getIfExists(ctx, s, "bab", "titogo"); gerr != nil || found { t.Fatalf("SetExecDelivery(off) created a manifest object (found = %v, err = %v)", found, gerr) } } func TestSetExecDeliveryOnNeverConfiguredAppCreatesTheBundle(t *testing.T) { // The "file" direction DOES create a bundle on first use — same as // SetEnv/SetSecret already do — since "file" is genuine content to // store, not an unset-only call against nothing. s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil { t.Fatalf("SetExecDelivery(file): %v", err) } m, err := Get(ctx, s, "bab", "titogo") if err != nil { t.Fatalf("Get: %v", err) } if m.ExecDelivery != ExecDeliveryFile { t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile) } } func TestNewerSchemaIsRefusedNotDowngraded(t *testing.T) { // json.Unmarshal drops fields this binary doesn't know; acting on a // newer manifest would silently discard them — and a write-back would // downgrade the object. Both paths must refuse instead. s := release.NewDirStore(t.TempDir()) ctx := context.Background() raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`) if _, err := s.Put(ctx, Key("bab", "keymail"), raw, ""); err != nil { t.Fatalf("Put: %v", err) } if _, err := Get(ctx, s, "bab", "keymail"); err == nil { t.Fatal("Get read a schema-2 manifest without complaint, want a refusal") } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err == nil { t.Fatal("SetEnv wrote over a schema-2 manifest, want a refusal") } if err := SetExecDelivery(ctx, s, "bab", "keymail", ExecDeliveryFile); err == nil { t.Fatal("SetExecDelivery wrote over a schema-2 manifest, want a refusal") } got, _, err := s.Get(ctx, Key("bab", "keymail")) if err != nil { t.Fatalf("Get raw: %v", err) } if string(got) != string(raw) { t.Fatalf("schema-2 object was modified: %s", got) } }