## Motivation and Context The database logic was getting a bit unwieldy and fragile - especially because business logic that should really have lived at the registry service level was being pushed into the persistence level. This PR tightens the interface, simplifies the code there, as well as rips out a bunch of mocks etc. we don't need. ## How Has This Been Tested? Locally everything works ## Breaking Changes The list endpoint was surprisingly always filtering for isLatest only, which seems like a bug? I have removed this filter.
266 lines
7.9 KiB
Go
266 lines
7.9 KiB
Go
package v0_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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/auth"
|
|
"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"
|
|
)
|
|
|
|
|
|
// Helper function to generate a valid JWT token for testing
|
|
func generateTestJWTToken(cfg *config.Config, claims auth.JWTClaims) (string, error) {
|
|
jwtManager := auth.NewJWTManager(cfg)
|
|
ctx := context.Background()
|
|
tokenResponse, err := jwtManager.GenerateTokenResponse(ctx, claims)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return tokenResponse.RegistryToken, nil
|
|
}
|
|
|
|
func TestPublishEndpoint(t *testing.T) {
|
|
testSeed := make([]byte, ed25519.SeedSize)
|
|
_, err := rand.Read(testSeed)
|
|
require.NoError(t, err)
|
|
testConfig := &config.Config{
|
|
JWTPrivateKey: hex.EncodeToString(testSeed),
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
requestBody interface{}
|
|
tokenClaims *auth.JWTClaims
|
|
authHeader string
|
|
setupRegistryService func(service.RegistryService)
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "successful publish with GitHub auth",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "io.github.example/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",
|
|
},
|
|
},
|
|
tokenClaims: &auth.JWTClaims{
|
|
AuthMethod: auth.MethodGitHubAT,
|
|
AuthMethodSubject: "example",
|
|
Permissions: []auth.Permission{
|
|
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.example/*"},
|
|
},
|
|
},
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "successful publish with no auth (AuthMethodNone)",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "example/test-server",
|
|
Description: "A test server without auth",
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/test-server",
|
|
Source: "github",
|
|
ID: "example/test-server",
|
|
},
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
tokenClaims: &auth.JWTClaims{
|
|
AuthMethod: auth.MethodNone,
|
|
Permissions: []auth.Permission{
|
|
{Action: auth.PermissionActionPublish, ResourcePattern: "example/*"},
|
|
},
|
|
},
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "missing authorization header",
|
|
requestBody: apiv0.ServerJSON{},
|
|
authHeader: "", // Empty auth header
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusUnprocessableEntity,
|
|
expectedError: "required header parameter is missing",
|
|
},
|
|
{
|
|
name: "invalid authorization header format",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "io.github.domdomegg/test-server",
|
|
Description: "Test server",
|
|
VersionDetail: model.VersionDetail{Version: "1.0.0"},
|
|
},
|
|
authHeader: "InvalidFormat",
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedError: "Invalid Authorization header format",
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "test-server",
|
|
Description: "A test server",
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
},
|
|
authHeader: "Bearer invalidToken",
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedError: "Invalid or expired Registry JWT token",
|
|
},
|
|
{
|
|
name: "permission denied",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "io.github.other/test-server",
|
|
Description: "A test server",
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/test-server",
|
|
Source: "github",
|
|
ID: "example/test-server",
|
|
},
|
|
},
|
|
tokenClaims: &auth.JWTClaims{
|
|
AuthMethod: auth.MethodGitHubAT,
|
|
Permissions: []auth.Permission{
|
|
{Action: auth.PermissionActionPublish, ResourcePattern: "io.github.example/*"},
|
|
},
|
|
},
|
|
setupRegistryService: func(_ service.RegistryService) {
|
|
// Empty registry - no setup needed
|
|
},
|
|
expectedStatus: http.StatusForbidden,
|
|
expectedError: "You do not have permission to publish this server",
|
|
},
|
|
{
|
|
name: "registry service error",
|
|
requestBody: apiv0.ServerJSON{
|
|
Name: "example/test-server",
|
|
Description: "A test server",
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/test-server",
|
|
Source: "github",
|
|
ID: "example/test-server",
|
|
},
|
|
},
|
|
tokenClaims: &auth.JWTClaims{
|
|
AuthMethod: auth.MethodNone,
|
|
Permissions: []auth.Permission{
|
|
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
|
|
},
|
|
},
|
|
setupRegistryService: func(registry service.RegistryService) {
|
|
// Pre-publish the same server to cause duplicate version error
|
|
existingServer := apiv0.ServerJSON{
|
|
Name: "example/test-server",
|
|
Description: "Existing test server",
|
|
VersionDetail: model.VersionDetail{
|
|
Version: "1.0.0",
|
|
},
|
|
Repository: model.Repository{
|
|
URL: "https://github.com/example/test-server-existing",
|
|
Source: "github",
|
|
ID: "example/test-server-existing",
|
|
},
|
|
}
|
|
_, _ = registry.Publish(existingServer)
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedError: "invalid version: cannot publish duplicate version",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create registry service
|
|
registryService := service.NewRegistryServiceWithDB(database.NewMemoryDB())
|
|
|
|
// Setup registry service
|
|
tc.setupRegistryService(registryService)
|
|
|
|
// Create a new ServeMux and Huma API
|
|
mux := http.NewServeMux()
|
|
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
|
|
|
// Register the endpoint with test config
|
|
v0.RegisterPublishEndpoint(api, registryService, testConfig)
|
|
|
|
// Prepare request body
|
|
var requestBody []byte
|
|
if tc.requestBody != nil {
|
|
var err error
|
|
requestBody, err = json.Marshal(tc.requestBody)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
// Create request
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/v0/publish", bytes.NewBuffer(requestBody))
|
|
assert.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Set auth header
|
|
if tc.authHeader != "" {
|
|
req.Header.Set("Authorization", tc.authHeader)
|
|
} else if tc.tokenClaims != nil {
|
|
// Generate a valid JWT token
|
|
token, err := generateTestJWTToken(testConfig, *tc.tokenClaims)
|
|
assert.NoError(t, err)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
|
|
// Perform request
|
|
rr := httptest.NewRecorder()
|
|
mux.ServeHTTP(rr, req)
|
|
|
|
// Assertions
|
|
assert.Equal(t, tc.expectedStatus, rr.Code, "status code mismatch")
|
|
|
|
if tc.expectedError != "" {
|
|
assert.Contains(t, rr.Body.String(), tc.expectedError)
|
|
}
|
|
|
|
// No mock expectations to verify
|
|
})
|
|
}
|
|
}
|