carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// internal/appconfig/store_test.go
2package appconfig
3
4import (
5 "context"
6 "errors"
7 "strings"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13func TestSetEnvThenGet(t *testing.T) {
14 s := release.NewDirStore(t.TempDir())
15 ctx := context.Background()
16 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil {
17 t.Fatalf("SetEnv: %v", err)
18 }
19 m, err := Get(ctx, s, "bab", "keymail")
20 if err != nil {
21 t.Fatalf("Get: %v", err)
22 }
23 if v := m.Vars["LOG_LEVEL"]; v.Kind != "env" || v.Value != "debug" {
24 t.Fatalf("LOG_LEVEL = %+v, want kind env value debug", v)
25 }
26}
27
28func TestGetOnMissingManifestReturnsEmpty(t *testing.T) {
29 s := release.NewDirStore(t.TempDir())
30 m, err := Get(context.Background(), s, "bab", "keymail")
31 if err != nil {
32 t.Fatalf("Get: %v", err)
33 }
34 if len(m.Vars) != 0 {
35 t.Fatalf("Vars = %v, want empty for a never-configured app", m.Vars)
36 }
37}
38
39func TestUnsetRemovesVar(t *testing.T) {
40 s := release.NewDirStore(t.TempDir())
41 ctx := context.Background()
42 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"}); err != nil {
43 t.Fatalf("SetEnv: %v", err)
44 }
45 removed, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"})
46 if err != nil {
47 t.Fatalf("Unset: %v", err)
48 }
49 if removed != 1 {
50 t.Fatalf("Unset removed %d, want 1", removed)
51 }
52 m, err := Get(ctx, s, "bab", "keymail")
53 if err != nil {
54 t.Fatalf("Get: %v", err)
55 }
56 if _, ok := m.Vars["LOG_LEVEL"]; ok {
57 t.Fatalf("LOG_LEVEL still present after Unset")
58 }
59}
60
61// Review CRITICAL 2: an unset against an app that has never been configured
62// must NOT create the manifest it was asked to remove from. A created-empty
63// config.json reads as "configured, then fully unset" — the state Sync
64// converges every one of that app's env files to EMPTY for, as root.
65func TestUnsetOnNeverConfiguredAppErrorsAndWritesNothing(t *testing.T) {
66 s := release.NewDirStore(t.TempDir())
67 ctx := context.Background()
68 removed, err := Unset(ctx, s, "bab", "keymail", []string{"OLDKEY"})
69 if err == nil {
70 t.Fatalf("Unset on a never-configured app succeeded (removed %d), want a refusal", removed)
71 }
72 if !strings.Contains(err.Error(), "nothing to unset") {
73 t.Errorf("error = %v, want it to say there is nothing to unset", err)
74 }
75 if _, found, gerr := getIfExists(ctx, s, "bab", "keymail"); gerr != nil || found {
76 t.Fatalf("unset created a manifest object (found = %v, err = %v)", found, gerr)
77 }
78}
79
80// Unsetting a key that is not set, on a bundle that DOES exist, stays a
81// success (heroku config:unset's idempotence) — it just removes nothing.
82func TestUnsetOfAnAbsentKeyOnAnExistingBundleIsANoopSuccess(t *testing.T) {
83 s := release.NewDirStore(t.TempDir())
84 ctx := context.Background()
85 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"KEEP": "1"}); err != nil {
86 t.Fatalf("SetEnv: %v", err)
87 }
88 removed, err := Unset(ctx, s, "bab", "keymail", []string{"NEVER_SET"})
89 if err != nil {
90 t.Fatalf("Unset of an absent key: %v", err)
91 }
92 if removed != 0 {
93 t.Fatalf("Unset reported %d removals, want 0 — the count is of removals, not arguments", removed)
94 }
95 m, err := Get(ctx, s, "bab", "keymail")
96 if err != nil {
97 t.Fatalf("Get: %v", err)
98 }
99 if m.Vars["KEEP"].Value != "1" {
100 t.Fatalf("KEEP = %+v, want it untouched", m.Vars["KEEP"])
101 }
102}
103
104func TestSetSecretStoresEnvelopeOnly(t *testing.T) {
105 s := release.NewDirStore(t.TempDir())
106 ctx := context.Background()
107 if err := SetSecret(ctx, s, "bab", "keymail", map[string]string{"SES_SECRET": "opaque-envelope-bytes"}); err != nil {
108 t.Fatalf("SetSecret: %v", err)
109 }
110 m, err := Get(ctx, s, "bab", "keymail")
111 if err != nil {
112 t.Fatalf("Get: %v", err)
113 }
114 v := m.Vars["SES_SECRET"]
115 if v.Kind != "secret" || v.Envelope != "opaque-envelope-bytes" || v.Value != "" {
116 t.Fatalf("SES_SECRET = %+v, want kind secret, envelope set, value empty", v)
117 }
118}
119
120func TestSetRejectsInvalidVarName(t *testing.T) {
121 s := release.NewDirStore(t.TempDir())
122 err := SetEnv(context.Background(), s, "bab", "keymail", map[string]string{"has-hyphen": "x"})
123 if err == nil {
124 t.Fatal("SetEnv accepted an invalid var name, want an error")
125 }
126}
127
128// racingStore wraps a release.Store and lets a competing write land
129// between a reader's Get and its subsequent Put — the interleaving apply's
130// read-modify-write must lose, visibly.
131type racingStore struct {
132 release.Store
133 betweenGetAndPut func()
134}
135
136func (r *racingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
137 raw, etag, err := r.Store.Get(ctx, key)
138 if r.betweenGetAndPut != nil {
139 f := r.betweenGetAndPut
140 r.betweenGetAndPut = nil
141 f()
142 }
143 return raw, etag, err
144}
145
146// TestConcurrentWriteSurfacesCASConflict proves SetEnv's real
147// read-modify-write path surfaces ErrCASConflict (rather than retrying or
148// clobbering) when a competing write lands between its read and its write
149// — the same discipline release.Promote already uses for channel pointers.
150func TestConcurrentWriteSurfacesCASConflict(t *testing.T) {
151 base := release.NewDirStore(t.TempDir())
152 ctx := context.Background()
153 if err := SetEnv(ctx, base, "bab", "keymail", map[string]string{"A": "1"}); err != nil {
154 t.Fatalf("SetEnv: %v", err)
155 }
156 rs := &racingStore{Store: base, betweenGetAndPut: func() {
157 // A second operator's write lands first, moving the object on.
158 if err := SetEnv(ctx, base, "bab", "keymail", map[string]string{"B": "2"}); err != nil {
159 t.Fatalf("SetEnv (racing writer): %v", err)
160 }
161 }}
162 err := SetEnv(ctx, rs, "bab", "keymail", map[string]string{"C": "3"})
163 if !errors.Is(err, release.ErrCASConflict) {
164 t.Fatalf("SetEnv against a moved object = %v, want ErrCASConflict", err)
165 }
166 // The racing writer's value survived; the loser's never landed.
167 m, err := Get(ctx, base, "bab", "keymail")
168 if err != nil {
169 t.Fatalf("Get: %v", err)
170 }
171 if _, ok := m.Vars["C"]; ok {
172 t.Fatal("losing write landed anyway")
173 }
174 if v := m.Vars["B"]; v.Value != "2" {
175 t.Fatalf("racing writer's var = %+v, want value 2", v)
176 }
177}
178
179func TestSetEnvRejectsUnrepresentableValue(t *testing.T) {
180 // systemd's EnvironmentFile parser mangles backslashes and quotes;
181 // those must be refused at set time, where the operator can still see
182 // the plaintext — not hours later in the adopt log.
183 s := release.NewDirStore(t.TempDir())
184 ctx := context.Background()
185 for _, bad := range []string{`back\slash`, `dou"ble`, `sin'gle`, "new\nline", "nul\x00byte"} {
186 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"V": bad}); err == nil {
187 t.Fatalf("SetEnv accepted %q, want a rejection", bad)
188 }
189 }
190 // The rejection stays narrow: the shapes real credentials take must pass.
191 for _, good := range []string{"postgres://u:p@h/db?sslmode=require", "dGVzdA==", "100%", "a+b c"} {
192 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"V": good}); err != nil {
193 t.Fatalf("SetEnv rejected %q: %v", good, err)
194 }
195 }
196}
197
198func TestSetExecDeliveryRoundTrips(t *testing.T) {
199 s := release.NewDirStore(t.TempDir())
200 ctx := context.Background()
201 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil {
202 t.Fatalf("SetEnv: %v", err)
203 }
204 if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
205 t.Fatalf("SetExecDelivery(file): %v", err)
206 }
207 m, err := Get(ctx, s, "bab", "titogo")
208 if err != nil {
209 t.Fatalf("Get: %v", err)
210 }
211 if m.ExecDelivery != ExecDeliveryFile {
212 t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile)
213 }
214 // The var set earlier survives — SetExecDelivery is a field write, not
215 // a bundle replace.
216 if m.Vars["K"].Value != "1" {
217 t.Fatalf("Vars[K] = %+v, want value 1 (SetExecDelivery must not clobber existing vars)", m.Vars["K"])
218 }
219 if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err != nil {
220 t.Fatalf("SetExecDelivery(off): %v", err)
221 }
222 m, err = Get(ctx, s, "bab", "titogo")
223 if err != nil {
224 t.Fatalf("Get: %v", err)
225 }
226 if m.ExecDelivery != "" {
227 t.Fatalf("ExecDelivery after off = %q, want empty", m.ExecDelivery)
228 }
229}
230
231func TestSetExecDeliveryRejectsUnknownValue(t *testing.T) {
232 s := release.NewDirStore(t.TempDir())
233 ctx := context.Background()
234 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "1"}); err != nil {
235 t.Fatalf("SetEnv: %v", err)
236 }
237 for _, bad := range []string{"grpc", "FILE", "true", "yes"} {
238 if err := SetExecDelivery(ctx, s, "bab", "titogo", bad); err == nil {
239 t.Fatalf("SetExecDelivery(%q) succeeded, want a refusal", bad)
240 }
241 }
242 // The refusal must not have touched the bucket object at all.
243 m, err := Get(ctx, s, "bab", "titogo")
244 if err != nil {
245 t.Fatalf("Get: %v", err)
246 }
247 if m.ExecDelivery != "" {
248 t.Fatalf("ExecDelivery = %q after a refused set, want untouched (empty)", m.ExecDelivery)
249 }
250}
251
252func TestSetExecDeliveryOffOnNeverConfiguredAppIsRefused(t *testing.T) {
253 // Mirrors TestUnsetOnNeverConfiguredAppErrorsAndWritesNothing: turning
254 // the field off must not mint an empty default bundle just to hold an
255 // absent flag.
256 s := release.NewDirStore(t.TempDir())
257 ctx := context.Background()
258 if err := SetExecDelivery(ctx, s, "bab", "titogo", ""); err == nil {
259 t.Fatal("SetExecDelivery(off) on a never-configured app succeeded, want a refusal")
260 }
261 if _, found, gerr := getIfExists(ctx, s, "bab", "titogo"); gerr != nil || found {
262 t.Fatalf("SetExecDelivery(off) created a manifest object (found = %v, err = %v)", found, gerr)
263 }
264}
265
266func TestSetExecDeliveryOnNeverConfiguredAppCreatesTheBundle(t *testing.T) {
267 // The "file" direction DOES create a bundle on first use — same as
268 // SetEnv/SetSecret already do — since "file" is genuine content to
269 // store, not an unset-only call against nothing.
270 s := release.NewDirStore(t.TempDir())
271 ctx := context.Background()
272 if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
273 t.Fatalf("SetExecDelivery(file): %v", err)
274 }
275 m, err := Get(ctx, s, "bab", "titogo")
276 if err != nil {
277 t.Fatalf("Get: %v", err)
278 }
279 if m.ExecDelivery != ExecDeliveryFile {
280 t.Fatalf("ExecDelivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile)
281 }
282}
283
284func TestNewerSchemaIsRefusedNotDowngraded(t *testing.T) {
285 // json.Unmarshal drops fields this binary doesn't know; acting on a
286 // newer manifest would silently discard them — and a write-back would
287 // downgrade the object. Both paths must refuse instead.
288 s := release.NewDirStore(t.TempDir())
289 ctx := context.Background()
290 raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`)
291 if _, err := s.Put(ctx, Key("bab", "keymail"), raw, ""); err != nil {
292 t.Fatalf("Put: %v", err)
293 }
294 if _, err := Get(ctx, s, "bab", "keymail"); err == nil {
295 t.Fatal("Get read a schema-2 manifest without complaint, want a refusal")
296 }
297 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err == nil {
298 t.Fatal("SetEnv wrote over a schema-2 manifest, want a refusal")
299 }
300 if err := SetExecDelivery(ctx, s, "bab", "keymail", ExecDeliveryFile); err == nil {
301 t.Fatal("SetExecDelivery wrote over a schema-2 manifest, want a refusal")
302 }
303 got, _, err := s.Get(ctx, Key("bab", "keymail"))
304 if err != nil {
305 t.Fatalf("Get raw: %v", err)
306 }
307 if string(got) != string(raw) {
308 t.Fatalf("schema-2 object was modified: %s", got)
309 }
310}
311