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 "net/http"
7 "slices"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/provision"
12)
13
14// The create door stamps a named config bundle onto the record (2026-09-04).
15//
16// Why it matters, and why it was worth changing a door that deliberately
17// omitted the field: config in CARLOS is per-app, so a second instance
18// standing alongside a live one inherits the default bundle — including
19// whatever single origin the app's framework compares the request's Origin
20// header against. Every POST to the new host then fails closed while every
21// GET looks perfectly healthy. `carlos canary` mints exactly such a route.
22func TestAPIInstanceCreateStampsEnvironment(t *testing.T) {
23 fx, token := instanceFixture(t, "code-inst-env-create")
24 enableInstances(t, fx, "titogo", titoManifest())
25 u := instancesURL(fx, "bab", "titogo")
26
27 status, _, out := rawReq(t, http.MethodPost, u, token,
28 `{"host":"canary.go.tito.io","region":"ie","environment":"jam-canary"}`)
29 if status != http.StatusCreated {
30 t.Fatalf("create with environment = %d %s, want 201", status, out)
31 }
32 var echoed provision.Record
33 if err := json.Unmarshal([]byte(out), &echoed); err != nil {
34 t.Fatalf("create body %s: %v", out, err)
35 }
36 if echoed.Environment != "jam-canary" {
37 t.Fatalf("echoed environment = %q, want jam-canary", echoed.Environment)
38 }
39 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "canary.go.tito.io")
40 if err != nil {
41 t.Fatal(err)
42 }
43 if stored.Environment != "jam-canary" {
44 t.Fatalf("stored environment = %q, want jam-canary — the record is what convergeOwned reads", stored.Environment)
45 }
46}
47
48// An omitted environment still means the app's default bundle, byte for byte
49// what this door did before the field existed. Every record in production was
50// written without it.
51func TestAPIInstanceCreateWithoutEnvironmentIsUnchanged(t *testing.T) {
52 fx, token := instanceFixture(t, "code-inst-env-absent")
53 enableInstances(t, fx, "titogo", titoManifest())
54
55 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
56 `{"host":"plain.go.tito.io","region":"ie"}`)
57 if status != http.StatusCreated {
58 t.Fatalf("create = %d %s, want 201", status, out)
59 }
60 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "plain.go.tito.io")
61 if err != nil {
62 t.Fatal(err)
63 }
64 if stored.Environment != "" {
65 t.Fatalf("stored environment = %q, want empty for an omitted field", stored.Environment)
66 }
67}
68
69// A malformed name is ValidateShape's own 400, at the same door the box's
70// reconcile pass re-checks against — neither side trusts the other about
71// what is in the bucket.
72func TestAPIInstanceCreateRefusesBadEnvironment(t *testing.T) {
73 fx, token := instanceFixture(t, "code-inst-env-bad")
74 enableInstances(t, fx, "titogo", titoManifest())
75
76 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
77 `{"host":"bad.go.tito.io","region":"ie","environment":"Not A Bundle"}`)
78 if status != http.StatusBadRequest {
79 t.Fatalf("create with a malformed environment = %d %s, want 400", status, out)
80 }
81 if !strings.Contains(out, "environment") {
82 t.Errorf("refusal %s does not name the offending field", out)
83 }
84}
85
86// The PATCH door's fourth field: bind an existing route to a bundle, and
87// clear it again. This is the door `carlos instances set-environment` and
88// `carlos canary --environment` both reach, and the one that closes the gap
89// convergeOwned opened by reverting `carlos route -environment`.
90func TestAPIInstanceUpdateEnvironmentRoundTrip(t *testing.T) {
91 fx, token := instanceFixture(t, "code-inst-env-round")
92 letStateUpstreams(t, fx, "alice@keymail.dev")
93 enableInstances(t, fx, "titogo", titoManifest())
94 u := instancesURL(fx, "bab", "titogo")
95 if status, _, out := rawReq(t, http.MethodPost, u, token,
96 `{"host":"env.go.tito.io","region":"ie","addrs":["127.0.0.1:7001"]}`); status != http.StatusCreated {
97 t.Fatalf("create = %d %s", status, out)
98 }
99 afterCreate := serial(t, fx)
100
101 pu := updateURL(fx, "bab", "titogo", "env.go.tito.io")
102 status, _, out := rawReq(t, http.MethodPatch, pu, token, `{"environment":"jam-canary"}`)
103 if status != http.StatusOK {
104 t.Fatalf("patch environment = %d %s, want 200", status, out)
105 }
106 var echoed provision.Record
107 if err := json.Unmarshal([]byte(out), &echoed); err != nil {
108 t.Fatalf("patch body %s: %v", out, err)
109 }
110 if echoed.Environment != "jam-canary" {
111 t.Fatalf("echoed environment = %q, want jam-canary", echoed.Environment)
112 }
113 // An environment-only patch must leave the upstream set exactly alone —
114 // the same independence the addrs/probe/steering fields already have.
115 if !slices.Equal(echoed.Addrs, []string{"127.0.0.1:7001"}) {
116 t.Fatalf("environment-only patch moved addrs: %v", echoed.Addrs)
117 }
118 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "env.go.tito.io")
119 if err != nil {
120 t.Fatal(err)
121 }
122 if stored.Environment != "jam-canary" {
123 t.Fatalf("stored environment = %q, want jam-canary", stored.Environment)
124 }
125 if got := serial(t, fx); got != afterCreate+1 {
126 t.Fatalf("serial after patch = %d, want exactly one bump past %d", got, afterCreate)
127 }
128 afterBind := serial(t, fx)
129
130 // "" is a real request — return this route to the app's default bundle —
131 // and not a "leave it alone". That is why the client sends the field
132 // without omitempty.
133 status, _, out = rawReq(t, http.MethodPatch, pu, token, `{"environment":""}`)
134 if status != http.StatusOK {
135 t.Fatalf("patch environment off = %d %s, want 200", status, out)
136 }
137 stored, err = provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "env.go.tito.io")
138 if err != nil {
139 t.Fatal(err)
140 }
141 if stored.Environment != "" {
142 t.Fatalf("stored environment after clearing = %q, want empty", stored.Environment)
143 }
144 if !slices.Equal(stored.Addrs, []string{"127.0.0.1:7001"}) {
145 t.Fatalf("environment-only patch moved addrs: %v", stored.Addrs)
146 }
147 if got := serial(t, fx); got != afterBind+1 {
148 t.Fatalf("serial after clearing = %d, want exactly one bump past %d", got, afterBind)
149 }
150}
151
152// The fourth field joins the "nothing to update" rail rather than making it
153// optional — a body naming none of the four is still a 400 that costs no
154// bucket round trip and no fleet-wide serial bump.
155func TestAPIInstanceUpdateEmptyBodyNamesEnvironment(t *testing.T) {
156 fx, token := instanceFixture(t, "code-inst-env-empty")
157 enableInstances(t, fx, "titogo", titoManifest())
158 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
159 `{"host":"none.go.tito.io","region":"ie"}`); status != http.StatusCreated {
160 t.Fatalf("create = %d %s", status, out)
161 }
162 before := serial(t, fx)
163 status, _, out := rawReq(t, http.MethodPatch,
164 updateURL(fx, "bab", "titogo", "none.go.tito.io"), token, `{}`)
165 if status != http.StatusBadRequest {
166 t.Fatalf("patch with no fields = %d %s, want 400", status, out)
167 }
168 if !strings.Contains(out, "environment") {
169 t.Errorf("empty-body refusal = %s, want it to name environment among the patchable fields", out)
170 }
171 if got := serial(t, fx); got != before {
172 t.Errorf("serial after a refused empty patch = %d, want it unmoved from %d", got, before)
173 }
174}
175
176// A malformed name is refused on the PATCH door too, and the stored record is
177// left exactly as it was — a refusal must not half-apply.
178func TestAPIInstanceUpdateRefusesBadEnvironment(t *testing.T) {
179 fx, token := instanceFixture(t, "code-inst-env-patch-bad")
180 enableInstances(t, fx, "titogo", titoManifest())
181 if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token,
182 `{"host":"keep.go.tito.io","region":"ie","environment":"good-one"}`); status != http.StatusCreated {
183 t.Fatalf("create = %d %s", status, out)
184 }
185 status, _, out := rawReq(t, http.MethodPatch,
186 updateURL(fx, "bab", "titogo", "keep.go.tito.io"), token, `{"environment":"Not A Bundle"}`)
187 if status != http.StatusBadRequest {
188 t.Fatalf("patch with a malformed environment = %d %s, want 400", status, out)
189 }
190 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "keep.go.tito.io")
191 if err != nil {
192 t.Fatal(err)
193 }
194 if stored.Environment != "good-one" {
195 t.Fatalf("stored environment = %q after a refused patch, want the original good-one", stored.Environment)
196 }
197}
198