1package registry
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "strings"
11 "testing"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15 "github.com/stretchr/testify/require"
16
17 "gordon/internal/config"
18 "gordon/internal/events"
19 registryMocks "gordon/internal/registry/mocks"
20)
21
22// MockEventBus is a mock implementation of events.EventBus
23type MockEventBus struct {
24 mock.Mock
25}
26
27func (m *MockEventBus) Publish(eventType events.EventType, payload interface{}) error {
28 args := m.Called(eventType, payload)
29 return args.Error(0)
30}
31
32func (m *MockEventBus) Subscribe(handler events.EventHandler) error {
33 args := m.Called(handler)
34 return args.Error(0)
35}
36
37func (m *MockEventBus) Unsubscribe(handler events.EventHandler) error {
38 args := m.Called(handler)
39 return args.Error(0)
40}
41
42func (m *MockEventBus) Start() error {
43 args := m.Called()
44 return args.Error(0)
45}
46
47func (m *MockEventBus) Stop() error {
48 args := m.Called()
49 return args.Error(0)
50}
51
52func TestNewServer(t *testing.T) {
53 mockEventBus := &MockEventBus{}
54 cfg := &config.Config{
55 Server: config.ServerConfig{
56 DataDir: t.TempDir(),
57 },
58 }
59
60 server, err := NewServer(cfg, mockEventBus)
61
62 require.NoError(t, err)
63 assert.NotNil(t, server)
64 assert.Equal(t, cfg, server.config)
65 assert.Equal(t, mockEventBus, server.eventBus)
66 assert.NotNil(t, server.storage)
67 assert.NotNil(t, server.mux)
68}
69
70func TestNewServer_StorageInitError(t *testing.T) {
71 mockEventBus := &MockEventBus{}
72 cfg := &config.Config{
73 Server: config.ServerConfig{
74 DataDir: "/invalid/path/that/cannot/be/created",
75 },
76 }
77
78 server, err := NewServer(cfg, mockEventBus)
79
80 assert.Error(t, err)
81 assert.Nil(t, server)
82 assert.Contains(t, err.Error(), "failed to initialize storage")
83}
84
85func TestServer_HandleBase(t *testing.T) {
86 server := createTestServer(t)
87
88 req := httptest.NewRequest("GET", "/v2/", nil)
89 w := httptest.NewRecorder()
90
91 server.handleBase(w, req)
92
93 assert.Equal(t, http.StatusOK, w.Code)
94 assert.Equal(t, "registry/2.0", w.Header().Get("Docker-Distribution-API-Version"))
95}
96
97func TestServer_HandleGetManifest_Success(t *testing.T) {
98 server := createTestServerWithMockStorage(t)
99 mockStorage := server.storage.(*registryMocks.MockStorage)
100
101 name := "myapp"
102 reference := "latest"
103 manifestData := []byte(`{"test": "manifest"}`)
104 contentType := "application/vnd.docker.distribution.manifest.v2+json"
105
106 mockStorage.On("GetManifest", name, reference).Return(manifestData, contentType, nil)
107
108 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
109 req.SetPathValue("name", name)
110 req.SetPathValue("reference", reference)
111 w := httptest.NewRecorder()
112
113 server.handleGetManifest(w, req)
114
115 assert.Equal(t, http.StatusOK, w.Code)
116 assert.Equal(t, contentType, w.Header().Get("Content-Type"))
117 assert.Equal(t, fmt.Sprintf("%d", len(manifestData)), w.Header().Get("Content-Length"))
118 assert.Equal(t, manifestData, w.Body.Bytes())
119 mockStorage.AssertExpectations(t)
120}
121
122func TestServer_HandleGetManifest_HEAD(t *testing.T) {
123 server := createTestServerWithMockStorage(t)
124 mockStorage := server.storage.(*registryMocks.MockStorage)
125
126 name := "myapp"
127 reference := "latest"
128 manifestData := []byte(`{"test": "manifest"}`)
129 contentType := "application/vnd.docker.distribution.manifest.v2+json"
130
131 mockStorage.On("GetManifest", name, reference).Return(manifestData, contentType, nil)
132
133 req := httptest.NewRequest("HEAD", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
134 req.SetPathValue("name", name)
135 req.SetPathValue("reference", reference)
136 w := httptest.NewRecorder()
137
138 server.handleGetManifest(w, req)
139
140 assert.Equal(t, http.StatusOK, w.Code)
141 assert.Equal(t, contentType, w.Header().Get("Content-Type"))
142 assert.Equal(t, fmt.Sprintf("%d", len(manifestData)), w.Header().Get("Content-Length"))
143 assert.Empty(t, w.Body.Bytes()) // HEAD should not return body
144 mockStorage.AssertExpectations(t)
145}
146
147func TestServer_HandleGetManifest_NotFound(t *testing.T) {
148 server := createTestServerWithMockStorage(t)
149 mockStorage := server.storage.(*registryMocks.MockStorage)
150
151 name := "nonexistent"
152 reference := "latest"
153
154 mockStorage.On("GetManifest", name, reference).Return(nil, "", fmt.Errorf("manifest not found"))
155
156 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), nil)
157 req.SetPathValue("name", name)
158 req.SetPathValue("reference", reference)
159 w := httptest.NewRecorder()
160
161 server.handleGetManifest(w, req)
162
163 assert.Equal(t, http.StatusNotFound, w.Code)
164 mockStorage.AssertExpectations(t)
165}
166
167func TestServer_HandlePutManifest_Success(t *testing.T) {
168 server := createTestServerWithMockStorage(t)
169 mockStorage := server.storage.(*registryMocks.MockStorage)
170 mockEventBus := server.eventBus.(*MockEventBus)
171
172 name := "myapp"
173 reference := "latest"
174 manifestData := []byte(`{"test": "manifest"}`)
175 contentType := "application/vnd.docker.distribution.manifest.v2+json"
176
177 mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(nil)
178 mockEventBus.On("Publish", events.ImagePushed, mock.MatchedBy(func(payload events.ImagePushedPayload) bool {
179 return payload.Name == name && payload.Reference == reference && bytes.Equal(payload.Manifest, manifestData)
180 })).Return(nil)
181
182 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
183 req.Header.Set("Content-Type", contentType)
184 req.SetPathValue("name", name)
185 req.SetPathValue("reference", reference)
186 w := httptest.NewRecorder()
187
188 server.handlePutManifest(w, req)
189
190 assert.Equal(t, http.StatusCreated, w.Code)
191 assert.Contains(t, w.Header().Get("Docker-Content-Digest"), "sha256:")
192 assert.Equal(t, fmt.Sprintf("/v2/%s/manifests/%s", name, reference), w.Header().Get("Location"))
193 mockStorage.AssertExpectations(t)
194 mockEventBus.AssertExpectations(t)
195}
196
197func TestServer_HandlePutManifest_NoContentType(t *testing.T) {
198 server := createTestServerWithMockStorage(t)
199
200 name := "myapp"
201 reference := "latest"
202 manifestData := []byte(`{"test": "manifest"}`)
203
204 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
205 // No Content-Type header
206 req.SetPathValue("name", name)
207 req.SetPathValue("reference", reference)
208 w := httptest.NewRecorder()
209
210 server.handlePutManifest(w, req)
211
212 assert.Equal(t, http.StatusBadRequest, w.Code)
213 assert.Contains(t, w.Body.String(), "Content-Type header required")
214}
215
216func TestServer_HandlePutManifest_StorageError(t *testing.T) {
217 server := createTestServerWithMockStorage(t)
218 mockStorage := server.storage.(*registryMocks.MockStorage)
219
220 name := "myapp"
221 reference := "latest"
222 manifestData := []byte(`{"test": "manifest"}`)
223 contentType := "application/vnd.docker.distribution.manifest.v2+json"
224
225 mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(fmt.Errorf("storage error"))
226
227 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
228 req.Header.Set("Content-Type", contentType)
229 req.SetPathValue("name", name)
230 req.SetPathValue("reference", reference)
231 w := httptest.NewRecorder()
232
233 server.handlePutManifest(w, req)
234
235 assert.Equal(t, http.StatusInternalServerError, w.Code)
236 mockStorage.AssertExpectations(t)
237}
238
239func TestServer_HandlePutManifest_WithAnnotations(t *testing.T) {
240 server := createTestServerWithMockStorage(t)
241 mockStorage := server.storage.(*registryMocks.MockStorage)
242 mockEventBus := server.eventBus.(*MockEventBus)
243
244 name := "myapp"
245 reference := "latest"
246 // OCI manifest with annotations
247 manifestData := []byte(`{
248 "schemaVersion": 2,
249 "mediaType": "application/vnd.oci.image.manifest.v1+json",
250 "annotations": {
251 "version": "v1.0",
252 "description": "Test image"
253 }
254 }`)
255 contentType := "application/vnd.oci.image.manifest.v1+json"
256
257 mockStorage.On("PutManifest", name, reference, contentType, manifestData).Return(nil)
258 mockEventBus.On("Publish", events.ImagePushed, mock.MatchedBy(func(payload events.ImagePushedPayload) bool {
259 return payload.Name == name &&
260 payload.Reference == reference &&
261 len(payload.Annotations) == 2 &&
262 payload.Annotations["version"] == "v1.0"
263 })).Return(nil)
264
265 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/manifests/%s", name, reference), bytes.NewReader(manifestData))
266 req.Header.Set("Content-Type", contentType)
267 req.SetPathValue("name", name)
268 req.SetPathValue("reference", reference)
269 w := httptest.NewRecorder()
270
271 server.handlePutManifest(w, req)
272
273 assert.Equal(t, http.StatusCreated, w.Code)
274 mockStorage.AssertExpectations(t)
275 mockEventBus.AssertExpectations(t)
276}
277
278func TestServer_HandleGetBlob_Success(t *testing.T) {
279 server := createTestServerWithMockStorage(t)
280 mockStorage := server.storage.(*registryMocks.MockStorage)
281
282 name := "myapp"
283 digest := "sha256:abc123"
284 blobPath := "/tmp/test-blob"
285
286 // Create test blob file
287 testContent := "test blob content"
288 err := os.WriteFile(blobPath, []byte(testContent), 0644)
289 require.NoError(t, err)
290 defer os.Remove(blobPath)
291
292 mockStorage.On("GetBlobPath", digest).Return(blobPath, nil)
293
294 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/blobs/%s", name, digest), nil)
295 req.SetPathValue("name", name)
296 req.SetPathValue("digest", digest)
297 w := httptest.NewRecorder()
298
299 server.handleGetBlob(w, req)
300
301 assert.Equal(t, http.StatusOK, w.Code)
302 assert.Equal(t, testContent, w.Body.String())
303 mockStorage.AssertExpectations(t)
304}
305
306func TestServer_HandleGetBlob_NotFound(t *testing.T) {
307 server := createTestServerWithMockStorage(t)
308 mockStorage := server.storage.(*registryMocks.MockStorage)
309
310 name := "myapp"
311 digest := "sha256:nonexistent"
312
313 mockStorage.On("GetBlobPath", digest).Return("", fmt.Errorf("blob not found"))
314
315 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/blobs/%s", name, digest), nil)
316 req.SetPathValue("name", name)
317 req.SetPathValue("digest", digest)
318 w := httptest.NewRecorder()
319
320 server.handleGetBlob(w, req)
321
322 assert.Equal(t, http.StatusNotFound, w.Code)
323 mockStorage.AssertExpectations(t)
324}
325
326func TestServer_HandleStartBlobUpload_Success(t *testing.T) {
327 server := createTestServerWithMockStorage(t)
328 mockStorage := server.storage.(*registryMocks.MockStorage)
329
330 name := "myapp"
331 uuid := "test-upload-uuid"
332
333 mockStorage.On("StartBlobUpload", name).Return(uuid, nil)
334
335 req := httptest.NewRequest("POST", fmt.Sprintf("/v2/%s/blobs/uploads/", name), nil)
336 req.SetPathValue("name", name)
337 w := httptest.NewRecorder()
338
339 server.handleStartBlobUpload(w, req)
340
341 assert.Equal(t, http.StatusAccepted, w.Code)
342 assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), w.Header().Get("Location"))
343 assert.Equal(t, "0-0", w.Header().Get("Range"))
344 mockStorage.AssertExpectations(t)
345}
346
347func TestServer_HandleStartBlobUpload_Error(t *testing.T) {
348 server := createTestServerWithMockStorage(t)
349 mockStorage := server.storage.(*registryMocks.MockStorage)
350
351 name := "myapp"
352
353 mockStorage.On("StartBlobUpload", name).Return("", fmt.Errorf("storage error"))
354
355 req := httptest.NewRequest("POST", fmt.Sprintf("/v2/%s/blobs/uploads/", name), nil)
356 req.SetPathValue("name", name)
357 w := httptest.NewRecorder()
358
359 server.handleStartBlobUpload(w, req)
360
361 assert.Equal(t, http.StatusInternalServerError, w.Code)
362 mockStorage.AssertExpectations(t)
363}
364
365func TestServer_HandleBlobUpload_PATCH(t *testing.T) {
366 server := createTestServerWithMockStorage(t)
367 mockStorage := server.storage.(*registryMocks.MockStorage)
368
369 name := "myapp"
370 uuid := "test-upload-uuid"
371 chunk := []byte("chunk data")
372
373 mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
374
375 req := httptest.NewRequest("PATCH", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), bytes.NewReader(chunk))
376 req.SetPathValue("name", name)
377 req.SetPathValue("uuid", uuid)
378 w := httptest.NewRecorder()
379
380 server.handleBlobUpload(w, req)
381
382 assert.Equal(t, http.StatusAccepted, w.Code)
383 assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), w.Header().Get("Location"))
384 assert.Equal(t, fmt.Sprintf("0-%d", len(chunk)-1), w.Header().Get("Range"))
385 mockStorage.AssertExpectations(t)
386}
387
388func TestServer_HandleBlobUpload_PUT_Success(t *testing.T) {
389 server := createTestServerWithMockStorage(t)
390 mockStorage := server.storage.(*registryMocks.MockStorage)
391
392 name := "myapp"
393 uuid := "test-upload-uuid"
394 digest := "sha256:abc123"
395 chunk := []byte("final chunk")
396
397 mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
398 mockStorage.On("FinishBlobUpload", uuid, digest).Return(nil)
399
400 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/blobs/uploads/%s?digest=%s", name, uuid, digest), bytes.NewReader(chunk))
401 req.SetPathValue("name", name)
402 req.SetPathValue("uuid", uuid)
403 w := httptest.NewRecorder()
404
405 server.handleBlobUpload(w, req)
406
407 assert.Equal(t, http.StatusCreated, w.Code)
408 assert.Equal(t, fmt.Sprintf("/v2/%s/blobs/%s", name, digest), w.Header().Get("Location"))
409 assert.Equal(t, digest, w.Header().Get("Docker-Content-Digest"))
410 mockStorage.AssertExpectations(t)
411}
412
413func TestServer_HandleBlobUpload_PUT_FinishError(t *testing.T) {
414 server := createTestServerWithMockStorage(t)
415 mockStorage := server.storage.(*registryMocks.MockStorage)
416
417 name := "myapp"
418 uuid := "test-upload-uuid"
419 digest := "sha256:abc123"
420 chunk := []byte("final chunk")
421
422 mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(len(chunk)), nil)
423 mockStorage.On("FinishBlobUpload", uuid, digest).Return(fmt.Errorf("digest mismatch"))
424 mockStorage.On("CancelBlobUpload", uuid).Return(nil)
425
426 req := httptest.NewRequest("PUT", fmt.Sprintf("/v2/%s/blobs/uploads/%s?digest=%s", name, uuid, digest), bytes.NewReader(chunk))
427 req.SetPathValue("name", name)
428 req.SetPathValue("uuid", uuid)
429 w := httptest.NewRecorder()
430
431 server.handleBlobUpload(w, req)
432
433 assert.Equal(t, http.StatusBadRequest, w.Code)
434 assert.Contains(t, w.Body.String(), "digest mismatch")
435 mockStorage.AssertExpectations(t)
436}
437
438func TestServer_HandleBlobUpload_AppendError(t *testing.T) {
439 server := createTestServerWithMockStorage(t)
440 mockStorage := server.storage.(*registryMocks.MockStorage)
441
442 name := "myapp"
443 uuid := "test-upload-uuid"
444 chunk := []byte("chunk data")
445
446 mockStorage.On("AppendBlobChunk", name, uuid, chunk).Return(int64(0), fmt.Errorf("append error"))
447
448 req := httptest.NewRequest("PATCH", fmt.Sprintf("/v2/%s/blobs/uploads/%s", name, uuid), bytes.NewReader(chunk))
449 req.SetPathValue("name", name)
450 req.SetPathValue("uuid", uuid)
451 w := httptest.NewRecorder()
452
453 server.handleBlobUpload(w, req)
454
455 assert.Equal(t, http.StatusInternalServerError, w.Code)
456 mockStorage.AssertExpectations(t)
457}
458
459func TestServer_HandleListTags_Success(t *testing.T) {
460 server := createTestServerWithMockStorage(t)
461 mockStorage := server.storage.(*registryMocks.MockStorage)
462
463 name := "myapp"
464 tags := []string{"latest", "v1.0", "stable"}
465
466 mockStorage.On("ListTags", name).Return(tags, nil)
467
468 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/tags/list", name), nil)
469 req.SetPathValue("name", name)
470 w := httptest.NewRecorder()
471
472 server.handleListTags(w, req)
473
474 assert.Equal(t, http.StatusOK, w.Code)
475 assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
476
477 // The server correctly uses JSON marshaling for tags
478 expectedResponse := fmt.Sprintf(`{"name":"%s","tags":["latest","v1.0","stable"]}`, name)
479 assert.Equal(t, expectedResponse, w.Body.String())
480 mockStorage.AssertExpectations(t)
481}
482
483func TestServer_HandleListTags_NotFound(t *testing.T) {
484 server := createTestServerWithMockStorage(t)
485 mockStorage := server.storage.(*registryMocks.MockStorage)
486
487 name := "nonexistent"
488
489 mockStorage.On("ListTags", name).Return(nil, fmt.Errorf("not found"))
490
491 req := httptest.NewRequest("GET", fmt.Sprintf("/v2/%s/tags/list", name), nil)
492 req.SetPathValue("name", name)
493 w := httptest.NewRecorder()
494
495 server.handleListTags(w, req)
496
497 assert.Equal(t, http.StatusNotFound, w.Code)
498 mockStorage.AssertExpectations(t)
499}
500
501func TestServer_Start_Integration(t *testing.T) {
502 // This is more of an integration test to verify server startup
503 server := createTestServer(t)
504 server.config.Server.RegistryPort = 0 // Use any available port
505
506 ctx, cancel := context.WithCancel(context.Background())
507 defer cancel()
508
509 // Start server in background
510 errChan := make(chan error, 1)
511 go func() {
512 errChan <- server.Start(ctx)
513 }()
514
515 // Cancel context to shutdown server
516 cancel()
517
518 // Wait for server to shutdown
519 select {
520 case err := <-errChan:
521 assert.NoError(t, err)
522 }
523}
524
525func TestServer_SetupRoutes(t *testing.T) {
526 server := createTestServer(t)
527
528 // Test that routes are properly set up by checking a few key endpoints
529 testCases := []struct {
530 method string
531 path string
532 expectedCode int
533 }{
534 {"GET", "/v2/", http.StatusOK},
535 {"GET", "/v2/myapp/manifests/latest", http.StatusNotFound}, // Expected since manifest doesn't exist
536 {"HEAD", "/v2/myapp/manifests/latest", http.StatusNotFound}, // Expected since manifest doesn't exist
537 {"PUT", "/v2/myapp/manifests/latest", http.StatusBadRequest}, // Expected since no Content-Type header
538 {"GET", "/v2/myapp/blobs/sha256:abc123", http.StatusNotFound}, // Expected since blob doesn't exist
539 {"HEAD", "/v2/myapp/blobs/sha256:abc123", http.StatusNotFound}, // Expected since blob doesn't exist
540 {"POST", "/v2/myapp/blobs/uploads/", http.StatusAccepted},
541 {"PATCH", "/v2/myapp/blobs/uploads/uuid123", http.StatusInternalServerError}, // Expected since upload doesn't exist
542 {"PUT", "/v2/myapp/blobs/uploads/uuid123", http.StatusInternalServerError}, // Expected since upload doesn't exist
543 {"GET", "/v2/myapp/tags/list", http.StatusOK}, // Returns empty list when no tags exist
544 }
545
546 for _, tc := range testCases {
547 t.Run(fmt.Sprintf("%s %s", tc.method, tc.path), func(t *testing.T) {
548 req := httptest.NewRequest(tc.method, tc.path, nil)
549 w := httptest.NewRecorder()
550
551 // Set path values for routes that need them
552 if strings.Contains(tc.path, "myapp") {
553 req.SetPathValue("name", "myapp")
554 }
555 if strings.Contains(tc.path, "latest") {
556 req.SetPathValue("reference", "latest")
557 }
558 if strings.Contains(tc.path, "sha256:abc123") {
559 req.SetPathValue("digest", "sha256:abc123")
560 }
561 if strings.Contains(tc.path, "uuid123") {
562 req.SetPathValue("uuid", "uuid123")
563 }
564
565 server.mux.ServeHTTP(w, req)
566
567 // We verify that the route exists and returns the expected status
568 assert.Equal(t, tc.expectedCode, w.Code, "Route %s %s should return %d", tc.method, tc.path, tc.expectedCode)
569 })
570 }
571}
572
573// Helper functions
574
575func createTestServer(t *testing.T) *Server {
576 mockEventBus := &MockEventBus{}
577 cfg := &config.Config{
578 Server: config.ServerConfig{
579 DataDir: t.TempDir(),
580 RegistryPort: 5000,
581 },
582 RegistryAuth: config.RegistryAuthConfig{
583 Enabled: false,
584 },
585 }
586
587 server, err := NewServer(cfg, mockEventBus)
588 require.NoError(t, err)
589 return server
590}
591
592func createTestServerWithMockStorage(t *testing.T) *Server {
593 mockEventBus := &MockEventBus{}
594 mockStorage := ®istryMocks.MockStorage{}
595
596 cfg := &config.Config{
597 Server: config.ServerConfig{
598 DataDir: t.TempDir(),
599 RegistryPort: 5000,
600 },
601 RegistryAuth: config.RegistryAuthConfig{
602 Enabled: false,
603 },
604 }
605
606 server := &Server{
607 config: cfg,
608 mux: http.NewServeMux(),
609 storage: mockStorage,
610 eventBus: mockEventBus,
611 }
612 server.setupRoutes()
613 return server
614}