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