## Summary - Applied `gofmt -s -w` across the entire codebase to ensure consistent Go formatting - This will trigger a deployment with minimal code changes ## Test plan - [x] Pre-commit hooks pass - [x] No functional changes, formatting only 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
512 lines
15 KiB
Go
512 lines
15 KiB
Go
package v0_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
|
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
|
|
"github.com/modelcontextprotocol/registry/internal/config"
|
|
"github.com/modelcontextprotocol/registry/internal/database"
|
|
"github.com/modelcontextprotocol/registry/internal/service"
|
|
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
|
|
"github.com/modelcontextprotocol/registry/pkg/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestListServersEndpoint(t *testing.T) {
|
|
ctx := context.Background()
|
|
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())
|
|
|
|
// Setup test data
|
|
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: "com.example/server-alpha",
|
|
Description: "Alpha test server",
|
|
Version: "1.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: "com.example/server-beta",
|
|
Description: "Beta test server",
|
|
Version: "2.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
v0.RegisterServersEndpoints(api, registryService)
|
|
|
|
tests := []struct {
|
|
name string
|
|
queryParams string
|
|
expectedStatus int
|
|
expectedCount int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "list all servers",
|
|
queryParams: "",
|
|
expectedStatus: http.StatusOK,
|
|
expectedCount: 2,
|
|
},
|
|
{
|
|
name: "list with limit",
|
|
queryParams: "?limit=1",
|
|
expectedStatus: http.StatusOK,
|
|
expectedCount: 1,
|
|
},
|
|
{
|
|
name: "search servers",
|
|
queryParams: "?search=alpha",
|
|
expectedStatus: http.StatusOK,
|
|
expectedCount: 1,
|
|
},
|
|
{
|
|
name: "filter latest only",
|
|
queryParams: "?version=latest",
|
|
expectedStatus: http.StatusOK,
|
|
expectedCount: 2,
|
|
},
|
|
{
|
|
name: "invalid limit",
|
|
queryParams: "?limit=abc",
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedError: "validation failed",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers"+tt.queryParams, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var resp apiv0.ServerListResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, resp.Servers, tt.expectedCount)
|
|
assert.Equal(t, tt.expectedCount, resp.Metadata.Count)
|
|
|
|
// Verify structure
|
|
for _, server := range resp.Servers {
|
|
assert.NotEmpty(t, server.Server.Name)
|
|
assert.NotEmpty(t, server.Server.Description)
|
|
assert.NotNil(t, server.Meta.Official)
|
|
}
|
|
} else if tt.expectedError != "" {
|
|
assert.Contains(t, w.Body.String(), tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetServerByNameEndpoint(t *testing.T) {
|
|
ctx := context.Background()
|
|
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())
|
|
|
|
// Setup test data
|
|
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: "com.example/detail-server",
|
|
Description: "Server for detail testing",
|
|
Version: "1.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
v0.RegisterServersEndpoints(api, registryService)
|
|
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "get existing server",
|
|
serverName: "com.example/detail-server",
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "get non-existent server",
|
|
serverName: "com.example/non-existent",
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Server not found",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// URL encode the server name
|
|
encodedName := url.PathEscape(tt.serverName)
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var resp apiv0.ServerResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.serverName, resp.Server.Name)
|
|
assert.NotNil(t, resp.Meta.Official)
|
|
} else if tt.expectedError != "" {
|
|
assert.Contains(t, w.Body.String(), tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetServerVersionEndpoint(t *testing.T) {
|
|
ctx := context.Background()
|
|
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())
|
|
|
|
serverName := "com.example/version-server"
|
|
|
|
// Setup test data with multiple versions
|
|
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: serverName,
|
|
Description: "Version test server v1",
|
|
Version: "1.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: serverName,
|
|
Description: "Version test server v2",
|
|
Version: "2.0.0",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Add version with build metadata for URL encoding test
|
|
_, err = registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: serverName,
|
|
Description: "Version test server with build metadata",
|
|
Version: "1.0.0+20130313144700",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Create API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
v0.RegisterServersEndpoints(api, registryService)
|
|
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
version string
|
|
expectedStatus int
|
|
expectedError string
|
|
checkResult func(*testing.T, *apiv0.ServerResponse)
|
|
}{
|
|
{
|
|
name: "get existing version",
|
|
serverName: serverName,
|
|
version: "1.0.0",
|
|
expectedStatus: http.StatusOK,
|
|
checkResult: func(t *testing.T, resp *apiv0.ServerResponse) {
|
|
t.Helper()
|
|
assert.Equal(t, "1.0.0", resp.Server.Version)
|
|
assert.Equal(t, "Version test server v1", resp.Server.Description)
|
|
assert.False(t, resp.Meta.Official.IsLatest)
|
|
},
|
|
},
|
|
{
|
|
name: "get latest version",
|
|
serverName: serverName,
|
|
version: "2.0.0",
|
|
expectedStatus: http.StatusOK,
|
|
checkResult: func(t *testing.T, resp *apiv0.ServerResponse) {
|
|
t.Helper()
|
|
assert.Equal(t, "2.0.0", resp.Server.Version)
|
|
assert.True(t, resp.Meta.Official.IsLatest)
|
|
},
|
|
},
|
|
{
|
|
name: "get non-existent version",
|
|
serverName: serverName,
|
|
version: "3.0.0",
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Server not found",
|
|
},
|
|
{
|
|
name: "get non-existent server",
|
|
serverName: "com.example/non-existent",
|
|
version: "1.0.0",
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Server not found",
|
|
},
|
|
{
|
|
name: "get version with build metadata (URL encoded)",
|
|
serverName: serverName,
|
|
version: "1.0.0+20130313144700",
|
|
expectedStatus: http.StatusOK,
|
|
checkResult: func(t *testing.T, resp *apiv0.ServerResponse) {
|
|
t.Helper()
|
|
assert.Equal(t, "1.0.0+20130313144700", resp.Server.Version)
|
|
assert.Equal(t, "Version test server with build metadata", resp.Server.Description)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// URL encode the server name and version
|
|
encodedName := url.PathEscape(tt.serverName)
|
|
encodedVersion := url.PathEscape(tt.version)
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName+"/versions/"+encodedVersion, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var resp apiv0.ServerResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.serverName, resp.Server.Name)
|
|
assert.Equal(t, tt.version, resp.Server.Version)
|
|
assert.NotNil(t, resp.Meta.Official)
|
|
|
|
if tt.checkResult != nil {
|
|
tt.checkResult(t, &resp)
|
|
}
|
|
} else if tt.expectedError != "" {
|
|
assert.Contains(t, w.Body.String(), tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAllVersionsEndpoint(t *testing.T) {
|
|
ctx := context.Background()
|
|
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())
|
|
|
|
serverName := "com.example/multi-version-server"
|
|
|
|
// Setup test data with multiple versions
|
|
versions := []string{"1.0.0", "1.1.0", "2.0.0"}
|
|
for _, version := range versions {
|
|
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: serverName,
|
|
Description: "Multi-version test server " + version,
|
|
Version: version,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
v0.RegisterServersEndpoints(api, registryService)
|
|
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
expectedStatus int
|
|
expectedCount int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "get all versions of existing server",
|
|
serverName: serverName,
|
|
expectedStatus: http.StatusOK,
|
|
expectedCount: 3,
|
|
},
|
|
{
|
|
name: "get versions of non-existent server",
|
|
serverName: "com.example/non-existent",
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Server not found",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// URL encode the server name
|
|
encodedName := url.PathEscape(tt.serverName)
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName+"/versions", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var resp apiv0.ServerListResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, resp.Servers, tt.expectedCount)
|
|
assert.Equal(t, tt.expectedCount, resp.Metadata.Count)
|
|
|
|
// Verify all versions are for the same server
|
|
for _, server := range resp.Servers {
|
|
assert.Equal(t, tt.serverName, server.Server.Name)
|
|
assert.NotNil(t, server.Meta.Official)
|
|
}
|
|
|
|
// Verify all expected versions are present
|
|
versionSet := make(map[string]bool)
|
|
for _, server := range resp.Servers {
|
|
versionSet[server.Server.Version] = true
|
|
}
|
|
for _, expectedVersion := range versions {
|
|
assert.True(t, versionSet[expectedVersion], "Version %s should be present", expectedVersion)
|
|
}
|
|
|
|
// Verify exactly one is marked as latest
|
|
latestCount := 0
|
|
for _, server := range resp.Servers {
|
|
if server.Meta.Official.IsLatest {
|
|
latestCount++
|
|
}
|
|
}
|
|
assert.Equal(t, 1, latestCount, "Exactly one version should be marked as latest")
|
|
} else if tt.expectedError != "" {
|
|
assert.Contains(t, w.Body.String(), tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServersEndpointEdgeCases(t *testing.T) {
|
|
ctx := context.Background()
|
|
registryService := service.NewRegistryService(database.NewTestDB(t), config.NewConfig())
|
|
|
|
// Setup test data with edge case names that comply with constraints
|
|
specialServers := []struct {
|
|
name string
|
|
description string
|
|
version string
|
|
}{
|
|
{"io.dots.and-dashes/server-name", "Server with dots and dashes", "1.0.0"},
|
|
{"com.long-namespace-name/very-long-server-name-here", "Long names", "1.0.0"},
|
|
{"org.test123/server_with_underscores", "Server with underscores", "1.0.0"},
|
|
}
|
|
|
|
for _, server := range specialServers {
|
|
_, err := registryService.CreateServer(ctx, &apiv0.ServerJSON{
|
|
Name: server.name,
|
|
Description: server.description,
|
|
Version: server.version,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
v0.RegisterServersEndpoints(api, registryService)
|
|
|
|
t.Run("URL encoding edge cases", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serverName string
|
|
}{
|
|
{"dots and dashes", "io.dots.and-dashes/server-name"},
|
|
{"long server name", "com.long-namespace-name/very-long-server-name-here"},
|
|
{"underscores", "org.test123/server_with_underscores"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test server detail endpoint
|
|
encodedName := url.PathEscape(tt.serverName)
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers/"+encodedName, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp apiv0.ServerResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.serverName, resp.Server.Name)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("query parameter edge cases", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
queryParams string
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{"limit too high", "?limit=1000", http.StatusUnprocessableEntity, "validation failed"},
|
|
{"negative limit", "?limit=-1", http.StatusUnprocessableEntity, "validation failed"},
|
|
{"invalid updated_since format", "?updated_since=invalid", http.StatusBadRequest, "Invalid updated_since format"},
|
|
{"future updated_since", "?updated_since=2030-01-01T00:00:00Z", http.StatusOK, ""},
|
|
{"very old updated_since", "?updated_since=1990-01-01T00:00:00Z", http.StatusOK, ""},
|
|
{"empty search parameter", "?search=", http.StatusOK, ""},
|
|
{"search with special characters", "?search=测试", http.StatusOK, ""},
|
|
{"combined valid parameters", "?search=server&limit=5&version=latest", http.StatusOK, ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers"+tt.queryParams, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
|
|
if tt.expectedStatus == http.StatusOK {
|
|
var resp apiv0.ServerListResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, resp.Metadata)
|
|
} else if tt.expectedError != "" {
|
|
assert.Contains(t, w.Body.String(), tt.expectedError)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("response structure validation", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/v0/servers", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
mux.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
|
|
|
var resp apiv0.ServerListResponse
|
|
err := json.NewDecoder(w.Body).Decode(&resp)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify metadata structure
|
|
assert.NotNil(t, resp.Metadata)
|
|
assert.GreaterOrEqual(t, resp.Metadata.Count, 0)
|
|
|
|
// Verify each server has complete structure
|
|
for _, server := range resp.Servers {
|
|
assert.NotEmpty(t, server.Server.Name)
|
|
assert.NotEmpty(t, server.Server.Description)
|
|
assert.NotEmpty(t, server.Server.Version)
|
|
assert.NotNil(t, server.Meta)
|
|
assert.NotNil(t, server.Meta.Official)
|
|
assert.NotZero(t, server.Meta.Official.PublishedAt)
|
|
assert.Contains(t, []model.Status{model.StatusActive, model.StatusDeprecated, model.StatusDeleted}, server.Meta.Official.Status)
|
|
}
|
|
})
|
|
}
|