registry/internal/api/handlers/v0/publish_test.go
adam jones 9d9a02c57f
Database implementation cleanup (#335)
## 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.
2025-09-02 13:37:28 +01:00

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
})
}
}