Improved parameter validation
Change-Id: If2e7ec1b063a6e114a6c5582463af784b75c37b8
diff --git a/tools/schema_test.go b/tools/schema_test.go
new file mode 100644
index 0000000..4365537
--- /dev/null
+++ b/tools/schema_test.go
@@ -0,0 +1,362 @@
+package tools
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/korap/korap-mcp/service"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestAllToolsSchemaCompliance ensures all tools have properly structured schemas
+func TestAllToolsSchemaCompliance(t *testing.T) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Verify basic schema structure
+ validateBasicSchemaStructure(t, schema, tool.Name())
+
+ // Verify schema is valid JSON
+ validateSchemaIsValidJSON(t, schema, tool.Name())
+
+ // Verify schema documentation completeness
+ validateSchemaDocumentation(t, schema, tool.Name())
+ })
+ }
+}
+
+// validateBasicSchemaStructure checks that the schema has the required top-level properties
+func validateBasicSchemaStructure(t *testing.T, schema map[string]interface{}, toolName string) {
+ // Must be object type
+ assert.Equal(t, "object", schema["type"], "Tool %s schema must be object type", toolName)
+
+ // Must have properties
+ properties, exists := schema["properties"]
+ assert.True(t, exists, "Tool %s schema must have properties", toolName)
+ assert.IsType(t, map[string]interface{}{}, properties, "Tool %s properties must be an object", toolName)
+
+ // Must have required array
+ required, exists := schema["required"]
+ assert.True(t, exists, "Tool %s schema must have required array", toolName)
+ assert.IsType(t, []string{}, required, "Tool %s required must be string array", toolName)
+
+ // Should have title and description
+ assert.Contains(t, schema, "title", "Tool %s schema should have title", toolName)
+ assert.Contains(t, schema, "description", "Tool %s schema should have description", toolName)
+
+ // Should prevent additional properties for strict validation
+ assert.Contains(t, schema, "additionalProperties", "Tool %s schema should specify additionalProperties", toolName)
+ assert.Equal(t, false, schema["additionalProperties"], "Tool %s should not allow additional properties", toolName)
+}
+
+// validateSchemaIsValidJSON ensures the schema can be properly serialized to JSON
+func validateSchemaIsValidJSON(t *testing.T, schema map[string]interface{}, toolName string) {
+ jsonBytes, err := json.Marshal(schema)
+ require.NoError(t, err, "Tool %s schema must be serializable to JSON", toolName)
+
+ var unmarshalled map[string]interface{}
+ err = json.Unmarshal(jsonBytes, &unmarshalled)
+ require.NoError(t, err, "Tool %s schema JSON must be valid", toolName)
+
+ // Verify the unmarshalled schema has the same structure
+ assert.Equal(t, schema["type"], unmarshalled["type"], "Tool %s schema type must survive JSON round-trip", toolName)
+}
+
+// validateSchemaDocumentation checks that all properties have proper documentation
+func validateSchemaDocumentation(t *testing.T, schema map[string]interface{}, toolName string) {
+ properties, ok := schema["properties"].(map[string]interface{})
+ require.True(t, ok, "Tool %s properties must be accessible", toolName)
+
+ for propName, propSchema := range properties {
+ propMap, ok := propSchema.(map[string]interface{})
+ require.True(t, ok, "Tool %s property %s must be an object", toolName, propName)
+
+ // Must have type
+ assert.Contains(t, propMap, "type", "Tool %s property %s must have type", toolName, propName)
+
+ // Must have description
+ assert.Contains(t, propMap, "description", "Tool %s property %s must have description", toolName, propName)
+
+ description, ok := propMap["description"].(string)
+ assert.True(t, ok, "Tool %s property %s description must be string", toolName, propName)
+ assert.NotEmpty(t, description, "Tool %s property %s description must not be empty", toolName, propName)
+ assert.Greater(t, len(description), 10, "Tool %s property %s description should be descriptive", toolName, propName)
+
+ // String properties should have examples
+ if propType, ok := propMap["type"].(string); ok && propType == "string" {
+ if enumValues, hasEnum := propMap["enum"]; hasEnum {
+ // Enum properties should have examples matching enum values
+ assert.Contains(t, propMap, "examples", "Tool %s property %s with enum should have examples", toolName, propName)
+ examples, ok := propMap["examples"].([]string)
+ assert.True(t, ok, "Tool %s property %s examples must be string array", toolName, propName)
+
+ enumArray, ok := enumValues.([]string)
+ assert.True(t, ok, "Tool %s property %s enum must be string array", toolName, propName)
+
+ // All examples should be valid enum values
+ for _, example := range examples {
+ assert.Contains(t, enumArray, example, "Tool %s property %s example '%s' must be valid enum value", toolName, propName, example)
+ }
+ } else {
+ // Regular string properties should have examples
+ assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+ }
+ }
+
+ // Integer properties should have examples and constraints
+ if propType, ok := propMap["type"].(string); ok && propType == "integer" {
+ assert.Contains(t, propMap, "examples", "Tool %s property %s should have examples", toolName, propName)
+
+ // Should have minimum/maximum constraints
+ assert.Contains(t, propMap, "minimum", "Tool %s property %s should have minimum", toolName, propName)
+ assert.Contains(t, propMap, "maximum", "Tool %s property %s should have maximum", toolName, propName)
+ }
+ }
+}
+
+// TestSchemaExamples verifies that schema examples are realistic and valid
+func TestSchemaExamples(t *testing.T) {
+ client := &service.Client{}
+
+ t.Run("SearchTool", func(t *testing.T) {
+ tool := NewSearchTool(client)
+ schema := tool.InputSchema()
+
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test query examples
+ queryProp := properties["query"].(map[string]interface{})
+ examples := queryProp["examples"].([]string)
+
+ for _, example := range examples {
+ assert.NotEmpty(t, example, "Query example should not be empty")
+ assert.LessOrEqual(t, len(example), 100, "Query example should be reasonably short")
+ }
+
+ // Test corpus examples
+ corpusProp := properties["corpus"].(map[string]interface{})
+ corpusExamples := corpusProp["examples"].([]string)
+
+ for _, example := range corpusExamples {
+ assert.NotEmpty(t, example, "Corpus example should not be empty")
+ // Should match the pattern defined in the schema
+ pattern := corpusProp["pattern"].(string)
+ assert.NotEmpty(t, pattern, "Corpus should have validation pattern")
+ }
+ })
+
+ t.Run("MetadataTool", func(t *testing.T) {
+ tool := NewMetadataTool(client)
+ schema := tool.InputSchema()
+
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test action examples
+ actionProp := properties["action"].(map[string]interface{})
+ examples := actionProp["examples"].([]string)
+ enumValues := actionProp["enum"].([]string)
+
+ for _, example := range examples {
+ assert.Contains(t, enumValues, example, "Action example should be valid enum value")
+ }
+ })
+}
+
+// TestSchemaConstraints verifies that schema constraints are reasonable
+func TestSchemaConstraints(t *testing.T) {
+ client := &service.Client{}
+
+ t.Run("SearchTool", func(t *testing.T) {
+ tool := NewSearchTool(client)
+ schema := tool.InputSchema()
+ properties := schema["properties"].(map[string]interface{})
+
+ // Test query constraints
+ queryProp := properties["query"].(map[string]interface{})
+ minLength := queryProp["minLength"].(int)
+ maxLength := queryProp["maxLength"].(int)
+
+ assert.Greater(t, minLength, 0, "Query minimum length should be positive")
+ assert.Greater(t, maxLength, minLength, "Query maximum length should be greater than minimum")
+ assert.LessOrEqual(t, maxLength, 10000, "Query maximum length should be reasonable")
+
+ // Test count constraints
+ countProp := properties["count"].(map[string]interface{})
+ minimum := countProp["minimum"].(int)
+ maximum := countProp["maximum"].(int)
+ defaultValue := countProp["default"].(int)
+
+ assert.GreaterOrEqual(t, minimum, 0, "Count minimum should be non-negative")
+ assert.Greater(t, maximum, minimum, "Count maximum should be greater than minimum")
+ assert.GreaterOrEqual(t, defaultValue, minimum, "Count default should be >= minimum")
+ assert.LessOrEqual(t, defaultValue, maximum, "Count default should be <= maximum")
+ })
+}
+
+// TestSchemaVersioning ensures schema structure is backwards compatible
+func TestSchemaVersioning(t *testing.T) {
+ client := &service.Client{}
+
+ // This test would be extended when we add new versions
+ // For now, we ensure the basic structure is stable
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Core schema structure must remain stable
+ assert.Equal(t, "object", schema["type"])
+ assert.Contains(t, schema, "properties")
+ assert.Contains(t, schema, "required")
+
+ // Tool-specific stability checks
+ switch tool.Name() {
+ case "korap_search":
+ properties := schema["properties"].(map[string]interface{})
+ assert.Contains(t, properties, "query", "Search tool must always have query parameter")
+
+ required := schema["required"].([]string)
+ assert.Contains(t, required, "query", "Search tool must always require query parameter")
+
+ case "korap_metadata":
+ properties := schema["properties"].(map[string]interface{})
+ assert.Contains(t, properties, "action", "Metadata tool must always have action parameter")
+
+ required := schema["required"].([]string)
+ assert.Contains(t, required, "action", "Metadata tool must always require action parameter")
+ }
+ })
+ }
+}
+
+// BenchmarkSchemaGeneration measures schema generation performance
+func BenchmarkSchemaGeneration(b *testing.B) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ b.Run(tool.Name(), func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ schema := tool.InputSchema()
+ _ = schema // Prevent optimization
+ }
+ })
+ }
+}
+
+// TestSchemaDeepCopy ensures schemas are independent instances
+func TestSchemaDeepCopy(t *testing.T) {
+ client := &service.Client{}
+ tool := NewSearchTool(client)
+
+ // Get two schema instances
+ schema1 := tool.InputSchema()
+ schema2 := tool.InputSchema()
+
+ // They should be equal in content but different instances
+ assert.True(t, reflect.DeepEqual(schema1, schema2), "Schemas should have equal content")
+
+ // Modifying one should not affect the other
+ schema1["modified"] = true
+ assert.NotContains(t, schema2, "modified", "Schema instances should be independent")
+}
+
+// TestSchemaHelpGeneration verifies schema can be used to generate help text
+func TestSchemaHelpGeneration(t *testing.T) {
+ client := &service.Client{}
+
+ tools := []Tool{
+ NewSearchTool(client),
+ NewMetadataTool(client),
+ }
+
+ for _, tool := range tools {
+ t.Run(tool.Name(), func(t *testing.T) {
+ schema := tool.InputSchema()
+
+ // Generate help text from schema
+ helpText := generateHelpText(schema, tool.Name())
+
+ // Verify help text contains essential information
+ assert.Contains(t, helpText, tool.Name(), "Help text should contain tool name")
+ assert.Contains(t, helpText, "Parameters:", "Help text should list parameters")
+
+ // Should contain information about required parameters
+ required := schema["required"].([]string)
+ for _, param := range required {
+ assert.Contains(t, helpText, param, "Help text should mention required parameter %s", param)
+ assert.Contains(t, helpText, "(required)", "Help text should mark required parameters")
+ }
+ })
+ }
+}
+
+// generateHelpText creates human-readable help text from a schema
+func generateHelpText(schema map[string]interface{}, toolName string) string {
+ help := fmt.Sprintf("Tool: %s\n", toolName)
+
+ if desc, ok := schema["description"].(string); ok {
+ help += fmt.Sprintf("Description: %s\n\n", desc)
+ }
+
+ properties, ok := schema["properties"].(map[string]interface{})
+ if !ok {
+ return help
+ }
+
+ required, _ := schema["required"].([]string)
+ requiredMap := make(map[string]bool)
+ for _, req := range required {
+ requiredMap[req] = true
+ }
+
+ help += "Parameters:\n"
+ for paramName, paramSchema := range properties {
+ paramMap, ok := paramSchema.(map[string]interface{})
+ if !ok {
+ continue
+ }
+
+ requiredText := ""
+ if requiredMap[paramName] {
+ requiredText = " (required)"
+ }
+
+ paramType, _ := paramMap["type"].(string)
+ description, _ := paramMap["description"].(string)
+
+ help += fmt.Sprintf(" %s (%s)%s: %s\n", paramName, paramType, requiredText, description)
+
+ // Add enum values if present
+ if enum, ok := paramMap["enum"].([]string); ok {
+ help += fmt.Sprintf(" Allowed values: %v\n", enum)
+ }
+
+ // Add examples if present
+ if examples, ok := paramMap["examples"]; ok {
+ help += fmt.Sprintf(" Examples: %v\n", examples)
+ }
+ }
+
+ return help
+}