carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11 "time"
12)
13
14func TestPutIfNoneMatchConflict(t *testing.T) {
15 s := NewDirStore(t.TempDir())
16 ctx := context.Background()
17 if _, err := s.Put(ctx, "a/b.json", []byte("one"), ""); err != nil {
18 t.Fatalf("create: %v", err)
19 }
20 // A second create-only Put of an existing key must lose.
21 if _, err := s.Put(ctx, "a/b.json", []byte("two"), ""); !errors.Is(err, ErrCASConflict) {
22 t.Fatalf("create over existing: err = %v, want ErrCASConflict", err)
23 }
24 // The loser changed nothing.
25 body, _, err := s.Get(ctx, "a/b.json")
26 if err != nil || string(body) != "one" {
27 t.Fatalf("Get = %q, %v; want %q", body, err, "one")
28 }
29}
30
31func TestPutIfMatchStaleEtagFails(t *testing.T) {
32 s := NewDirStore(t.TempDir())
33 ctx := context.Background()
34 etag1, err := s.Put(ctx, "k", []byte("one"), "")
35 if err != nil {
36 t.Fatalf("create: %v", err)
37 }
38 etag2, err := s.Put(ctx, "k", []byte("two"), etag1)
39 if err != nil {
40 t.Fatalf("CAS replace: %v", err)
41 }
42 if etag2 == etag1 {
43 t.Fatalf("etag didn't change on replace")
44 }
45 // Replaying the old etag must lose.
46 if _, err := s.Put(ctx, "k", []byte("three"), etag1); !errors.Is(err, ErrCASConflict) {
47 t.Fatalf("stale etag: err = %v, want ErrCASConflict", err)
48 }
49 // And a CAS write against a key that doesn't exist must lose too.
50 if _, err := s.Put(ctx, "missing", []byte("x"), etag1); !errors.Is(err, ErrCASConflict) {
51 t.Fatalf("CAS on missing key: err = %v, want ErrCASConflict", err)
52 }
53 body, _, _ := s.Get(ctx, "k")
54 if string(body) != "two" {
55 t.Fatalf("Get = %q, want %q", body, "two")
56 }
57}
58
59func TestGetNotFoundWrapsErrNotExist(t *testing.T) {
60 s := NewDirStore(t.TempDir())
61 if _, _, err := s.Get(context.Background(), "no/such/key"); !errors.Is(err, os.ErrNotExist) {
62 t.Fatalf("err = %v, want os.ErrNotExist-wrapped", err)
63 }
64}
65
66func TestListPrefix(t *testing.T) {
67 s := NewDirStore(t.TempDir())
68 ctx := context.Background()
69 for _, k := range []string{"apps/x/releases/v1/manifest.json", "apps/x/releases/v1/bin", "apps/x/channels/stable.json", "apps/y/one"} {
70 if _, err := s.Put(ctx, k, []byte(k), ""); err != nil {
71 t.Fatalf("put %s: %v", k, err)
72 }
73 }
74 keys, err := s.List(ctx, "apps/x/releases/v1/")
75 if err != nil {
76 t.Fatalf("list: %v", err)
77 }
78 want := []string{"apps/x/releases/v1/bin", "apps/x/releases/v1/manifest.json"}
79 if len(keys) != len(want) || keys[0] != want[0] || keys[1] != want[1] {
80 t.Fatalf("List = %v, want %v", keys, want)
81 }
82 // An empty store (or unmatched prefix) lists empty, like an empty bucket.
83 keys, err = s.List(ctx, "nothing/")
84 if err != nil || len(keys) != 0 {
85 t.Fatalf("List(nothing/) = %v, %v; want empty", keys, err)
86 }
87}
88
89func TestDirStoreRejectsTraversal(t *testing.T) {
90 s := NewDirStore(t.TempDir())
91 ctx := context.Background()
92 for _, k := range []string{"../evil", "a/../../evil", "/abs", "a//b", ""} {
93 if _, err := s.Put(ctx, k, []byte("x"), ""); err == nil {
94 t.Fatalf("Put(%q) accepted a traversal key", k)
95 }
96 }
97}
98
99func TestDirStoreDelete(t *testing.T) {
100 s := NewDirStore(t.TempDir())
101 ctx := context.Background()
102 if _, err := s.Put(ctx, "metrics/boxes/b/raw/2026-08-01-10.json", []byte(`{}`), ""); err != nil {
103 t.Fatal(err)
104 }
105 if err := s.Delete(ctx, "metrics/boxes/b/raw/2026-08-01-10.json"); err != nil {
106 t.Fatalf("Delete: %v", err)
107 }
108 if _, _, err := s.Get(ctx, "metrics/boxes/b/raw/2026-08-01-10.json"); !errors.Is(err, os.ErrNotExist) {
109 t.Fatalf("Get after Delete = %v, want ErrNotExist", err)
110 }
111 // Idempotent: deleting a key that isn't there is fine.
112 if err := s.Delete(ctx, "metrics/boxes/b/raw/2026-08-01-10.json"); err != nil {
113 t.Fatalf("second Delete: %v", err)
114 }
115}
116
117// TestDirStoreListDirs: the delimiter-aware listing returns the immediate
118// child prefixes and nothing else — not the objects beneath them (which is
119// the whole point: cost bounded by the number of children, not by their
120// histories), and not the FILES sitting at the same level, which is what
121// keeps a ledger's ledgers.json out of its own list of ledgers.
122func TestDirStoreListDirs(t *testing.T) {
123 s := NewDirStore(t.TempDir())
124 ctx := context.Background()
125 for _, k := range []string{
126 "ledgers/bab/titogo/carbon/HEAD.json",
127 "ledgers/bab/titogo/carbon/entries/20260803T000000Z-aaaaaaaaaaaa.json",
128 "ledgers/bab/titogo/carbon/entries/20260803T000001Z-bbbbbbbbbbbb.json",
129 "ledgers/bab/titogo/carbon/blobs/deadbeef",
130 "ledgers/bab/titogo/prompts/HEAD.json",
131 "ledgers/bab/titogo/ledgers.json", // a FILE at this level
132 "ledgers/bab/otherapp/elsewhere/HEAD.json",
133 } {
134 if _, err := s.Put(ctx, k, []byte(`{}`), ""); err != nil {
135 t.Fatal(err)
136 }
137 }
138
139 got, err := s.ListDirs(ctx, "ledgers/bab/titogo/")
140 if err != nil {
141 t.Fatalf("ListDirs: %v", err)
142 }
143 want := []string{"ledgers/bab/titogo/carbon/", "ledgers/bab/titogo/prompts/"}
144 if len(got) != len(want) {
145 t.Fatalf("ListDirs = %v, want %v (no files, no grandchildren)", got, want)
146 }
147 for i := range want {
148 if got[i] != want[i] {
149 t.Fatalf("ListDirs = %v, want %v", got, want)
150 }
151 }
152 // A prefix without the trailing slash answers identically — the two
153 // implementations must not disagree about that.
154 bare, err := s.ListDirs(ctx, "ledgers/bab/titogo")
155 if err != nil || len(bare) != len(want) || bare[0] != want[0] {
156 t.Fatalf("ListDirs without a trailing slash = %v, %v; want %v", bare, err, want)
157 }
158 // One level up sees the apps, not the chains under them.
159 up, err := s.ListDirs(ctx, "ledgers/bab/")
160 if err != nil || len(up) != 2 || up[0] != "ledgers/bab/otherapp/" || up[1] != "ledgers/bab/titogo/" {
161 t.Fatalf("ListDirs one level up = %v, %v", up, err)
162 }
163 // A prefix with nothing under it lists empty, like an empty bucket
164 // prefix — not an error.
165 empty, err := s.ListDirs(ctx, "ledgers/nosuch/")
166 if err != nil || len(empty) != 0 {
167 t.Fatalf("ListDirs of a missing prefix = %v, %v; want empty and no error", empty, err)
168 }
169 // The root lists the top-level prefixes.
170 root, err := s.ListDirs(ctx, "")
171 if err != nil || len(root) != 1 || root[0] != "ledgers/" {
172 t.Fatalf("ListDirs of the root = %v, %v; want [ledgers/]", root, err)
173 }
174}
175
176// TestDirStoreListMod: the mod-time-aware listing answers exactly like List
177// — same keys, same order, same prefix filter — with each key's mtime
178// alongside, so a caller can rank by recency without reading a single body.
179func TestDirStoreListMod(t *testing.T) {
180 dir := t.TempDir()
181 s := NewDirStore(dir)
182 ctx := context.Background()
183 older := time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)
184 newer := time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
185 for k, mod := range map[string]time.Time{
186 "apps/bab/kass/releases/v1/manifest.json": older,
187 "apps/bab/kass/releases/v2/manifest.json": newer,
188 "apps/bab/kass/channels/stable.json": newer, // outside the prefix
189 } {
190 if _, err := s.Put(ctx, k, []byte(`{}`), ""); err != nil {
191 t.Fatal(err)
192 }
193 if err := os.Chtimes(filepath.Join(dir, filepath.FromSlash(k)), mod, mod); err != nil {
194 t.Fatal(err)
195 }
196 }
197 got, err := s.ListMod(ctx, "apps/bab/kass/releases/")
198 if err != nil {
199 t.Fatalf("ListMod: %v", err)
200 }
201 want := []KeyMod{
202 {Key: "apps/bab/kass/releases/v1/manifest.json", Mod: older},
203 {Key: "apps/bab/kass/releases/v2/manifest.json", Mod: newer},
204 }
205 if len(got) != len(want) {
206 t.Fatalf("ListMod = %+v, want %+v", got, want)
207 }
208 for i := range want {
209 if got[i].Key != want[i].Key || !got[i].Mod.Equal(want[i].Mod) {
210 t.Fatalf("ListMod[%d] = %+v, want %+v", i, got[i], want[i])
211 }
212 }
213 // A missing prefix lists empty like an empty bucket, not an error.
214 empty, err := s.ListMod(ctx, "apps/nosuch/")
215 if err != nil || len(empty) != 0 {
216 t.Fatalf("ListMod of a missing prefix = %v, %v; want empty and no error", empty, err)
217 }
218}
219
220func TestDirStoreListSizes(t *testing.T) {
221 s := NewDirStore(t.TempDir())
222 ctx := context.Background()
223 if _, err := s.Put(ctx, "apps/bab/hello/one.bin", []byte("12345"), ""); err != nil {
224 t.Fatal(err)
225 }
226 if _, err := s.Put(ctx, "apps/bab/hello/two.bin", []byte("1234567890"), ""); err != nil {
227 t.Fatal(err)
228 }
229 if _, err := s.Put(ctx, "apps/other/x.bin", []byte("1"), ""); err != nil {
230 t.Fatal(err)
231 }
232 sizes, err := s.ListSizes(ctx, "apps/bab/")
233 if err != nil {
234 t.Fatal(err)
235 }
236 total := int64(0)
237 for _, ks := range sizes {
238 total += ks.Size
239 }
240 if len(sizes) != 2 || total != 15 {
241 t.Fatalf("sizes = %+v", sizes)
242 }
243 var _ SizeLister = s // interface satisfaction
244}
245
246// TestS3StoreLive exercises the production store against a real bucket —
247// only when CARLOS_TEST_S3_BUCKET is set; the type otherwise just compiles.
248func TestS3StoreLive(t *testing.T) {
249 bucket := os.Getenv("CARLOS_TEST_S3_BUCKET")
250 if bucket == "" {
251 t.Skip("CARLOS_TEST_S3_BUCKET not set — skipping live S3Store test")
252 }
253 ctx := context.Background()
254 s, err := NewS3Store(ctx, bucket)
255 if err != nil {
256 t.Fatalf("NewS3Store: %v", err)
257 }
258 key := fmt.Sprintf("carlos-release-test/%d/ptr.json", time.Now().UnixNano())
259
260 if _, _, err := s.Get(ctx, key); !errors.Is(err, os.ErrNotExist) {
261 t.Fatalf("Get missing: err = %v, want os.ErrNotExist-wrapped", err)
262 }
263 etag, err := s.Put(ctx, key, []byte("one"), "")
264 if err != nil {
265 t.Fatalf("create: %v", err)
266 }
267 if _, err := s.Put(ctx, key, []byte("two"), ""); !errors.Is(err, ErrCASConflict) {
268 t.Fatalf("create over existing: err = %v, want ErrCASConflict", err)
269 }
270 body, gotEtag, err := s.Get(ctx, key)
271 if err != nil || string(body) != "one" || gotEtag != etag {
272 t.Fatalf("Get = %q, %q, %v; want %q, %q", body, gotEtag, err, "one", etag)
273 }
274 etag2, err := s.Put(ctx, key, []byte("two"), etag)
275 if err != nil {
276 t.Fatalf("CAS replace: %v", err)
277 }
278 if _, err := s.Put(ctx, key, []byte("three"), etag); !errors.Is(err, ErrCASConflict) {
279 t.Fatalf("stale etag: err = %v, want ErrCASConflict", err)
280 }
281 _ = etag2
282 keys, err := s.List(ctx, key)
283 if err != nil || len(keys) != 1 || keys[0] != key {
284 t.Fatalf("List = %v, %v; want [%s]", keys, err, key)
285 }
286
287 // ListDirs against the same live bucket: the object written above sits
288 // one level below its parent prefix, so the parent's parent must see the
289 // child PREFIX (CommonPrefixes) and not the object itself.
290 parent := key[:strings.LastIndex(key, "/")+1] // .../<nanos>/
291 grandparent := parent[:strings.LastIndex(parent[:len(parent)-1], "/")+1]
292 dirs, err := s.ListDirs(ctx, grandparent)
293 if err != nil {
294 t.Fatalf("ListDirs: %v", err)
295 }
296 found := false
297 for _, d := range dirs {
298 if d == parent {
299 found = true
300 }
301 if d == key {
302 t.Fatalf("ListDirs returned an OBJECT key %q, want only child prefixes", d)
303 }
304 }
305 if !found {
306 t.Fatalf("ListDirs(%q) = %v, want %q among them", grandparent, dirs, parent)
307 }
308 // And at the object's own level there are no child prefixes at all.
309 leaf, err := s.ListDirs(ctx, parent)
310 if err != nil || len(leaf) != 0 {
311 t.Fatalf("ListDirs(%q) = %v, %v; want empty (the only child is an object)", parent, leaf, err)
312 }
313}
314
315// ---- prefixStore -------------------------------------------------------------
316
317// TestNewPrefixStoreRebasesKeys pins NewPrefixStore's own contract in
318// isolation (customer-fleets plan, Task 5 — moved here from internal/host,
319// its first caller, on review: it's a property of Store itself, needed by
320// any package that must read a namespaced store, not just statusLoop):
321// Put/Get/List/ListDirs all operate in "unprefixed" key space from the
322// caller's side, while the underlying store actually holds everything under
323// the fixed base — and a key List hands back must round-trip through the
324// SAME wrapper's Get without doubling the prefix.
325func TestNewPrefixStoreRebasesKeys(t *testing.T) {
326 ctx := context.Background()
327 under := NewDirStore(t.TempDir())
328 wrapped := NewPrefixStore(under, "fleets/pi/")
329
330 if _, err := wrapped.Put(ctx, "status/boxes/pi-box.json", []byte(`{"ok":true}`), ""); err != nil {
331 t.Fatal(err)
332 }
333
334 // The underlying store must hold the REBASED key, not the caller's one.
335 if _, _, err := under.Get(ctx, "fleets/pi/status/boxes/pi-box.json"); err != nil {
336 t.Errorf("underlying store missing rebased key: %v", err)
337 }
338 if _, _, err := under.Get(ctx, "status/boxes/pi-box.json"); err == nil {
339 t.Error("underlying store must not hold the unrebased key")
340 }
341
342 // List, queried in the caller's unprefixed space, must find it and hand
343 // back a key that still round-trips through the SAME wrapper's Get.
344 keys, err := wrapped.List(ctx, "status/boxes/")
345 if err != nil {
346 t.Fatal(err)
347 }
348 if len(keys) != 1 || keys[0] != "status/boxes/pi-box.json" {
349 t.Fatalf("List = %v, want exactly [status/boxes/pi-box.json]", keys)
350 }
351 if body, _, err := wrapped.Get(ctx, keys[0]); err != nil || string(body) != `{"ok":true}` {
352 t.Errorf("Get(%q) via the wrapper = %q, %v", keys[0], body, err)
353 }
354
355 // An empty prefix must be a pure passthrough: the same store, unwrapped
356 // — not a prefixStore with an empty base. Verified by interface
357 // equality, not merely by observing identical behavior.
358 if s := NewPrefixStore(under, ""); s != Store(under) {
359 t.Errorf("NewPrefixStore with empty prefix = %v, want the underlying store unchanged", s)
360 }
361}
362
363// TestPrefixStoreListDirsRebasesAndStrips is List's sibling test for the
364// delimiter-aware listing: ListDirs must see child prefixes queried under
365// the rebased key space, and hand results back stripped of the base, same
366// as List.
367func TestPrefixStoreListDirsRebasesAndStrips(t *testing.T) {
368 ctx := context.Background()
369 under := NewDirStore(t.TempDir())
370 wrapped := NewPrefixStore(under, "fleets/pi/")
371
372 if _, err := wrapped.Put(ctx, "status/boxes/pi-box.json", []byte(`{}`), ""); err != nil {
373 t.Fatal(err)
374 }
375
376 dirs, err := wrapped.ListDirs(ctx, "status/")
377 if err != nil {
378 t.Fatal(err)
379 }
380 if len(dirs) != 1 || dirs[0] != "status/boxes/" {
381 t.Fatalf("ListDirs = %v, want exactly [status/boxes/]", dirs)
382 }
383}
384
385func TestNewS3StoreInPinsTheRegion(t *testing.T) {
386 t.Setenv("AWS_REGION", "us-east-1")
387 t.Setenv("AWS_ACCESS_KEY_ID", "k")
388 t.Setenv("AWS_SECRET_ACCESS_KEY", "s")
389 s, err := NewS3StoreIn(context.Background(), "b", "eu-west-1")
390 if err != nil {
391 t.Fatal(err)
392 }
393 if got := s.Client.Options().Region; got != "eu-west-1" {
394 t.Fatalf("region = %q, want eu-west-1", got)
395 }
396}
397