返回 DeepSeek-Reasonix
store_test.go
根目录 / internal / sessioncontent / store_test.go
1 package sessioncontent
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "io"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "testing"
14 )
15
16 func TestStorePutOpenAndDeduplicate(t *testing.T) {
17 t.Parallel()
18 root := t.TempDir()
19 store := New(root)
20 body := bytes.Repeat([]byte("reasonix-content\n"), 10000)
21 sum := sha256.Sum256(body)
22 wantDigest := hex.EncodeToString(sum[:])
23
24 first, err := store.Put(context.Background(), bytes.NewReader(body), Metadata{
25 MediaType: "text/plain; charset=utf-8",
26 Name: "../../not-a-storage-path.txt",
27 })
28 if err != nil {
29 t.Fatalf("Put first: %v", err)
30 }
31 second, err := store.Put(context.Background(), bytes.NewReader(body), Metadata{
32 MediaType: "application/octet-stream",
33 Name: "different-display-name.bin",
34 })
35 if err != nil {
36 t.Fatalf("Put second: %v", err)
37 }
38 if first.Digest != wantDigest || second.Digest != wantDigest {
39 t.Fatalf("digest = %q / %q, want %q", first.Digest, second.Digest, wantDigest)
40 }
41 if first.Bytes != int64(len(body)) || second.Bytes != int64(len(body)) {
42 t.Fatalf("bytes = %d / %d, want %d", first.Bytes, second.Bytes, len(body))
43 }
44 if first.Name != "../../not-a-storage-path.txt" || second.Name != "different-display-name.bin" {
45 t.Fatalf("display metadata was not preserved: %#v / %#v", first, second)
46 }
47
48 r, err := store.Open(context.Background(), first)
49 if err != nil {
50 t.Fatalf("Open: %v", err)
51 }
52 got, readErr := io.ReadAll(r)
53 closeErr := r.Close()
54 if readErr != nil || closeErr != nil {
55 t.Fatalf("read/close: %v / %v", readErr, closeErr)
56 }
57 if !bytes.Equal(got, body) {
58 t.Fatal("opened bytes differ from input")
59 }
60
61 var objects int
62 err = filepath.WalkDir(filepath.Join(root, "objects"), func(_ string, entry os.DirEntry, walkErr error) error {
63 if walkErr != nil {
64 return walkErr
65 }
66 if !entry.IsDir() {
67 objects++
68 }
69 return nil
70 })
71 if err != nil {
72 t.Fatalf("walk objects: %v", err)
73 }
74 if objects != 1 {
75 t.Fatalf("physical objects = %d, want 1", objects)
76 }
77 if _, err := os.Stat(filepath.Join(root, "not-a-storage-path.txt")); !os.IsNotExist(err) {
78 t.Fatalf("display name escaped content store: %v", err)
79 }
80 }
81
82 func TestStoreReadRangeAndStat(t *testing.T) {
83 t.Parallel()
84 store := New(t.TempDir())
85 body := []byte("0123456789")
86 ref, err := store.Put(context.Background(), bytes.NewReader(body), Metadata{MediaType: "text/plain"})
87 if err != nil {
88 t.Fatalf("Put: %v", err)
89 }
90 got, err := store.ReadRange(context.Background(), ref, 3, 4)
91 if err != nil {
92 t.Fatalf("ReadRange: %v", err)
93 }
94 if string(got) != "3456" {
95 t.Fatalf("range = %q, want 3456", got)
96 }
97 stat, err := store.Stat(context.Background(), ref)
98 if err != nil {
99 t.Fatalf("Stat: %v", err)
100 }
101 if stat.Digest != ref.Digest || stat.Bytes != int64(len(body)) {
102 t.Fatalf("Stat = %#v", stat)
103 }
104 if _, err := store.ReadRange(context.Background(), ref, -1, 1); err == nil {
105 t.Fatal("negative range offset was accepted")
106 }
107 if _, err := store.ReadRange(context.Background(), ref, 9, 2); err == nil {
108 t.Fatal("out-of-bounds range was accepted")
109 }
110 }
111
112 func TestStoreRejectsUntrustedIntegrityIndexPath(t *testing.T) {
113 t.Parallel()
114 store := New(t.TempDir())
115 ref := Ref{Digest: "../../outside", Bytes: 1, IndexDigest: strings.Repeat("0", sha256.Size*2), IntegrityBlock: IntegrityBlockBytes}
116 if _, err := store.readIndex(t.Context(), ref); err == nil {
117 t.Fatal("readIndex accepted an untrusted digest path")
118 }
119 }
120
121 func TestStoreReadRangeVerifiesOnlyTouchedIntegrityBlocks(t *testing.T) {
122 t.Parallel()
123 store := New(t.TempDir())
124 body := bytes.Repeat([]byte("a"), 3*IntegrityBlockBytes)
125 ref, err := store.Put(t.Context(), bytes.NewReader(body), Metadata{})
126 if err != nil {
127 t.Fatal(err)
128 }
129 object, err := os.OpenFile(store.objectPath(ref.Digest), os.O_RDWR, 0o600)
130 if err != nil {
131 t.Fatal(err)
132 }
133 if _, err := object.WriteAt([]byte("x"), 16); err != nil {
134 _ = object.Close()
135 t.Fatal(err)
136 }
137 if err := object.Close(); err != nil {
138 t.Fatal(err)
139 }
140 if got, err := store.ReadRange(t.Context(), ref, 2*IntegrityBlockBytes, 32); err != nil || !bytes.Equal(got, body[2*IntegrityBlockBytes:2*IntegrityBlockBytes+32]) {
141 t.Fatalf("untouched range = %d bytes, err=%v", len(got), err)
142 }
143 if _, err := store.ReadRange(t.Context(), ref, 0, 32); err == nil {
144 t.Fatal("tampered range passed block verification")
145 }
146 if err := store.Verify(t.Context(), ref); err == nil {
147 t.Fatal("full verification accepted tampered object")
148 }
149 }
150
151 func TestStoreRejectsCancellationAndTampering(t *testing.T) {
152 t.Parallel()
153 root := t.TempDir()
154 store := New(root)
155 canceled, cancel := context.WithCancel(context.Background())
156 cancel()
157 if _, err := store.Put(canceled, bytes.NewReader([]byte("never published")), Metadata{}); err == nil {
158 t.Fatal("Put with canceled context succeeded")
159 }
160
161 ref, err := store.Put(context.Background(), bytes.NewReader([]byte("original")), Metadata{})
162 if err != nil {
163 t.Fatalf("Put: %v", err)
164 }
165 if err := os.WriteFile(store.objectPath(ref.Digest), []byte("tampered"), 0o600); err != nil {
166 t.Fatalf("tamper object: %v", err)
167 }
168 if _, err := store.Open(context.Background(), ref); err == nil {
169 t.Fatal("Open accepted a tampered object")
170 }
171 if _, err := store.Stat(context.Background(), ref); err == nil {
172 t.Fatal("Stat accepted a tampered object")
173 }
174 }
175
176 func TestStoreConcurrentPutSameContent(t *testing.T) {
177 t.Parallel()
178 store := New(t.TempDir())
179 body := bytes.Repeat([]byte("same"), 250000)
180 const workers = 8
181 refs := make([]Ref, workers)
182 errs := make([]error, workers)
183 var wg sync.WaitGroup
184 for i := range workers {
185 wg.Go(func() {
186 refs[i], errs[i] = store.Put(context.Background(), bytes.NewReader(body), Metadata{})
187 })
188 }
189 wg.Wait()
190 for i := range workers {
191 if errs[i] != nil {
192 t.Fatalf("Put[%d]: %v", i, errs[i])
193 }
194 if refs[i].Digest != refs[0].Digest {
195 t.Fatalf("digest[%d] = %q, want %q", i, refs[i].Digest, refs[0].Digest)
196 }
197 }
198 r, err := store.Open(context.Background(), refs[0])
199 if err != nil {
200 t.Fatalf("Open: %v", err)
201 }
202 defer r.Close()
203 got, err := io.ReadAll(r)
204 if err != nil || !bytes.Equal(got, body) {
205 t.Fatalf("published object invalid: err=%v bytes=%d", err, len(got))
206 }
207 }
208
208 lines GO