1package filesystem
2
3import (
4 "bytes"
5 "io"
6 "os"
7 "path/filepath"
8 "testing"
9
10 "github.com/bnema/zerowrap"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13)
14
15const (
16 testDigest1 = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
17 testDigest2 = "sha256:b3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d5"
18 testDigestSHA512 = "sha512:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
19 testDigestMissing = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
20)
21
22func testLogger() zerowrap.Logger {
23 return zerowrap.Default()
24}
25
26func TestNewBlobStorage(t *testing.T) {
27 tmpDir := t.TempDir()
28 log := testLogger()
29
30 storage, err := NewBlobStorage(tmpDir, log)
31
32 require.NoError(t, err)
33 assert.NotNil(t, storage)
34
35 // Verify directories were created
36 assert.DirExists(t, filepath.Join(tmpDir, "blobs"))
37 assert.DirExists(t, filepath.Join(tmpDir, "uploads"))
38}
39
40func TestNewBlobStorage_InvalidPath(t *testing.T) {
41 log := testLogger()
42
43 // Try to create in a path that can't be created (no permissions)
44 _, err := NewBlobStorage("/nonexistent/deeply/nested/path/that/should/fail", log)
45
46 assert.Error(t, err)
47}
48
49func TestBlobStorage_PutAndGetBlob(t *testing.T) {
50 tmpDir := t.TempDir()
51 log := testLogger()
52
53 storage, err := NewBlobStorage(tmpDir, log)
54 require.NoError(t, err)
55
56 blobData := []byte("test blob content")
57
58 // Put blob
59 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
60 require.NoError(t, err)
61
62 // Get blob
63 reader, err := storage.GetBlob(testDigest1)
64 require.NoError(t, err)
65 defer reader.Close()
66
67 data, err := io.ReadAll(reader)
68 require.NoError(t, err)
69
70 assert.Equal(t, blobData, data)
71}
72
73func TestBlobStorage_GetBlob_NotFound(t *testing.T) {
74 tmpDir := t.TempDir()
75 log := testLogger()
76
77 storage, err := NewBlobStorage(tmpDir, log)
78 require.NoError(t, err)
79
80 reader, err := storage.GetBlob(testDigestMissing)
81
82 assert.Error(t, err)
83 assert.Nil(t, reader)
84 assert.Contains(t, err.Error(), "blob not found")
85}
86
87func TestBlobStorage_GetBlobPath(t *testing.T) {
88 tmpDir := t.TempDir()
89 log := testLogger()
90
91 storage, err := NewBlobStorage(tmpDir, log)
92 require.NoError(t, err)
93
94 blobData := []byte("test blob content")
95
96 // Put blob first
97 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
98 require.NoError(t, err)
99
100 // Get blob path
101 path, err := storage.GetBlobPath(testDigest1)
102 require.NoError(t, err)
103 assert.NotEmpty(t, path)
104
105 // Verify file exists at path
106 _, err = os.Stat(path)
107 assert.NoError(t, err)
108}
109
110func TestBlobStorage_GetBlobPath_NotFound(t *testing.T) {
111 tmpDir := t.TempDir()
112 log := testLogger()
113
114 storage, err := NewBlobStorage(tmpDir, log)
115 require.NoError(t, err)
116
117 path, err := storage.GetBlobPath(testDigestMissing)
118
119 assert.Error(t, err)
120 assert.Empty(t, path)
121 assert.Contains(t, err.Error(), "blob not found")
122}
123
124func TestBlobStorage_PutBlob_SizeMismatch(t *testing.T) {
125 tmpDir := t.TempDir()
126 log := testLogger()
127
128 storage, err := NewBlobStorage(tmpDir, log)
129 require.NoError(t, err)
130
131 blobData := []byte("test blob content")
132
133 // Put blob with wrong size
134 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 999)
135
136 assert.Error(t, err)
137 assert.Contains(t, err.Error(), "size mismatch")
138}
139
140func TestBlobStorage_PutBlob_ZeroSize(t *testing.T) {
141 tmpDir := t.TempDir()
142 log := testLogger()
143
144 storage, err := NewBlobStorage(tmpDir, log)
145 require.NoError(t, err)
146
147 blobData := []byte("test blob content")
148
149 // Put blob with size=0 (should skip size check)
150 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), 0)
151
152 assert.NoError(t, err)
153
154 // Verify blob was stored
155 assert.True(t, storage.BlobExists(testDigest2))
156}
157
158func TestBlobStorage_DeleteBlob(t *testing.T) {
159 tmpDir := t.TempDir()
160 log := testLogger()
161
162 storage, err := NewBlobStorage(tmpDir, log)
163 require.NoError(t, err)
164
165 blobData := []byte("test blob content")
166
167 // Put blob
168 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
169 require.NoError(t, err)
170
171 // Verify it exists
172 assert.True(t, storage.BlobExists(testDigest2))
173
174 // Delete blob
175 err = storage.DeleteBlob(testDigest2)
176 require.NoError(t, err)
177
178 // Verify it's gone
179 assert.False(t, storage.BlobExists(testDigest2))
180}
181
182func TestBlobStorage_DeleteBlob_NotFound(t *testing.T) {
183 tmpDir := t.TempDir()
184 log := testLogger()
185
186 storage, err := NewBlobStorage(tmpDir, log)
187 require.NoError(t, err)
188
189 err = storage.DeleteBlob(testDigestMissing)
190
191 assert.Error(t, err)
192 assert.Contains(t, err.Error(), "blob not found")
193}
194
195func TestBlobStorage_BlobExists(t *testing.T) {
196 tmpDir := t.TempDir()
197 log := testLogger()
198
199 storage, err := NewBlobStorage(tmpDir, log)
200 require.NoError(t, err)
201
202 // Should not exist initially
203 assert.False(t, storage.BlobExists(testDigest2))
204
205 // Put blob
206 blobData := []byte("test blob content")
207 err = storage.PutBlob(testDigest2, bytes.NewReader(blobData), int64(len(blobData)))
208 require.NoError(t, err)
209
210 // Should exist now
211 assert.True(t, storage.BlobExists(testDigest2))
212}
213
214func TestBlobStorage_StartBlobUpload(t *testing.T) {
215 tmpDir := t.TempDir()
216 log := testLogger()
217
218 storage, err := NewBlobStorage(tmpDir, log)
219 require.NoError(t, err)
220
221 uuid, err := storage.StartBlobUpload("myapp")
222
223 require.NoError(t, err)
224 assert.NotEmpty(t, uuid)
225 assert.Len(t, uuid, 36) // standard UUID format
226
227 // Verify upload file was created
228 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
229 _, err = os.Stat(uploadPath)
230 assert.NoError(t, err)
231}
232
233func TestBlobStorage_AppendBlobChunk(t *testing.T) {
234 tmpDir := t.TempDir()
235 log := testLogger()
236
237 storage, err := NewBlobStorage(tmpDir, log)
238 require.NoError(t, err)
239
240 uuid, err := storage.StartBlobUpload("myapp")
241 require.NoError(t, err)
242
243 // Append first chunk
244 chunk1 := []byte("first chunk")
245 size1, err := storage.AppendBlobChunk("myapp", uuid, chunk1)
246 require.NoError(t, err)
247 assert.Equal(t, int64(len(chunk1)), size1)
248
249 // Append second chunk
250 chunk2 := []byte(" second chunk")
251 size2, err := storage.AppendBlobChunk("myapp", uuid, chunk2)
252 require.NoError(t, err)
253 assert.Equal(t, int64(len(chunk1)+len(chunk2)), size2)
254}
255
256func TestBlobStorage_AppendBlobChunk_NotFound(t *testing.T) {
257 tmpDir := t.TempDir()
258 log := testLogger()
259
260 storage, err := NewBlobStorage(tmpDir, log)
261 require.NoError(t, err)
262
263 _, err = storage.AppendBlobChunk("myapp", "00000000-0000-4000-a000-000000000000", []byte("data"))
264
265 assert.Error(t, err)
266 assert.Contains(t, err.Error(), "upload not found")
267}
268
269func TestBlobStorage_GetBlobUpload(t *testing.T) {
270 tmpDir := t.TempDir()
271 log := testLogger()
272
273 storage, err := NewBlobStorage(tmpDir, log)
274 require.NoError(t, err)
275
276 uuid, err := storage.StartBlobUpload("myapp")
277 require.NoError(t, err)
278
279 writer, err := storage.GetBlobUpload(uuid)
280 require.NoError(t, err)
281 defer writer.Close()
282
283 // Write some data
284 _, err = writer.Write([]byte("test data"))
285 assert.NoError(t, err)
286}
287
288func TestBlobStorage_GetBlobUpload_NotFound(t *testing.T) {
289 tmpDir := t.TempDir()
290 log := testLogger()
291
292 storage, err := NewBlobStorage(tmpDir, log)
293 require.NoError(t, err)
294
295 writer, err := storage.GetBlobUpload("00000000-0000-4000-a000-000000000000")
296
297 assert.Error(t, err)
298 assert.Nil(t, writer)
299 assert.Contains(t, err.Error(), "upload not found")
300}
301
302func TestBlobStorage_FinishBlobUpload(t *testing.T) {
303 tmpDir := t.TempDir()
304 log := testLogger()
305
306 storage, err := NewBlobStorage(tmpDir, log)
307 require.NoError(t, err)
308
309 // Start upload
310 uuid, err := storage.StartBlobUpload("myapp")
311 require.NoError(t, err)
312
313 // Append data - content that hashes to testDigest1
314 blobData := []byte("complete blob content")
315 _, err = storage.AppendBlobChunk("myapp", uuid, blobData)
316 require.NoError(t, err)
317
318 // Finish upload - use testDigest1 (validation will pass, but hash won't match)
319 // This tests the flow, actual digest verification happens in verifyUploadDigest
320 err = storage.FinishBlobUpload(uuid, testDigest1)
321 // This will fail because content doesn't match digest
322 assert.Error(t, err)
323 assert.Contains(t, err.Error(), "digest")
324}
325
326func TestBlobStorage_CancelBlobUpload(t *testing.T) {
327 tmpDir := t.TempDir()
328 log := testLogger()
329
330 storage, err := NewBlobStorage(tmpDir, log)
331 require.NoError(t, err)
332
333 // Start upload
334 uuid, err := storage.StartBlobUpload("myapp")
335 require.NoError(t, err)
336
337 // Verify upload file exists
338 uploadPath := filepath.Join(tmpDir, "uploads", uuid)
339 _, err = os.Stat(uploadPath)
340 require.NoError(t, err)
341
342 // Cancel upload
343 err = storage.CancelBlobUpload(uuid)
344 require.NoError(t, err)
345
346 // Verify upload file is gone
347 _, err = os.Stat(uploadPath)
348 assert.True(t, os.IsNotExist(err))
349}
350
351func TestBlobStorage_CancelBlobUpload_NotFound(t *testing.T) {
352 tmpDir := t.TempDir()
353 log := testLogger()
354
355 storage, err := NewBlobStorage(tmpDir, log)
356 require.NoError(t, err)
357
358 err = storage.CancelBlobUpload("00000000-0000-4000-a000-000000000000")
359
360 assert.Error(t, err)
361 assert.Contains(t, err.Error(), "upload not found")
362}
363
364func TestBlobStorage_BlobPathStructure(t *testing.T) {
365 tmpDir := t.TempDir()
366 log := testLogger()
367
368 storage, err := NewBlobStorage(tmpDir, log)
369 require.NoError(t, err)
370
371 // Test that blobs are stored with proper directory structure
372 blobData := []byte("test blob content")
373
374 err = storage.PutBlob(testDigest1, bytes.NewReader(blobData), int64(len(blobData)))
375 require.NoError(t, err)
376
377 // Check the path structure: blobs/sha256/a3/a3ed95caeb02...
378 // First 2 chars of hash after sha256: are "a3"
379 expectedPath := filepath.Join(tmpDir, "blobs", "sha256", "a3", "a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
380 _, err = os.Stat(expectedPath)
381 assert.NoError(t, err)
382}
383
384func TestBlobStorage_DigestFormats(t *testing.T) {
385 tmpDir := t.TempDir()
386 log := testLogger()
387
388 storage, err := NewBlobStorage(tmpDir, log)
389 require.NoError(t, err)
390
391 tests := []struct {
392 name string
393 digest string
394 }{
395 {"sha256 standard", testDigest1},
396 {"sha512", testDigestSHA512},
397 }
398
399 for _, tt := range tests {
400 t.Run(tt.name, func(t *testing.T) {
401 blobData := []byte("test content for " + tt.digest)
402
403 err := storage.PutBlob(tt.digest, bytes.NewReader(blobData), int64(len(blobData)))
404 require.NoError(t, err)
405
406 assert.True(t, storage.BlobExists(tt.digest))
407
408 reader, err := storage.GetBlob(tt.digest)
409 require.NoError(t, err)
410 defer reader.Close()
411
412 data, err := io.ReadAll(reader)
413 require.NoError(t, err)
414 assert.Equal(t, blobData, data)
415 })
416 }
417}
418
419func TestBlobStorage_CompleteUploadFlow(t *testing.T) {
420 tmpDir := t.TempDir()
421 log := testLogger()
422
423 storage, err := NewBlobStorage(tmpDir, log)
424 require.NoError(t, err)
425
426 // Simulate a complete upload flow
427 repoName := "myorg/myapp"
428
429 // 1. Start upload
430 uuid, err := storage.StartBlobUpload(repoName)
431 require.NoError(t, err)
432
433 // 2. Send multiple chunks
434 chunks := [][]byte{
435 []byte("first chunk of data"),
436 []byte(" - middle chunk"),
437 []byte(" - final chunk"),
438 }
439
440 var totalSize int64
441 for _, chunk := range chunks {
442 size, err := storage.AppendBlobChunk(repoName, uuid, chunk)
443 require.NoError(t, err)
444 totalSize = size
445 }
446
447 expectedTotal := int64(len(chunks[0]) + len(chunks[1]) + len(chunks[2]))
448 assert.Equal(t, expectedTotal, totalSize)
449
450 // 3. Cancel upload (since we can't easily compute the real digest)
451 err = storage.CancelBlobUpload(uuid)
452 require.NoError(t, err)
453}