Skip to content

Commit 713be29

Browse files
Agent Orchestratorclaude
andcommitted
test: add schema regex validation test suite
Adds a test suite that extracts regex patterns from the JSON schema and validates them against test cases. Tests cover: - Transport URL pattern (http/https or template variables like {baseUrl}) - Icon sizes pattern (WxH format or "any") - Server name pattern (namespace/name format) - File SHA-256 pattern (64 hex chars) Includes helper functions for cleaner schema traversal and pattern testing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4f24b22 commit 713be29

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package validators_test
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"regexp"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
const serverSchemaPath = "../../docs/reference/server-json/server.schema.json"
14+
15+
// schemaHelper provides utilities for extracting values from the JSON schema
16+
type schemaHelper struct {
17+
t *testing.T
18+
schema map[string]interface{}
19+
}
20+
21+
func loadSchema(t *testing.T) *schemaHelper {
22+
t.Helper()
23+
data, err := os.ReadFile(serverSchemaPath)
24+
require.NoError(t, err, "Failed to read schema file")
25+
26+
var schema map[string]interface{}
27+
err = json.Unmarshal(data, &schema)
28+
require.NoError(t, err, "Failed to parse schema JSON")
29+
30+
return &schemaHelper{t: t, schema: schema}
31+
}
32+
33+
// getDefinition returns a definition from the schema by name
34+
func (s *schemaHelper) getDefinition(name string) map[string]interface{} {
35+
s.t.Helper()
36+
definitions := s.schema["definitions"].(map[string]interface{})
37+
def, ok := definitions[name].(map[string]interface{})
38+
require.True(s.t, ok, "Definition %q not found in schema", name)
39+
return def
40+
}
41+
42+
// getPropertyPattern extracts a regex pattern from a definition's property
43+
func (s *schemaHelper) getPropertyPattern(definitionName, propertyName string) string {
44+
s.t.Helper()
45+
def := s.getDefinition(definitionName)
46+
props := def["properties"].(map[string]interface{})
47+
prop, ok := props[propertyName].(map[string]interface{})
48+
require.True(s.t, ok, "Property %q not found in %s", propertyName, definitionName)
49+
pattern, ok := prop["pattern"].(string)
50+
require.True(s.t, ok, "Pattern not found for %s.%s", definitionName, propertyName)
51+
return pattern
52+
}
53+
54+
// getItemsPattern extracts a regex pattern from an array property's items
55+
func (s *schemaHelper) getItemsPattern(definitionName, propertyName string) string {
56+
s.t.Helper()
57+
def := s.getDefinition(definitionName)
58+
props := def["properties"].(map[string]interface{})
59+
prop, ok := props[propertyName].(map[string]interface{})
60+
require.True(s.t, ok, "Property %q not found in %s", propertyName, definitionName)
61+
items, ok := prop["items"].(map[string]interface{})
62+
require.True(s.t, ok, "Items not found for %s.%s", definitionName, propertyName)
63+
pattern, ok := items["pattern"].(string)
64+
require.True(s.t, ok, "Pattern not found in items for %s.%s", definitionName, propertyName)
65+
return pattern
66+
}
67+
68+
// testPattern runs a set of test cases against a regex pattern
69+
func testPattern(t *testing.T, pattern string, validCases, invalidCases []string) {
70+
t.Helper()
71+
re, err := regexp.Compile(pattern)
72+
require.NoError(t, err, "Pattern should be valid regex: %s", pattern)
73+
74+
for _, tc := range validCases {
75+
assert.True(t, re.MatchString(tc), "Expected %q to match pattern", tc)
76+
}
77+
for _, tc := range invalidCases {
78+
assert.False(t, re.MatchString(tc), "Expected %q to NOT match pattern", tc)
79+
}
80+
}
81+
82+
// TestTransportURLPattern validates the URL pattern used by StreamableHttpTransport and SseTransport.
83+
// URLs must start with http://, https://, or a template variable like {baseUrl}.
84+
func TestTransportURLPattern(t *testing.T) {
85+
schema := loadSchema(t)
86+
87+
streamablePattern := schema.getPropertyPattern("StreamableHttpTransport", "url")
88+
ssePattern := schema.getPropertyPattern("SseTransport", "url")
89+
90+
// Verify both transport types use the same pattern
91+
assert.Equal(t, streamablePattern, ssePattern,
92+
"StreamableHttpTransport and SseTransport should use identical URL patterns")
93+
94+
t.Logf("Pattern: %s", streamablePattern)
95+
96+
validCases := []string{
97+
// Standard URLs
98+
"https://api.example.com/mcp",
99+
"http://localhost:8080/sse",
100+
"https://example.com/path?query=value",
101+
"https://api.example.com/v1/mcp",
102+
// Template variables
103+
"{baseUrl}",
104+
"{baseUrl}/mcp",
105+
"{server_url}/api/v1",
106+
"{API_ENDPOINT}",
107+
"{a}",
108+
"{_private}/endpoint",
109+
}
110+
111+
invalidCases := []string{
112+
"ftp://example.com", // wrong protocol
113+
"example.com", // missing protocol or variable
114+
"/relative/path", // relative path
115+
"{invalid-name}/path", // hyphen in variable name
116+
"{123invalid}", // variable starts with number
117+
"", // empty string
118+
"mailto:test@example.com",// wrong protocol
119+
"file:///path/to/file", // wrong protocol
120+
"{}/empty", // empty variable name
121+
"{{nested}}/path", // nested braces
122+
}
123+
124+
testPattern(t, streamablePattern, validCases, invalidCases)
125+
}
126+
127+
// TestIconSizesPattern validates the pattern for icon size strings.
128+
// Sizes must be in WxH format (e.g., "48x48") or "any" for scalable formats.
129+
func TestIconSizesPattern(t *testing.T) {
130+
schema := loadSchema(t)
131+
pattern := schema.getItemsPattern("Icon", "sizes")
132+
133+
t.Logf("Pattern: %s", pattern)
134+
135+
validCases := []string{
136+
"48x48",
137+
"96x96",
138+
"128x128",
139+
"1x1",
140+
"1920x1080",
141+
"any",
142+
}
143+
144+
invalidCases := []string{
145+
"48", // missing dimension
146+
"x48", // missing width
147+
"48x", // missing height
148+
"48X48", // uppercase X
149+
"any x any", // spaces
150+
"48 x 48", // spaces
151+
"large", // arbitrary string
152+
"", // empty
153+
"48x48x48", // three dimensions
154+
}
155+
156+
testPattern(t, pattern, validCases, invalidCases)
157+
}
158+
159+
// TestServerNamePattern validates the pattern for server names.
160+
// Names must be in reverse-DNS format with exactly one slash: namespace/name
161+
func TestServerNamePattern(t *testing.T) {
162+
schema := loadSchema(t)
163+
def := schema.getDefinition("ServerDetail")
164+
props := def["properties"].(map[string]interface{})
165+
nameProp := props["name"].(map[string]interface{})
166+
pattern := nameProp["pattern"].(string)
167+
168+
t.Logf("Pattern: %s", pattern)
169+
170+
validCases := []string{
171+
"com.example/server",
172+
"io.github.user/my-server",
173+
"com.microsoft.azure/webapp",
174+
"org.apache/kafka-mcp",
175+
"a/b",
176+
"com.example/server_name",
177+
"com.example/server.name",
178+
}
179+
180+
invalidCases := []string{
181+
"com.example", // no slash
182+
"com.example/", // empty name
183+
"/server", // empty namespace
184+
"com.example/a/b", // multiple slashes
185+
"com example/server", // space in namespace
186+
"com.example/my server", // space in name
187+
"", // empty
188+
}
189+
190+
testPattern(t, pattern, validCases, invalidCases)
191+
}
192+
193+
// TestFileSHA256Pattern validates the pattern for SHA-256 file hashes.
194+
func TestFileSHA256Pattern(t *testing.T) {
195+
schema := loadSchema(t)
196+
pattern := schema.getPropertyPattern("Package", "fileSha256")
197+
198+
t.Logf("Pattern: %s", pattern)
199+
200+
validCases := []string{
201+
"fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
202+
"0000000000000000000000000000000000000000000000000000000000000000",
203+
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
204+
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
205+
}
206+
207+
invalidCases := []string{
208+
"fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633c", // 63 chars
209+
"fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633cee", // 65 chars
210+
"FE333E598595000AE021BD27117DB32EC69AF6987F507BA7A63C90638FF633CE", // uppercase
211+
"ge333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce", // invalid char 'g'
212+
"", // empty
213+
}
214+
215+
testPattern(t, pattern, validCases, invalidCases)
216+
}

0 commit comments

Comments
 (0)