v2.4.0 79b3461

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

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