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}