carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package ledger
2
3import (
4 "context"
5 "errors"
6 "os"
7 "slices"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14func TestManifestRoundTrip(t *testing.T) {
15 st := release.NewDirStore(t.TempDir())
16 ctx := context.Background()
17 if _, err := ReadManifest(ctx, st, "bab", "platform"); !errors.Is(err, os.ErrNotExist) {
18 t.Fatalf("missing manifest: %v, want os.ErrNotExist", err)
19 }
20 m := Manifest{Schema: Schema, Public: true, Ledgers: []string{"carbon"}, Title: "CARLOS"}
21 if err := WriteManifest(ctx, st, "bab", "platform", m); err != nil {
22 t.Fatal(err)
23 }
24 got, err := ReadManifest(ctx, st, "bab", "platform")
25 if err != nil || !got.Public || got.Title != "CARLOS" || len(got.Ledgers) != 1 {
26 t.Fatalf("%+v %v", got, err)
27 }
28 // Overwrite flips public off — WriteManifest must replace, not create-only.
29 m.Public = false
30 if err := WriteManifest(ctx, st, "bab", "platform", m); err != nil {
31 t.Fatal(err)
32 }
33 got, _ = ReadManifest(ctx, st, "bab", "platform")
34 if got.Public {
35 t.Fatal("manifest overwrite did not take")
36 }
37}
38
39// TestUpdateManifestReadModifyWrite: the manifest is a SET of published
40// ledgers accrued over many separate publish calls, so the write path that
41// serves those calls must read-modify-write. A whole-value replace here is
42// what silently unpublishes an app's other ledgers.
43func TestUpdateManifestReadModifyWrite(t *testing.T) {
44 st := release.NewDirStore(t.TempDir())
45 ctx := context.Background()
46
47 // Missing manifest is the zero value, not an error: the first publish
48 // of an app creates it.
49 m, err := UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error {
50 m.Public = true
51 m.Ledgers = append(m.Ledgers, "carbon")
52 m.Title = "CARLOS"
53 return nil
54 })
55 if err != nil {
56 t.Fatalf("first update: %v", err)
57 }
58 if !m.Public || m.Schema != Schema || m.Title != "CARLOS" || !slices.Equal(m.Ledgers, []string{"carbon"}) {
59 t.Fatalf("first update = %+v, want the created manifest", m)
60 }
61
62 // A second update sees the first one's state.
63 m, err = UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error {
64 m.Ledgers = append(m.Ledgers, "prompts")
65 return nil
66 })
67 if err != nil {
68 t.Fatalf("second update: %v", err)
69 }
70 if !slices.Equal(m.Ledgers, []string{"carbon", "prompts"}) || m.Title != "CARLOS" {
71 t.Fatalf("second update = %+v, want carbon preserved beside prompts", m)
72 }
73 stored, err := ReadManifest(ctx, st, "bab", "platform")
74 if err != nil || !slices.Equal(stored.Ledgers, []string{"carbon", "prompts"}) {
75 t.Fatalf("stored = %+v %v, want what the update returned", stored, err)
76 }
77}
78
79// conflictOnce loses the first ledgers.json CAS race to a writer that landed
80// a DIFFERENT manifest — the shape that tells a retry apart from a re-apply.
81// A loop that retried with its own stale copy would erase "iron".
82type conflictOnce struct {
83 release.Store
84 lost bool
85}
86
87func (c *conflictOnce) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
88 if strings.HasSuffix(key, "/ledgers.json") && !c.lost {
89 c.lost = true
90 if _, err := c.Store.Put(ctx, key, []byte(`{"schema":1,"public":true,"ledgers":["iron"],"title":"Other"}`), ifMatch); err != nil {
91 return "", err
92 }
93 return "", release.ErrCASConflict
94 }
95 return c.Store.Put(ctx, key, body, ifMatch)
96}
97
98// TestUpdateManifestCASReapplies: on a lost race the update must re-READ and
99// re-APPLY, not re-Put its stale copy — otherwise two contributors publishing
100// two ledgers at once is a lost update, and the loser never learns.
101func TestUpdateManifestCASReapplies(t *testing.T) {
102 st := &conflictOnce{Store: release.NewDirStore(t.TempDir())}
103 m, err := UpdateManifest(context.Background(), st, "bab", "platform", func(m *Manifest) error {
104 m.Public = true
105 if !slices.Contains(m.Ledgers, "carbon") {
106 m.Ledgers = append(m.Ledgers, "carbon")
107 }
108 return nil
109 })
110 if err != nil {
111 t.Fatalf("update: %v", err)
112 }
113 if !st.lost {
114 t.Fatal("the fake never forced a CAS conflict")
115 }
116 if !slices.Equal(m.Ledgers, []string{"iron", "carbon"}) {
117 t.Fatalf("after a lost race = %+v, want the winner's iron preserved beside carbon", m)
118 }
119 if m.Title != "Other" {
120 t.Fatalf("title = %q, want the winner's title preserved", m.Title)
121 }
122}
123
124// TestUpdateManifestSurfacesLostRaces: contention that outlasts the retries
125// is reported, never swallowed — the console maps it to a retry-worthy 503.
126func TestUpdateManifestSurfacesLostRaces(t *testing.T) {
127 st := alwaysConflict{release.NewDirStore(t.TempDir())}
128 if _, err := UpdateManifest(context.Background(), st, "bab", "platform", func(m *Manifest) error {
129 m.Public = true
130 return nil
131 }); !errors.Is(err, release.ErrCASConflict) {
132 t.Fatalf("update under total contention = %v, want a CAS conflict", err)
133 }
134}
135
136// TestUpdateManifestApplyRefusalWritesNothing: apply is where a caller
137// enforces a rule only the STORED manifest can decide (the console's cap on
138// how many ledgers an app may publish). A refusal has to abort before the Put
139// — a loop that wrote first and reported the refusal afterwards would store
140// exactly the state it just called illegal.
141func TestUpdateManifestApplyRefusalWritesNothing(t *testing.T) {
142 ctx := context.Background()
143 st := release.NewDirStore(t.TempDir())
144 if err := WriteManifest(ctx, st, "bab", "platform",
145 Manifest{Public: true, Ledgers: []string{"carbon"}}); err != nil {
146 t.Fatal(err)
147 }
148 refused := errors.New("no")
149 if _, err := UpdateManifest(ctx, st, "bab", "platform", func(m *Manifest) error {
150 m.Ledgers = append(m.Ledgers, "prompts")
151 return refused
152 }); !errors.Is(err, refused) {
153 t.Fatalf("update = %v, want apply's own error returned untouched", err)
154 }
155 stored, err := ReadManifest(ctx, st, "bab", "platform")
156 if err != nil {
157 t.Fatal(err)
158 }
159 if !slices.Equal(stored.Ledgers, []string{"carbon"}) {
160 t.Fatalf("stored = %+v, want the refused change never written", stored)
161 }
162}
163
164type alwaysConflict struct{ release.Store }
165
166func (a alwaysConflict) Put(context.Context, string, []byte, string) (string, error) {
167 return "", release.ErrCASConflict
168}
169