| 1 | package main |
| 2 | |
| 3 | import ( |
| 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 | |
| 19 | func 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 | |
| 32 | func 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". |
| 63 | func 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 | |
| 75 | func 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 | |
| 96 | func 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 | |
| 114 | func 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. |
| 128 | type doorLog struct { |
| 129 | mu sync.Mutex |
| 130 | calls []string |
| 131 | } |
| 132 | |
| 133 | func (d *doorLog) add(s string) { |
| 134 | d.mu.Lock() |
| 135 | defer d.mu.Unlock() |
| 136 | d.calls = append(d.calls, s) |
| 137 | } |
| 138 | |
| 139 | func (d *doorLog) got() []string { |
| 140 | d.mu.Lock() |
| 141 | defer d.mu.Unlock() |
| 142 | return append([]string(nil), d.calls...) |
| 143 | } |
| 144 | |
| 145 | func 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 | |
| 163 | func 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 | |
| 179 | func 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 | |
| 194 | func 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. |
| 213 | func 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 | |
| 232 | func 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 | |
| 244 | func 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. |
| 258 | func 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 | |