carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "net/http"
8 "os"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/appconfig"
13)
14
15func TestAPISwapSetsAndEchoesThePolicy(t *testing.T) {
16 fx := newConsoleFixture(t)
17 token := cliToken(t, fx, "paul@keymail.dev", "code-swap")
18 u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap"
19
20 code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"hold","swap_concurrency":2}`)
21 if code != http.StatusOK {
22 t.Fatalf("set = %d %s", code, body)
23 }
24 var out struct {
25 Swap string `json:"swap"`
26 Concurrency int `json:"swap_concurrency"`
27 }
28 if err := json.Unmarshal([]byte(body), &out); err != nil || out.Swap != "hold" || out.Concurrency != 2 {
29 t.Fatalf("echo = %s (%v), want hold/2", body, err)
30 }
31 got, err := appconfig.GetSwapPolicy(context.Background(), fx.rel, fx.sqid, "kass")
32 if err != nil || got != (appconfig.SwapPolicy{Swap: "hold", Concurrency: 2}) {
33 t.Fatalf("stored = %+v, %v", got, err)
34 }
35 if n := serial(t, fx); n != 1 {
36 t.Fatalf("serial = %d, want 1: boxes act on the serial", n)
37 }
38 code, _, body = rawReq(t, http.MethodPut, u, token, `{"swap_concurrency":1}`)
39 if code != http.StatusOK || json.Unmarshal([]byte(body), &out) != nil || out.Swap != "hold" || out.Concurrency != 1 {
40 t.Fatalf("concurrency only = %d %s, want hold/1", code, body)
41 }
42}
43
44func TestAPISwapRefusesBeforeWriting(t *testing.T) {
45 fx := newConsoleFixture(t)
46 token := cliToken(t, fx, "paul@keymail.dev", "code-swap-bad")
47 u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap"
48 for _, bad := range []string{`{}`, `{"swap":"fast"}`, `{"swap":""}`, `{"swap_concurrency":0}`,
49 `{"swap_concurrency":-1}`, `{"swap_concurrency":1.5}`, `{"swap":"hold","swap_concurrency":0}`,
50 // A misspelt key is refused by name, not dropped: without the strict
51 // reader this body would read as "nothing to set" at best, and as a
52 // half-applied request at worst.
53 `{"swap_conurrency":3}`, `{"swap":"hold","swap_conurrency":3}`} {
54 if code, _, body := rawReq(t, http.MethodPut, u, token, bad); code != http.StatusBadRequest {
55 t.Errorf("%s = %d %s, want 400", bad, code, body)
56 }
57 }
58 if _, _, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "kass")); !errors.Is(err, os.ErrNotExist) {
59 t.Fatalf("a refused request wrote config.json: %v", err)
60 }
61}
62
63func TestAPISwapNamesAMisspeltKey(t *testing.T) {
64 fx := newConsoleFixture(t)
65 token := cliToken(t, fx, "paul@keymail.dev", "code-swap-typo")
66 u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap"
67 code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"hold","swap_conurrency":3}`)
68 if code != http.StatusBadRequest || !strings.Contains(body, "swap_conurrency") {
69 t.Fatalf("typo = %d %s, want a 400 naming swap_conurrency", code, body)
70 }
71}
72
73// Astra plan 6 R1 #9: the swap door's rate-limit answer is its own marked
74// draft (zdd.api.swap-limited), not the sibling doors' em-dash sentence.
75func TestAPISwapRateLimitIsAMarkedDraftWithNoEmDash(t *testing.T) {
76 fx := newConsoleFixture(t)
77 token := cliToken(t, fx, "paul@keymail.dev", "code-swap-limited")
78 u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap"
79 for i := 0; i < cliWriteBurst+3; i++ {
80 code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"overlap"}`)
81 if code != http.StatusTooManyRequests {
82 continue
83 }
84 var out struct{ Error string }
85 if err := json.Unmarshal([]byte(body), &out); err != nil || out.Error != "too many requests. wait a minute and try again" {
86 t.Fatalf("429 = %s (%v)", body, err)
87 }
88 return
89 }
90 t.Fatalf("the swap door was never rate-limited in %d attempts", cliWriteBurst+3)
91}
92
93// Astra plan 6 R1 #9: the Deploys card's Save button is new copy. It is a
94// literal in the template, so the pinned copy record names the template for it
95// (zdd.settings.save), and this keeps the button inside the Deploys card.
96func TestDeploysCardSaveButtonIsInTheCard(t *testing.T) {
97 raw, err := os.ReadFile("templates/app_settings.html")
98 if err != nil {
99 t.Fatal(err)
100 }
101 _, card, ok := strings.Cut(string(raw), "{{.SwapCopy.Heading}}")
102 if !ok {
103 t.Fatal("no Deploys card in app_settings.html")
104 }
105 card, _, _ = strings.Cut(card, "</form>")
106 if !strings.Contains(card, `<button type="submit" class="btn-sm">Save</button>`) {
107 t.Fatalf("the Deploys card has no Save button:\n%s", card)
108 }
109}
110