package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "net/http/httptest" "reflect" "strings" "sync" "testing" "github.com/carlosframework/platform/internal/updatewords" ) func parsed(t *testing.T, args ...string) (*flag.FlagSet, *string, *string, *string, *string) { t.Helper() fs := flag.NewFlagSet("t", flag.ContinueOnError) c := fs.String("instance-control", "", "") u := fs.String("default-updates", "", "") ch := fs.String("default-channel", "", "") m := fs.String("default-major", "", "") if err := fs.Parse(args); err != nil { t.Fatal(err) } return fs, c, u, ch, m } func TestUpdatePolicyRequestSendsOnlyWhatWasGiven(t *testing.T) { fs, c, u, ch, m := parsed(t, "--default-updates", "manual") req, err := updatePolicyRequest(fs, *c, *u, *ch, *m) if err != nil || !reflect.DeepEqual(req, updatePolicyReq{"default_updates": "manual"}) { t.Fatalf("req = %v, %v", req, err) } fs, c, u, ch, m = parsed(t, "--instance-control", "updates,pin", "--default-major", "") req, err = updatePolicyRequest(fs, *c, *u, *ch, *m) if err != nil || !reflect.DeepEqual(req, updatePolicyReq{"instance_control": []string{"updates", "pin"}, "default_major": nil}) { t.Fatalf("req = %v, %v", req, err) } fs, c, u, ch, m = parsed(t, "--default-major", "2") req, _ = updatePolicyRequest(fs, *c, *u, *ch, *m) if req["default_major"] != 2 { t.Fatalf("major = %v", req["default_major"]) } for _, bad := range []string{"two", "-1", "1.5"} { fs, c, u, ch, m = parsed(t, "--default-major", bad) _, err := updatePolicyRequest(fs, *c, *u, *ch, *m) if err == nil || err.Error() != updatewords.Text("settings.cli.bad-major-flag") { t.Fatalf("--default-major %q: err = %v", bad, err) } } fs, c, u, ch, m = parsed(t) if req, _ := updatePolicyRequest(fs, *c, *u, *ch, *m); req != nil { t.Fatalf("no flags, yet a request: %v", req) } } // An empty --instance-control clears: it goes out as JSON null, which the // console reads as "keep them all for the app". func TestUpdatePolicyRequestEmptyControlIsNull(t *testing.T) { fs, c, u, ch, m := parsed(t, "--instance-control", " , ", "--default-updates", "", "--default-channel", "beta") req, err := updatePolicyRequest(fs, *c, *u, *ch, *m) if err != nil { t.Fatal(err) } want := updatePolicyReq{"instance_control": []string(nil), "default_updates": "", "default_channel": "beta"} if !reflect.DeepEqual(req, want) { t.Fatalf("req = %#v", req) } } func TestAPISessionSetUpdatePolicyWireShape(t *testing.T) { var method, path, body string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) method, path, body = r.Method, r.URL.Path, string(b) w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{"instance_control":["updates","pin"],"default_updates":"manual","default_channel":"stable","default_major":2}`) })) defer ts.Close() got, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop", updatePolicyReq{"default_updates": "manual", "default_major": 2}) two := 2 want := apiUpdatePolicy{InstanceControl: []string{"updates", "pin"}, DefaultUpdates: "manual", DefaultChannel: "stable", DefaultMajor: &two} if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("setUpdatePolicy = %+v, %v", got, err) } if method != http.MethodPut || path != "/api/cli/apps/bab/shop/updates" || body != `{"default_major":2,"default_updates":"manual"}` { t.Fatalf("sent %s %s %s", method, path, body) } } func TestAPISessionSetUpdatePolicyNullsOnTheWire(t *testing.T) { var body string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) body = string(b) io.WriteString(w, `{"instance_control":[],"default_updates":"","default_channel":"stable","default_major":null}`) })) defer ts.Close() got, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop", updatePolicyReq{"instance_control": []string(nil), "default_major": nil}) if err != nil || got.DefaultMajor != nil || len(got.InstanceControl) != 0 { t.Fatalf("setUpdatePolicy = %+v, %v", got, err) } if body != `{"default_major":null,"instance_control":null}` { t.Fatalf("sent %s", body) } } func TestAPISessionSetUpdatePolicyErrorVerbatim(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusConflict) io.WriteString(w, `{"error":"the console says no, in its own words"}`) })) defer ts.Close() _, err := newTestSession(ts.URL).setUpdatePolicy(context.Background(), apiAcct{Seg: "bab"}, "shop", updatePolicyReq{"default_channel": ""}) if err == nil || err.Error() != "the console says no, in its own words" { t.Fatalf("err = %v", err) } } // doorLog is a console that records which doors were called, in order. type doorLog struct { mu sync.Mutex calls []string } func (d *doorLog) add(s string) { d.mu.Lock() defer d.mu.Unlock() d.calls = append(d.calls, s) } func (d *doorLog) got() []string { d.mu.Lock() defer d.mu.Unlock() return append([]string(nil), d.calls...) } func appsSetConsole(t *testing.T, d *doorLog) *apiSession { t.Helper() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d.add(r.Method + " " + r.URL.Path) w.Header().Set("Content-Type", "application/json") switch { case strings.HasSuffix(r.URL.Path, "/swap"): io.WriteString(w, `{"swap":"hold","swap_concurrency":2}`) case strings.HasSuffix(r.URL.Path, "/updates"): io.WriteString(w, `{"instance_control":["updates","pin"],"default_updates":"manual","default_channel":"stable","default_major":3}`) default: w.WriteHeader(http.StatusNotFound) } })) t.Cleanup(ts.Close) return newTestSession(ts.URL) } func TestAppsSetRunPolicyFlagsOnlyCallOnlyTheUpdatesDoor(t *testing.T) { out := captureStdout(t) var d doorLog policy := updatePolicyReq{"default_updates": "manual"} if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", nil, policy); err != nil { t.Fatal(err) } if want := []string{"PUT /api/cli/apps/bab/shop/updates"}; !reflect.DeepEqual(d.got(), want) { t.Fatalf("doors = %v", d.got()) } want := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", "updates, pin", "manual", "stable", "3") if out.String() != want { t.Fatalf("stdout = %q, want %q", out.String(), want) } } func TestAppsSetRunSwapFlagsOnlyCallOnlyTheSwapDoor(t *testing.T) { out := captureStdout(t) var d doorLog hold := "hold" if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", &swapPolicyReq{Swap: &hold}, nil); err != nil { t.Fatal(err) } if want := []string{"PUT /api/cli/apps/bab/shop/swap"}; !reflect.DeepEqual(d.got(), want) { t.Fatalf("doors = %v", d.got()) } if want := fmt.Sprintf(appsSetDone+"\n", "shop", "hold", 2); !strings.HasPrefix(out.String(), want) || strings.Contains(out.String(), "default updates") { t.Fatalf("stdout = %q", out.String()) } } func TestAppsSetRunBothCallSwapThenUpdates(t *testing.T) { out := captureStdout(t) var d doorLog hold := "hold" if err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", &swapPolicyReq{Swap: &hold}, updatePolicyReq{"default_major": 3}); err != nil { t.Fatal(err) } if want := []string{"PUT /api/cli/apps/bab/shop/swap", "PUT /api/cli/apps/bab/shop/updates"}; !reflect.DeepEqual(d.got(), want) { t.Fatalf("doors = %v", d.got()) } swapLine := fmt.Sprintf(appsSetDone+"\n", "shop", "hold", 2) policyLine := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", "updates, pin", "manual", "stable", "3") if !strings.HasPrefix(out.String(), swapLine) || !strings.HasSuffix(out.String(), policyLine) { t.Fatalf("stdout = %q", out.String()) } } // A refused swap stops the run: the policy door is not tried after it. func TestAppsSetRunSwapFailureSkipsPolicy(t *testing.T) { var d doorLog ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d.add(r.Method + " " + r.URL.Path) w.WriteHeader(http.StatusConflict) io.WriteString(w, `{"error":"no"}`) })) defer ts.Close() hold := "hold" err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop", &swapPolicyReq{Swap: &hold}, updatePolicyReq{"default_major": 3}) if err == nil || err.Error() != fmt.Sprintf(appsSetError, errors.New("no")) { t.Fatalf("err = %v", err) } if len(d.got()) != 1 { t.Fatalf("doors = %v", d.got()) } } func TestAppsSetRunPolicyFailureCarriesTheConsoleWords(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusConflict) io.WriteString(w, `{"error":"the console says no"}`) })) defer ts.Close() err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop", nil, updatePolicyReq{"default_channel": ""}) if err == nil || err.Error() != "apps set: the console says no" { t.Fatalf("err = %v", err) } } func TestAppsSetRunNothingToSet(t *testing.T) { var d doorLog err := appsSetRun(context.Background(), appsSetConsole(t, &d), apiAcct{Seg: "bab"}, "shop", nil, nil) if err == nil || err.Error() != appsSetNothing { t.Fatalf("err = %v", err) } if len(d.got()) != 0 { t.Fatalf("a request went out: %v", d.got()) } } // Astra r1 triage: an app with no default updates has instances update // automatically, so the line says the setting's own value word, auto, not // "none". Unset lists, channels and majors still read none. func TestAppsSetPolicyDoneSaysAutoForUnsetDefaultUpdates(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") io.WriteString(w, `{}`) })) t.Cleanup(ts.Close) out := captureStdout(t) if err := appsSetRun(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "bab"}, "shop", nil, updatePolicyReq{"default_updates": ""}); err != nil { t.Fatal(err) } none := updatewords.Text("settings.cli.none") want := fmt.Sprintf(updatewords.Text("settings.cli.apps-set-policy-done")+"\n", "shop", none, "auto", none, none) if out.String() != want { t.Fatalf("stdout = %q, want %q", out.String(), want) } }