// Tests for the `carlos instances enable` core — flag parsing/validation and // printing live in instances_cli.go's runInstancesEnable, which os.Exit()s on // a bad invocation via log.Fatal and so isn't itself a good unit-test target // (no established env-verb argv test harness exists in this package — // appconfig_cli.go, the pattern this verb follows, has none either). What's // tested directly instead is instancesEnable, the core PutManifest wrapper, // and parseSeed, the -seed flag's whitespace-split helper. package main import ( "context" "net/http" "net/http/httptest" "testing" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestInstanceDeleteCarriesActivityMessageHeader(t *testing.T) { var gotMessage string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotMessage = r.Header.Get("X-Carlos-Message") w.WriteHeader(http.StatusOK) })) defer ts.Close() err := newTestSession(ts.URL).instanceDelete(context.Background(), apiAcct{Seg: "bab"}, "titogo", "web.example.test", "retire the old web copy") if err != nil { t.Fatal(err) } if gotMessage != "retire the old web copy" { t.Fatalf("message header = %q", gotMessage) } } func TestInstancesEnableWritesManifest(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) m := provision.Manifest{ Schema: 1, App: "tito", Domain: "go.tito.io", Pool: "tito", Channel: "stable", Regions: []string{"ie"}, Seed: []string{"seed", "-v"}, } if err := instancesEnable(ctx, s, "bad", "tito", m); err != nil { t.Fatalf("instancesEnable: %v", err) } got, err := provision.GetManifest(ctx, s, "bad", "tito") if err != nil { t.Fatalf("GetManifest: %v", err) } if got.App != m.App || got.Domain != m.Domain || got.Pool != m.Pool || got.Channel != m.Channel || len(got.Regions) != 1 || got.Regions[0] != "ie" || len(got.Seed) != 2 || got.Seed[0] != "seed" || got.Seed[1] != "-v" { t.Errorf("GetManifest after instancesEnable mismatch: got %+v, want %+v", got, m) } // Re-running with a changed pool must update the manifest in place — a // CAS upsert, not a create-only write (the "carlos env set" pattern // spec §3.1 names). m.Pool = "other" if err := instancesEnable(ctx, s, "bad", "tito", m); err != nil { t.Fatalf("instancesEnable (update): %v", err) } got, err = provision.GetManifest(ctx, s, "bad", "tito") if err != nil { t.Fatalf("GetManifest after update: %v", err) } if got.Pool != "other" { t.Errorf("Pool after update = %q, want %q", got.Pool, "other") } } // TestUpstreamsCell pins the LIST column's rendering (Task 8): "up/total" // when the record carries a joined upstream table, and the literal "-" the // task brief specifies for the absent case — deliberately not HEALTH's em // dash "—", since an absent upstream join means "not a multi-upstream // record" (the overwhelming majority), not "an old console". func TestUpstreamsCell(t *testing.T) { cases := []struct { name string ups []apiUpstreamState want string }{ {"nil", nil, "-"}, {"empty", []apiUpstreamState{}, "-"}, {"all up", []apiUpstreamState{{Addr: "a:1", Up: true}, {Addr: "b:1", Up: true}}, "2/2"}, {"one down", []apiUpstreamState{{Addr: "a:1", Up: true}, {Addr: "b:1", Up: false}, {Addr: "c:1", Up: true}}, "2/3"}, {"all down", []apiUpstreamState{{Addr: "a:1", Up: false}}, "0/1"}, } for _, c := range cases { if got := upstreamsCell(c.ups); got != c.want { t.Errorf("upstreamsCell(%q) = %q, want %q", c.name, got, c.want) } } } // TestParseAddrList pins the given-vs-absent split -addr/-addrs share // (Task 8): a flag never named on the command line must return nil (the // wire's "leave/default"), never mistaken for the empty list an explicitly // blank value produces. func TestParseAddrList(t *testing.T) { if got := parseAddrList("anything", false); got != nil { t.Errorf("parseAddrList(_, given=false) = %v, want nil", got) } if got := parseAddrList("", true); got != nil { t.Errorf("parseAddrList(\"\", given=true) = %v, want nil (no elements)", got) } if got := parseAddrList(" , ", true); got != nil { t.Errorf("parseAddrList(whitespace-only, given=true) = %v, want nil", got) } got := parseAddrList(" a:1 , b:2 ,,c:3", true) want := []string{"a:1", "b:2", "c:3"} if len(got) != len(want) { t.Fatalf("parseAddrList = %v, want %v", got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("parseAddrList = %v, want %v", got, want) } } } // TestParseProbeFlags pins the same given-vs-absent split for // -health-path/-health-status: nil unless at least one was named, so a // caller touching neither never sends a "probe" key at all — the door's own // "leave the probe alone" contract (apiinstances.go). func TestParseProbeFlags(t *testing.T) { if got := parseProbeFlags("", 0, false, false); got != nil { t.Errorf("parseProbeFlags(neither given) = %+v, want nil", got) } got := parseProbeFlags("/healthz", 0, true, false) if got == nil || got.Path != "/healthz" || got.Status != 0 { t.Errorf("parseProbeFlags(path only) = %+v, want {Path: /healthz, Status: 0}", got) } got = parseProbeFlags("", 204, false, true) if got == nil || got.Path != "" || got.Status != 204 { t.Errorf("parseProbeFlags(status only) = %+v, want {Path: \"\", Status: 204}", got) } got = parseProbeFlags(" /up ", 200, true, true) if got == nil || got.Path != "/up" || got.Status != 200 { t.Errorf("parseProbeFlags(both) = %+v, want {Path: /up, Status: 200}", got) } } func TestParseSeed(t *testing.T) { cases := []struct { in string want []string }{ {"", nil}, {"seed", []string{"seed"}}, {"seed -v", []string{"seed", "-v"}}, {" seed -v ", []string{"seed", "-v"}}, } for _, c := range cases { got := parseSeed(c.in) if len(got) != len(c.want) { t.Errorf("parseSeed(%q) = %v, want %v", c.in, got, c.want) continue } for i := range got { if got[i] != c.want[i] { t.Errorf("parseSeed(%q) = %v, want %v", c.in, got, c.want) break } } } }