carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// internal/appconfig/generation_test.go
2package appconfig
3
4import (
5 "context"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "os"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// genEtag reads the app's generation etag, "" for absent — the exact
17// observation the agent's fast loop makes, so a test that asserts on it is
18// asserting on the signal itself rather than on this package's internals.
19func genEtag(t *testing.T, s release.Store, acct, app string) string {
20 t.Helper()
21 _, etag, err := s.Get(context.Background(), GenKey(acct, app))
22 if err != nil {
23 if errors.Is(err, os.ErrNotExist) {
24 return ""
25 }
26 t.Fatalf("read generation: %v", err)
27 }
28 return etag
29}
30
31func TestTouchGenerationCreatesAndMoves(t *testing.T) {
32 s := release.NewDirStore(t.TempDir())
33 ctx := context.Background()
34 if err := TouchGeneration(ctx, s, "bab", "keymail"); err != nil {
35 t.Fatalf("first TouchGeneration: %v", err)
36 }
37 raw, first, err := s.Get(ctx, GenKey("bab", "keymail"))
38 if err != nil {
39 t.Fatalf("Get after first touch: %v", err)
40 }
41 var g Generation
42 if err := json.Unmarshal(raw, &g); err != nil {
43 t.Fatalf("decode generation: %v", err)
44 }
45 if g.Schema != 1 {
46 t.Fatalf("Schema = %d, want 1", g.Schema)
47 }
48 if _, err := time.Parse(time.RFC3339, g.TouchedAt); err != nil {
49 t.Fatalf("TouchedAt %q is not RFC3339: %v", g.TouchedAt, err)
50 }
51 if nonce, err := hex.DecodeString(g.Nonce); err != nil || len(nonce) == 0 {
52 t.Fatalf("Nonce = %q, want nonempty hex (decode err %v)", g.Nonce, err)
53 }
54 // The etag is the whole point: a second touch must be a DIFFERENT
55 // object, or the loop watching it sees nothing. DirStore's etag is
56 // content-derived, so this also pins that each touch's bytes differ.
57 if err := TouchGeneration(ctx, s, "bab", "keymail"); err != nil {
58 t.Fatalf("second TouchGeneration: %v", err)
59 }
60 if second := genEtag(t, s, "bab", "keymail"); second == first {
61 t.Fatalf("etag %q unchanged after a second touch — the change signal never fires", second)
62 }
63}
64
65// racingPutStore lands a different writer's value at key just before
66// forwarding the caller's Put, which then fails CAS against the etag the
67// caller read — the exact race two operators (or an operator and the
68// console door) running config writes at the same moment produce.
69type racingPutStore struct {
70 release.Store
71 key string
72 done bool
73}
74
75func (r *racingPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
76 if key == r.key && !r.done {
77 r.done = true
78 other := []byte(`{"schema":1,"touched_at":"2026-01-01T00:00:00Z","nonce":"00112233445566778899aabbccddeeff"}`)
79 if _, err := r.Store.Put(ctx, key, other, ifMatch); err != nil {
80 return "", err
81 }
82 }
83 return r.Store.Put(ctx, key, body, ifMatch)
84}
85
86func TestTouchGenerationCASConflictIsSuccess(t *testing.T) {
87 inner := release.NewDirStore(t.TempDir())
88 ctx := context.Background()
89 if err := TouchGeneration(ctx, inner, "bab", "keymail"); err != nil {
90 t.Fatalf("seed TouchGeneration: %v", err)
91 }
92 racing := &racingPutStore{Store: inner, key: GenKey("bab", "keymail")}
93 if err := TouchGeneration(ctx, racing, "bab", "keymail"); err != nil {
94 t.Fatalf("TouchGeneration on a lost CAS race: %v, want nil (the signal already fired)", err)
95 }
96 if !racing.done {
97 t.Fatal("the racing writer never ran — this test proved nothing")
98 }
99}
100
101// TestTouchGenerationSurfacesRealPutErrors is the other half of the CAS
102// carve-out: only ErrCASConflict is success. Any other store failure means
103// nobody touched the generation, so the caller must hear about it.
104func TestTouchGenerationSurfacesRealPutErrors(t *testing.T) {
105 s := &failingPutStore{Store: release.NewDirStore(t.TempDir()), err: errors.New("bucket is on fire")}
106 if err := TouchGeneration(context.Background(), s, "bab", "keymail"); err == nil {
107 t.Fatal("TouchGeneration swallowed a non-CAS Put error, want it surfaced")
108 }
109}
110
111// failingPutStore fails every Put with a fixed non-CAS error.
112type failingPutStore struct {
113 release.Store
114 err error
115}
116
117func (f *failingPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
118 return "", f.err
119}
120
121func TestTouchGenerationRefusesInvalidNames(t *testing.T) {
122 s := release.NewDirStore(t.TempDir())
123 ctx := context.Background()
124 if err := TouchGeneration(ctx, s, "../etc", "keymail"); err == nil {
125 t.Fatal("TouchGeneration accepted an invalid account")
126 }
127 if err := TouchGeneration(ctx, s, "bab", "../etc"); err == nil {
128 t.Fatal("TouchGeneration accepted an invalid app")
129 }
130}
131
132// TestWritesBumpGeneration is the contract the console door and the CLI both
133// depend on: EVERY successful config write leaves a moved etag behind, and a
134// REFUSED write leaves none — a failed write has nothing to converge to, and
135// a spurious touch would cost the fleet a SyncExec pass for nothing.
136func TestWritesBumpGeneration(t *testing.T) {
137 s := release.NewDirStore(t.TempDir())
138 ctx := context.Background()
139 envelope := testEnvelope(t)
140
141 writes := []struct {
142 name string
143 run func() error
144 }{
145 {"SetEnv", func() error {
146 return SetEnv(ctx, s, "bab", "keymail", map[string]string{"LOG_LEVEL": "debug"})
147 }},
148 {"SetSecret", func() error {
149 return SetSecret(ctx, s, "bab", "keymail", map[string]string{"SES_SECRET": envelope})
150 }},
151 {"Unset", func() error {
152 _, err := Unset(ctx, s, "bab", "keymail", []string{"LOG_LEVEL"})
153 return err
154 }},
155 {"SaveEnvironment", func() error {
156 return SaveEnvironment(ctx, s, "bab", "keymail", "staging", map[string]Var{"PORT": {Kind: "env", Value: "8080"}}, nil)
157 }},
158 {"UnsetEnvironment", func() error {
159 _, err := UnsetEnvironment(ctx, s, "bab", "keymail", "staging", []string{"PORT"})
160 return err
161 }},
162 {"SetExecDelivery", func() error {
163 return SetExecDelivery(ctx, s, "bab", "keymail", ExecDeliveryFile)
164 }},
165 {"Apply", func() error {
166 _, err := Apply(ctx, s, "bab", "keymail", map[string]Var{"A": {Kind: "env", Value: "1"}}, []string{"SES_SECRET"})
167 return err
168 }},
169 {"ApplyEnvironment", func() error {
170 _, err := ApplyEnvironment(ctx, s, "bab", "keymail", "staging", map[string]Var{"B": {Kind: "env", Value: "2"}}, nil)
171 return err
172 }},
173 }
174 for _, w := range writes {
175 before := genEtag(t, s, "bab", "keymail")
176 if err := w.run(); err != nil {
177 t.Fatalf("%s: %v", w.name, err)
178 }
179 if after := genEtag(t, s, "bab", "keymail"); after == before {
180 t.Fatalf("%s left the generation etag at %q — no convergence signal", w.name, before)
181 }
182 }
183
184 // A REFUSED write: unsetting from an app that has no bundle at all is
185 // applyAt's create-refusal. Nothing landed, so nothing may be signalled.
186 before := genEtag(t, s, "bab", "unconfigured")
187 if _, err := Unset(ctx, s, "bab", "unconfigured", []string{"NOPE"}); err == nil {
188 t.Fatal("Unset on an app with no bundle succeeded, want a refusal")
189 }
190 if after := genEtag(t, s, "bab", "unconfigured"); after != before {
191 t.Fatalf("a refused write bumped the generation (%q -> %q)", before, after)
192 }
193}
194
195// testEnvelope seals a throwaway value so secret-shaped writes in these
196// tests carry a real envelope rather than a string that only looks like one.
197func testEnvelope(t *testing.T) string {
198 t.Helper()
199 _, pub, err := GenerateKey()
200 if err != nil {
201 t.Fatalf("GenerateKey: %v", err)
202 }
203 pubKey, err := ParsePublicKey(pub, "test")
204 if err != nil {
205 t.Fatalf("ParsePublicKey: %v", err)
206 }
207 envelope, err := Seal(pubKey, Binding{Account: "bab", App: "keymail"}, []byte("shh"))
208 if err != nil {
209 t.Fatalf("Seal: %v", err)
210 }
211 return envelope
212}
213
214// genFailingStore fails Put only for one app's GENERATION key — the shape
215// TestTouchFailureNeverFailsTheWrite needs: the manifest write must succeed
216// while the follow-up touch cannot.
217type genFailingStore struct {
218 release.Store
219 genKey string
220 err error
221}
222
223func (g *genFailingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
224 if key == g.genKey {
225 return "", g.err
226 }
227 return g.Store.Put(ctx, key, body, ifMatch)
228}
229
230// TestTouchFailureNeverFailsTheWrite pins the spec §1 property touchLogged
231// exists for: when the manifest write LANDS and only the generation touch
232// fails, the caller sees success — returning an error would send an operator
233// re-running a write that is already live, and the real cost of the failed
234// touch is only that convergence falls back to the hourly pass.
235func TestTouchFailureNeverFailsTheWrite(t *testing.T) {
236 ctx := context.Background()
237 s := &genFailingStore{
238 Store: release.NewDirStore(t.TempDir()),
239 genKey: GenKey("bab", "keymail"),
240 err: errors.New("bucket is on fire"),
241 }
242 if err := SetEnv(ctx, s, "bab", "keymail", map[string]string{"PORT": "8080"}); err != nil {
243 t.Fatalf("SetEnv with a failing generation touch = %v, want nil (the manifest write landed)", err)
244 }
245 m, err := Get(ctx, s, "bab", "keymail")
246 if err != nil || m.Vars["PORT"].Value != "8080" {
247 t.Fatalf("manifest did not land: %v %v", m, err)
248 }
249 if _, _, err := s.Get(ctx, GenKey("bab", "keymail")); !errors.Is(err, os.ErrNotExist) {
250 t.Fatal("generation object exists despite the failing Put — the fixture proves nothing")
251 }
252}
253