v2.1.0 0fb867d

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:abc123").Return(tmpFile.Name(), nil)
237
238	req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:abc123", 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:notexists").Return("", assert.AnError)
255
256	req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:notexists", 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:abc123", 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("upload-uuid-123", 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/upload-uuid-123")
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", "upload-uuid-123", chunkData).Return(int64(len(chunkData)), nil)
324
325	req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/upload-uuid-123", 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/upload-uuid-123")
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", "upload-uuid-123", chunkData).Return(int64(len(chunkData)), nil)
343	registrySvc.EXPECT().FinishUpload(mock.Anything, "upload-uuid-123", "sha256:abc123").Return(nil)
344
345	req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/upload-uuid-123?digest=sha256:abc123", 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:abc123")
352	assert.Equal(t, "sha256:abc123", 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	blobStorage.EXPECT().AppendBlobChunk("myapp", "upload-uuid-123", chunkData).Return(int64(len(chunkData)), nil)
364	registrySvc.EXPECT().FinishUpload(mock.Anything, "upload-uuid-123", "sha256:wrong").Return(assert.AnError)
365	registrySvc.EXPECT().CancelUpload(mock.Anything, "upload-uuid-123").Return(nil)
366
367	req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/upload-uuid-123?digest=sha256:wrong", bytes.NewReader(chunkData))
368	rec := httptest.NewRecorder()
369
370	handler.ServeHTTP(rec, req)
371
372	assert.Equal(t, http.StatusBadRequest, rec.Code)
373}
374
375func TestHandler_ListTags_Success(t *testing.T) {
376	registrySvc := inmocks.NewMockRegistryService(t)
377	blobStorage := mocks.NewMockBlobStorage(t)
378	eventBus := mocks.NewMockEventPublisher(t)
379
380	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
381
382	registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
383
384	req := httptest.NewRequest("GET", "/v2/myapp/tags/list", nil)
385	rec := httptest.NewRecorder()
386
387	handler.ServeHTTP(rec, req)
388
389	assert.Equal(t, http.StatusOK, rec.Code)
390	assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
391	assert.Contains(t, rec.Body.String(), "myapp")
392	assert.Contains(t, rec.Body.String(), "latest")
393}
394
395func TestHandler_ListTags_NotFound(t *testing.T) {
396	registrySvc := inmocks.NewMockRegistryService(t)
397	blobStorage := mocks.NewMockBlobStorage(t)
398	eventBus := mocks.NewMockEventPublisher(t)
399
400	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
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}
411
412func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
413	registrySvc := inmocks.NewMockRegistryService(t)
414	blobStorage := mocks.NewMockBlobStorage(t)
415	eventBus := mocks.NewMockEventPublisher(t)
416
417	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
418
419	req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)
420	rec := httptest.NewRecorder()
421
422	handler.ServeHTTP(rec, req)
423
424	assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
425}
426
427func TestHandler_RegisterRoutes(t *testing.T) {
428	registrySvc := inmocks.NewMockRegistryService(t)
429	blobStorage := mocks.NewMockBlobStorage(t)
430	eventBus := mocks.NewMockEventPublisher(t)
431
432	handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
433
434	mux := http.NewServeMux()
435	handler.RegisterRoutes(mux)
436
437	// Verify routes are registered by making a request through the mux
438	req := httptest.NewRequest("GET", "/v2/", nil)
439	rec := httptest.NewRecorder()
440
441	mux.ServeHTTP(rec, req)
442
443	assert.Equal(t, http.StatusOK, rec.Code)
444}

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