Skip to content

Commit 8352aca

Browse files
ossamalafhelAdam Jones
andcommitted
feat: add regex validation to server name in API types
- Add pattern validation (^[^/]+/[^/]+$) to ensure exactly one slash - Update tests to expect HTTP 422 for validation errors (correct status) - Remove unnecessary error handling complexity in publish handler - This makes the validation visible in API documentation Following @domdomegg's suggestion to align API specs with implementation. Co-Authored-By: Adam Jones <[email protected]>
1 parent 63cf08e commit 8352aca

File tree

5 files changed

+33
-32
lines changed

5 files changed

+33
-32
lines changed

internal/api/handlers/v0/edit_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,8 @@ func TestEditServerEndpoint(t *testing.T) {
236236
Version: "1.0.0",
237237
},
238238
serverID: testServerID,
239-
expectedStatus: http.StatusBadRequest,
240-
expectedError: "Bad Request",
239+
expectedStatus: http.StatusUnprocessableEntity,
240+
expectedError: "pattern",
241241
},
242242
{
243243
name: "cannot undelete server",

internal/api/handlers/v0/publish.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func RegisterPublishEndpoint(api huma.API, registry service.RegistryService, cfg
5656
// Publish the server with extensions
5757
publishedServer, err := registry.Publish(input.Body)
5858
if err != nil {
59-
return nil, huma.Error400BadRequest("Failed to publish server", err)
59+
return nil, huma.Error422UnprocessableEntity("Failed to publish server", err)
6060
}
6161

6262
// Return the published server in flattened format

internal/api/handlers/v0/publish_registry_validation_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestPublishRegistryValidation(t *testing.T) {
8080
rr := httptest.NewRecorder()
8181
mux.ServeHTTP(rr, req)
8282

83-
assert.Equal(t, http.StatusBadRequest, rr.Code)
83+
assert.Equal(t, http.StatusUnprocessableEntity, rr.Code)
8484
assert.Contains(t, rr.Body.String(), "registry validation failed")
8585
})
8686

@@ -180,7 +180,7 @@ func TestPublishRegistryValidation(t *testing.T) {
180180
rr := httptest.NewRecorder()
181181
mux.ServeHTTP(rr, req)
182182

183-
assert.Equal(t, http.StatusBadRequest, rr.Code)
183+
assert.Equal(t, http.StatusUnprocessableEntity, rr.Code)
184184
assert.Contains(t, rr.Body.String(), "registry validation failed for package 1")
185185
assert.Contains(t, rr.Body.String(), "nonexistent-second-package-abc123")
186186
})
@@ -231,7 +231,7 @@ func TestPublishRegistryValidation(t *testing.T) {
231231
rr := httptest.NewRecorder()
232232
mux.ServeHTTP(rr, req)
233233

234-
assert.Equal(t, http.StatusBadRequest, rr.Code)
234+
assert.Equal(t, http.StatusUnprocessableEntity, rr.Code)
235235
assert.Contains(t, rr.Body.String(), "registry validation failed for package 0")
236236
assert.Contains(t, rr.Body.String(), "nonexistent-first-package-xyz789")
237237
})

internal/api/handlers/v0/publish_test.go

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func TestPublishEndpoint(t *testing.T) {
8080
{
8181
name: "successful publish with no auth (AuthMethodNone)",
8282
requestBody: apiv0.ServerJSON{
83-
Name: "example/test-server",
83+
Name: "com.example/test-server",
8484
Description: "A test server without auth",
8585
Repository: model.Repository{
8686
URL: "https://github.com/example/test-server",
@@ -92,7 +92,7 @@ func TestPublishEndpoint(t *testing.T) {
9292
tokenClaims: &auth.JWTClaims{
9393
AuthMethod: auth.MethodNone,
9494
Permissions: []auth.Permission{
95-
{Action: auth.PermissionActionPublish, ResourcePattern: "example/*"},
95+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
9696
},
9797
},
9898
setupRegistryService: func(_ service.RegistryService) {
@@ -127,7 +127,7 @@ func TestPublishEndpoint(t *testing.T) {
127127
{
128128
name: "invalid token",
129129
requestBody: apiv0.ServerJSON{
130-
Name: "test-server",
130+
Name: "com.example/test-server",
131131
Description: "A test server",
132132
Version: "1.0.0",
133133
},
@@ -165,7 +165,7 @@ func TestPublishEndpoint(t *testing.T) {
165165
{
166166
name: "registry service error",
167167
requestBody: apiv0.ServerJSON{
168-
Name: "example/test-server",
168+
Name: "com.example/test-server",
169169
Description: "A test server",
170170
Version: "1.0.0",
171171
Repository: model.Repository{
@@ -183,7 +183,7 @@ func TestPublishEndpoint(t *testing.T) {
183183
setupRegistryService: func(registry service.RegistryService) {
184184
// Pre-publish the same server to cause duplicate version error
185185
existingServer := apiv0.ServerJSON{
186-
Name: "example/test-server",
186+
Name: "com.example/test-server",
187187
Description: "Existing test server",
188188
Version: "1.0.0",
189189
Repository: model.Repository{
@@ -194,7 +194,7 @@ func TestPublishEndpoint(t *testing.T) {
194194
}
195195
_, _ = registry.Publish(existingServer)
196196
},
197-
expectedStatus: http.StatusBadRequest,
197+
expectedStatus: http.StatusUnprocessableEntity,
198198
expectedError: "invalid version: cannot publish duplicate version",
199199
},
200200
{
@@ -243,8 +243,8 @@ func TestPublishEndpoint(t *testing.T) {
243243
},
244244
},
245245
setupRegistryService: func(_ service.RegistryService) {},
246-
expectedStatus: http.StatusBadRequest,
247-
expectedError: "server name cannot contain multiple slashes",
246+
expectedStatus: http.StatusUnprocessableEntity,
247+
expectedError: "expected string to match pattern",
248248
},
249249
{
250250
name: "invalid server name - multiple slashes (three slashes)",
@@ -260,8 +260,8 @@ func TestPublishEndpoint(t *testing.T) {
260260
},
261261
},
262262
setupRegistryService: func(_ service.RegistryService) {},
263-
expectedStatus: http.StatusBadRequest,
264-
expectedError: "server name cannot contain multiple slashes",
263+
expectedStatus: http.StatusUnprocessableEntity,
264+
expectedError: "expected string to match pattern",
265265
},
266266
{
267267
name: "invalid server name - consecutive slashes",
@@ -277,8 +277,8 @@ func TestPublishEndpoint(t *testing.T) {
277277
},
278278
},
279279
setupRegistryService: func(_ service.RegistryService) {},
280-
expectedStatus: http.StatusBadRequest,
281-
expectedError: "server name cannot contain multiple slashes",
280+
expectedStatus: http.StatusUnprocessableEntity,
281+
expectedError: "expected string to match pattern",
282282
},
283283
{
284284
name: "invalid server name - URL-like path",
@@ -294,8 +294,8 @@ func TestPublishEndpoint(t *testing.T) {
294294
},
295295
},
296296
setupRegistryService: func(_ service.RegistryService) {},
297-
expectedStatus: http.StatusBadRequest,
298-
expectedError: "server name cannot contain multiple slashes",
297+
expectedStatus: http.StatusUnprocessableEntity,
298+
expectedError: "expected string to match pattern",
299299
},
300300
{
301301
name: "invalid server name - many slashes",
@@ -311,8 +311,8 @@ func TestPublishEndpoint(t *testing.T) {
311311
},
312312
},
313313
setupRegistryService: func(_ service.RegistryService) {},
314-
expectedStatus: http.StatusBadRequest,
315-
expectedError: "server name cannot contain multiple slashes",
314+
expectedStatus: http.StatusUnprocessableEntity,
315+
expectedError: "expected string to match pattern",
316316
},
317317
{
318318
name: "invalid server name - with packages and remotes",
@@ -349,8 +349,8 @@ func TestPublishEndpoint(t *testing.T) {
349349
},
350350
},
351351
setupRegistryService: func(_ service.RegistryService) {},
352-
expectedStatus: http.StatusBadRequest,
353-
expectedError: "server name cannot contain multiple slashes",
352+
expectedStatus: http.StatusUnprocessableEntity,
353+
expectedError: "expected string to match pattern",
354354
},
355355
}
356356

@@ -433,25 +433,25 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) {
433433
{
434434
name: "invalid - trailing slash after valid name",
435435
serverName: "com.example/server/",
436-
expectedStatus: http.StatusBadRequest,
436+
expectedStatus: http.StatusUnprocessableEntity,
437437
description: "Trailing slash creates multiple slashes",
438438
},
439439
{
440440
name: "invalid - leading and middle slash",
441441
serverName: "/com.example/server",
442-
expectedStatus: http.StatusBadRequest,
442+
expectedStatus: http.StatusUnprocessableEntity,
443443
description: "Leading slash with middle slash",
444444
},
445445
{
446446
name: "invalid - file system style path",
447447
serverName: "usr/local/bin/server",
448-
expectedStatus: http.StatusBadRequest,
448+
expectedStatus: http.StatusUnprocessableEntity,
449449
description: "File system style paths should be rejected",
450450
},
451451
{
452452
name: "invalid - version-like suffix",
453453
serverName: "com.example/server/v1.0.0",
454-
expectedStatus: http.StatusBadRequest,
454+
expectedStatus: http.StatusUnprocessableEntity,
455455
description: "Version suffixes with slash should be rejected",
456456
},
457457
}
@@ -502,9 +502,10 @@ func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) {
502502
assert.Equal(t, tc.expectedStatus, rr.Code,
503503
"%s: expected status %d, got %d", tc.description, tc.expectedStatus, rr.Code)
504504

505-
if tc.expectedStatus == http.StatusBadRequest {
506-
assert.Contains(t, rr.Body.String(), "server name cannot contain multiple slashes",
507-
"%s: should contain specific error message", tc.description)
505+
if tc.expectedStatus == http.StatusUnprocessableEntity {
506+
// Huma returns a generic pattern validation error
507+
assert.Contains(t, rr.Body.String(), "pattern",
508+
"%s: should contain pattern validation error", tc.description)
508509
}
509510
})
510511
}

pkg/api/v0/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type ServerMeta struct {
2929
// ServerJSON represents complete server information as defined in the MCP spec, with extension support
3030
type ServerJSON struct {
3131
Schema string `json:"$schema,omitempty"`
32-
Name string `json:"name" minLength:"1" maxLength:"200"`
32+
Name string `json:"name" minLength:"1" maxLength:"200" pattern:"^[a-zA-Z0-9.-]+/[a-zA-Z0-9._-]+$" doc:"Server name in format 'reverse-dns-namespace/name' (e.g., 'com.example/server')"`
3333
Description string `json:"description" minLength:"1" maxLength:"100"`
3434
Status model.Status `json:"status,omitempty" minLength:"1"`
3535
Repository model.Repository `json:"repository,omitempty"`

0 commit comments

Comments
 (0)