v3.3.0 b164fe9

handler_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close