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 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "os"
9 "testing"
10 "time"
11)
12
13// restartEtag reads the app's restart-generation etag, "" for absent — the
14// exact observation a box's sweep makes, so a test asserting on it asserts on
15// the signal itself rather than on this file's internals.
16func restartEtag(t *testing.T, s Store, acct, app string) string {
17 t.Helper()
18 _, etag, err := s.Get(context.Background(), RestartGenKey(acct, app))
19 if err != nil {
20 if errors.Is(err, os.ErrNotExist) {
21 return ""
22 }
23 t.Fatalf("read restart generation: %v", err)
24 }
25 return etag
26}
27
28func TestTouchRestartGenCreatesAndMoves(t *testing.T) {
29 s := NewDirStore(t.TempDir())
30 ctx := context.Background()
31 if err := TouchRestartGen(ctx, s, "bab", "keymail"); err != nil {
32 t.Fatalf("first TouchRestartGen: %v", err)
33 }
34 raw, first, err := s.Get(ctx, RestartGenKey("bab", "keymail"))
35 if err != nil {
36 t.Fatalf("Get after first touch: %v", err)
37 }
38 var g RestartGeneration
39 if err := json.Unmarshal(raw, &g); err != nil {
40 t.Fatalf("decode restart generation: %v", err)
41 }
42 if g.Schema != 1 {
43 t.Fatalf("Schema = %d, want 1", g.Schema)
44 }
45 if _, err := time.Parse(time.RFC3339, g.TouchedAt); err != nil {
46 t.Fatalf("TouchedAt %q is not RFC3339: %v", g.TouchedAt, err)
47 }
48 if nonce, err := hex.DecodeString(g.Nonce); err != nil || len(nonce) == 0 {
49 t.Fatalf("Nonce = %q, want nonempty hex (decode err %v)", g.Nonce, err)
50 }
51 // The etag is the whole point: a second request must be a DIFFERENT
52 // object, or the sweep watching it sees nothing and nothing restarts.
53 if err := TouchRestartGen(ctx, s, "bab", "keymail"); err != nil {
54 t.Fatalf("second TouchRestartGen: %v", err)
55 }
56 if second := restartEtag(t, s, "bab", "keymail"); second == first {
57 t.Fatalf("etag %q unchanged after a second request — the restart signal never fires", second)
58 }
59}
60
61// racingRestartPutStore lands a different writer's value at key just before
62// forwarding the caller's Put, which then fails CAS against the etag the
63// caller read — two people (or a person and a retrying CI job) asking for a
64// restart at the same moment.
65type racingRestartPutStore struct {
66 Store
67 key string
68 done bool
69}
70
71func (r *racingRestartPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
72 if key == r.key && !r.done {
73 r.done = true
74 other := []byte(`{"schema":1,"touched_at":"2026-01-01T00:00:00Z","nonce":"00112233445566778899aabbccddeeff"}`)
75 if _, err := r.Store.Put(ctx, key, other, ifMatch); err != nil {
76 return "", err
77 }
78 }
79 return r.Store.Put(ctx, key, body, ifMatch)
80}
81
82func TestTouchRestartGenCASConflictIsSuccess(t *testing.T) {
83 inner := NewDirStore(t.TempDir())
84 ctx := context.Background()
85 if err := TouchRestartGen(ctx, inner, "bab", "keymail"); err != nil {
86 t.Fatalf("seed TouchRestartGen: %v", err)
87 }
88 racing := &racingRestartPutStore{Store: inner, key: RestartGenKey("bab", "keymail")}
89 if err := TouchRestartGen(ctx, racing, "bab", "keymail"); err != nil {
90 t.Fatalf("TouchRestartGen on a lost CAS race: %v, want nil (the request already fired)", err)
91 }
92 if !racing.done {
93 t.Fatal("the racing writer never ran — this test proved nothing")
94 }
95}
96
97// failingRestartPutStore fails every Put with a fixed non-CAS error.
98type failingRestartPutStore struct {
99 Store
100 err error
101}
102
103func (f *failingRestartPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
104 return "", f.err
105}
106
107// TestTouchRestartGenSurfacesRealPutErrors is the other half of the CAS
108// carve-out: ONLY ErrCASConflict is success. Any other store failure means
109// nobody asked for anything, and a caller told "requested" would stand there
110// waiting for a restart that is never coming.
111func TestTouchRestartGenSurfacesRealPutErrors(t *testing.T) {
112 s := &failingRestartPutStore{Store: NewDirStore(t.TempDir()), err: errors.New("bucket is on fire")}
113 if err := TouchRestartGen(context.Background(), s, "bab", "keymail"); err == nil {
114 t.Fatal("TouchRestartGen swallowed a non-CAS Put error, want it surfaced")
115 }
116}
117
118func TestTouchRestartGenRefusesInvalidNames(t *testing.T) {
119 s := NewDirStore(t.TempDir())
120 ctx := context.Background()
121 if err := TouchRestartGen(ctx, s, "../etc", "keymail"); err == nil {
122 t.Fatal("TouchRestartGen accepted an invalid account")
123 }
124 if err := TouchRestartGen(ctx, s, "bab", "../etc"); err == nil {
125 t.Fatal("TouchRestartGen accepted an invalid app")
126 }
127}
128
129// TestRestartGenKeyIsAnAppSibling pins the key's shape: under the app's own
130// account-scoped prefix, beside config-gen.json rather than inside anything.
131func TestRestartGenKeyIsAnAppSibling(t *testing.T) {
132 if got, want := RestartGenKey("bab", "keymail"), "apps/bab/keymail/restart-gen.json"; got != want {
133 t.Fatalf("RestartGenKey = %q, want %q", got, want)
134 }
135}
136