package console import ( "context" "encoding/json" "errors" "net/http" "os" "strings" "testing" "github.com/carlosframework/platform/internal/appconfig" ) func TestAPISwapSetsAndEchoesThePolicy(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-swap") u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap" code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"hold","swap_concurrency":2}`) if code != http.StatusOK { t.Fatalf("set = %d %s", code, body) } var out struct { Swap string `json:"swap"` Concurrency int `json:"swap_concurrency"` } if err := json.Unmarshal([]byte(body), &out); err != nil || out.Swap != "hold" || out.Concurrency != 2 { t.Fatalf("echo = %s (%v), want hold/2", body, err) } got, err := appconfig.GetSwapPolicy(context.Background(), fx.rel, fx.sqid, "kass") if err != nil || got != (appconfig.SwapPolicy{Swap: "hold", Concurrency: 2}) { t.Fatalf("stored = %+v, %v", got, err) } if n := serial(t, fx); n != 1 { t.Fatalf("serial = %d, want 1: boxes act on the serial", n) } code, _, body = rawReq(t, http.MethodPut, u, token, `{"swap_concurrency":1}`) if code != http.StatusOK || json.Unmarshal([]byte(body), &out) != nil || out.Swap != "hold" || out.Concurrency != 1 { t.Fatalf("concurrency only = %d %s, want hold/1", code, body) } } func TestAPISwapRefusesBeforeWriting(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-swap-bad") u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap" for _, bad := range []string{`{}`, `{"swap":"fast"}`, `{"swap":""}`, `{"swap_concurrency":0}`, `{"swap_concurrency":-1}`, `{"swap_concurrency":1.5}`, `{"swap":"hold","swap_concurrency":0}`, // A misspelt key is refused by name, not dropped: without the strict // reader this body would read as "nothing to set" at best, and as a // half-applied request at worst. `{"swap_conurrency":3}`, `{"swap":"hold","swap_conurrency":3}`} { if code, _, body := rawReq(t, http.MethodPut, u, token, bad); code != http.StatusBadRequest { t.Errorf("%s = %d %s, want 400", bad, code, body) } } if _, _, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "kass")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a refused request wrote config.json: %v", err) } } func TestAPISwapNamesAMisspeltKey(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-swap-typo") u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap" code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"hold","swap_conurrency":3}`) if code != http.StatusBadRequest || !strings.Contains(body, "swap_conurrency") { t.Fatalf("typo = %d %s, want a 400 naming swap_conurrency", code, body) } } // Astra plan 6 R1 #9: the swap door's rate-limit answer is its own marked // draft (zdd.api.swap-limited), not the sibling doors' em-dash sentence. func TestAPISwapRateLimitIsAMarkedDraftWithNoEmDash(t *testing.T) { fx := newConsoleFixture(t) token := cliToken(t, fx, "paul@keymail.dev", "code-swap-limited") u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap" for i := 0; i < cliWriteBurst+3; i++ { code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"overlap"}`) if code != http.StatusTooManyRequests { continue } var out struct{ Error string } if err := json.Unmarshal([]byte(body), &out); err != nil || out.Error != "too many requests. wait a minute and try again" { t.Fatalf("429 = %s (%v)", body, err) } return } t.Fatalf("the swap door was never rate-limited in %d attempts", cliWriteBurst+3) } // Astra plan 6 R1 #9: the Deploys card's Save button is new copy. It is a // literal in the template, so the pinned copy record names the template for it // (zdd.settings.save), and this keeps the button inside the Deploys card. func TestDeploysCardSaveButtonIsInTheCard(t *testing.T) { raw, err := os.ReadFile("templates/app_settings.html") if err != nil { t.Fatal(err) } _, card, ok := strings.Cut(string(raw), "{{.SwapCopy.Heading}}") if !ok { t.Fatal("no Deploys card in app_settings.html") } card, _, _ = strings.Cut(card, "") if !strings.Contains(card, ``) { t.Fatalf("the Deploys card has no Save button:\n%s", card) } }