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 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/release"
15)
16
17func chainOf(t *testing.T, st release.Store, n int) {
18 t.Helper()
19 for i := range n {
20 at := func() time.Time { return fixedNow().Add(time.Duration(i) * time.Minute) }
21 if _, err := Append(context.Background(), st, "bab", "platform", "carbon",
22 "w", json.RawMessage(`{"i":`+string(rune('0'+i))+`}`), at); err != nil {
23 t.Fatal(err)
24 }
25 }
26}
27
28func TestVerifyGoodChain(t *testing.T) {
29 st := release.NewDirStore(t.TempDir())
30 chainOf(t, st, 3)
31 res, err := Verify(context.Background(), StoreGetter(st), "bab", "platform", "carbon")
32 if err != nil {
33 t.Fatal(err)
34 }
35 if res.Count != 3 {
36 t.Fatalf("count = %d, want 3", res.Count)
37 }
38}
39
40func TestVerifyDetectsTamper(t *testing.T) {
41 dir := t.TempDir()
42 st := release.NewDirStore(dir)
43 chainOf(t, st, 3)
44 // Flip one byte in the OLDEST entry's stored file, behind the store's
45 // back — exactly the mutation a hash chain exists to catch.
46 keys, err := st.List(context.Background(), Prefix("bab", "platform", "carbon")+"entries/")
47 if err != nil || len(keys) != 3 {
48 t.Fatalf("entries: %v %v", keys, err)
49 }
50 p := filepath.Join(dir, filepath.FromSlash(keys[0]))
51 b, err := os.ReadFile(p)
52 if err != nil {
53 t.Fatal(err)
54 }
55 b[len(b)-2] ^= 1
56 if err := os.WriteFile(p, b, 0o644); err != nil {
57 t.Fatal(err)
58 }
59 if _, err := Verify(context.Background(), StoreGetter(st), "bab", "platform", "carbon"); err == nil {
60 t.Fatal("tampered chain verified clean")
61 }
62}
63
64// TestVerifyPinsTheWalkToItsOwnLedger: a verifier follows prev_key, and
65// prev_key is bytes the chain itself supplies — so a chain that points its
66// walk at ANOTHER ledger's entries (or at an object outside the ledger
67// prefix entirely) must fail, not quietly borrow that object's history and
68// report a clean verification. The count and the hashes can all be made to
69// agree; what cannot be forged is the entry's own ledger name and the prefix
70// its keys live under.
71func TestVerifyPinsTheWalkToItsOwnLedger(t *testing.T) {
72 ctx := context.Background()
73 st := release.NewDirStore(t.TempDir())
74 // A real two-entry chain in "iron", whose genesis we will graft onto a
75 // hand-built "carbon" chain.
76 if _, err := Append(ctx, st, "bab", "platform", "iron", "w",
77 json.RawMessage(`{"i":0}`), fixedNow); err != nil {
78 t.Fatal(err)
79 }
80 ib, _, err := st.Get(ctx, HeadKey("bab", "platform", "iron"))
81 if err != nil {
82 t.Fatal(err)
83 }
84 var ih Head
85 if err := json.Unmarshal(ib, &ih); err != nil {
86 t.Fatal(err)
87 }
88
89 // carbon's single entry chains to iron's genesis: every hash checks out,
90 // the walk terminates, and the count matches.
91 e := Entry{Schema: Schema, Ledger: "carbon", Writer: "w", TS: "2026-08-03T21:00:00Z",
92 Payload: json.RawMessage(`{"i":1}`), PrevKey: ih.HeadKey, PrevSHA256: ih.HeadSHA256}
93 eb, err := json.Marshal(e)
94 if err != nil {
95 t.Fatal(err)
96 }
97 sha := SHA256Hex(eb)
98 key := EntryKey("bab", "platform", "carbon", e.TS, sha)
99 if _, err := st.Put(ctx, key, eb, ""); err != nil {
100 t.Fatal(err)
101 }
102 hb, err := json.Marshal(Head{Schema: Schema, UpdatedAt: e.TS, HeadKey: key, HeadSHA256: sha, Count: 2})
103 if err != nil {
104 t.Fatal(err)
105 }
106 if _, err := st.Put(ctx, HeadKey("bab", "platform", "carbon"), hb, ""); err != nil {
107 t.Fatal(err)
108 }
109
110 _, err = Verify(ctx, StoreGetter(st), "bab", "platform", "carbon")
111 if err == nil {
112 t.Fatal("a chain walking into another ledger's entries verified clean")
113 }
114 if !strings.Contains(err.Error(), "carbon") {
115 t.Fatalf("error = %v, want it to name the ledger the walk left", err)
116 }
117}
118
119func TestVerifyOverHTTP(t *testing.T) {
120 dir := t.TempDir()
121 st := release.NewDirStore(dir)
122 chainOf(t, st, 2)
123 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
124 b, _, err := st.Get(r.Context(), strings.TrimPrefix(r.URL.Path, "/"))
125 if err != nil {
126 http.NotFound(w, r)
127 return
128 }
129 w.Write(b)
130 }))
131 defer ts.Close()
132 res, err := Verify(context.Background(), HTTPGetter(ts.Client(), ts.URL), "bab", "platform", "carbon")
133 if err != nil || res.Count != 2 {
134 t.Fatalf("res=%+v err=%v", res, err)
135 }
136}
137