From a5abab50c4b078249fd2ec68514f3cff8f042e8e Mon Sep 17 00:00:00 2001 From: Avinash Sridhar Date: Tue, 27 May 2025 13:21:14 -0400 Subject: [PATCH] feat(tests): add unit tests for servers and servers detail handlers --- internal/api/handlers/v0/servers_test.go | 551 +++++++++++++++++++++++ 1 file changed, 551 insertions(+) create mode 100644 internal/api/handlers/v0/servers_test.go diff --git a/internal/api/handlers/v0/servers_test.go b/internal/api/handlers/v0/servers_test.go new file mode 100644 index 0000000..d6de7ca --- /dev/null +++ b/internal/api/handlers/v0/servers_test.go @@ -0,0 +1,551 @@ +package v0 + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/modelcontextprotocol/registry/internal/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestServersHandler(t *testing.T) { + testCases := []struct { + name string + method string + queryParams string + setupMocks func(*MockRegistryService) + expectedStatus int + expectedServers []model.Server + expectedMeta *Metadata + expectedError string + }{ + { + name: "successful list with default parameters", + method: http.MethodGet, + setupMocks: func(registry *MockRegistryService) { + servers := []model.Server{ + { + ID: "test-id-1", + Name: "test-server-1", + Description: "First test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-1", + Source: "github", + ID: "example/test-server-1", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + { + ID: "test-id-2", + Name: "test-server-2", + Description: "Second test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-2", + Source: "github", + ID: "example/test-server-2", + }, + VersionDetail: model.VersionDetail{ + Version: "2.0.0", + ReleaseDate: "2025-05-26T00:00:00Z", + IsLatest: true, + }, + }, + } + registry.On("List", "", 30).Return(servers, "", nil) + }, + expectedStatus: http.StatusOK, + expectedServers: []model.Server{ + { + ID: "test-id-1", + Name: "test-server-1", + Description: "First test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-1", + Source: "github", + ID: "example/test-server-1", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + { + ID: "test-id-2", + Name: "test-server-2", + Description: "Second test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-2", + Source: "github", + ID: "example/test-server-2", + }, + VersionDetail: model.VersionDetail{ + Version: "2.0.0", + ReleaseDate: "2025-05-26T00:00:00Z", + IsLatest: true, + }, + }, + }, + }, + { + name: "successful list with cursor and limit", + method: http.MethodGet, + queryParams: "?cursor=test-id-3" + "&limit=10", + setupMocks: func(registry *MockRegistryService) { + servers := []model.Server{ + { + ID: "test-id-3", + Name: "test-server-3", + Description: "Third test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-3", + Source: "github", + ID: "example/test-server-3", + }, + VersionDetail: model.VersionDetail{ + Version: "1.5.0", + ReleaseDate: "2025-05-27T00:00:00Z", + IsLatest: true, + }, + }, + } + nextCursor := uuid.New().String() + registry.On("List", mock.AnythingOfType("string"), 10).Return(servers, nextCursor, nil) + }, + expectedStatus: http.StatusOK, + expectedServers: []model.Server{ + { + ID: "test-id-3", + Name: "test-server-3", + Description: "Third test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server-3", + Source: "github", + ID: "example/test-server-3", + }, + VersionDetail: model.VersionDetail{ + Version: "1.5.0", + ReleaseDate: "2025-05-27T00:00:00Z", + IsLatest: true, + }, + }, + }, + expectedMeta: &Metadata{ + NextCursor: "", // This will be dynamically set in the test + Count: 1, + }, + }, + { + name: "successful list with limit capping at 100", + method: http.MethodGet, + queryParams: "?limit=150", + setupMocks: func(registry *MockRegistryService) { + servers := []model.Server{} + registry.On("List", "", 100).Return(servers, "", nil) + }, + expectedStatus: http.StatusOK, + expectedServers: []model.Server{}, + }, + { + name: "invalid cursor parameter", + method: http.MethodGet, + queryParams: "?cursor=invalid-uuid", + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid cursor parameter", + }, + { + name: "invalid limit parameter - non-numeric", + method: http.MethodGet, + queryParams: "?limit=abc", + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid limit parameter", + }, + { + name: "invalid limit parameter - zero", + method: http.MethodGet, + queryParams: "?limit=0", + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusBadRequest, + expectedError: "Limit must be greater than 0", + }, + { + name: "invalid limit parameter - negative", + method: http.MethodGet, + queryParams: "?limit=-5", + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusBadRequest, + expectedError: "Limit must be greater than 0", + }, + { + name: "registry service error", + method: http.MethodGet, + setupMocks: func(registry *MockRegistryService) { + registry.On("List", "", 30).Return([]model.Server{}, "", errors.New("database connection error")) + }, + expectedStatus: http.StatusInternalServerError, + expectedError: "database connection error", + }, + { + name: "method not allowed", + method: http.MethodPost, + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusMethodNotAllowed, + expectedError: "Method not allowed", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create mock registry service + mockRegistry := new(MockRegistryService) + tc.setupMocks(mockRegistry) + + // Create handler + handler := ServersHandler(mockRegistry) + + // Create request + url := "/v0/servers" + tc.queryParams + req, err := http.NewRequest(tc.method, url, nil) + if err != nil { + t.Fatal(err) + } + + // Create response recorder + rr := httptest.NewRecorder() + + // Call the handler + handler.ServeHTTP(rr, req) + + // Check status code + assert.Equal(t, tc.expectedStatus, rr.Code) + + if tc.expectedStatus == http.StatusOK { + // Check content type + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + + // Parse response body + var resp PaginatedResponse + err = json.NewDecoder(rr.Body).Decode(&resp) + assert.NoError(t, err) + + // Check the response data + assert.Equal(t, tc.expectedServers, resp.Data) + + // Check metadata if expected + if tc.expectedMeta != nil { + assert.Equal(t, tc.expectedMeta.Count, resp.Metadata.Count) + if tc.expectedMeta.NextCursor != "" { + assert.NotEmpty(t, resp.Metadata.NextCursor) + } + } + } else { + // Check error message for non-200 responses + if tc.expectedError != "" { + assert.Contains(t, rr.Body.String(), tc.expectedError) + } + } + + // Verify mock expectations + mockRegistry.AssertExpectations(t) + }) + } +} + +func TestServersDetailHandler(t *testing.T) { + validServerID := uuid.New().String() + + testCases := []struct { + name string + method string + serverID string + setupMocks func(*MockRegistryService) + expectedStatus int + expectedServerDetail *model.ServerDetail + expectedError string + }{ + { + name: "successful get server detail", + method: http.MethodGet, + serverID: validServerID, + setupMocks: func(registry *MockRegistryService) { + serverDetail := &model.ServerDetail{ + Server: model.Server{ + ID: validServerID, + Name: "test-server", + Description: "A test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server", + Source: "github", + ID: "example/test-server", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + Packages: []model.Package{ + { + RegistryName: "test-package", + Name: "example-package", + Version: "1.0.0", + }, + }, + Remotes: []model.Remote{ + { + TransportType: "http", + URL: "https://example.com/mcp", + }, + }, + } + registry.On("GetByID", validServerID).Return(serverDetail, nil) + }, + expectedStatus: http.StatusOK, + expectedServerDetail: &model.ServerDetail{ + Server: model.Server{ + ID: validServerID, + Name: "test-server", + Description: "A test server", + Repository: model.Repository{ + URL: "https://github.com/example/test-server", + Source: "github", + ID: "example/test-server", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-25T00:00:00Z", + IsLatest: true, + }, + }, + Packages: []model.Package{ + { + RegistryName: "test-package", + Name: "example-package", + Version: "1.0.0", + }, + }, + Remotes: []model.Remote{ + { + TransportType: "http", + URL: "https://example.com/mcp", + }, + }, + }, + }, + { + name: "server not found", + method: http.MethodGet, + serverID: uuid.New().String(), + setupMocks: func(registry *MockRegistryService) { + registry.On("GetByID", mock.AnythingOfType("string")).Return((*model.ServerDetail)(nil), errors.New("record not found")) + }, + expectedStatus: http.StatusNotFound, + expectedError: "Server not found", + }, + { + name: "invalid server ID format", + method: http.MethodGet, + serverID: "invalid-uuid", + setupMocks: func(registry *MockRegistryService) { + // Mock won't be called due to early validation + }, + expectedStatus: http.StatusBadRequest, + expectedError: "Invalid server ID format", + }, + { + name: "registry service error", + method: http.MethodGet, + serverID: validServerID, + setupMocks: func(registry *MockRegistryService) { + registry.On("GetByID", validServerID).Return((*model.ServerDetail)(nil), errors.New("database connection error")) + }, + expectedStatus: http.StatusInternalServerError, + expectedError: "Error retrieving server details", + }, + { + name: "method not allowed", + method: http.MethodPost, + serverID: validServerID, + setupMocks: func(registry *MockRegistryService) {}, + expectedStatus: http.StatusMethodNotAllowed, + expectedError: "Method not allowed", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create mock registry service + mockRegistry := new(MockRegistryService) + tc.setupMocks(mockRegistry) + + // Create handler + handler := ServersDetailHandler(mockRegistry) + + // Create request with path value + url := fmt.Sprintf("/v0/servers/%s", tc.serverID) + req, err := http.NewRequest(tc.method, url, nil) + if err != nil { + t.Fatal(err) + } + + // Set the path value for the server ID (simulating mux behavior) + req.SetPathValue("id", tc.serverID) + + // Create response recorder + rr := httptest.NewRecorder() + + // Call the handler + handler.ServeHTTP(rr, req) + + // Check status code + assert.Equal(t, tc.expectedStatus, rr.Code) + + if tc.expectedStatus == http.StatusOK { + // Check content type + assert.Equal(t, "application/json", rr.Header().Get("Content-Type")) + + // Parse response body + var resp model.ServerDetail + err = json.NewDecoder(rr.Body).Decode(&resp) + assert.NoError(t, err) + + // Check the response data + assert.Equal(t, *tc.expectedServerDetail, resp) + } else { + // Check error message for non-200 responses + if tc.expectedError != "" { + assert.Contains(t, rr.Body.String(), tc.expectedError) + } + } + + // Verify mock expectations + mockRegistry.AssertExpectations(t) + }) + } +} + +// TestServersHandlerIntegration tests the servers list handler with actual HTTP requests +func TestServersHandlerIntegration(t *testing.T) { + // Create mock registry service + mockRegistry := new(MockRegistryService) + + servers := []model.Server{ + { + ID: "integration-test-id", + Name: "integration-test-server", + Description: "Integration test server", + Repository: model.Repository{ + URL: "https://github.com/example/integration-test", + Source: "github", + ID: "example/integration-test", + }, + VersionDetail: model.VersionDetail{ + Version: "1.0.0", + ReleaseDate: "2025-05-27T00:00:00Z", + IsLatest: true, + }, + }, + } + + mockRegistry.On("List", "", 30).Return(servers, "", nil) + + // Create test server + server := httptest.NewServer(ServersHandler(mockRegistry)) + defer server.Close() + + // Send request to the test server + resp, err := http.Get(server.URL) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + // Check status code + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Check content type + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + // Parse response body + var paginatedResp PaginatedResponse + err = json.NewDecoder(resp.Body).Decode(&paginatedResp) + assert.NoError(t, err) + + // Check the response data + assert.Equal(t, servers, paginatedResp.Data) + assert.Empty(t, paginatedResp.Metadata.NextCursor) + + // Verify mock expectations + mockRegistry.AssertExpectations(t) +} + +// TestServersDetailHandlerIntegration tests the servers detail handler with actual HTTP requests +func TestServersDetailHandlerIntegration(t *testing.T) { + serverID := uuid.New().String() + + // Create mock registry service + mockRegistry := new(MockRegistryService) + + serverDetail := &model.ServerDetail{ + Server: model.Server{ + ID: serverID, + Name: "integration-test-server-detail", + Description: "Integration test server detail", + Repository: model.Repository{ + URL: "https://github.com/example/integration-test-detail", + Source: "github", + ID: "example/integration-test-detail", + }, + VersionDetail: model.VersionDetail{ + Version: "2.0.0", + ReleaseDate: "2025-05-27T12:00:00Z", + IsLatest: true, + }, + }, + } + + mockRegistry.On("GetByID", serverID).Return(serverDetail, nil) + + // Create test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.SetPathValue("id", serverID) + ServersDetailHandler(mockRegistry).ServeHTTP(w, r) + })) + defer server.Close() + + // Send request to the test server + resp, err := http.Get(server.URL) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + // Check status code + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Check content type + assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + // Parse response body + var serverDetailResp model.ServerDetail + err = json.NewDecoder(resp.Body).Decode(&serverDetailResp) + assert.NoError(t, err) + + // Check the response data + assert.Equal(t, *serverDetail, serverDetailResp) + + // Verify mock expectations + mockRegistry.AssertExpectations(t) +}