353 lines
11 KiB
Rust
353 lines
11 KiB
Rust
|
|
// Integration tests for ValidationPipeline
|
||
|
|
// Demonstrates end-to-end schema validation workflow
|
||
|
|
|
||
|
|
use std::path::PathBuf;
|
||
|
|
use std::sync::Arc;
|
||
|
|
|
||
|
|
use serde_json::json;
|
||
|
|
use vapora_shared::validation::{
|
||
|
|
CompiledSchema, Contract, FieldSchema, FieldType, SchemaRegistry, SchemaSource,
|
||
|
|
ValidationPipeline,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Create a test schema for MCP tool validation
|
||
|
|
fn create_mcp_tool_schema() -> CompiledSchema {
|
||
|
|
CompiledSchema {
|
||
|
|
name: "tools/kanban_create_task".to_string(),
|
||
|
|
fields: vec![
|
||
|
|
FieldSchema {
|
||
|
|
name: "project_id".to_string(),
|
||
|
|
field_type: FieldType::String,
|
||
|
|
required: true,
|
||
|
|
contracts: vec![Contract::NonEmpty, Contract::Uuid],
|
||
|
|
default: None,
|
||
|
|
doc: Some("Project UUID".to_string()),
|
||
|
|
},
|
||
|
|
FieldSchema {
|
||
|
|
name: "title".to_string(),
|
||
|
|
field_type: FieldType::String,
|
||
|
|
required: true,
|
||
|
|
contracts: vec![
|
||
|
|
Contract::NonEmpty,
|
||
|
|
Contract::MinLength(3),
|
||
|
|
Contract::MaxLength(200),
|
||
|
|
],
|
||
|
|
default: None,
|
||
|
|
doc: Some("Task title (3-200 chars)".to_string()),
|
||
|
|
},
|
||
|
|
FieldSchema {
|
||
|
|
name: "description".to_string(),
|
||
|
|
field_type: FieldType::String,
|
||
|
|
required: false,
|
||
|
|
contracts: vec![],
|
||
|
|
default: Some(json!("")),
|
||
|
|
doc: Some("Task description".to_string()),
|
||
|
|
},
|
||
|
|
FieldSchema {
|
||
|
|
name: "priority".to_string(),
|
||
|
|
field_type: FieldType::String,
|
||
|
|
required: true,
|
||
|
|
contracts: vec![],
|
||
|
|
default: None,
|
||
|
|
doc: Some("Task priority".to_string()),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
custom_contracts: vec![],
|
||
|
|
source_path: PathBuf::from("schemas/tools/kanban_create_task.ncl"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a test schema for agent task assignment
|
||
|
|
fn create_agent_assignment_schema() -> CompiledSchema {
|
||
|
|
CompiledSchema {
|
||
|
|
name: "agents/task_assignment".to_string(),
|
||
|
|
fields: vec![
|
||
|
|
FieldSchema {
|
||
|
|
name: "role".to_string(),
|
||
|
|
field_type: FieldType::Enum(vec![
|
||
|
|
"developer".to_string(),
|
||
|
|
"reviewer".to_string(),
|
||
|
|
"architect".to_string(),
|
||
|
|
"tester".to_string(),
|
||
|
|
]),
|
||
|
|
required: true,
|
||
|
|
contracts: vec![],
|
||
|
|
default: None,
|
||
|
|
doc: Some("Agent role".to_string()),
|
||
|
|
},
|
||
|
|
FieldSchema {
|
||
|
|
name: "title".to_string(),
|
||
|
|
field_type: FieldType::String,
|
||
|
|
required: true,
|
||
|
|
contracts: vec![Contract::NonEmpty, Contract::MaxLength(500)],
|
||
|
|
default: None,
|
||
|
|
doc: Some("Task title".to_string()),
|
||
|
|
},
|
||
|
|
FieldSchema {
|
||
|
|
name: "priority".to_string(),
|
||
|
|
field_type: FieldType::Number,
|
||
|
|
required: false,
|
||
|
|
contracts: vec![Contract::Range {
|
||
|
|
min: 0.0,
|
||
|
|
max: 100.0,
|
||
|
|
}],
|
||
|
|
default: Some(json!(50)),
|
||
|
|
doc: Some("Priority score (0-100)".to_string()),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
custom_contracts: vec![],
|
||
|
|
source_path: PathBuf::from("schemas/agents/task_assignment.ncl"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_mcp_tool_valid_input() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
|
||
|
|
// Load pre-compiled schema
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let source = SchemaSource::Compiled(schema);
|
||
|
|
let loaded_schema = registry
|
||
|
|
.load_from_source(source, "tools/kanban_create_task")
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"title": "Implement validation pipeline",
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline
|
||
|
|
.validate_against_schema(&loaded_schema, &input)
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
assert!(result.valid, "Valid input should pass validation");
|
||
|
|
assert!(result.errors.is_empty());
|
||
|
|
assert!(result.validated_data.is_some());
|
||
|
|
|
||
|
|
// Check defaults applied
|
||
|
|
let validated = result.validated_data.unwrap();
|
||
|
|
assert_eq!(validated["description"], "");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_mcp_tool_missing_required_field() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"title": "Test Task",
|
||
|
|
// Missing project_id (required)
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("project_id"));
|
||
|
|
assert!(result.error_messages()[0].contains("Required field missing"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_mcp_tool_invalid_uuid() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "not-a-valid-uuid",
|
||
|
|
"title": "Test Task",
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("project_id"));
|
||
|
|
|
||
|
|
let error_msg = result.error_messages()[0].to_lowercase();
|
||
|
|
assert!(error_msg.contains("uuid") || error_msg.contains("format"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_mcp_tool_title_too_short() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"title": "AB", // Only 2 chars, min is 3
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("title"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_mcp_tool_title_too_long() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let long_title = "A".repeat(201); // Max is 200
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"title": long_title,
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("title"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_agent_assignment_valid_input() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_agent_assignment_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"role": "developer",
|
||
|
|
"title": "Fix authentication bug"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(result.valid);
|
||
|
|
assert!(result.errors.is_empty());
|
||
|
|
|
||
|
|
// Check default priority applied
|
||
|
|
let validated = result.validated_data.unwrap();
|
||
|
|
assert_eq!(validated["priority"], 50);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_agent_assignment_invalid_role() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_agent_assignment_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"role": "invalid_role", // Not in enum
|
||
|
|
"title": "Test Task"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("role"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_agent_assignment_priority_out_of_range() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_agent_assignment_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"role": "developer",
|
||
|
|
"title": "Test Task",
|
||
|
|
"priority": 150 // > 100
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("priority"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_strict_mode_rejects_unknown_fields() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry).with_strict_mode(true);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"title": "Test Task",
|
||
|
|
"priority": "high",
|
||
|
|
"unknown_field": "value" // Not in schema
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
assert!(result.has_field_error("unknown_field"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_non_strict_mode_allows_unknown_fields() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry).with_strict_mode(false);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
|
|
"title": "Test Task",
|
||
|
|
"priority": "high",
|
||
|
|
"unknown_field": "value" // Allowed in non-strict mode
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
// Should pass, but unknown_field won't be in validated_data
|
||
|
|
assert!(result.valid);
|
||
|
|
let validated = result.validated_data.unwrap();
|
||
|
|
assert!(!validated.as_object().unwrap().contains_key("unknown_field"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_multiple_validation_errors() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"project_id": "invalid-uuid",
|
||
|
|
"title": "AB", // Too short
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
assert!(!result.valid);
|
||
|
|
// Should have errors for both project_id and title
|
||
|
|
assert!(result.errors.len() >= 2);
|
||
|
|
assert!(result.has_field_error("project_id"));
|
||
|
|
assert!(result.has_field_error("title"));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_validation_result_serialization() {
|
||
|
|
let registry = Arc::new(SchemaRegistry::new(PathBuf::from("schemas")));
|
||
|
|
let schema = create_mcp_tool_schema();
|
||
|
|
let pipeline = ValidationPipeline::new(registry);
|
||
|
|
|
||
|
|
let input = json!({
|
||
|
|
"title": "", // Empty violates NonEmpty
|
||
|
|
"priority": "high"
|
||
|
|
});
|
||
|
|
|
||
|
|
let result = pipeline.validate_against_schema(&schema, &input).unwrap();
|
||
|
|
|
||
|
|
// Serialize to JSON
|
||
|
|
let json_result = serde_json::to_string(&result).unwrap();
|
||
|
|
assert!(json_result.contains("valid"));
|
||
|
|
assert!(json_result.contains("errors"));
|
||
|
|
|
||
|
|
// Deserialize back
|
||
|
|
let _deserialized: vapora_shared::validation::ValidationResult =
|
||
|
|
serde_json::from_str(&json_result).unwrap();
|
||
|
|
}
|