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
137func TestHandler_PutManifest_Success(t *testing.T) {
138 registrySvc := inmocks.NewMockRegistryService(t)
139
140 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
141
142 manifestData := []byte(`{"schemaVersion": 2}`)
143 registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {
144 return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"
145 })).Return("sha256:abc123", nil)
146
147 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
148 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
149 rec := httptest.NewRecorder()
150
151 handler.ServeHTTP(rec, req)
152
153 assert.Equal(t, http.StatusCreated, rec.Code)
154 assert.Equal(t, "sha256:abc123", rec.Header().Get("Docker-Content-Digest"))
155 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")
156}
157
158func TestHandler_PutManifest_MissingContentType(t *testing.T) {
159 registrySvc := inmocks.NewMockRegistryService(t)
160
161 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
162
163 manifestData := []byte(`{"schemaVersion": 2}`)
164
165 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
166 // No Content-Type header
167 rec := httptest.NewRecorder()
168
169 handler.ServeHTTP(rec, req)
170
171 assert.Equal(t, http.StatusBadRequest, rec.Code)
172}
173
174func TestHandler_PutManifest_StorageError(t *testing.T) {
175 registrySvc := inmocks.NewMockRegistryService(t)
176
177 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
178
179 manifestData := []byte(`{"schemaVersion": 2}`)
180 registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError)
181
182 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
183 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
184 rec := httptest.NewRecorder()
185
186 handler.ServeHTTP(rec, req)
187
188 assert.Equal(t, http.StatusInternalServerError, rec.Code)
189}
190
191func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
192 registrySvc := inmocks.NewMockRegistryService(t)
193
194 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
195
196 req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)
197 rec := httptest.NewRecorder()
198
199 handler.ServeHTTP(rec, req)
200
201 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
202}
203
204func TestHandler_GetBlob_Success(t *testing.T) {
205 registrySvc := inmocks.NewMockRegistryService(t)
206
207 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
208 server := httptest.NewServer(handler)
209 defer server.Close()
210
211 // Create a temp file for http.ServeFile
212 tmpFile, err := os.CreateTemp("", "blob-*")
213 assert.NoError(t, err)
214 defer tmpFile.Close()
215
216 blobContent := []byte("blob content here")
217 _, err = tmpFile.Write(blobContent)
218 assert.NoError(t, err)
219
220 registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
221
222 resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
223 require.NoError(t, err)
224 defer resp.Body.Close()
225 body, err := io.ReadAll(resp.Body)
226 require.NoError(t, err)
227
228 assert.Equal(t, http.StatusOK, resp.StatusCode)
229 assert.Equal(t, blobContent, body)
230}
231
232func TestHandler_GetBlob_NotFound(t *testing.T) {
233 registrySvc := inmocks.NewMockRegistryService(t)
234
235 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
236 server := httptest.NewServer(handler)
237 defer server.Close()
238
239 registrySvc.EXPECT().GetBlobPath(mock.Anything, "myapp", "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
240
241 resp, err := server.Client().Get(server.URL + "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000")
242 require.NoError(t, err)
243 defer resp.Body.Close()
244
245 assert.Equal(t, http.StatusNotFound, resp.StatusCode)
246}
247
248func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
249 registrySvc := inmocks.NewMockRegistryService(t)
250
251 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
252
253 req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
254 rec := httptest.NewRecorder()
255
256 handler.ServeHTTP(rec, req)
257
258 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
259 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
260}
261
262func TestHandler_StartBlobUpload_Success(t *testing.T) {
263 registrySvc := inmocks.NewMockRegistryService(t)
264
265 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
266
267 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
268
269 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
270 rec := httptest.NewRecorder()
271
272 handler.ServeHTTP(rec, req)
273
274 assert.Equal(t, http.StatusAccepted, rec.Code)
275 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000")
276 assert.Equal(t, "0-0", rec.Header().Get("Range"))
277}
278
279func TestHandler_StartBlobUpload_Error(t *testing.T) {
280 registrySvc := inmocks.NewMockRegistryService(t)
281
282 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
283
284 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)
285
286 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
287 rec := httptest.NewRecorder()
288
289 handler.ServeHTTP(rec, req)
290
291 assert.Equal(t, http.StatusInternalServerError, rec.Code)
292 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
293}
294
295func TestHandler_BlobUpload_PATCH(t *testing.T) {
296 registrySvc := inmocks.NewMockRegistryService(t)
297
298 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
299
300 chunkData := []byte("chunk content")
301 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
302
303 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
304 rec := httptest.NewRecorder()
305
306 handler.ServeHTTP(rec, req)
307
308 assert.Equal(t, http.StatusAccepted, rec.Code)
309 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000")
310}
311
312func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
313 registrySvc := inmocks.NewMockRegistryService(t)
314
315 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
316
317 chunkData := []byte("final chunk")
318 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
319 registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
320
321 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
322 rec := httptest.NewRecorder()
323
324 handler.ServeHTTP(rec, req)
325
326 assert.Equal(t, http.StatusCreated, rec.Code)
327 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
328 assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest"))
329}
330
331func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
332 registrySvc := inmocks.NewMockRegistryService(t)
333
334 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
335
336 chunkData := []byte("final chunk")
337 // Note: Invalid digest format is rejected by validation before reaching storage layer
338 // This test verifies that invalid digests are properly rejected
339
340 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:wrong", bytes.NewReader(chunkData))
341 rec := httptest.NewRecorder()
342
343 handler.ServeHTTP(rec, req)
344
345 assert.Equal(t, http.StatusBadRequest, rec.Code)
346 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
347}
348
349func TestHandler_ListTags_Success(t *testing.T) {
350 registrySvc := inmocks.NewMockRegistryService(t)
351
352 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
353
354 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
355
356 req := httptest.NewRequest("GET", "/v2/myapp/tags/list", nil)
357 rec := httptest.NewRecorder()
358
359 handler.ServeHTTP(rec, req)
360
361 assert.Equal(t, http.StatusOK, rec.Code)
362 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
363
364 var response struct {
365 Name string `json:"name"`
366 Tags []string `json:"tags"`
367 }
368 assert.NoError(t, json.NewDecoder(rec.Body).Decode(&response))
369 assert.Equal(t, "myapp", response.Name)
370 assert.Contains(t, response.Tags, "latest")
371}
372
373func TestHandler_ListTags_NotFound(t *testing.T) {
374 registrySvc := inmocks.NewMockRegistryService(t)
375
376 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
377
378 registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)
379
380 req := httptest.NewRequest("GET", "/v2/notexists/tags/list", nil)
381 rec := httptest.NewRecorder()
382
383 handler.ServeHTTP(rec, req)
384
385 assert.Equal(t, http.StatusNotFound, rec.Code)
386 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
387}
388
389func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
390 registrySvc := inmocks.NewMockRegistryService(t)
391
392 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
393
394 req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)
395 rec := httptest.NewRecorder()
396
397 handler.ServeHTTP(rec, req)
398
399 assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
400 assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
401}
402
403func TestHandler_RegisterRoutes(t *testing.T) {
404 registrySvc := inmocks.NewMockRegistryService(t)
405
406 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
407
408 mux := http.NewServeMux()
409 handler.RegisterRoutes(mux)
410
411 // Verify routes are registered by making a request through the mux
412 req := httptest.NewRequest("GET", "/v2/", nil)
413 rec := httptest.NewRecorder()
414
415 mux.ServeHTTP(rec, req)
416
417 assert.Equal(t, http.StatusOK, rec.Code)
418}
419
420func TestHandler_StartBlobUpload_ReturnsDockerUploadUUID(t *testing.T) {
421 registrySvc := inmocks.NewMockRegistryService(t)
422 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
423
424 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("550e8400-e29b-41d4-a716-446655440000", nil)
425
426 req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
427 rec := httptest.NewRecorder()
428
429 handler.ServeHTTP(rec, req)
430
431 assert.Equal(t, http.StatusAccepted, rec.Code)
432 assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
433}
434
435func TestHandler_BlobUpload_PATCH_ReturnsDockerUploadUUID(t *testing.T) {
436 registrySvc := inmocks.NewMockRegistryService(t)
437 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
438
439 chunkData := []byte("chunk content")
440 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
441
442 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
443 rec := httptest.NewRecorder()
444
445 handler.ServeHTTP(rec, req)
446
447 assert.Equal(t, http.StatusAccepted, rec.Code)
448 assert.Equal(t, "550e8400-e29b-41d4-a716-446655440000", rec.Header().Get("Docker-Upload-UUID"))
449}
450
451func TestDefaultMaxBlobChunkSize_Is95MB(t *testing.T) {
452 assert.Equal(t, int64(95*1024*1024), int64(DefaultMaxBlobChunkSize))
453}
454
455func TestHandler_BlobUpload_PATCH_RespectsConfiguredMaxBlobChunkSize(t *testing.T) {
456 registrySvc := inmocks.NewMockRegistryService(t)
457 handler := NewHandler(registrySvc, testLogger(), 5)
458
459 // The handler wraps r.Body in MaxBytesReader and passes it to AppendBlobChunk.
460 // The mock must read the body to trigger MaxBytesError, which the handler detects.
461 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).
462 Run(func(_ context.Context, _ string, _ string, data io.Reader, _ int64, _ int64) {
463 // Drain the reader to trigger MaxBytesError after the storage layer may
464 // have written the limit-sized prefix.
465 _, _ = io.ReadAll(data)
466 }).
467 Return(int64(0), &http.MaxBytesError{Limit: 5})
468 registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil)
469
470 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("chunk content")))
471 rec := httptest.NewRecorder()
472
473 handler.ServeHTTP(rec, req)
474
475 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
476}
477
478func TestHandler_BlobUpload_PATCH_ReturnsRangeHeader(t *testing.T) {
479 registrySvc := inmocks.NewMockRegistryService(t)
480 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize)
481
482 chunkData := []byte("chunk content")
483 registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "550e8400-e29b-41d4-a716-446655440000", mock.Anything, mock.Anything, mock.Anything).Return(int64(len(chunkData)), nil)
484
485 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader(chunkData))
486 rec := httptest.NewRecorder()
487
488 handler.ServeHTTP(rec, req)
489
490 assert.Equal(t, http.StatusAccepted, rec.Code)
491 // OCI spec: Range header should reflect total received bytes
492 assert.Equal(t, "0-12", rec.Header().Get("Range"))
493}
494
495func TestDefaultMaxBlobSize_Is1GB(t *testing.T) {
496 assert.Equal(t, int64(1024*1024*1024), int64(DefaultMaxBlobSize))
497}
498
499func TestHandler_BlobUpload_PATCH_MaxBlobSizeExceededReturnsSizeInvalidAndCancels(t *testing.T) {
500 registrySvc := inmocks.NewMockRegistryService(t)
501 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
502
503 registrySvc.EXPECT().AppendBlobChunk(
504 mock.Anything,
505 "myapp",
506 "550e8400-e29b-41d4-a716-446655440000",
507 mock.Anything,
508 int64(6),
509 int64(10),
510 ).Return(int64(0), domain.ErrBlobSizeExceeded)
511 registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil)
512
513 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("123456")))
514 rec := httptest.NewRecorder()
515
516 handler.ServeHTTP(rec, req)
517
518 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
519 assert.Contains(t, rec.Body.String(), "SIZE_INVALID")
520}
521
522func TestHandler_BlobUpload_PATCH_ContentLengthTooLargeReturnsSizeInvalidAndCancels(t *testing.T) {
523 registrySvc := inmocks.NewMockRegistryService(t)
524 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
525
526 registrySvc.EXPECT().AppendBlobChunk(
527 mock.Anything,
528 "myapp",
529 "550e8400-e29b-41d4-a716-446655440000",
530 mock.Anything,
531 int64(11),
532 int64(10),
533 ).Return(int64(0), domain.ErrBlobSizeExceeded)
534 registrySvc.EXPECT().CancelUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000").Return(nil)
535
536 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000", bytes.NewReader([]byte("12345678901")))
537 rec := httptest.NewRecorder()
538
539 handler.ServeHTTP(rec, req)
540
541 assert.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
542 assert.Contains(t, rec.Body.String(), "SIZE_INVALID")
543}
544
545func TestHandler_BlobUpload_PUT_FinalizeUnderMaxBlobSize(t *testing.T) {
546 registrySvc := inmocks.NewMockRegistryService(t)
547 handler := NewHandler(registrySvc, testLogger(), DefaultMaxBlobChunkSize, 10)
548
549 chunkData := []byte("1234567890")
550 registrySvc.EXPECT().AppendBlobChunk(
551 mock.Anything,
552 "myapp",
553 "550e8400-e29b-41d4-a716-446655440000",
554 mock.Anything,
555 int64(len(chunkData)),
556 int64(10),
557 ).Return(int64(len(chunkData)), nil)
558 registrySvc.EXPECT().FinishUpload(mock.Anything, "550e8400-e29b-41d4-a716-446655440000", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
559
560 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/550e8400-e29b-41d4-a716-446655440000?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
561 rec := httptest.NewRecorder()
562
563 handler.ServeHTTP(rec, req)
564
565 assert.Equal(t, http.StatusCreated, rec.Code)
566 assert.Equal(t, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", rec.Header().Get("Docker-Content-Digest"))
567}