1package registry
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "os"
11 "testing"
12
13 "github.com/bnema/zerowrap"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/mock"
16 "github.com/stretchr/testify/require"
17
18 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
19 "github.com/bnema/gordon/internal/domain"
20)
21
22func testLogger() zerowrap.Logger {
23 return zerowrap.Default()
24}
25
26func TestHandler_Base(t *testing.T) {
27 registrySvc := inmocks.NewMockRegistryService(t)
28
29 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
30
31 req := httptest.NewRequest("GET", "/v2/", nil)
32 rec := httptest.NewRecorder()
33
34 handler.ServeHTTP(rec, req)
35
36 assert.Equal(t, http.StatusOK, rec.Code)
37 assert.Equal(t, "registry/2.0", rec.Header().Get("Docker-Distribution-API-Version"))
38}
39
40func TestHandler_NotFound(t *testing.T) {
41 registrySvc := inmocks.NewMockRegistryService(t)
42
43 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
44
45 req := httptest.NewRequest("GET", "/v2/unknown/path", nil)
46 rec := httptest.NewRecorder()
47
48 handler.ServeHTTP(rec, req)
49
50 assert.Equal(t, http.StatusNotFound, rec.Code)
51}
52
53func TestHandler_GetManifest_Success(t *testing.T) {
54 registrySvc := inmocks.NewMockRegistryService(t)
55
56 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
57
58 manifestData := []byte(`{"schemaVersion": 2}`)
59 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
60 Name: "myapp",
61 Reference: "latest",
62 ContentType: "application/vnd.docker.distribution.manifest.v2+json",
63 Data: manifestData,
64 }, nil)
65
66 req := httptest.NewRequest("GET", "/v2/myapp/manifests/latest", nil)
67 rec := httptest.NewRecorder()
68
69 handler.ServeHTTP(rec, req)
70
71 assert.Equal(t, http.StatusOK, rec.Code)
72 assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type"))
73 assert.Equal(t, manifestData, rec.Body.Bytes())
74}
75
76func TestHandler_GetManifest_HEAD(t *testing.T) {
77 registrySvc := inmocks.NewMockRegistryService(t)
78
79 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
80
81 manifestData := []byte(`{"schemaVersion": 2}`)
82 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
83 Name: "myapp",
84 Reference: "latest",
85 ContentType: "application/vnd.docker.distribution.manifest.v2+json",
86 Data: manifestData,
87 }, nil)
88
89 req := httptest.NewRequest("HEAD", "/v2/myapp/manifests/latest", nil)
90 rec := httptest.NewRecorder()
91
92 handler.ServeHTTP(rec, req)
93
94 assert.Equal(t, http.StatusOK, rec.Code)
95 assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type"))
96 // HEAD should not return body
97 assert.Empty(t, rec.Body.Bytes())
98}
99
100func TestHandler_GetManifest_NotFound(t *testing.T) {
101 registrySvc := inmocks.NewMockRegistryService(t)
102
103 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
104
105 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError)
106
107 req := httptest.NewRequest("GET", "/v2/myapp/manifests/notexists", nil)
108 rec := httptest.NewRecorder()
109
110 handler.ServeHTTP(rec, req)
111
112 assert.Equal(t, http.StatusNotFound, rec.Code)
113 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
114}
115
116func TestHandler_GetManifest_NestedName(t *testing.T) {
117 registrySvc := inmocks.NewMockRegistryService(t)
118
119 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
120
121 manifestData := []byte(`{"schemaVersion": 2}`)
122 registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{
123 Name: "org/project/app",
124 Reference: "v1.0",
125 ContentType: "application/vnd.docker.distribution.manifest.v2+json",
126 Data: manifestData,
127 }, nil)
128
129 req := httptest.NewRequest("GET", "/v2/org/project/app/manifests/v1.0", nil)
130 rec := httptest.NewRecorder()
131
132 handler.ServeHTTP(rec, req)
133
134 assert.Equal(t, http.StatusOK, rec.Code)
135}
136
137// TestHandler_ReservedLookingRepositoryIsServedExactly proves the handler
138// dispatches on the same repository the parser resolves from the last
139// route marker, so authorization cannot name a different repository than
140// the one accessed.
141func TestHandler_ReservedLookingRepositoryIsServedExactly(t *testing.T) {
142 registrySvc := inmocks.NewMockRegistryService(t)
143
144 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
145
146 registrySvc.EXPECT().GetManifest(mock.Anything, "manifests/victim", "latest").Return(&domain.Manifest{
147 Name: "manifests/victim",
148 Reference: "latest",
149 ContentType: "application/vnd.docker.distribution.manifest.v2+json",
150 Data: []byte(`{"schemaVersion": 2}`),
151 }, nil)
152
153 req := httptest.NewRequest("GET", "/v2/manifests/victim/manifests/latest", nil)
154 rec := httptest.NewRecorder()
155
156 handler.ServeHTTP(rec, req)
157
158 assert.Equal(t, http.StatusOK, rec.Code)
159}
160
161func TestHandler_PutManifest_Success(t *testing.T) {
162 registrySvc := inmocks.NewMockRegistryService(t)
163
164 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
165
166 manifestData := []byte(`{"schemaVersion": 2}`)
167 registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {
168 return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"
169 })).Return("sha256:abc123", nil)
170
171 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
172 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
173 rec := httptest.NewRecorder()
174
175 handler.ServeHTTP(rec, req)
176
177 assert.Equal(t, http.StatusCreated, rec.Code)
178 assert.Equal(t, "sha256:abc123", rec.Header().Get("Docker-Content-Digest"))
179 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")
180}
181
182func TestHandler_PutManifest_MissingContentType(t *testing.T) {
183 registrySvc := inmocks.NewMockRegistryService(t)
184
185 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
186
187 manifestData := []byte(`{"schemaVersion": 2}`)
188
189 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
190 // No Content-Type header
191 rec := httptest.NewRecorder()
192
193 handler.ServeHTTP(rec, req)
194
195 assert.Equal(t, http.StatusBadRequest, rec.Code)
196}
197
198func TestHandler_PutManifest_StorageError(t *testing.T) {
199 registrySvc := inmocks.NewMockRegistryService(t)
200
201 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
202
203 manifestData := []byte(`{"schemaVersion": 2}`)
204 registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError)
205
206 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
207 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
208 rec := httptest.NewRecorder()
209
210 handler.ServeHTTP(rec, req)
211
212 assert.Equal(t, http.StatusInternalServerError, rec.Code)
213}
214
215func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
216 registrySvc := inmocks.NewMockRegistryService(t)
217
218 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
219
220 req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)
221 rec := httptest.NewRecorder()
222
223 handler.ServeHTTP(rec, req)
224
225 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
226}
227
228func TestHandler_GetBlob_Success(t *testing.T) {
229 registrySvc := inmocks.NewMockRegistryService(t)
230
231 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
232 server := httptest.NewServer(handler)
233 defer server.Close()
234
235 // Create a temp file for http.ServeFile
236 tmpFile, err := os.CreateTemp("", "blob-*")
237 assert.NoError(t, err)
238 defer tmpFile.Close()
239
240 blobContent := []byte("blob content here")
241 _, err = tmpFile.Write(blobContent)
242 assert.NoError(t, err)
243
244 registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
245
246 resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
247 require.NoError(t, err)
248 defer resp.Body.Close()
249 body, err := io.ReadAll(resp.Body)
250 require.NoError(t, err)
251
252 assert.Equal(t, http.StatusOK, resp.StatusCode)
253 assert.Equal(t, blobContent, body)
254}
255
256func TestHandler_GetBlob_NotFound(t *testing.T) {
257 registrySvc := inmocks.NewMockRegistryService(t)
258
259 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
260 server := httptest.NewServer(handler)
261 defer server.Close()
262
263 registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
264
265 resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000")
266 require.NoError(t, err)
267 defer resp.Body.Close()
268
269 assert.Equal(t, http.StatusNotFound, resp.StatusCode)
270}
271
272func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
273 registrySvc := inmocks.NewMockRegistryService(t)
274
275 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
276
277 req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
278 rec := httptest.NewRecorder()
279
280 handler.ServeHTTP(rec, req)
281
282 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
283 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
284}
285
286func TestHandler_StartBlobUpload_Success(t *testing.T) {
287 registrySvc := inmocks.NewMockRegistryService(t)
288
289 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
290
291 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
292
293 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
294 rec := httptest.NewRecorder()
295
296 handler.ServeHTTP(rec, req)
297
298 assert.Equal(t, http.StatusAccepted, rec.Code)
299 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000")
300 assert.Equal(t, "0-0", rec.Header().Get("Range"))
301}
302
303func TestHandler_StartBlobUpload_Error(t *testing.T) {
304 registrySvc := inmocks.NewMockRegistryService(t)
305
306 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
307
308 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)
309
310 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
311 rec := httptest.NewRecorder()
312
313 handler.ServeHTTP(rec, req)
314
315 assert.Equal(t, http.StatusInternalServerError, rec.Code)
316 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
317}
318
319func TestHandler_BlobUpload_PATCH(t *testing.T) {
320 registrySvc := inmocks.NewMockRegistryService(t)
321
322 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
323
324 chunkData := []byte("chunk content")
325 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
326
327 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
328 rec := httptest.NewRecorder()
329
330 handler.ServeHTTP(rec, req)
331
332 assert.Equal(t, http.StatusAccepted, rec.Code)
333 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000")
334}
335
336func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
337 registrySvc := inmocks.NewMockRegistryService(t)
338
339 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
340
341 chunkData := []byte("final chunk")
342 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
343 registrySvc.EXPECT().FinishUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
344
345 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
346 rec := httptest.NewRecorder()
347
348 handler.ServeHTTP(rec, req)
349
350 assert.Equal(t, http.StatusCreated, rec.Code)
351 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
352 assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest"))
353}
354
355func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
356 registrySvc := inmocks.NewMockRegistryService(t)
357
358 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
359
360 chunkData := []byte("final chunk")
361 // Note: Invalid digest format is rejected by validation before reaching storage layer
362 // This test verifies that invalid digests are properly rejected
363
364 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:wrong", bytes.NewReader(chunkData))
365 rec := httptest.NewRecorder()
366
367 handler.ServeHTTP(rec, req)
368
369 assert.Equal(t, http.StatusBadRequest, rec.Code)
370 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
371}
372
373func TestHandler_ListTags_Success(t *testing.T) {
374 registrySvc := inmocks.NewMockRegistryService(t)
375
376 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
377
378 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
379
380 req := httptest.NewRequest("GET", "/v2/myapp/tags/list", nil)
381 rec := httptest.NewRecorder()
382
383 handler.ServeHTTP(rec, req)
384
385 assert.Equal(t, http.StatusOK, rec.Code)
386 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
387
388 var response struct {
389 Name string `json:"name"`
390 Tags []string `json:"tags"`
391 }
392 assert.NoError(t, json.NewDecoder(rec.Body).Decode(&response))
393 assert.Equal(t, "myapp", response.Name)
394 assert.Contains(t, response.Tags, "latest")
395}
396
397func TestHandler_ListTags_NotFound(t *testing.T) {
398 registrySvc := inmocks.NewMockRegistryService(t)
399
400 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
401
402 registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)
403
404 req := httptest.NewRequest("GET", "/v2/notexists/tags/list", nil)
405 rec := httptest.NewRecorder()
406
407 handler.ServeHTTP(rec, req)
408
409 assert.Equal(t, http.StatusNotFound, rec.Code)
410 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
411}
412
413func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
414 registrySvc := inmocks.NewMockRegistryService(t)
415
416 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
417
418 req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)
419 rec := httptest.NewRecorder()
420
421 handler.ServeHTTP(rec, req)
422
423 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
424 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
425}
426
427func TestHandler_RegisterRoutes(t *testing.T) {
428 registrySvc := inmocks.NewMockRegistryService(t)
429
430 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
431
432 mux := http.NewServeMux()
433 handler.RegisterRoutes(mux)
434
435 // Verify routes are registered by making a request through the mux
436 req := httptest.NewRequest("GET", "/v2/", nil)
437 rec := httptest.NewRecorder()
438
439 mux.ServeHTTP(rec, req)
440
441 assert.Equal(t, http.StatusOK, rec.Code)
442}
443
444func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) {
445 registrySvc := inmocks.NewMockRegistryService(t)
446 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
447
448 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
449
450 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
451 rec := httptest.NewRecorder()
452
453 handler.ServeHTTP(rec, req)
454
455 assert.Equal(t, http.StatusAccepted, rec.Code)
456 assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
457}
458
459func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) {
460 registrySvc := inmocks.NewMockRegistryService(t)
461 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
462
463 chunkData := []byte("chunk content")
464 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
465
466 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
467 rec := httptest.NewRecorder()
468
469 handler.ServeHTTP(rec, req)
470
471 assert.Equal(t, http.StatusAccepted, rec.Code)
472 assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
473}
474
475func TestDefaultMaxBlobChunkSize_Is95MB(t *testing.T) {
476 assert.Equal(t, int64(95*1024*1024), int64(DefaultMaxBlobChunkSize))
477}
478
479func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing.T) {
480 registrySvc := inmocks.NewMockRegistryService(t)
481 handler := NewHandler(registrySvc, testLogger(), 5)
482
483 // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk.
484 // The mock must read the body to trigger MaxBytesError, which the handler detects.
485 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).
486 Run(func(_ context.Context, _ string, _ string, data io.Reader, _ int64, _ int64) {
487 // Drain the reader to trigger MaxBytesError after the storage layer may
488 // have written the limit-sized prefix.
489 _, _ = io.ReadAll(data)
490 }).
491 Return(int64(0), &http.MaxBytesError{Limit: 5})
492 registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil)
493
494 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("chunk content")))
495 rec := httptest.NewRecorder()
496
497 handler.ServeHTTP(rec, req)
498
499 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
500}
501
502func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) {
503 registrySvc := inmocks.NewMockRegistryService(t)
504 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
505
506 chunkData := []byte("chunk content")
507 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
508
509 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
510 rec := httptest.NewRecorder()
511
512 handler.ServeHTTP(rec, req)
513
514 assert.Equal(t, http.StatusAccepted, rec.Code)
515 // OCI spec: Range header should reflect total received bytes
516 assert.Equal(t, "0-12", rec.Header().Get("Range"))
517}
518
519func TestDefaultMaxBlobSize_Is1GB(t *testing.T) {
520 assert.Equal(t, int64(1024*1024*1024), int64(DefaultMaxBlobSize))
521}
522
523func TestHandler_BlobUpload_PATCH_MaxBlobSizeExceededReturnsSizeInvalidAndCancels(t *testing.T) {
524 registrySvc := inmocks.NewMockRegistryService(t)
525 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
526
527 registrySvc.EXPECT().AppendBlobChunk(
528 mock.Anything,
529 "myapp",
530 "550e8400-e29b-41d4-a716-446655440000",
531 mock.Anything,
532 int64(6),
533 int64(10),
534 ).Return(int64(0), domain.ErrBlobSizeExceeded)
535 registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil)
536
537 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("123456")))
538 rec := httptest.NewRecorder()
539
540 handler.ServeHTTP(rec, req)
541
542 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
543 assert.Contains(t, rec.Body.String(), "SIZE_INVALID")
544}
545
546func TestHandler_BlobUpload_PATCH_ContentLengthTooLargeReturnsSizeInvalidAndCancels(t *testing.T) {
547 registrySvc := inmocks.NewMockRegistryService(t)
548 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
549
550 registrySvc.EXPECT().AppendBlobChunk(
551 mock.Anything,
552 "myapp",
553 "550e8400-e29b-41d4-a716-446655440000",
554 mock.Anything,
555 int64(11),
556 int64(10),
557 ).Return(int64(0), domain.ErrBlobSizeExceeded)
558 registrySvc.EXPECT().CancelUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000").Return(nil)
559
560 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("12345678901")))
561 rec := httptest.NewRecorder()
562
563 handler.ServeHTTP(rec, req)
564
565 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
566 assert.Contains(t, rec.Body.String(), "SIZE_INVALID")
567}
568
569func TestHandler_BlobUpload_PUT_FinalizeUnderMaxBlobSize(t *testing.T) {
570 registrySvc := inmocks.NewMockRegistryService(t)
571 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
572
573 chunkData := []byte("1234567890")
574 registrySvc.EXPECT().AppendBlobChunk(
575 mock.Anything,
576 "myapp",
577 "550e8400-e29b-41d4-a716-446655440000",
578 mock.Anything,
579 int64(len(chunkData)),
580 int64(10),
581 ).Return(int64(len(chunkData)), nil)
582 registrySvc.EXPECT().FinishUpload(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
583
584 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
585 rec := httptest.NewRecorder()
586
587 handler.ServeHTTP(rec, req)
588
589 assert.Equal(t, http.StatusCreated, rec.Code)
590 assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest"))
591}