carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Tests for the `carlos instances enable` core — flag parsing/validation and
2// printing live in instances_cli.go's runInstancesEnable, which os.Exit()s on
3// a bad invocation via log.Fatal and so isn't itself a good unit-test target
4// (no established env-verb argv test harness exists in this package —
5// appconfig_cli.go, the pattern this verb follows, has none either). What's
6// tested directly instead is instancesEnable, the core PutManifest wrapper,
7// and parseSeed, the -seed flag's whitespace-split helper.
8package main
9
10import (
11 "context"
12 "net/http"
13 "net/http/httptest"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/release"
18)
19
20func TestInstanceDeleteCarriesActivityMessageHeader(t *testing.T) {
21 var gotMessage string
22 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 gotMessage = r.Header.Get("X-Carlos-Message")
24 w.WriteHeader(http.StatusOK)
25 }))
26 defer ts.Close()
27
28 err := newTestSession(ts.URL).instanceDelete(context.Background(), apiAcct{Seg: "bab"},
29 "titogo", "web.example.test", "retire the old web copy")
30 if err != nil {
31 t.Fatal(err)
32 }
33 if gotMessage != "retire the old web copy" {
34 t.Fatalf("message header = %q", gotMessage)
35 }
36}
37
38func TestInstancesEnableWritesManifest(t *testing.T) {
39 ctx := context.Background()
40 s := release.NewDirStore(t.TempDir())
41 m := provision.Manifest{
42 Schema: 1, App: "tito", Domain: "go.tito.io", Pool: "tito", Channel: "stable",
43 Regions: []string{"ie"}, Seed: []string{"seed", "-v"},
44 }
45
46 if err := instancesEnable(ctx, s, "bad", "tito", m); err != nil {
47 t.Fatalf("instancesEnable: %v", err)
48 }
49
50 got, err := provision.GetManifest(ctx, s, "bad", "tito")
51 if err != nil {
52 t.Fatalf("GetManifest: %v", err)
53 }
54 if got.App != m.App || got.Domain != m.Domain || got.Pool != m.Pool ||
55 got.Channel != m.Channel || len(got.Regions) != 1 || got.Regions[0] != "ie" ||
56 len(got.Seed) != 2 || got.Seed[0] != "seed" || got.Seed[1] != "-v" {
57 t.Errorf("GetManifest after instancesEnable mismatch: got %+v, want %+v", got, m)
58 }
59
60 // Re-running with a changed pool must update the manifest in place — a
61 // CAS upsert, not a create-only write (the "carlos env set" pattern
62 // spec §3.1 names).
63 m.Pool = "other"
64 if err := instancesEnable(ctx, s, "bad", "tito", m); err != nil {
65 t.Fatalf("instancesEnable (update): %v", err)
66 }
67 got, err = provision.GetManifest(ctx, s, "bad", "tito")
68 if err != nil {
69 t.Fatalf("GetManifest after update: %v", err)
70 }
71 if got.Pool != "other" {
72 t.Errorf("Pool after update = %q, want %q", got.Pool, "other")
73 }
74}
75
76// TestUpstreamsCell pins the LIST column's rendering (Task 8): "up/total"
77// when the record carries a joined upstream table, and the literal "-" the
78// task brief specifies for the absent case — deliberately not HEALTH's em
79// dash "—", since an absent upstream join means "not a multi-upstream
80// record" (the overwhelming majority), not "an old console".
81func TestUpstreamsCell(t *testing.T) {
82 cases := []struct {
83 name string
84 ups []apiUpstreamState
85 want string
86 }{
87 {"nil", nil, "-"},
88 {"empty", []apiUpstreamState{}, "-"},
89 {"all up", []apiUpstreamState{{Addr: "a:1", Up: true}, {Addr: "b:1", Up: true}}, "2/2"},
90 {"one down", []apiUpstreamState{{Addr: "a:1", Up: true}, {Addr: "b:1", Up: false}, {Addr: "c:1", Up: true}}, "2/3"},
91 {"all down", []apiUpstreamState{{Addr: "a:1", Up: false}}, "0/1"},
92 }
93 for _, c := range cases {
94 if got := upstreamsCell(c.ups); got != c.want {
95 t.Errorf("upstreamsCell(%q) = %q, want %q", c.name, got, c.want)
96 }
97 }
98}
99
100// TestParseAddrList pins the given-vs-absent split -addr/-addrs share
101// (Task 8): a flag never named on the command line must return nil (the
102// wire's "leave/default"), never mistaken for the empty list an explicitly
103// blank value produces.
104func TestParseAddrList(t *testing.T) {
105 if got := parseAddrList("anything", false); got != nil {
106 t.Errorf("parseAddrList(_, given=false) = %v, want nil", got)
107 }
108 if got := parseAddrList("", true); got != nil {
109 t.Errorf("parseAddrList(\"\", given=true) = %v, want nil (no elements)", got)
110 }
111 if got := parseAddrList(" , ", true); got != nil {
112 t.Errorf("parseAddrList(whitespace-only, given=true) = %v, want nil", got)
113 }
114 got := parseAddrList(" a:1 , b:2 ,,c:3", true)
115 want := []string{"a:1", "b:2", "c:3"}
116 if len(got) != len(want) {
117 t.Fatalf("parseAddrList = %v, want %v", got, want)
118 }
119 for i := range want {
120 if got[i] != want[i] {
121 t.Fatalf("parseAddrList = %v, want %v", got, want)
122 }
123 }
124}
125
126// TestParseProbeFlags pins the same given-vs-absent split for
127// -health-path/-health-status: nil unless at least one was named, so a
128// caller touching neither never sends a "probe" key at all — the door's own
129// "leave the probe alone" contract (apiinstances.go).
130func TestParseProbeFlags(t *testing.T) {
131 if got := parseProbeFlags("", 0, false, false); got != nil {
132 t.Errorf("parseProbeFlags(neither given) = %+v, want nil", got)
133 }
134 got := parseProbeFlags("/healthz", 0, true, false)
135 if got == nil || got.Path != "/healthz" || got.Status != 0 {
136 t.Errorf("parseProbeFlags(path only) = %+v, want {Path: /healthz, Status: 0}", got)
137 }
138 got = parseProbeFlags("", 204, false, true)
139 if got == nil || got.Path != "" || got.Status != 204 {
140 t.Errorf("parseProbeFlags(status only) = %+v, want {Path: \"\", Status: 204}", got)
141 }
142 got = parseProbeFlags(" /up ", 200, true, true)
143 if got == nil || got.Path != "/up" || got.Status != 200 {
144 t.Errorf("parseProbeFlags(both) = %+v, want {Path: /up, Status: 200}", got)
145 }
146}
147
148func TestParseSeed(t *testing.T) {
149 cases := []struct {
150 in string
151 want []string
152 }{
153 {"", nil},
154 {"seed", []string{"seed"}},
155 {"seed -v", []string{"seed", "-v"}},
156 {" seed -v ", []string{"seed", "-v"}},
157 }
158 for _, c := range cases {
159 got := parseSeed(c.in)
160 if len(got) != len(c.want) {
161 t.Errorf("parseSeed(%q) = %v, want %v", c.in, got, c.want)
162 continue
163 }
164 for i := range got {
165 if got[i] != c.want[i] {
166 t.Errorf("parseSeed(%q) = %v, want %v", c.in, got, c.want)
167 break
168 }
169 }
170 }
171}
172