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 "github.com/bnema/gordon/internal/boundaries/in/mocks"
16 "github.com/bnema/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}