carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "context"
5 "errors"
6 "flag"
7 "fmt"
8 "io"
9 "net/http"
10 "net/http/httptest"
11 "reflect"
12 "strings"
13 "sync"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/updatewords"
17)
18
19func parsed(t *testing.T, args ...string) (*flag.FlagSet, *string, *string, *string, *string) {
20 t.Helper()
21 fs := flag.NewFlagSet("t", flag.ContinueOnError)
22 c := fs.String("instance-control", "", "")
23 u := fs.String("default-updates", "", "")
24 ch := fs.String("default-channel", "", "")
25 m := fs.String("default-major", "", "")
26 if err := fs.Parse(args); err != nil {
27 t.Fatal(err)
28 }
29 return fs, c, u, ch, m
30}
31
32func TestUpdatePolicyRequestSendsOnlyWhatWasGiven(t *testing.T) {
33 fs, c, u, ch, m := parsed(t, "--default-updates", "manual")
34 req, err := updatePolicyRequest(fs, *c, *u, *ch, *m)
35 if err != nil || !reflect.DeepEqual(req, updatePolicyReq{"default_updates": "manual"}) {
36 t.Fatalf("req = %v, %v", req, err)
37 }
38 fs, c, u, ch, m = parsed(t, "--instance-control", "updates,pin", "--default-major", "")
39 req, err = updatePolicyRequest(fs, *c, *u, *ch, *m)
40 if err != nil || !reflect.DeepEqual(req, updatePolicyReq{"instance_control": []string{"updates", "pin"}, "default_major": nil}) {
41 t.Fatalf("req = %v, %v", req, err)
42 }
43 fs, c, u, ch, m = parsed(t, "--default-major", "2")
44 req, _ = updatePolicyRequest(fs, *c, *u, *ch, *m)
45 if req["default_major"] != 2 {
46 t.Fatalf("major = %v", req["default_major"])
47 }
48 for _, bad := range []string{"two", "-1", "1.5"} {
49 fs, c, u, ch, m = parsed(t, "--default-major", bad)
50 _, err := updatePolicyRequest(fs, *c, *u, *ch, *m)
51 if err == nil || err.Error() != updatewords.Text("settings.cli.bad-major-flag") {
52 t.Fatalf("--default-major %q: err = %v", bad, err)
53 }
54 }
55 fs, c, u, ch, m = parsed(t)
56 if req, _ := updatePolicyRequest(fs, *c, *u, *ch, *m); req != nil {
57 t.Fatalf("no flags, yet a request: %v", req)
58 }
59}
60
61// An empty --instance-control clears: it goes out as JSON null, which the
62// console reads as "keep them all for the app".
63func TestUpdatePolicyRequestEmptyControlIsNull(t *testing.T) {
64 fs, c, u, ch, m := parsed(t, "--instance-control", " , ", "--default-updates", "", "--default-channel", "beta")
65 req, err := updatePolicyRequest(fs, *c, *u, *ch, *m)
66 if err != nil {
67 t.Fatal(err)
68 }
69 want := updatePolicyReq{"instance_control": []string(nil), "default_updates": "", "default_channel": "beta"}
70 if !reflect.DeepEqual(req, want) {
71 t.Fatalf("req = %#v", req)
72 }
73}
74
75func TestAPISessionSetUpdatePolicyWireShape(t *testing.T) {
76 var method, path, body string
77 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
78 b, _ := io.ReadAll(r.Body)
79 method, path, body = r.Method, r.URL.Path, string(b)
80 w.Header().Set("Content-Type", "application/json")
81 io.WriteString(w, `{"instance_control":["updates","pin"],"default_updates":"manual","default_channel":"stable","default_major":2}`)
82 }))
83 defer ts.Close()
84 got, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop",
85 updatePolicyReq{"default_updates": "manual", "default_major": 2})
86 two := 2
87 want := apiUpdatePolicy{InstanceControl: []string{"updates", "pin"}, DefaultUpdates: "manual", DefaultChannel: "stable", DefaultMajor: &two}
88 if err != nil || !reflect.DeepEqual(got, want) {
89 t.Fatalf("setUpdatePolicy = %+v, %v", got, err)
90 }
91 if method != http.MethodPut || path != "/api/cli/apps/bab/shop/updates" || body != `{"default_major":2,"default_updates":"manual"}` {
92 t.Fatalf("sent %s %s %s", method, path, body)
93 }
94}
95
96func TestAPISessionSetUpdatePolicyNullsOnTheWire(t *testing.T) {
97 var body string
98 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99 b, _ := io.ReadAll(r.Body)
100 body = string(b)
101 io.WriteString(w, `{"instance_control":[],"default_updates":"","default_channel":"stable","default_major":null}`)
102 }))
103 defer ts.Close()
104 got, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop",
105 updatePolicyReq{"instance_control": []string(nil), "default_major": nil})
106 if err != nil || got.DefaultMajor != nil || len(got.InstanceControl) != 0 {
107 t.Fatalf("setUpdatePolicy = %+v, %v", got, err)
108 }
109 if body != `{"default_major":null,"instance_control":null}` {
110 t.Fatalf("sent %s", body)
111 }
112}
113
114func TestAPISessionSetUpdatePolicyErrorVerbatim(t *testing.T) {
115 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116 w.Header().Set("Content-Type", "application/json")
117 w.WriteHeader(http.StatusConflict)
118 io.WriteString(w, `{"error":"the console says no, in its own words"}`)
119 }))
120 defer ts.Close()
121 _, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop", updatePolicyReq{"default_channel": ""})
122 if err == nil || err.Error() != "the console says no, in its own words" {
123 t.Fatalf("err = %v", err)
124 }
125}
126
127// doorLog is a console that records which doors were called, in order.
128type doorLog struct {
129 mu sync.Mutex
130 calls []string
131}
132
133func (d *doorLog) add(s string) {
134 d.mu.Lock()
135 defer d.mu.Unlock()
136 d.calls = append(d.calls, s)
137}
138
139func (d *doorLog) got() []string {
140 d.mu.Lock()
141 defer d.mu.Unlock()
142 return append([]string(nil), d.calls...)
143}
144
145func appsSetConsole(t *testing.T, d *doorLog) *apiSession {
146 t.Helper()
147 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
148 d.add(r.Method + " " + r.URL.Path)
149 w.Header().Set("Content-Type", "application/json")
150 switch {
151 case strings.HasSuffix(r.URL.Path, "/swap"):
152 io.WriteString(w, `{"swap":"hold","swap_concurrency":2}`)
153 case strings.HasSuffix(r.URL.Path, "/updates"):
154 io.WriteString(w, `{"instance_control":["updates","pin"],"default_updates":"manual","default_channel":"stable","default_major":3}`)
155 default:
156 w.WriteHeader(http.StatusNotFound)
157 }
158 }))
159 t.Cleanup(ts.Close)
160 return newTestSession(ts.URL)
161}
162
163func TestAppsSetRunPolicyFlagsOnlyCallOnlyTheUpdatesDoor(t *testing.T) {
164 out := captureStdout(t)
165 var d doorLog
166 policy := updatePolicyReq{"default_updates": "manual"}
167 if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", nil, policy); err != nil {
168 t.Fatal(err)
169 }
170 if want := []string{"PUT /api/cli/apps/bab/shop/updates"}; !reflect.DeepEqual(d.got(), want) {
171 t.Fatalf("doors = %v", d.got())
172 }
173 want := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", "updates, pin", "manual", "stable", "3")
174 if out.String() != want {
175 t.Fatalf("stdout = %q, want %q", out.String(), want)
176 }
177}
178
179func TestAppsSetRunSwapFlagsOnlyCallOnlyTheSwapDoor(t *testing.T) {
180 out := captureStdout(t)
181 var d doorLog
182 hold := "hold"
183 if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", &swapPolicyReq{Swap: &hold}, nil); err != nil {
184 t.Fatal(err)
185 }
186 if want := []string{"PUT /api/cli/apps/bab/shop/swap"}; !reflect.DeepEqual(d.got(), want) {
187 t.Fatalf("doors = %v", d.got())
188 }
189 if want := fmt.Sprintf(appsSetDone+"\n", "shop", "hold", 2); !strings.HasPrefix(out.String(), want) || strings.Contains(out.String(), "default updates") {
190 t.Fatalf("stdout = %q", out.String())
191 }
192}
193
194func TestAppsSetRunBothCallSwapThenUpdates(t *testing.T) {
195 out := captureStdout(t)
196 var d doorLog
197 hold := "hold"
198 if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop",
199 &swapPolicyReq{Swap: &hold}, updatePolicyReq{"default_major": 3}); err != nil {
200 t.Fatal(err)
201 }
202 if want := []string{"PUT /api/cli/apps/bab/shop/swap", "PUT /api/cli/apps/bab/shop/updates"}; !reflect.DeepEqual(d.got(), want) {
203 t.Fatalf("doors = %v", d.got())
204 }
205 swapLine := fmt.Sprintf(appsSetDone+"\n", "shop", "hold", 2)
206 policyLine := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", "updates, pin", "manual", "stable", "3")
207 if !strings.HasPrefix(out.String(), swapLine) || !strings.HasSuffix(out.String(), policyLine) {
208 t.Fatalf("stdout = %q", out.String())
209 }
210}
211
212// A refused swap stops the run: the policy door is not tried after it.
213func TestAppsSetRunSwapFailureSkipsPolicy(t *testing.T) {
214 var d doorLog
215 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
216 d.add(r.Method + " " + r.URL.Path)
217 w.WriteHeader(http.StatusConflict)
218 io.WriteString(w, `{"error":"no"}`)
219 }))
220 defer ts.Close()
221 hold := "hold"
222 err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop",
223 &swapPolicyReq{Swap: &hold}, updatePolicyReq{"default_major": 3})
224 if err == nil || err.Error() != fmt.Sprintf(appsSetError, errors.New("no")) {
225 t.Fatalf("err = %v", err)
226 }
227 if len(d.got()) != 1 {
228 t.Fatalf("doors = %v", d.got())
229 }
230}
231
232func TestAppsSetRunPolicyFailureCarriesTheConsoleWords(t *testing.T) {
233 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
234 w.WriteHeader(http.StatusConflict)
235 io.WriteString(w, `{"error":"the console says no"}`)
236 }))
237 defer ts.Close()
238 err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop", nil, updatePolicyReq{"default_channel": ""})
239 if err == nil || err.Error() != "apps set: the console says no" {
240 t.Fatalf("err = %v", err)
241 }
242}
243
244func TestAppsSetRunNothingToSet(t *testing.T) {
245 var d doorLog
246 err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", nil, nil)
247 if err == nil || err.Error() != appsSetNothing {
248 t.Fatalf("err = %v", err)
249 }
250 if len(d.got()) != 0 {
251 t.Fatalf("a request went out: %v", d.got())
252 }
253}
254
255// Astra r1 triage: an app with no default updates has instances update
256// automatically, so the line says the setting's own value word, auto, not
257// "none". Unset lists, channels and majors still read none.
258func TestAppsSetPolicyDoneSaysAutoForUnsetDefaultUpdates(t *testing.T) {
259 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
260 w.Header().Set("Content-Type", "application/json")
261 io.WriteString(w, `{}`)
262 }))
263 t.Cleanup(ts.Close)
264 out := captureStdout(t)
265 if err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop", nil, updatePolicyReq{"default_updates": ""}); err != nil {
266 t.Fatal(err)
267 }
268 none := updatewords.Text("settings.cli.none")
269 want := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", none, "auto", none, none)
270 if out.String() != want {
271 t.Fatalf("stdout = %q, want %q", out.String(), want)
272 }
273}
274