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}