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 sidecars` core — flag parsing/validation and
2// printing live in sidecars_cli.go's run* functions, which os.Exit() on a
3// bad invocation via log.Fatal and so aren't themselves a good unit-test
4// target (instances_cli_test.go's own doc comment: no argv test harness
5// exists in this package for os.Exit paths). What's tested directly
6// instead is sidecarsEnable, the core PutSidecarManifest wrapper;
7// parseArgv, the -args flag's whitespace-split helper; parseDurationSeconds,
8// the -idle/-max-run duration-to-seconds conversion; the two small render
9// helpers `ls`'s VERSION and PLACEMENT columns go through; and
10// TestSidecarsListTruncatedNotice, which drives the real runSidecarsList
11// against a fake console (apimode_test.go's newTestSession/loginTo/
12// apiTestAccount verb-level fixtures) to pin the truncation honesty notice
13// added in review round 1.
14package main
15
16import (
17 "context"
18 "encoding/json"
19 "net/http"
20 "net/http/httptest"
21 "strings"
22 "testing"
23
24 "github.com/carlosframework/platform/internal/provision"
25 "github.com/carlosframework/platform/internal/release"
26)
27
28func TestSidecarsEnableWritesManifest(t *testing.T) {
29 ctx := context.Background()
30 s := release.NewDirStore(t.TempDir())
31 no := false
32 m := provision.SidecarManifest{
33 Schema: 1, App: "titogo", Pool: "tito", Channel: "stable",
34 Placement: provision.PlacementNoInbound, Regions: []string{"ie"},
35 Args: []string{"sidecar", "run"}, Hibernate: &no,
36 IdleSeconds: 300, MaxRunSeconds: 3600,
37 }
38
39 if err := sidecarsEnable(ctx, s, "bad", "titogo", m); err != nil {
40 t.Fatalf("sidecarsEnable: %v", err)
41 }
42
43 got, err := provision.GetSidecarManifest(ctx, s, "bad", "titogo")
44 if err != nil {
45 t.Fatalf("GetSidecarManifest: %v", err)
46 }
47 if got.App != m.App || got.Pool != m.Pool || got.Channel != m.Channel ||
48 got.Placement != m.Placement || len(got.Regions) != 1 || got.Regions[0] != "ie" ||
49 len(got.Args) != 2 || got.Args[0] != "sidecar" || got.Args[1] != "run" ||
50 got.Hibernate == nil || *got.Hibernate != false ||
51 got.IdleSeconds != 300 || got.MaxRunSeconds != 3600 {
52 t.Errorf("GetSidecarManifest after sidecarsEnable mismatch: got %+v, want %+v", got, m)
53 }
54
55 // Re-running with a changed pool must update the manifest in place — a
56 // CAS upsert, not a create-only write, exactly as instancesEnable's own
57 // re-run test pins (instances_cli_test.go).
58 m.Pool = "other"
59 if err := sidecarsEnable(ctx, s, "bad", "titogo", m); err != nil {
60 t.Fatalf("sidecarsEnable (update): %v", err)
61 }
62 got, err = provision.GetSidecarManifest(ctx, s, "bad", "titogo")
63 if err != nil {
64 t.Fatalf("GetSidecarManifest after update: %v", err)
65 }
66 if got.Pool != "other" {
67 t.Errorf("Pool after update = %q, want %q", got.Pool, "other")
68 }
69}
70
71func TestParseArgv(t *testing.T) {
72 cases := []struct {
73 in string
74 want []string
75 }{
76 {"", nil},
77 {"sidecar", []string{"sidecar"}},
78 {"sidecar run", []string{"sidecar", "run"}},
79 {" sidecar run ", []string{"sidecar", "run"}},
80 }
81 for _, c := range cases {
82 got := parseArgv(c.in)
83 if len(got) != len(c.want) {
84 t.Errorf("parseArgv(%q) = %v, want %v", c.in, got, c.want)
85 continue
86 }
87 for i := range got {
88 if got[i] != c.want[i] {
89 t.Errorf("parseArgv(%q) = %v, want %v", c.in, got, c.want)
90 break
91 }
92 }
93 }
94}
95
96// TestParseDurationSeconds pins the wire's own contract (design §4.1):
97// idle/max-run cross as whole SECONDS, never a duration string — "5m" must
98// become exactly 300, "1h" exactly 3600.
99func TestParseDurationSeconds(t *testing.T) {
100 cases := []struct {
101 in string
102 want int
103 wantErr bool
104 }{
105 {"5m", 300, false},
106 {"1h", 3600, false},
107 {"0s", 0, false},
108 {"30s", 30, false},
109 {"-5m", 0, true},
110 {"banana", 0, true},
111 }
112 for _, c := range cases {
113 got, err := parseDurationSeconds(c.in)
114 if c.wantErr {
115 if err == nil {
116 t.Errorf("parseDurationSeconds(%q) = %d, <nil>, want an error", c.in, got)
117 }
118 continue
119 }
120 if err != nil {
121 t.Errorf("parseDurationSeconds(%q) unexpected error: %v", c.in, err)
122 continue
123 }
124 if got != c.want {
125 t.Errorf("parseDurationSeconds(%q) = %d, want %d", c.in, got, c.want)
126 }
127 }
128}
129
130// TestSidecarVersionCell pins `ls`'s honesty rule directly (design §9.2,
131// binding on this task): a sidecar that has never reported a version must
132// render "unknown", never the record's own channel or anything else that
133// would read as a current, confirmed version.
134func TestSidecarVersionCell(t *testing.T) {
135 cases := []struct{ in, want string }{
136 {"", "unknown"},
137 {"abc1234", "abc1234"},
138 }
139 for _, c := range cases {
140 if got := sidecarVersionCell(c.in); got != c.want {
141 t.Errorf("sidecarVersionCell(%q) = %q, want %q", c.in, got, c.want)
142 }
143 }
144}
145
146func TestSidecarPlacementCell(t *testing.T) {
147 cases := []struct{ in, want string }{
148 {"", provision.PlacementAny},
149 {provision.PlacementNoInbound, provision.PlacementNoInbound},
150 {provision.PlacementAny, provision.PlacementAny},
151 }
152 for _, c := range cases {
153 if got := sidecarPlacementCell(c.in); got != c.want {
154 t.Errorf("sidecarPlacementCell(%q) = %q, want %q", c.in, got, c.want)
155 }
156 }
157}
158
159// TestSidecarsListTruncatedNotice pins the truncation honesty rule added in
160// Task 14 review round 1: the console caps one `ls` answer at
161// sidecarListMax (500, internal/console/apisidecars.go) and reports it via
162// the response's `truncated` flag; a caller that prints the records and
163// drops that flag on the floor renders a list that quietly stops at a
164// prefix and says nothing — the same overclaim VERSION's "unknown" exists
165// to prevent, and not hypothetical for the motivating customer (Eleven: a
166// bot per enrolled member, so exceeding 500 is growth, not misuse).
167//
168// This drives the real runSidecarsList — not just the client method —
169// against a fake console built the way apimode_test.go's other verb-level
170// tests are (newTestSession/loginTo/apiTestAccount), asserting the notice
171// appears for a truncated answer and stays silent for a whole one. Those
172// two cases are the mutation check the brief asked for: a change that
173// ignores the flag entirely (always prints it, or never does) fails
174// exactly one of the two subtests.
175func TestSidecarsListTruncatedNotice(t *testing.T) {
176 for _, tc := range []struct {
177 name string
178 truncated bool
179 }{
180 {"truncated", true},
181 {"not truncated", false},
182 } {
183 t.Run(tc.name, func(t *testing.T) {
184 useTempCredentials(t)
185 clearDeploymentEnv(t)
186 checkNoRegistry := requireNoRegistryTouch(t)
187
188 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
189 json.NewEncoder(w).Encode(map[string]any{
190 "sidecars": []map[string]any{
191 {"name": "bot-acme", "host": "bot-acme.titogo.bab.sidecar.internal", "channel": "stable"},
192 },
193 "truncated": tc.truncated,
194 })
195 }))
196 defer ts.Close()
197 loginTo(t, ts.URL, "vicky@keymail.dev")
198
199 buf := captureStdout(t)
200 runSidecarsList([]string{"-app", "titogo", "-account", apiTestAccount})
201
202 out := buf.String()
203 const notice = "more exist"
204 if tc.truncated && !strings.Contains(out, notice) {
205 t.Fatalf("ls stdout = %q, want a truncation notice containing %q", out, notice)
206 }
207 if !tc.truncated && strings.Contains(out, notice) {
208 t.Fatalf("ls stdout = %q, must NOT claim more exist for a whole (untruncated) answer", out)
209 }
210 checkNoRegistry()
211 })
212 }
213}
214