v2.2.0 64d39bf

handler_test.go RawHistoryFolder

1package registry
2
3import (
4	"bytes"
5	"net/http"
6	"net/http/httptest"
7	"os"
8	"testing"
9
10	"github.com/bnema/zerowrap"
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/mock"
13
14	inmocks "gordon/internal/boundaries/in/mocks"
15	"gordon/internal/boundaries/out/mocks"
16	"gordon/internal/domain"
17)
18
19func testLogger() zerowrap.Logger {
20	return zerowrap.Default()
21}
22
23func TestHandler_Base(t *testing.T) {
24	registrySvc := inmocks.NewMockRegistryService(t)
25	blobStorage := mocks.NewMockBlobStorage(t)
26	eventBus := mocks.NewMockEventPublisher(t)
27
28	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
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	blobStorage := mocks.NewMockBlobStorage(t)
42	eventBus := mocks.NewMockEventPublisher(t)
43
44	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
45
46	req := httptest.NewRequest("GET", "/v2/unknown/path", nil)
47	rec := httptest.NewRecorder()
48
49	handler.ServeHTTP(rec, req)
50
51	assert.Equal(t, http.StatusNotFound, rec.Code)
52}
53
54func TestHandler_GetManifest_Success(t *testing.T) {
55	registrySvc := inmocks.NewMockRegistryService(t)
56	blobStorage := mocks.NewMockBlobStorage(t)
57	eventBus := mocks.NewMockEventPublisher(t)
58
59	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
60
61	manifestData := []byte(`{"schemaVersion": 2}`)
62	registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
63		Name:        "myapp",
64		Reference:   "latest",
65		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
66		Data:        manifestData,
67	}, nil)
68
69	req := httptest.NewRequest("GET", "/v2/myapp/manifests/latest", nil)
70	rec := httptest.NewRecorder()
71
72	handler.ServeHTTP(rec, req)
73
74	assert.Equal(t, http.StatusOK, rec.Code)
75	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type"))
76	assert.Equal(t, manifestData, rec.Body.Bytes())
77}
78
79func TestHandler_GetManifest_HEAD(t *testing.T) {
80	registrySvc := inmocks.NewMockRegistryService(t)
81	blobStorage := mocks.NewMockBlobStorage(t)
82	eventBus := mocks.NewMockEventPublisher(t)
83
84	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
85
86	manifestData := []byte(`{"schemaVersion": 2}`)
87	registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
88		Name:        "myapp",
89		Reference:   "latest",
90		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
91		Data:        manifestData,
92	}, nil)
93
94	req := httptest.NewRequest("HEAD", "/v2/myapp/manifests/latest", nil)
95	rec := httptest.NewRecorder()
96
97	handler.ServeHTTP(rec, req)
98
99	assert.Equal(t, http.StatusOK, rec.Code)
100	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", rec.Header().Get("Content-Type"))
101	// HEAD should not return body
102	assert.Empty(t, rec.Body.Bytes())
103}
104
105func TestHandler_GetManifest_NotFound(t *testing.T) {
106	registrySvc := inmocks.NewMockRegistryService(t)
107	blobStorage := mocks.NewMockBlobStorage(t)
108	eventBus := mocks.NewMockEventPublisher(t)
109
110	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
111
112	registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError)
113
114	req := httptest.NewRequest("GET", "/v2/myapp/manifests/notexists", nil)
115	rec := httptest.NewRecorder()
116
117	handler.ServeHTTP(rec, req)
118
119	assert.Equal(t, http.StatusNotFound, rec.Code)
120}
121
122func TestHandler_GetManifest_NestedName(t *testing.T) {
123	registrySvc := inmocks.NewMockRegistryService(t)
124	blobStorage := mocks.NewMockBlobStorage(t)
125	eventBus := mocks.NewMockEventPublisher(t)
126
127	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
128
129	manifestData := []byte(`{"schemaVersion": 2}`)
130	registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{
131		Name:        "org/project/app",
132		Reference:   "v1.0",
133		ContentType: "application/vnd.docker.distribution.manifest.v2+json",
134		Data:        manifestData,
135	}, nil)
136
137	req := httptest.NewRequest("GET", "/v2/org/project/app/manifests/v1.0", nil)
138	rec := httptest.NewRecorder()
139
140	handler.ServeHTTP(rec, req)
141
142	assert.Equal(t, http.StatusOK, rec.Code)
143}
144
145func TestHandler_PutManifest_Success(t *testing.T) {
146	registrySvc := inmocks.NewMockRegistryService(t)
147	blobStorage := mocks.NewMockBlobStorage(t)
148	eventBus := mocks.NewMockEventPublisher(t)
149
150	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
151
152	manifestData := []byte(`{"schemaVersion": 2}`)
153	registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {
154		return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"
155	})).Return(nil)
156
157	req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
158	req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
159	rec := httptest.NewRecorder()
160
161	handler.ServeHTTP(rec, req)
162
163	assert.Equal(t, http.StatusCreated, rec.Code)
164	assert.NotEmpty(t, rec.Header().Get("Docker-Content-Digest"))
165	assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")
166}
167
168func TestHandler_PutManifest_MissingContentType(t *testing.T) {
169	registrySvc := inmocks.NewMockRegistryService(t)
170	blobStorage := mocks.NewMockBlobStorage(t)
171	eventBus := mocks.NewMockEventPublisher(t)
172
173	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
174
175	manifestData := []byte(`{"schemaVersion": 2}`)
176
177	req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
178	// No Content-Type header
179	rec := httptest.NewRecorder()
180
181	handler.ServeHTTP(rec, req)
182
183	assert.Equal(t, http.StatusBadRequest, rec.Code)
184}
185
186func TestHandler_PutManifest_StorageError(t *testing.T) {
187	registrySvc := inmocks.NewMockRegistryService(t)
188	blobStorage := mocks.NewMockBlobStorage(t)
189	eventBus := mocks.NewMockEventPublisher(t)
190
191	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
192
193	manifestData := []byte(`{"schemaVersion": 2}`)
194	registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return(assert.AnError)
195
196	req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
197	req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
198	rec := httptest.NewRecorder()
199
200	handler.ServeHTTP(rec, req)
201
202	assert.Equal(t, http.StatusInternalServerError, rec.Code)
203}
204
205func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
206	registrySvc := inmocks.NewMockRegistryService(t)
207	blobStorage := mocks.NewMockBlobStorage(t)
208	eventBus := mocks.NewMockEventPublisher(t)
209
210	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
211
212	req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)
213	rec := httptest.NewRecorder()
214
215	handler.ServeHTTP(rec, req)
216
217	assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
218}
219
220func TestHandler_GetBlob_Success(t *testing.T) {
221	registrySvc := inmocks.NewMockRegistryService(t)
222	blobStorage := mocks.NewMockBlobStorage(t)
223	eventBus := mocks.NewMockEventPublisher(t)
224
225	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
226
227	// Create a temp file for http.ServeFile
228	tmpFile, err := os.CreateTemp("", "blob-*")
229	assert.NoError(t, err)
230	defer tmpFile.Close()
231
232	blobContent := []byte("blob content here")
233	_, err = tmpFile.Write(blobContent)
234	assert.NoError(t, err)
235
236	blobStorage.EXPECT().GetBlobPath("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
237
238	req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
239	rec := httptest.NewRecorder()
240
241	handler.ServeHTTP(rec, req)
242
243	assert.Equal(t, http.StatusOK, rec.Code)
244	assert.Equal(t, blobContent, rec.Body.Bytes())
245}
246
247func TestHandler_GetBlob_NotFound(t *testing.T) {
248	registrySvc := inmocks.NewMockRegistryService(t)
249	blobStorage := mocks.NewMockBlobStorage(t)
250	eventBus := mocks.NewMockEventPublisher(t)
251
252	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
253
254	blobStorage.EXPECT().GetBlobPath("sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
255
256	req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000", nil)
257	rec := httptest.NewRecorder()
258
259	handler.ServeHTTP(rec, req)
260
261	assert.Equal(t, http.StatusNotFound, rec.Code)
262}
263
264func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
265	registrySvc := inmocks.NewMockRegistryService(t)
266	blobStorage := mocks.NewMockBlobStorage(t)
267	eventBus := mocks.NewMockEventPublisher(t)
268
269	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
270
271	req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
272	rec := httptest.NewRecorder()
273
274	handler.ServeHTTP(rec, req)
275
276	assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
277}
278
279func TestHandler_StartBlobUpload_Success(t *testing.T) {
280	registrySvc := inmocks.NewMockRegistryService(t)
281	blobStorage := mocks.NewMockBlobStorage(t)
282	eventBus := mocks.NewMockEventPublisher(t)
283
284	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
285
286	registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil)
287
288	req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
289	rec := httptest.NewRecorder()
290
291	handler.ServeHTTP(rec, req)
292
293	assert.Equal(t, http.StatusAccepted, rec.Code)
294	assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp")
295	assert.Equal(t, "0-0", rec.Header().Get("Range"))
296}
297
298func TestHandler_StartBlobUpload_Error(t *testing.T) {
299	registrySvc := inmocks.NewMockRegistryService(t)
300	blobStorage := mocks.NewMockBlobStorage(t)
301	eventBus := mocks.NewMockEventPublisher(t)
302
303	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
304
305	registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)
306
307	req := httptest.NewRequest("POST", "/v2/myapp/blobs/uploads/", nil)
308	rec := httptest.NewRecorder()
309
310	handler.ServeHTTP(rec, req)
311
312	assert.Equal(t, http.StatusInternalServerError, rec.Code)
313}
314
315func TestHandler_BlobUpload_PATCH(t *testing.T) {
316	registrySvc := inmocks.NewMockRegistryService(t)
317	blobStorage := mocks.NewMockBlobStorage(t)
318	eventBus := mocks.NewMockEventPublisher(t)
319
320	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
321
322	chunkData := []byte("chunk content")
323	blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
324
325	req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData))
326	rec := httptest.NewRecorder()
327
328	handler.ServeHTTP(rec, req)
329
330	assert.Equal(t, http.StatusAccepted, rec.Code)
331	assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/blobs/uploads/1234567890-myapp")
332}
333
334func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
335	registrySvc := inmocks.NewMockRegistryService(t)
336	blobStorage := mocks.NewMockBlobStorage(t)
337	eventBus := mocks.NewMockEventPublisher(t)
338
339	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
340
341	chunkData := []byte("final chunk")
342	blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
343	registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
344
345	req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?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	blobStorage := mocks.NewMockBlobStorage(t)
358	eventBus := mocks.NewMockEventPublisher(t)
359
360	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
361
362	chunkData := []byte("final chunk")
363	// Note: Invalid digest format is rejected by validation before reaching storage layer
364	// This test verifies that invalid digests are properly rejected
365
366	req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:wrong", bytes.NewReader(chunkData))
367	rec := httptest.NewRecorder()
368
369	handler.ServeHTTP(rec, req)
370
371	assert.Equal(t, http.StatusBadRequest, rec.Code)
372}
373
374func TestHandler_ListTags_Success(t *testing.T) {
375	registrySvc := inmocks.NewMockRegistryService(t)
376	blobStorage := mocks.NewMockBlobStorage(t)
377	eventBus := mocks.NewMockEventPublisher(t)
378
379	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
380
381	registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
382
383	req := httptest.NewRequest("GET", "/v2/myapp/tags/list", nil)
384	rec := httptest.NewRecorder()
385
386	handler.ServeHTTP(rec, req)
387
388	assert.Equal(t, http.StatusOK, rec.Code)
389	assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
390	assert.Contains(t, rec.Body.String(), "myapp")
391	assert.Contains(t, rec.Body.String(), "latest")
392}
393
394func TestHandler_ListTags_NotFound(t *testing.T) {
395	registrySvc := inmocks.NewMockRegistryService(t)
396	blobStorage := mocks.NewMockBlobStorage(t)
397	eventBus := mocks.NewMockEventPublisher(t)
398
399	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
400
401	registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)
402
403	req := httptest.NewRequest("GET", "/v2/notexists/tags/list", nil)
404	rec := httptest.NewRecorder()
405
406	handler.ServeHTTP(rec, req)
407
408	assert.Equal(t, http.StatusNotFound, rec.Code)
409}
410
411func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
412	registrySvc := inmocks.NewMockRegistryService(t)
413	blobStorage := mocks.NewMockBlobStorage(t)
414	eventBus := mocks.NewMockEventPublisher(t)
415
416	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
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}
425
426func TestHandler_RegisterRoutes(t *testing.T) {
427	registrySvc := inmocks.NewMockRegistryService(t)
428	blobStorage := mocks.NewMockBlobStorage(t)
429	eventBus := mocks.NewMockEventPublisher(t)
430
431	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
432
433	mux := http.NewServeMux()
434	handler.RegisterRoutes(mux)
435
436	// Verify routes are registered by making a request through the mux
437	req := httptest.NewRequest("GET", "/v2/", nil)
438	rec := httptest.NewRecorder()
439
440	mux.ServeHTTP(rec, req)
441
442	assert.Equal(t, http.StatusOK, rec.Code)
443}

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