// internal/appconfig/environments_test.go package appconfig import ( "context" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func TestEnvironmentKeyIsAccountAndAppScoped(t *testing.T) { got := EnvironmentKey("bab", "keymail", "demo") want := "apps/bab/keymail/environments/demo.json" if got != want { t.Fatalf("EnvironmentKey(bab, keymail, demo) = %q, want %q", got, want) } } func TestValidEnvironmentName(t *testing.T) { valid := []string{"demo", "a", "connect-timeslots", "yste-demo", "0-9"} invalid := []string{ "", "default", // reserved / empty "Demo", // uppercase "has space", // space "has_us", // underscore not allowed "UP", string(make([]byte, 33)), // too long (33 NUL bytes, also fails the char class) } for _, n := range valid { if !ValidEnvironmentName(n) { t.Errorf("ValidEnvironmentName(%q) = false, want true", n) } } for _, n := range invalid { if ValidEnvironmentName(n) { t.Errorf("ValidEnvironmentName(%q) = true, want false", n) } } } func TestValidEnvironmentNameLengthBoundary(t *testing.T) { ok32 := "01234567890123456789012345678901" // 32 chars if len(ok32) != 32 { t.Fatalf("test fixture is %d chars, want 32", len(ok32)) } if !ValidEnvironmentName(ok32) { t.Errorf("ValidEnvironmentName(32 chars) = false, want true") } tooLong := ok32 + "x" if ValidEnvironmentName(tooLong) { t.Errorf("ValidEnvironmentName(33 chars) = true, want false") } } func TestLoadEnvironmentOnMissingBundleReturnsEmpty(t *testing.T) { s := release.NewDirStore(t.TempDir()) m, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "demo") if err != nil { t.Fatalf("LoadEnvironment: %v", err) } if len(m.Vars) != 0 { t.Fatalf("Vars = %v, want empty for a never-configured environment", m.Vars) } } func TestLoadEnvironmentRejectsReservedName(t *testing.T) { s := release.NewDirStore(t.TempDir()) if _, err := LoadEnvironment(context.Background(), s, "bab", "keymail", "default"); err == nil { t.Fatal("LoadEnvironment(..., \"default\") accepted, want a refusal") } } func TestSaveEnvironmentThenLoad(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{"LOG_LEVEL": {Kind: "env", Value: "debug"}}, nil); err != nil { t.Fatalf("SaveEnvironment: %v", err) } m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo") if err != nil { t.Fatalf("LoadEnvironment: %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) } // The default bundle (config.json) must be untouched by a named-bundle write. def, err := Get(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("Get (default): %v", err) } if len(def.Vars) != 0 { t.Fatalf("default bundle vars = %v, want empty — SaveEnvironment must not touch config.json", def.Vars) } } func TestSaveEnvironmentRejectsReservedOrInvalidName(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() for _, bad := range []string{"", "default", "Has-Upper", "has_underscore"} { if err := SaveEnvironment(ctx, s, "bab", "keymail", bad, map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil { t.Errorf("SaveEnvironment(..., %q, ...) accepted, want a refusal", bad) } } } func TestSaveEnvironmentNewerSchemaRefused(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`) if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil { t.Fatalf("Put: %v", err) } if _, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo"); err == nil { t.Fatal("LoadEnvironment read a schema-2 bundle without complaint, want a refusal") } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err == nil { t.Fatal("SaveEnvironment wrote over a schema-2 bundle, want a refusal") } } func TestListEnvironments(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() for _, name := range []string{"demo", "connect", "timeslots"} { if err := SaveEnvironment(ctx, s, "bab", "tito", name, map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatalf("SaveEnvironment(%s): %v", name, err) } } // A different app's environment must not leak in. if err := SaveEnvironment(ctx, s, "bab", "other-app", "demo", map[string]Var{"X": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } names, err := ListEnvironments(ctx, s, "bab", "tito") if err != nil { t.Fatalf("ListEnvironments: %v", err) } want := []string{"connect", "demo", "timeslots"} if len(names) != len(want) { t.Fatalf("ListEnvironments = %v, want %v", names, want) } for i := range want { if names[i] != want[i] { t.Fatalf("ListEnvironments = %v, want %v", names, want) } } } func TestListEnvironmentsEmptyWhenNoneConfigured(t *testing.T) { s := release.NewDirStore(t.TempDir()) names, err := ListEnvironments(context.Background(), s, "bab", "keymail") if err != nil { t.Fatalf("ListEnvironments: %v", err) } if len(names) != 0 { t.Fatalf("ListEnvironments = %v, want empty", names) } } // --- LoadMerged --------------------------------------------------------- func TestLoadMergedEmptyEnvNameIsDefaultOnly(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "default-a"}); err != nil { t.Fatal(err) } m, err := LoadMerged(ctx, s, "bab", "keymail", "") if err != nil { t.Fatalf("LoadMerged: %v", err) } if len(m.Vars) != 1 || m.Vars["A"].Value != "default-a" { t.Fatalf("LoadMerged(envName=\"\") = %+v, want just the default bundle", m.Vars) } } // Matches Sync's current behaviour for an unconfigured app: no default // object AND envName == "" resolves to an empty manifest, not an error. func TestLoadMergedNoDefaultNoEnvNameIsEmptyNotError(t *testing.T) { s := release.NewDirStore(t.TempDir()) m, err := LoadMerged(context.Background(), s, "bab", "keymail", "") if err != nil { t.Fatalf("LoadMerged: %v, want no error for an unconfigured app", err) } if len(m.Vars) != 0 { t.Fatalf("Vars = %v, want empty", m.Vars) } } func TestLoadMergedEnvironmentOverlaysDefaultWinningPerKey(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{ "SHARED": "default-value", "DEFAULT_ONLY": "d", }); err != nil { t.Fatal(err) } if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "SHARED": {Kind: "env", Value: "demo-value"}, "ENV_ONLY": {Kind: "env", Value: "e"}, }, nil); err != nil { t.Fatal(err) } m, err := LoadMerged(ctx, s, "bab", "keymail", "demo") if err != nil { t.Fatalf("LoadMerged: %v", err) } if len(m.Vars) != 3 { t.Fatalf("Vars = %+v, want 3 keys (SHARED, DEFAULT_ONLY, ENV_ONLY)", m.Vars) } if m.Vars["SHARED"].Value != "demo-value" { t.Fatalf("SHARED = %q, want the environment's overlay value to win", m.Vars["SHARED"].Value) } if m.Vars["DEFAULT_ONLY"].Value != "d" { t.Fatalf("DEFAULT_ONLY = %q, want the default's value preserved", m.Vars["DEFAULT_ONLY"].Value) } if m.Vars["ENV_ONLY"].Value != "e" { t.Fatalf("ENV_ONLY = %q, want the environment-only value preserved", m.Vars["ENV_ONLY"].Value) } } // A MISSING default with a present, named environment resolves to the // environment alone — not an error, and not folded into an empty manifest. func TestLoadMergedEnvironmentOnlyNoDefault(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } m, err := LoadMerged(ctx, s, "bab", "keymail", "demo") if err != nil { t.Fatalf("LoadMerged: %v", err) } if len(m.Vars) != 1 || m.Vars["A"].Value != "1" { t.Fatalf("LoadMerged(env-only) = %+v, want just the environment's vars", m.Vars) } } // A missing environment named EXPLICITLY is an error: the caller bound a // route to it, so its absence is a defect, not an empty set — distinct from // envName == "", which is a legitimate "no environment bound" state. func TestLoadMergedMissingNamedEnvironmentErrors(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil { t.Fatal(err) } if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil { t.Fatal("LoadMerged with a named-but-absent environment: got nil error, want one") } } func TestLoadMergedRejectsInvalidEnvironmentName(t *testing.T) { s := release.NewDirStore(t.TempDir()) if _, err := LoadMerged(context.Background(), s, "bab", "keymail", "default"); err == nil { t.Fatal("LoadMerged(..., \"default\") accepted, want a refusal — \"default\" is not a named environment") } } func TestLoadMergedSchemaTooNewOnEitherSideRefuses(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() raw := []byte(`{"schema": 2, "app": "keymail", "vars": {}}`) if _, err := s.Put(ctx, EnvironmentKey("bab", "keymail", "demo"), raw, ""); err != nil { t.Fatal(err) } if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"A": "1"}); err != nil { t.Fatal(err) } if _, err := LoadMerged(ctx, s, "bab", "keymail", "demo"); err == nil { t.Fatal("LoadMerged over a schema-2 environment bundle: got nil error, want one") } } // Review CRITICAL 2, named-bundle half: an unset against a bundle that does // not exist must not MINT it. A phantom empty bundle passes the route // existence rail (`carlos route -environment`), which exists precisely to // refuse binding a bundle that isn't there — and a route bound to it silently // loses whatever environment it was on. func TestUnsetEnvironmentOnMissingBundleErrorsAndMintsNothing(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "typoo", []string{"NOTHING"}) if err == nil { t.Fatalf("unset against a nonexistent bundle 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) } names, err := ListEnvironments(ctx, s, "bab", "keymail") if err != nil { t.Fatalf("ListEnvironments: %v", err) } if len(names) != 0 { t.Fatalf("environments = %v, want none — the unset minted a phantom bundle", names) } } // The same command against a bundle that DOES exist works, and reports how // many keys it actually removed (review MINOR 9), not how many it was given. func TestUnsetEnvironmentReportsActualRemovals(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() if err := SaveEnvironment(ctx, s, "bab", "keymail", "demo", map[string]Var{ "A": {Kind: "env", Value: "1"}, "B": {Kind: "env", Value: "2"}, }, nil); err != nil { t.Fatal(err) } removed, err := UnsetEnvironment(ctx, s, "bab", "keymail", "demo", []string{"A", "NEVER_SET"}) if err != nil { t.Fatalf("UnsetEnvironment: %v", err) } if removed != 1 { t.Fatalf("removed = %d, want 1 (A removed, NEVER_SET was never there)", removed) } m, err := LoadEnvironment(ctx, s, "bab", "keymail", "demo") if err != nil { t.Fatal(err) } if _, ok := m.Vars["A"]; ok { t.Fatal("A survived the unset") } if m.Vars["B"].Value != "2" { t.Fatalf("B = %+v, want it untouched", m.Vars["B"]) } } // --- issue #195: exec_delivery at environment scope --- func TestSetExecDeliveryAtEnvironmentRoundTrips(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) // A bundle must exist before a flag can be written into it, exactly as // the default bundle must. if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { t.Fatalf("set: %v", err) } m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay") if err != nil { t.Fatal(err) } if m.ExecDelivery != ExecDeliveryFile { t.Fatalf("environment bundle exec_delivery = %q, want %q", m.ExecDelivery, ExecDeliveryFile) } // The vars already in the bundle survive the flag write — read-modify- // write, not replace. if m.Vars["A"].Value != "1" { t.Fatalf("A = %q, want it preserved at \"1\"", m.Vars["A"].Value) } // And the DEFAULT bundle is untouched: an environment-scoped write that // leaked into config.json would opt the whole fleet in, which is the // bug this closes. def, found, err := getIfExists(ctx, s, "bab", "titogo") if err != nil { t.Fatal(err) } if found && def.ExecDelivery != "" { t.Fatalf("default bundle exec_delivery = %q, want it untouched", def.ExecDelivery) } } func TestSetExecDeliveryAtEnvironmentClearsToInherit(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { t.Fatal(err) } if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ""); err != nil { t.Fatalf("clear: %v", err) } m, err := LoadEnvironment(ctx, s, "bab", "titogo", "relay") if err != nil { t.Fatal(err) } if m.ExecDelivery != "" { t.Fatalf("exec_delivery = %q, want it cleared to inherit", m.ExecDelivery) } } func TestSetExecDeliveryAtRefusesAbsentBundle(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "") if err == nil { t.Fatal("clearing the flag on a bundle that does not exist must be refused, not silently create one") } if !strings.Contains(err.Error(), "relay") { t.Fatalf("refusal %q must name the bundle it could not find", err) } } func TestSetExecDeliveryAtRefusesInvalidValue(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", "socket"); err == nil { t.Fatal("an unknown exec_delivery value must be refused before it reaches the bucket") } } func TestSetExecDeliveryAtRefusesInvalidEnvironmentName(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "Relay", ExecDeliveryFile); err == nil { t.Fatal("an invalid environment name must be refused before any bucket write") } } func TestSetExecDeliveryAtBumpsGeneration(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", map[string]Var{"A": {Kind: "env", Value: "1"}}, nil); err != nil { t.Fatal(err) } _, before, err := s.Get(ctx, GenKey("bab", "titogo")) if err != nil { t.Fatal(err) } if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { t.Fatal(err) } _, after, err := s.Get(ctx, GenKey("bab", "titogo")) if err != nil { t.Fatal(err) } if before == after { t.Fatal("an environment-scoped exec_delivery write must bump the generation object — the fast loop watches nothing else for it") } }