| new file mode 100644 |
| index 000000000..5aba6c91d |
| --- /dev/null |
| +++ b/apps_updates_test.go |
| @@ -0,0 +1,253 @@ |
| +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()) |
| + } |
| +} |