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
3// The config door's restart notice (discussion #16): a write that lands on
4// a process carlos does not own comes back naming it, so the CLI can say so
5// instead of reporting a bare success. And the empty-secret refusal from the
6// same report, which this door can enforce without a key because AES-GCM is
7// length-preserving.
8
9import (
10 "context"
11 "net/http"
12 "strings"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/appconfig"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// restartOf pulls the `restart` object out of a config PUT's response.
20// found==false means the door said nothing about it at all, which is a
21// distinct answer from an empty one and is asserted as such below.
22func restartOf(t *testing.T, out map[string]any) (map[string]any, bool) {
23 t.Helper()
24 r, ok := out["restart"].(map[string]any)
25 return r, ok
26}
27
28// restartHosts lists the hostnames in a restart object, in order.
29func restartHosts(t *testing.T, r map[string]any) []string {
30 t.Helper()
31 raw, _ := r["hosts"].([]any)
32 var out []string
33 for _, h := range raw {
34 m, _ := h.(map[string]any)
35 out = append(out, str(t, m, "host"))
36 }
37 return out
38}
39
40// optIn turns exec_delivery on for an app's default bundle — the opt-in that
41// makes a TCP-addr route eligible for a delivered file in the first place.
42func optIn(t *testing.T, fx *consoleFixture, app string) {
43 t.Helper()
44 if err := appconfig.SetExecDelivery(context.Background(), fx.rel, release.OpsAccount, app, appconfig.ExecDeliveryFile); err != nil {
45 t.Fatalf("SetExecDelivery: %v", err)
46 }
47}
48
49// TestAPIConfigPutNamesRoutesAwaitingRestart is the door's half of the
50// report. routesFixture's connect.go.tito.io is exactly the shape that was
51// filed: a hand-added TCP-addr relay bound to the "relay" environment, with
52// no instance record anywhere and nothing that will ever bounce it.
53func TestAPIConfigPutNamesRoutesAwaitingRestart(t *testing.T) {
54 fx, token := routesFixture(t, "code-restart-named")
55 optIn(t, fx, "titogo")
56
57 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
58 "environment": "relay",
59 "set": map[string]any{"LOG_LEVEL": map[string]any{"kind": "env", "value": "debug"}},
60 }, token)
61 if status != http.StatusOK {
62 t.Fatalf("PUT = %d %v", status, out)
63 }
64 r, ok := restartOf(t, out)
65 if !ok {
66 t.Fatalf("response carries no restart object: %v", out)
67 }
68 if r["known"] != true {
69 t.Fatalf("known = %v, want true — this console has a registry and read it", r["known"])
70 }
71 got := restartHosts(t, r)
72 if len(got) != 1 || got[0] != "connect.go.tito.io" {
73 t.Fatalf("hosts = %v, want just connect.go.tito.io", got)
74 }
75}
76
77// TestAPIConfigPutRestartUnknownWithoutRegistry is the field that carries
78// the whole point. A console that cannot see a registry has not looked, and
79// must not answer with an empty list: "nothing needs restarting" and "I
80// could not tell" rendering identically IS the bug being fixed.
81func TestAPIConfigPutRestartUnknownWithoutRegistry(t *testing.T) {
82 fx, token := routesFixture(t, "code-restart-noreg")
83 optIn(t, fx, "titogo")
84 fx.srv.cfg.Registry = ""
85
86 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
87 "environment": "relay",
88 "set": map[string]any{"LOG_LEVEL": map[string]any{"kind": "env", "value": "debug"}},
89 }, token)
90 if status != http.StatusOK {
91 t.Fatalf("PUT = %d %v", status, out)
92 }
93 r, ok := restartOf(t, out)
94 if !ok {
95 t.Fatalf("response carries no restart object: %v", out)
96 }
97 if r["known"] != false {
98 t.Fatalf("known = %v, want false for a console with no registry", r["known"])
99 }
100 if hosts := restartHosts(t, r); len(hosts) != 0 {
101 t.Fatalf("hosts = %v, want none alongside known:false", hosts)
102 }
103}
104
105// TestAPIConfigPutRestartSilentForUnoptedApp: the notice appears only where
106// a file is actually delivered. An app that never opted into exec delivery
107// has routes, and they get nothing, so there is nothing to restart over —
108// and this is a genuine "none", not a "could not tell".
109func TestAPIConfigPutRestartSilentForUnoptedApp(t *testing.T) {
110 fx, token := routesFixture(t, "code-restart-unopted")
111
112 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
113 "environment": "relay",
114 "set": map[string]any{"LOG_LEVEL": map[string]any{"kind": "env", "value": "debug"}},
115 }, token)
116 if status != http.StatusOK {
117 t.Fatalf("PUT = %d %v", status, out)
118 }
119 r, ok := restartOf(t, out)
120 if !ok {
121 t.Fatalf("response carries no restart object: %v", out)
122 }
123 if r["known"] != true {
124 t.Fatalf("known = %v, want true — the registry read fine, the answer is genuinely none", r["known"])
125 }
126 if hosts := restartHosts(t, r); len(hosts) != 0 {
127 t.Fatalf("hosts = %v, want none for an app that never opted in", hosts)
128 }
129}
130
131// TestAPIConfigPutRestartAbsentWhenNothingChanged: an unset that removed
132// nothing moved no file, so the door does not speculate about restarts.
133func TestAPIConfigPutRestartAbsentWhenNothingChanged(t *testing.T) {
134 fx, token := routesFixture(t, "code-restart-noop")
135 optIn(t, fx, "titogo")
136 // The bundle has to exist: unsetting from one that does not is a 409
137 // refusal, which is a different answer than "removed nothing".
138 if status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
139 "environment": "relay",
140 "set": map[string]any{"REAL_KEY": map[string]any{"kind": "env", "value": "v"}},
141 }, token); status != http.StatusOK {
142 t.Fatalf("seed PUT = %d %v", status, out)
143 }
144
145 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
146 "environment": "relay",
147 "unset": []string{"NEVER_SET"},
148 }, token)
149 if status != http.StatusOK {
150 t.Fatalf("PUT = %d %v", status, out)
151 }
152 if r, ok := restartOf(t, out); ok {
153 t.Fatalf("restart = %v, want the field absent when the write removed nothing", r)
154 }
155}
156
157// TestAPIConfigPutRefusesEmptySecret pins the door's half of the empty-value
158// refusal (the report's aside). The console holds no private key, so it
159// reads emptiness the only way it can: AES-GCM is length-preserving, so an
160// envelope at exactly the floor sealed nothing.
161//
162// The refusal must also leave the bucket untouched — a rejected write that
163// still stored something would be worse than the silence it replaces.
164func TestAPIConfigPutRefusesEmptySecret(t *testing.T) {
165 fx, token := configFixture(t, "code-config-empty-secret")
166 _, _, envelope := sealedFor(t, "")
167
168 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
169 "set": map[string]any{"CLIENT_SECRET": map[string]any{"kind": "secret", "envelope": envelope}},
170 }, token)
171 if status != http.StatusBadRequest {
172 t.Fatalf("PUT = %d %v, want 400", status, out)
173 }
174 msg := str(t, out, "error")
175 for _, want := range []string{"empty value", "secrets unset", "env set"} {
176 if !strings.Contains(msg, want) {
177 t.Fatalf("refusal %q does not mention %q", msg, want)
178 }
179 }
180 if strings.Contains(msg, envelope) {
181 t.Fatalf("refusal echoes the envelope back: %q", msg)
182 }
183 if _, found := storedManifest(t, fx, "titogo"); found {
184 t.Fatal("a refused empty secret still wrote a manifest")
185 }
186}
187
188// TestAPIConfigPutAcceptsOneByteSecret is the boundary from the other side:
189// the shortest real secret is one byte longer than an empty one, and the
190// length read must never eat it.
191func TestAPIConfigPutAcceptsOneByteSecret(t *testing.T) {
192 fx, token := configFixture(t, "code-config-one-byte")
193 _, _, envelope := sealedFor(t, "x")
194
195 status, out := putJSON(t, configURL(fx, "titogo"), map[string]any{
196 "set": map[string]any{"CLIENT_SECRET": map[string]any{"kind": "secret", "envelope": envelope}},
197 }, token)
198 if status != http.StatusOK {
199 t.Fatalf("PUT = %d %v, want 200 for a one-byte secret", status, out)
200 }
201 m, found := storedManifest(t, fx, "titogo")
202 if !found {
203 t.Fatal("a one-byte secret was accepted but stored nothing")
204 }
205 if m.Vars["CLIENT_SECRET"].Envelope != envelope {
206 t.Fatal("stored envelope is not the one that was sent")
207 }
208}
209