eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/hex"
7 "errors"
8 "io"
9 "io/fs"
10 "net/http"
11 "net/http/httptest"
12 "path/filepath"
13 "testing"
14)
15
16// newMirroredTestDB builds a real DB whose store is primary+mirror over two
17// fake S3 servers — the exact construction openDB performs from env.
18func newMirroredTestDB(t *testing.T) (*DB, *fakeS3, *fakeS3) {
19 t.Helper()
20 primary := &fakeS3{objects: map[string][]byte{}}
21 mirror := &fakeS3{objects: map[string][]byte{}}
22 psrv := httptest.NewServer(http.HandlerFunc(primary.handler))
23 msrv := httptest.NewServer(http.HandlerFunc(mirror.handler))
24 t.Cleanup(psrv.Close)
25 t.Cleanup(msrv.Close)
26
27 t.Setenv("AWS_ACCESS_KEY_ID", "test")
28 t.Setenv("AWS_SECRET_ACCESS_KEY", "test")
29 t.Setenv("AWS_REGION", "auto")
30 t.Setenv("LCHAT_BLOB_S3_BUCKET", "prim")
31 t.Setenv("LCHAT_BLOB_S3_ENDPOINT", psrv.URL)
32 t.Setenv("LCHAT_BLOB_S3_MIRROR_BUCKET", "mirr")
33 t.Setenv("LCHAT_BLOB_S3_MIRROR_ENDPOINT", msrv.URL)
34 // The mirror carries its own key pair (a different provider) — exercises
35 // the static-credentials path.
36 t.Setenv("LCHAT_BLOB_S3_MIRROR_ACCESS_KEY", "mirror-key")
37 t.Setenv("LCHAT_BLOB_S3_MIRROR_SECRET_KEY", "mirror-secret")
38
39 db, err := openDB(filepath.Join(t.TempDir(), "space.example.com.db"))
40 if err != nil {
41 t.Fatalf("openDB: %v", err)
42 }
43 t.Cleanup(func() { db.sql.Close() })
44 if _, ok := db.blobs.(*mirroredBlobStore); !ok {
45 t.Fatalf("store is %T, want mirroredBlobStore", db.blobs)
46 }
47 return db, primary, mirror
48}
49
50// The Phase A loop: a put lands on the primary only; the sweep copies it to
51// the mirror, records mirrored_at, and a second pass is a no-op (idempotence
52// = zero further requests). Reads survive a lost primary object via the
53// mirror, and deletes reap both stores.
54func TestBlobMirrorSweep(t *testing.T) {
55 db, primary, mirror := newMirroredTestDB(t)
56 payload := []byte("ciphertext-bytes-0123456789")
57
58 if err := db.putBlob("blobA", "t1", payload); err != nil {
59 t.Fatalf("putBlob: %v", err)
60 }
61 if got := len(keysOf(mirror)); got != 0 {
62 t.Fatalf("mirror has %d objects before the sweep", got)
63 }
64
65 n, err := db.mirrorOnce(32)
66 if err != nil || n != 1 {
67 t.Fatalf("mirrorOnce = (%d, %v), want (1, nil)", n, err)
68 }
69 mbytes, ok := mirror.objects["/mirr/space.example.com/blobA"]
70 if !ok || !bytes.Equal(mbytes, payload) {
71 t.Fatalf("mirror copy wrong: ok=%v have %v", ok, keysOf(mirror))
72 }
73
74 // Idempotent: nothing pending → zero puts on a second pass.
75 basePuts := mirror.puts
76 if n, err := db.mirrorOnce(32); err != nil || n != 0 {
77 t.Fatalf("second mirrorOnce = (%d, %v), want (0, nil)", n, err)
78 }
79 if mirror.puts != basePuts {
80 t.Fatalf("idle sweep PUT %d times", mirror.puts-basePuts)
81 }
82
83 // Redundancy pays out: primary object lost → the read serves from the
84 // mirror.
85 delete(primary.objects, "/prim/space.example.com/blobA")
86 r, _, _, err := db.blobs.open("blobA")
87 if err != nil {
88 t.Fatalf("open after primary loss: %v", err)
89 }
90 got, _ := io.ReadAll(r)
91 r.Close()
92 if !bytes.Equal(got, payload) {
93 t.Fatalf("mirror served %q", got)
94 }
95
96 // GC reaps both stores.
97 if err := db.deleteBlobs("t1", []string{"blobA"}); err != nil {
98 t.Fatalf("deleteBlobs: %v", err)
99 }
100 if len(keysOf(mirror)) != 0 {
101 t.Fatalf("mirror kept %v after delete", keysOf(mirror))
102 }
103}
104
105// A pre-hash row (sha256 = ”) gets its hash backfilled by the sweep — the
106// migration path for every blob uploaded before this feature.
107func TestBlobMirrorBackfillsHash(t *testing.T) {
108 db, _, _ := newMirroredTestDB(t)
109 payload := []byte("old blob, no hash on record")
110 if err := db.putBlob("oldblob", "t1", payload); err != nil {
111 t.Fatal(err)
112 }
113 if _, err := db.sql.Exec("UPDATE blobs SET sha256 = '' WHERE id = 'oldblob'"); err != nil {
114 t.Fatal(err)
115 }
116 if _, err := db.mirrorOnce(32); err != nil {
117 t.Fatal(err)
118 }
119 var sha string
120 if err := db.sql.QueryRow("SELECT sha256 FROM blobs WHERE id = 'oldblob'").Scan(&sha); err != nil {
121 t.Fatal(err)
122 }
123 want := sha256.Sum256(payload)
124 if sha != hex.EncodeToString(want[:]) {
125 t.Fatalf("backfilled hash = %.12s, want %.12s", sha, hex.EncodeToString(want[:]))
126 }
127}
128
129// A primary whose bytes no longer match the recorded hash is NEVER mirrored —
130// corruption must not spread to the copy — and the row stays unmirrored so it
131// keeps being noticed.
132func TestBlobMirrorRefusesCorruptPrimary(t *testing.T) {
133 db, primary, mirror := newMirroredTestDB(t)
134 if err := db.putBlob("blobC", "t1", []byte("genuine bytes")); err != nil {
135 t.Fatal(err)
136 }
137 primary.objects["/prim/space.example.com/blobC"] = []byte("tampered bytes")
138
139 n, err := db.mirrorOnce(32)
140 if err != nil || n != 0 {
141 t.Fatalf("mirrorOnce = (%d, %v), want (0, nil) — corrupt blob must not count as mirrored", n, err)
142 }
143 if len(keysOf(mirror)) != 0 {
144 t.Fatalf("corruption reached the mirror: %v", keysOf(mirror))
145 }
146 var mirroredAt int64
147 if err := db.sql.QueryRow("SELECT mirrored_at FROM blobs WHERE id = 'blobC'").Scan(&mirroredAt); err != nil || mirroredAt != 0 {
148 t.Fatalf("mirrored_at = %d, want 0 (still pending)", mirroredAt)
149 }
150}
151
152// Legacy in-DB rows (bytes in the data column) never enter the sweep — the
153// DB's own replication carries them.
154func TestBlobMirrorSkipsLegacyRows(t *testing.T) {
155 db, _, mirror := newMirroredTestDB(t)
156 if _, err := db.sql.Exec(
157 "INSERT INTO blobs(id, data, size, thread_id, created_at) VALUES('legacy', X'AABB', 2, 't1', 1)"); err != nil {
158 t.Fatal(err)
159 }
160 if n, err := db.mirrorOnce(32); err != nil || n != 0 {
161 t.Fatalf("mirrorOnce = (%d, %v), want (0, nil)", n, err)
162 }
163 if len(keysOf(mirror)) != 0 {
164 t.Fatalf("legacy row reached the mirror: %v", keysOf(mirror))
165 }
166}
167
168// The audit sweep catches a corrupted or missing copy on either store and
169// says which.
170func TestBlobAudit(t *testing.T) {
171 db, primary, mirror := newMirroredTestDB(t)
172 if err := db.putBlob("blobD", "t1", []byte("audited bytes")); err != nil {
173 t.Fatal(err)
174 }
175 if _, err := db.mirrorOnce(32); err != nil {
176 t.Fatal(err)
177 }
178
179 // All healthy: a clean pass.
180 res, err := db.auditBlobsOnce(5)
181 if err != nil || res.Checked != 1 || res.BadPrimary+res.BadMirror+res.MissingPrimary+res.MissingMirror != 0 {
182 t.Fatalf("healthy audit = %+v, %v", res, err)
183 }
184
185 // Corrupt the mirror; lose the primary. Both verdicts land.
186 mirror.objects["/mirr/space.example.com/blobD"] = []byte("rot")
187 delete(primary.objects, "/prim/space.example.com/blobD")
188 res, err = db.auditBlobsOnce(5)
189 if err != nil {
190 t.Fatal(err)
191 }
192 if res.BadMirror != 1 {
193 t.Fatalf("mirror corruption missed: %+v", res)
194 }
195 // The primary miss falls through to the DISK fallback, which still holds
196 // nothing for an S3-era blob → missing.
197 if res.MissingPrimary != 1 {
198 t.Fatalf("primary loss missed: %+v", res)
199 }
200}
201
202// A mirror configured without a primary is a config mistake openDB refuses.
203func TestBlobMirrorRequiresPrimary(t *testing.T) {
204 t.Setenv("LCHAT_BLOB_S3_BUCKET", "")
205 t.Setenv("LCHAT_BLOB_S3_MIRROR_BUCKET", "mirr")
206 if _, err := openDB(filepath.Join(t.TempDir(), "x.db")); err == nil {
207 t.Fatal("openDB accepted a mirror without a primary")
208 }
209}
210
211// The upload handler records the streamed sha256 — putBlob's twin assertion
212// (the handler itself is exercised in handlers tests; this pins the hash).
213func TestPutBlobRecordsHash(t *testing.T) {
214 db, _, _ := newMirroredTestDB(t)
215 payload := []byte("hash me")
216 if err := db.putBlob("hashed", "", payload); err != nil {
217 t.Fatal(err)
218 }
219 var sha string
220 if err := db.sql.QueryRow("SELECT sha256 FROM blobs WHERE id = 'hashed'").Scan(&sha); err != nil {
221 t.Fatal(err)
222 }
223 want := sha256.Sum256(payload)
224 if sha != hex.EncodeToString(want[:]) {
225 t.Fatalf("recorded hash = %.12s, want %.12s", sha, hex.EncodeToString(want[:]))
226 }
227}
228
229// The fs.ErrNotExist contract survives composition: an id nobody holds errors
230// identically through the mirrored store.
231func TestMirroredStoreNotExist(t *testing.T) {
232 db, _, _ := newMirroredTestDB(t)
233 if _, _, _, err := db.blobs.open("ghost"); !errors.Is(err, fs.ErrNotExist) {
234 t.Fatalf("open(ghost) = %v, want fs.ErrNotExist", err)
235 }
236}
237