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/environments_test.go
2package appconfig
3
4import (
5 "context"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12func TestEnvironmentKeyIsAccountAndAppScoped(t *testing.T) {
13 got := EnvironmentKey("bab", "keymail", "demo")
14 want := "apps/bab/keymail/environments/demo.json"
15 if got != want {
16 t.Fatalf("EnvironmentKey(bab, keymail, demo) = %q, want %q", got, want)
17 }
18}
19
20func TestValidEnvironmentName(t *testing.T) {
21 valid := []string{"demo", "a", "connect-timeslots", "yste-demo", "0-9"}
22 invalid := []string{
23 "", "default", // reserved / empty
24 "Demo", // uppercase
25 "has space", // space
26 "has_us", // underscore not allowed
27 "UP",
28 string(make([]byte, 33)), // too long (33 NUL bytes, also fails the char class)
29 }
30 for _, n := range valid {
31 if !ValidEnvironmentName(n) {
32 t.Errorf("ValidEnvironmentName(%q) = false, want true", n)
33 }
34 }
35 for _, n := range invalid {
36 if ValidEnvironmentName(n) {
37 t.Errorf("ValidEnvironmentName(%q) = true, want false", n)
38 }
39 }
40}
41
42func TestValidEnvironmentNameLengthBoundary(t *testing.T) {
43 ok32 := "01234567890123456789012345678901" // 32 chars
44 if len(ok32) != 32 {
45 t.Fatalf("test fixture is %d chars, want 32", len(ok32))
46 }
47 if !ValidEnvironmentName(ok32) {
48 t.Errorf("ValidEnvironmentName(32 chars) = false, want true")
49 }
50 tooLong := ok32 + "x"
51 if ValidEnvironmentName(tooLong) {
52 t.Errorf("ValidEnvironmentName(33 chars) = true, want false")
53 }
54}
55
56func TestLoadEnvironmentOnMissingBundleReturnsEmpty(t *testing.T) {
57 s := release.NewDirStore(t.TempDir())
58 m, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "demo")
59 if err != nil {
60 t.Fatalf("LoadEnvironment: %v", err)
61 }
62 if len(m.Vars) != 0 {
63 t.Fatalf("Vars = %v, want empty for a never-configured environment", m.Vars)
64 }
65}
66
67func TestLoadEnvironmentRejectsReservedName(t *testing.T) {
68 s := release.NewDirStore(t.TempDir())
69 if _, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "default"); err == nil {
70 t.Fatal("LoadEnvironment(..., \"default\") accepted, want a refusal")
71 }
72}
73
74func TestSaveEnvironmentThenLoad(t *testing.T) {
75 s := release.NewDirStore(t.TempDir())
76 ctx := context.Background()
77 if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo",
78 map[string]Var{"LOG_LEVEL": {Kind: "env", Value: "debug"}}, nil); err != nil {
79 t.Fatalf("SaveEnvironment: %v", err)
80 }
81 m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo")
82 if err != nil {
83 t.Fatalf("LoadEnvironment: %v", err)
84 }
85 if v := m.Vars["LOG_LEVEL"]; v.Kind != "env" || v.Value != "debug" {
86 t.Fatalf("LOG_LEVEL = %+v, want kind env value debug", v)
87 }
88 // The default bundle (config.json) must be untouched by a named-bundle write.
89 def, err := Get(ctx, s, "bab", "keymail")
90 if err != nil {
91 t.Fatalf("Get (default): %v", err)
92 }
93 if len(def.Vars) != 0 {
94 t.Fatalf("default bundle vars = %v, want empty — SaveEnvironment must not touch config.json", def.Vars)
95 }
96}
97
98func TestSaveEnvironmentRejectsReservedOrInvalidName(t *testing.T) {
99 s := release.NewDirStore(t.TempDir())
100 ctx := context.Background()
101 for _, bad := range []string{"", "default", "Has-Upper", "has_underscore"} {
102 if err := SaveEnvironment(ctx, s, "bab", "keymail", bad,
103 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil {
104 t.Errorf("SaveEnvironment(..., %q, ...) accepted, want a refusal", bad)
105 }
106 }
107}
108
109func TestSaveEnvironmentNewerSchemaRefused(t *testing.T) {
110 s := release.NewDirStore(t.TempDir())
111 ctx := context.Background()
112 raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`)
113 if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil {
114 t.Fatalf("Put: %v", err)
115 }
116 if _, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo"); err == nil {
117 t.Fatal("LoadEnvironment read a schema-2 bundle without complaint, want a refusal")
118 }
119 if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo",
120 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil {
121 t.Fatal("SaveEnvironment wrote over a schema-2 bundle, want a refusal")
122 }
123}
124
125func TestListEnvironments(t *testing.T) {
126 s := release.NewDirStore(t.TempDir())
127 ctx := context.Background()
128 for _, name := range []string{"demo", "connect", "timeslots"} {
129 if err := SaveEnvironment(ctx, s, "bab", "tito", name,
130 map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil {
131 t.Fatalf("SaveEnvironment(%s): %v", name, err)
132 }
133 }
134 // A different app's environment must not leak in.
135 if err := SaveEnvironment(ctx, s, "bab", "other-app", "demo",
136 map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil {
137 t.Fatal(err)
138 }
139 names, err := ListEnvironments(ctx, s, "bab", "tito")
140 if err != nil {
141 t.Fatalf("ListEnvironments: %v", err)
142 }
143 want := []string{"connect", "demo", "timeslots"}
144 if len(names) != len(want) {
145 t.Fatalf("ListEnvironments = %v, want %v", names, want)
146 }
147 for i := range want {
148 if names[i] != want[i] {
149 t.Fatalf("ListEnvironments = %v, want %v", names, want)
150 }
151 }
152}
153
154func TestListEnvironmentsEmptyWhenNoneConfigured(t *testing.T) {
155 s := release.NewDirStore(t.TempDir())
156 names, err := ListEnvironments(context.Background(), s, "bab", "keymail")
157 if err != nil {
158 t.Fatalf("ListEnvironments: %v", err)
159 }
160 if len(names) != 0 {
161 t.Fatalf("ListEnvironments = %v, want empty", names)
162 }
163}
164
165// --- LoadMerged ---------------------------------------------------------
166
167func TestLoadMergedEmptyEnvNameIsDefaultOnly(t *testing.T) {
168 s := release.NewDirStore(t.TempDir())
169 ctx := context.Background()
170 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "default-a"}); err != nil {
171 t.Fatal(err)
172 }
173 m, err := LoadMerged(ctx, s, "bab", "keymail", "")
174 if err != nil {
175 t.Fatalf("LoadMerged: %v", err)
176 }
177 if len(m.Vars) != 1 || m.Vars["A"].Value != "default-a" {
178 t.Fatalf("LoadMerged(envName=\"\") = %+v, want just the default bundle", m.Vars)
179 }
180}
181
182// Matches Sync's current behaviour for an unconfigured app: no default
183// object AND envName == "" resolves to an empty manifest, not an error.
184func TestLoadMergedNoDefaultNoEnvNameIsEmptyNotError(t *testing.T) {
185 s := release.NewDirStore(t.TempDir())
186 m, err := LoadMerged(context.Background(), s, "bab", "keymail", "")
187 if err != nil {
188 t.Fatalf("LoadMerged: %v, want no error for an unconfigured app", err)
189 }
190 if len(m.Vars) != 0 {
191 t.Fatalf("Vars = %v, want empty", m.Vars)
192 }
193}
194
195func TestLoadMergedEnvironmentOverlaysDefaultWinningPerKey(t *testing.T) {
196 s := release.NewDirStore(t.TempDir())
197 ctx := context.Background()
198 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{
199 "SHARED": "default-value", "DEFAULT_ONLY": "d",
200 }); err != nil {
201 t.Fatal(err)
202 }
203 if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{
204 "SHARED": {Kind: "env", Value: "demo-value"},
205 "ENV_ONLY": {Kind: "env", Value: "e"},
206 }, nil); err != nil {
207 t.Fatal(err)
208 }
209 m, err := LoadMerged(ctx, s, "bab", "keymail", "demo")
210 if err != nil {
211 t.Fatalf("LoadMerged: %v", err)
212 }
213 if len(m.Vars) != 3 {
214 t.Fatalf("Vars = %+v, want 3 keys (SHARED, DEFAULT_ONLY, ENV_ONLY)", m.Vars)
215 }
216 if m.Vars["SHARED"].Value != "demo-value" {
217 t.Fatalf("SHARED = %q, want the environment's overlay value to win", m.Vars["SHARED"].Value)
218 }
219 if m.Vars["DEFAULT_ONLY"].Value != "d" {
220 t.Fatalf("DEFAULT_ONLY = %q, want the default's value preserved", m.Vars["DEFAULT_ONLY"].Value)
221 }
222 if m.Vars["ENV_ONLY"].Value != "e" {
223 t.Fatalf("ENV_ONLY = %q, want the environment-only value preserved", m.Vars["ENV_ONLY"].Value)
224 }
225}
226
227// A MISSING default with a present, named environment resolves to the
228// environment alone — not an error, and not folded into an empty manifest.
229func TestLoadMergedEnvironmentOnlyNoDefault(t *testing.T) {
230 s := release.NewDirStore(t.TempDir())
231 ctx := context.Background()
232 if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo",
233 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil {
234 t.Fatal(err)
235 }
236 m, err := LoadMerged(ctx, s, "bab", "keymail", "demo")
237 if err != nil {
238 t.Fatalf("LoadMerged: %v", err)
239 }
240 if len(m.Vars) != 1 || m.Vars["A"].Value != "1" {
241 t.Fatalf("LoadMerged(env-only) = %+v, want just the environment's vars", m.Vars)
242 }
243}
244
245// A missing environment named EXPLICITLY is an error: the caller bound a
246// route to it, so its absence is a defect, not an empty set — distinct from
247// envName == "", which is a legitimate "no environment bound" state.
248func TestLoadMergedMissingNamedEnvironmentErrors(t *testing.T) {
249 s := release.NewDirStore(t.TempDir())
250 ctx := context.Background()
251 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil {
252 t.Fatal(err)
253 }
254 if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil {
255 t.Fatal("LoadMerged with a named-but-absent environment: got nil error, want one")
256 }
257}
258
259func TestLoadMergedRejectsInvalidEnvironmentName(t *testing.T) {
260 s := release.NewDirStore(t.TempDir())
261 if _, err := LoadMerged(context.Background(), s, "bab", "keymail", "default"); err == nil {
262 t.Fatal("LoadMerged(..., \"default\") accepted, want a refusal — \"default\" is not a named environment")
263 }
264}
265
266func TestLoadMergedSchemaTooNewOnEitherSideRefuses(t *testing.T) {
267 s := release.NewDirStore(t.TempDir())
268 ctx := context.Background()
269 raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`)
270 if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil {
271 t.Fatal(err)
272 }
273 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil {
274 t.Fatal(err)
275 }
276 if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil {
277 t.Fatal("LoadMerged over a schema-2 environment bundle: got nil error, want one")
278 }
279}
280
281// Review CRITICAL 2, named-bundle half: an unset against a bundle that does
282// not exist must not MINT it. A phantom empty bundle passes the route
283// existence rail (`carlos route -environment`), which exists precisely to
284// refuse binding a bundle that isn't there — and a route bound to it silently
285// loses whatever environment it was on.
286func TestUnsetEnvironmentOnMissingBundleErrorsAndMintsNothing(t *testing.T) {
287 s := release.NewDirStore(t.TempDir())
288 ctx := context.Background()
289 removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "typoo", []string{"NOTHING"})
290 if err == nil {
291 t.Fatalf("unset against a nonexistent bundle succeeded (removed %d), want a refusal", removed)
292 }
293 if !strings.Contains(err.Error(), "nothing to unset") {
294 t.Errorf("error = %v, want it to say there is nothing to unset", err)
295 }
296 names, err := ListEnvironments(ctx, s, "bab", "keymail")
297 if err != nil {
298 t.Fatalf("ListEnvironments: %v", err)
299 }
300 if len(names) != 0 {
301 t.Fatalf("environments = %v, want none — the unset minted a phantom bundle", names)
302 }
303}
304
305// The same command against a bundle that DOES exist works, and reports how
306// many keys it actually removed (review MINOR 9), not how many it was given.
307func TestUnsetEnvironmentReportsActualRemovals(t *testing.T) {
308 s := release.NewDirStore(t.TempDir())
309 ctx := context.Background()
310 if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{
311 "A": {Kind: "env", Value: "1"},
312 "B": {Kind: "env", Value: "2"},
313 }, nil); err != nil {
314 t.Fatal(err)
315 }
316 removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "demo", []string{"A", "NEVER_SET"})
317 if err != nil {
318 t.Fatalf("UnsetEnvironment: %v", err)
319 }
320 if removed != 1 {
321 t.Fatalf("removed = %d, want 1 (A removed, NEVER_SET was never there)", removed)
322 }
323 m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo")
324 if err != nil {
325 t.Fatal(err)
326 }
327 if _, ok := m.Vars["A"]; ok {
328 t.Fatal("A survived the unset")
329 }
330 if m.Vars["B"].Value != "2" {
331 t.Fatalf("B = %+v, want it untouched", m.Vars["B"])
332 }
333}
334
335// --- issue #195: exec_delivery at environment scope ---
336
337func TestSetExecDeliveryAtEnvironmentRoundTrips(t *testing.T) {
338 ctx := context.Background()
339 s := release.NewDirStore(t.TempDir())
340 // A bundle must exist before a flag can be written into it, exactly as
341 // the default bundle must.
342 if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay",
343 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil {
344 t.Fatal(err)
345 }
346 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil {
347 t.Fatalf("set: %v", err)
348 }
349 m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay")
350 if err != nil {
351 t.Fatal(err)
352 }
353 if m.ExecDelivery != ExecDeliveryFile {
354 t.Fatalf("environment bundle exec_delivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile)
355 }
356 // The vars already in the bundle survive the flag write — read-modify-
357 // write, not replace.
358 if m.Vars["A"].Value != "1" {
359 t.Fatalf("A = %q, want it preserved at \"1\"", m.Vars["A"].Value)
360 }
361 // And the DEFAULT bundle is untouched: an environment-scoped write that
362 // leaked into config.json would opt the whole fleet in, which is the
363 // bug this closes.
364 def, found, err := getIfExists(ctx, s, "bab", "titogo")
365 if err != nil {
366 t.Fatal(err)
367 }
368 if found && def.ExecDelivery != "" {
369 t.Fatalf("default bundle exec_delivery = %q, want it untouched", def.ExecDelivery)
370 }
371}
372
373func TestSetExecDeliveryAtEnvironmentClearsToInherit(t *testing.T) {
374 ctx := context.Background()
375 s := release.NewDirStore(t.TempDir())
376 if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay",
377 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil {
378 t.Fatal(err)
379 }
380 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil {
381 t.Fatal(err)
382 }
383 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ""); err != nil {
384 t.Fatalf("clear: %v", err)
385 }
386 m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay")
387 if err != nil {
388 t.Fatal(err)
389 }
390 if m.ExecDelivery != "" {
391 t.Fatalf("exec_delivery = %q, want it cleared to inherit", m.ExecDelivery)
392 }
393}
394
395func TestSetExecDeliveryAtRefusesAbsentBundle(t *testing.T) {
396 ctx := context.Background()
397 s := release.NewDirStore(t.TempDir())
398 err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "")
399 if err == nil {
400 t.Fatal("clearing the flag on a bundle that does not exist must be refused, not silently create one")
401 }
402 if !strings.Contains(err.Error(), "relay") {
403 t.Fatalf("refusal %q must name the bundle it could not find", err)
404 }
405}
406
407func TestSetExecDeliveryAtRefusesInvalidValue(t *testing.T) {
408 ctx := context.Background()
409 s := release.NewDirStore(t.TempDir())
410 if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay",
411 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil {
412 t.Fatal(err)
413 }
414 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "socket"); err == nil {
415 t.Fatal("an unknown exec_delivery value must be refused before it reaches the bucket")
416 }
417}
418
419func TestSetExecDeliveryAtRefusesInvalidEnvironmentName(t *testing.T) {
420 ctx := context.Background()
421 s := release.NewDirStore(t.TempDir())
422 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "Relay", ExecDeliveryFile); err == nil {
423 t.Fatal("an invalid environment name must be refused before any bucket write")
424 }
425}
426
427func TestSetExecDeliveryAtBumpsGeneration(t *testing.T) {
428 ctx := context.Background()
429 s := release.NewDirStore(t.TempDir())
430 if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay",
431 map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil {
432 t.Fatal(err)
433 }
434 _, before, err := s.Get(ctx, GenKey("bab", "titogo"))
435 if err != nil {
436 t.Fatal(err)
437 }
438 if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil {
439 t.Fatal(err)
440 }
441 _, after, err := s.Get(ctx, GenKey("bab", "titogo"))
442 if err != nil {
443 t.Fatal(err)
444 }
445 if before == after {
446 t.Fatal("an environment-scoped exec_delivery write must bump the generation object — the fast loop watches nothing else for it")
447 }
448}
449