Clean up publish and edit handler and service logic (#319)

Thanks Camila for being a great Go consultant and advising on how to
tidy this up!

(I'm still a lil grumpy about some parts, but it's in much better shape
😄)

- Consolidated publisher extension validation into a single function.
- Updated the `Publish` and `EditServer` methods to directly use
`XPublisher` from the request.
- Removed outdated tests for publisher extension extraction and
validation.
- Introduced new validation functions for server details and remote URL
matching.
- Enhanced error handling for server name format and remote URL
validation.
- Updated existing tests to reflect changes in validation logic and
structure.

## Motivation and Context
<!-- Why is this change needed? What problem does it solve? -->

Things were starting to become a bit spaghetti-y after lots of small PRs
and contributions from many people. Camila and I went through and
figured out how we could make this a lot cleaner.

## How Has This Been Tested?
<!-- Have you tested this in a real application? Which scenarios were
tested? -->

Tested locally, plus all the integration and unit tests still pass.

## Breaking Changes
<!-- Will users need to update their code or configurations? -->

None

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
adam jones 2025-08-28 20:20:43 +01:00 • committed by GitHub
commit 654cad4aa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 232 additions and 570 deletions

View file

@ -2,7 +2,6 @@ package v0
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
@ -19,9 +18,9 @@ import (
// EditServerInput represents the input for editing a server
type EditServerInput struct {
Authorization string `header:"Authorization" doc:"Registry JWT token with edit permissions" required:"true"`
ID string `path:"id" doc:"Server ID (UUID)" format:"uuid"`
RawBody []byte `body:"raw"`
Authorization string `header:"Authorization" doc:"Registry JWT token with edit permissions" required:"true"`
ID string `path:"id" doc:"Server ID (UUID)" format:"uuid"`
Body apiv0.PublishRequest `body:""`
}
// RegisterEditEndpoints registers the edit endpoint
@ -54,11 +53,6 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg *
return nil, huma.Error401Unauthorized("Invalid or expired Registry JWT token", err)
}
// Validate that only allowed extension fields are present
if err := validators.ValidatePublishRequestExtensions(input.RawBody); err != nil {
return nil, huma.Error400BadRequest("Invalid request format", err)
}
// Get current server to check permissions against existing name
currentServer, err := registry.GetByID(input.ID)
if err != nil {
@ -73,30 +67,23 @@ func RegisterEditEndpoints(api huma.API, registry service.RegistryService, cfg *
return nil, huma.Error403Forbidden("You do not have edit permissions for this server")
}
// Parse the validated request body
var editRequest apiv0.PublishRequest
if err := json.Unmarshal(input.RawBody, &editRequest); err != nil {
return nil, huma.Error400BadRequest("Invalid JSON format", err)
}
// Validate the server detail
validator := validators.NewObjectValidator()
if err := validator.Validate(&editRequest.Server); err != nil {
// Perform all schema validation
if err := validators.ValidatePublishRequest(input.Body); err != nil {
return nil, huma.Error400BadRequest(err.Error())
}
// Prevent renaming servers
if currentServer.Server.Name != editRequest.Server.Name {
if currentServer.Server.Name != input.Body.Server.Name {
return nil, huma.Error400BadRequest("Cannot rename server")
}
// Prevent undeleting servers - once deleted, they stay deleted
if currentServer.Server.Status == model.StatusDeleted && editRequest.Server.Status != model.StatusDeleted {
if currentServer.Server.Status == model.StatusDeleted && input.Body.Server.Status != model.StatusDeleted {
return nil, huma.Error400BadRequest("Cannot change status of deleted server. Deleted servers cannot be undeleted.")
}
// Edit the server
updatedServer, err := registry.EditServer(input.ID, editRequest)
updatedServer, err := registry.EditServer(input.ID, input.Body)
if err != nil {
if errors.Is(err, database.ErrNotFound) {
return nil, huma.Error404NotFound("Server not found")

View file

@ -95,7 +95,13 @@ func TestEditServerEndpoint(t *testing.T) {
name: "invalid authorization header format",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: "InvalidFormat token123",
requestBody: apiv0.PublishRequest{},
requestBody: apiv0.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Test server",
VersionDetail: model.VersionDetail{Version: "1.0.0"},
},
},
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: http.StatusUnauthorized,
expectedError: "Unauthorized",
@ -104,7 +110,13 @@ func TestEditServerEndpoint(t *testing.T) {
name: "invalid token",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: "Bearer invalid-token",
requestBody: apiv0.PublishRequest{},
requestBody: apiv0.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Test server",
VersionDetail: model.VersionDetail{Version: "1.0.0"},
},
},
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: http.StatusUnauthorized,
expectedError: "Unauthorized",
@ -240,7 +252,7 @@ func TestEditServerEndpoint(t *testing.T) {
expectedError: "Bad Request",
},
{
name: "invalid request body",
name: "validation error - invalid server name",
serverID: "550e8400-e29b-41d4-a716-446655440001",
authHeader: func() string {
cfg := &config.Config{JWTPrivateKey: "bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb825b23f7ef47c"}
@ -253,8 +265,23 @@ func TestEditServerEndpoint(t *testing.T) {
})
return "Bearer " + token
}(),
requestBody: "invalid json",
setupMocks: func(_ *MockRegistryService) {},
requestBody: apiv0.PublishRequest{
Server: model.ServerJSON{
Name: "invalid-name-format", // Missing namespace/name format
Description: "Test server",
VersionDetail: model.VersionDetail{Version: "1.0.0"},
},
},
setupMocks: func(registry *MockRegistryService) {
currentServer := &apiv0.ServerRecord{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Original server",
Status: model.StatusActive,
},
}
registry.On("GetByID", "550e8400-e29b-41d4-a716-446655440001").Return(currentServer, nil)
},
expectedStatus: http.StatusBadRequest,
expectedError: "Bad Request",
},

View file

@ -2,7 +2,6 @@ package v0
import (
"context"
"encoding/json"
"net/http"
"strings"
@ -16,8 +15,8 @@ import (
// PublishServerInput represents the input for publishing a server
type PublishServerInput struct {
Authorization string `header:"Authorization" doc:"Registry JWT token (obtained from /v0/auth/token/github)" required:"true"`
RawBody []byte `body:"raw"`
Authorization string `header:"Authorization" doc:"Registry JWT token (obtained from /v0/auth/token/github)" required:"true"`
Body apiv0.PublishRequest `body:""`
}
// RegisterPublishEndpoint registers the publish endpoint
@ -50,33 +49,18 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg
return nil, huma.Error401Unauthorized("Invalid or expired Registry JWT token", err)
}
// Validate that only allowed extension fields are present
if err := validators.ValidatePublishRequestExtensions(input.RawBody); err != nil {
return nil, huma.Error400BadRequest("Invalid request format", err)
}
// Parse the validated request body
var publishRequest apiv0.PublishRequest
if err := json.Unmarshal(input.RawBody, &publishRequest); err != nil {
return nil, huma.Error400BadRequest("Invalid JSON format", err)
}
// Get server details from request body
serverDetail := publishRequest.Server
// Validate the server detail
validator := validators.NewObjectValidator()
if err := validator.Validate(&serverDetail); err != nil {
// Perform all schema validation
if err := validators.ValidatePublishRequest(input.Body); err != nil {
return nil, huma.Error400BadRequest(err.Error())
}
// Verify that the token's repository matches the server being published
if !jwtManager.HasPermission(serverDetail.Name, auth.PermissionActionPublish, claims.Permissions) {
// Verify that the token has permission to publish the server
if !jwtManager.HasPermission(input.Body.Server.Name, auth.PermissionActionPublish, claims.Permissions) {
return nil, huma.Error403Forbidden("You do not have permission to publish this server")
}
// Publish the server with extensions
publishedServer, err := registry.Publish(publishRequest)
publishedServer, err := registry.Publish(input.Body)
if err != nil {
return nil, huma.Error400BadRequest("Failed to publish server", err)
}

View file

@ -169,7 +169,9 @@ func TestPublishIntegration(t *testing.T) {
t.Run("publish fails with invalid token", func(t *testing.T) {
publishReq := apiv0.PublishRequest{
Server: model.ServerJSON{
Name: "test-server",
Name: "io.github.domdomegg/test-server",
Description: "Test server",
VersionDetail: model.VersionDetail{Version: "1.0.0"},
},
}

View file

@ -150,8 +150,14 @@ func TestPublishEndpoint(t *testing.T) {
expectedError: "required header parameter is missing",
},
{
name: "invalid authorization header format",
requestBody: apiv0.PublishRequest{},
name: "invalid authorization header format",
requestBody: apiv0.PublishRequest{
Server: model.ServerJSON{
Name: "io.github.domdomegg/test-server",
Description: "Test server",
VersionDetail: model.VersionDetail{Version: "1.0.0"},
},
},
authHeader: "InvalidFormat",
setupMocks: func(_ *MockRegistryService) {},
expectedStatus: http.StatusUnauthorized,