| index 53d5519d9..9daa680cf 100644 |
| --- a/internal/appconfig/syncexec_test.go |
| +++ b/internal/appconfig/syncexec_test.go |
| @@ -1628,3 +1628,204 @@ func TestUnitConfigAppsNoRoutes(t *testing.T) { |
| t.Fatalf("UnitConfigApps(nil) = %v, want empty", got) |
| } |
| } |
| + |
| +// --- issue #195: the opt-in resolves from the route's bound bundle --- |
| + |
| +// TestSyncExecOptInResolvesPerEnvironment is issue #195's matrix: which |
| +// hosts get a <host>.config.env, for each combination of the app-level |
| +// flag and the bound bundle's own. Two TCP-addr exec routes of ONE app, |
| +// one bound to "relay" and one unbound — titogo's exact shape, where |
| +// turning the app-level flag on to deliver the relay's sealed Apple Wallet |
| +// material wrote a file for every router route too. |
| +func TestSyncExecOptInResolvesPerEnvironment(t *testing.T) { |
| + for _, tc := range []struct { |
| + name string |
| + appLevel string // "" (never set) | ExecDeliveryFile |
| + envLevel string // absent | silent | file | bogus |
| + wantRelayFile bool |
| + wantUnboundFile bool |
| + }{ |
| + {"both off", "", "silent", false, false}, |
| + {"app on, env silent — inherit on", ExecDeliveryFile, "silent", true, true}, |
| + {"app off, env on — the narrow case #195 exists for", "", "file", true, false}, |
| + {"app on, env on", ExecDeliveryFile, "file", true, true}, |
| + {"app off, env bundle absent", "", "absent", false, false}, |
| + // Bound to a bundle that does not exist: the opt-in inherits "on" |
| + // from the app default, but the route never reaches a write anyway |
| + // — LoadMerged refuses a bound-but-absent bundle, so the group |
| + // resolves to an ERROR and the file is left exactly as it is. The |
| + // unbound router is untouched by that and still gets its file. |
| + {"app on, env bundle absent — resolution errors before delivery", ExecDeliveryFile, "absent", false, true}, |
| + {"app on, env states a value this binary doesn't know — that statement decides", ExecDeliveryFile, "bogus", false, true}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + ctx, s, reg, dd, priv, _ := execKit(t) |
| + pk, err := ParsePrivateKey(priv, "test") |
| + if err != nil { |
| + t.Fatalf("ParsePrivateKey: %v", err) |
| + } |
| + // The default bundle always exists and always has a var, so the |
| + // only thing under test is WHETHER a file is written. |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"SHARED": "1"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if tc.appLevel != "" { |
| + if err := SetExecDelivery(ctx, s, "bab", "titogo", tc.appLevel); err != nil { |
| + t.Fatalf("SetExecDelivery: %v", err) |
| + } |
| + } |
| + switch tc.envLevel { |
| + case "absent": |
| + // Nothing written: the route is bound to a bundle that does |
| + // not exist. |
| + case "bogus": |
| + // Written past the setter's validation on purpose, and |
| + // without SaveEnvironment first (a create, not an update): |
| + // this is what an OLDER binary sees when a NEWER one |
| + // invents a value, and it must not brick the pass. |
| + raw := []byte(`{"schema":1,"app":"titogo","vars":{"SEALED":{"kind":"env","value":"2"}},"exec_delivery":"grpc"}`) |
| + if _, err := s.Put(ctx, EnvironmentKey("bab", "titogo", "relay"), raw, ""); err != nil { |
| + t.Fatalf("Put: %v", err) |
| + } |
| + default: |
| + if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| + map[string]Var{"SEALED": {Kind: "env", Value: "2"}}, nil); err != nil { |
| + t.Fatalf("SaveEnvironment: %v", err) |
| + } |
| + if tc.envLevel == "file" { |
| + if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| + t.Fatalf("SetExecDeliveryAt: %v", err) |
| + } |
| + } |
| + } |
| + for _, rt := range []registry.Route{ |
| + {Host: "connect.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", |
| + Status: "ready", Addr: "127.0.0.1:9001", Environment: "relay"}, |
| + {Host: "router-a.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", |
| + Status: "ready", Addr: "127.0.0.1:9002"}, |
| + } { |
| + if err := reg.Upsert(rt); err != nil { |
| + t.Fatalf("Upsert %s: %v", rt.Host, err) |
| + } |
| + } |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + assertConfigFile(t, dd, "connect.go.tito.io", tc.wantRelayFile) |
| + assertConfigFile(t, dd, "router-a.go.tito.io", tc.wantUnboundFile) |
| + }) |
| + } |
| +} |
| + |
| +// assertConfigFile asserts a route's <host>.config.env exists (or not). |
| +func assertConfigFile(t *testing.T, dir, host string, want bool) { |
| + t.Helper() |
| + _, err := os.Stat(filepath.Join(dir, host+configEnvSuffix)) |
| + switch { |
| + case want && err != nil: |
| + t.Fatalf("%s: want a config file, got %v", host, err) |
| + case !want && err == nil: |
| + t.Fatalf("%s: want NO config file, one was written", host) |
| + } |
| +} |
| + |
| +// TestSyncExecEnvScopedOptInDeliversTheMergedBundle: the flag decides |
| +// WHETHER a file is written, never what is in it. A route opted in through |
| +// its own bundle still gets the default bundle overlaid by that bundle — |
| +// the same merge LoadMerged has always produced. |
| +func TestSyncExecEnvScopedOptInDeliversTheMergedBundle(t *testing.T) { |
| + ctx, s, reg, dd, priv, _ := execKit(t) |
| + pk, err := ParsePrivateKey(priv, "test") |
| + if err != nil { |
| + t.Fatalf("ParsePrivateKey: %v", err) |
| + } |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"SHARED": "1", "OVERRIDDEN": "default"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", map[string]Var{ |
| + "OVERRIDDEN": {Kind: "env", Value: "relay"}, |
| + "WALLET": {Kind: "env", Value: "sealed"}, |
| + }, nil); err != nil { |
| + t.Fatalf("SaveEnvironment: %v", err) |
| + } |
| + if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| + t.Fatalf("SetExecDeliveryAt: %v", err) |
| + } |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "connect.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", |
| + Status: "ready", Addr: "127.0.0.1:9001", Environment: "relay", |
| + }); err != nil { |
| + t.Fatalf("Upsert: %v", err) |
| + } |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + got, err := os.ReadFile(filepath.Join(dd, "connect.go.tito.io"+configEnvSuffix)) |
| + if err != nil { |
| + t.Fatalf("read config file: %v", err) |
| + } |
| + want := execAddrConfigHeader + "OVERRIDDEN=relay\nSHARED=1\nWALLET=sealed\n" |
| + if string(got) != want { |
| + t.Fatalf("config env = %q, want %q (the merged bundle, unchanged by where the flag lives)", got, want) |
| + } |
| + // exec_delivery itself must never be rendered into the file: it is a |
| + // delivery flag, not a config var. |
| + if strings.Contains(string(got), "exec_delivery") || strings.Contains(string(got), "EXEC_DELIVERY") { |
| + t.Fatalf("the delivery flag leaked into the rendered file: %q", got) |
| + } |
| +} |
| + |
| +// TestSyncExecEnvBundleFetchErrorLeavesFileAlone: an unreadable |
| +// environment bundle is NOT a determination that the route is un-opted. |
| +// The route's existing file must survive it byte-identical — the same |
| +// contract a default-bundle fetch failure already has, extended to the |
| +// second bundle the opt-in now consults. |
| +func TestSyncExecEnvBundleFetchErrorLeavesFileAlone(t *testing.T) { |
| + ctx, s, reg, dd, priv, _ := execKit(t) |
| + pk, err := ParsePrivateKey(priv, "test") |
| + if err != nil { |
| + t.Fatalf("ParsePrivateKey: %v", err) |
| + } |
| + if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"SHARED": "1"}); err != nil { |
| + t.Fatalf("SetEnv: %v", err) |
| + } |
| + if err := SaveEnvironment(ctx, s, "bab", "titogo", "relay", |
| + map[string]Var{"WALLET": {Kind: "env", Value: "sealed"}}, nil); err != nil { |
| + t.Fatalf("SaveEnvironment: %v", err) |
| + } |
| + if err := SetExecDeliveryAt(ctx, s, "bab", "titogo", "relay", ExecDeliveryFile); err != nil { |
| + t.Fatalf("SetExecDeliveryAt: %v", err) |
| + } |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "connect.go.tito.io", Kind: "instance", App: "titogo", Account: "bab", |
| + Status: "ready", Addr: "127.0.0.1:9001", Environment: "relay", |
| + }); err != nil { |
| + t.Fatalf("Upsert: %v", err) |
| + } |
| + if err := SyncExec(ctx, reg, s, pk, dd, func(string) error { return nil }); err != nil { |
| + t.Fatalf("SyncExec: %v", err) |
| + } |
| + path := filepath.Join(dd, "connect.go.tito.io"+configEnvSuffix) |
| + before, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("first pass wrote no file: %v", err) |
| + } |
| + |
| + blip := &failingKeyStore{Store: s, failKey: EnvironmentKey("bab", "titogo", "relay"), |
| + err: errors.New("503 SlowDown")} |
| + out := captureStdout(t, func() { |
| + if err := SyncExec(ctx, reg, blip, pk, dd, func(string) error { return nil }); err != nil { |
| + t.Fatalf("SyncExec (blip): %v", err) |
| + } |
| + }) |
| + after, err := os.ReadFile(path) |
| + if err != nil { |
| + t.Fatalf("a bundle fetch blip removed a live route's config file: %v", err) |
| + } |
| + if !bytes.Equal(before, after) { |
| + t.Fatalf("config file changed on a fetch blip: %q -> %q", before, after) |
| + } |
| + if !strings.Contains(out, "unresolved") || !strings.Contains(out, "relay") { |
| + t.Fatalf("a fetch blip against the bound bundle must say so loudly and name the environment; got %q", out) |
| + } |
| +} |