v2.31.0 e02f654

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
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}

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