| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "errors" |
| 6 | "io" |
| 7 | "io/fs" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // blobPath points tests at the default disk store's file for a blob id — |
| 13 | // test-only sugar (asserting bytes really live on disk / were unlinked). |
| 14 | // Production code goes through db.blobs and must not assume bytes are files. |
| 15 | func (db *DB) blobPath(id string) string { return db.blobs.(*diskBlobStore).path(id) } |
| 16 | |
| 17 | // The blobStore contract, exercised against implementation #1 (disk): a staged |
| 18 | // write committed under an id round-trips byte-for-byte, and remove makes it |
| 19 | // unopenable. Any future implementation (an object store, per issue #550) must |
| 20 | // pass this same sequence. |
| 21 | func TestDiskBlobStoreRoundTrip(t *testing.T) { |
| 22 | store, err := newDiskBlobStore(filepath.Join(t.TempDir(), "blobs")) |
| 23 | if err != nil { |
| 24 | t.Fatalf("newDiskBlobStore: %v", err) |
| 25 | } |
| 26 | payload := []byte("pretend-this-is-encrypted-bytes\x00\x01\x02") |
| 27 | |
| 28 | // put: stream in two writes to prove staging accumulates, then publish. |
| 29 | w, err := store.create() |
| 30 | if err != nil { |
| 31 | t.Fatalf("create: %v", err) |
| 32 | } |
| 33 | if _, err := w.Write(payload[:10]); err != nil { |
| 34 | t.Fatalf("write: %v", err) |
| 35 | } |
| 36 | if _, err := w.Write(payload[10:]); err != nil { |
| 37 | t.Fatalf("write: %v", err) |
| 38 | } |
| 39 | if err := w.commit("blobid_123-abc"); err != nil { |
| 40 | t.Fatalf("commit: %v", err) |
| 41 | } |
| 42 | |
| 43 | // open: bytes, size, and a usable modtime all come back. |
| 44 | r, size, modTime, err := store.open("blobid_123-abc") |
| 45 | if err != nil { |
| 46 | t.Fatalf("open: %v", err) |
| 47 | } |
| 48 | got, err := io.ReadAll(r) |
| 49 | r.Close() |
| 50 | if err != nil || !bytes.Equal(got, payload) { |
| 51 | t.Fatalf("round-trip mismatch: err=%v got=%q", err, got) |
| 52 | } |
| 53 | if size != int64(len(payload)) { |
| 54 | t.Fatalf("size = %d, want %d", size, len(payload)) |
| 55 | } |
| 56 | if modTime.IsZero() { |
| 57 | t.Fatal("zero modtime") |
| 58 | } |
| 59 | |
| 60 | // delete → open fails with the contractual not-exist error. |
| 61 | if err := store.remove("blobid_123-abc"); err != nil { |
| 62 | t.Fatalf("remove: %v", err) |
| 63 | } |
| 64 | if _, _, _, err := store.open("blobid_123-abc"); !errors.Is(err, fs.ErrNotExist) { |
| 65 | t.Fatalf("open after remove: want fs.ErrNotExist, got %v", err) |
| 66 | } |
| 67 | // GC unlinks best-effort, so removing an id that's already gone is not an |
| 68 | // error. |
| 69 | if err := store.remove("blobid_123-abc"); err != nil { |
| 70 | t.Fatalf("remove of missing id: %v", err) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // An aborted staged write leaves nothing fetchable — the id is never published. |
| 75 | func TestDiskBlobStoreAbortDiscards(t *testing.T) { |
| 76 | store, err := newDiskBlobStore(filepath.Join(t.TempDir(), "blobs")) |
| 77 | if err != nil { |
| 78 | t.Fatalf("newDiskBlobStore: %v", err) |
| 79 | } |
| 80 | w, err := store.create() |
| 81 | if err != nil { |
| 82 | t.Fatalf("create: %v", err) |
| 83 | } |
| 84 | if _, err := w.Write([]byte("half an upload")); err != nil { |
| 85 | t.Fatalf("write: %v", err) |
| 86 | } |
| 87 | w.abort() |
| 88 | if _, _, _, err := store.open("neverpublished"); !errors.Is(err, fs.ErrNotExist) { |
| 89 | t.Fatalf("open of aborted write: want fs.ErrNotExist, got %v", err) |
| 90 | } |
| 91 | } |
| 92 | |