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:
parent
c0a94cca8c
commit
654cad4aa7
5 changed files with 232 additions and 570 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue