v2.7.1 b3a38c8

handler_test.go RawHistoryFolder

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

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 8

Tags 106

↑↓ moveenter openesc close