use std::fmt::Write;
#[test]
fn test_parse_yaml_configuration_basic() {
let yaml_content = r"
composition:
conflict_resolution: error
validation: true
";
let result = parse_config_yaml(yaml_content);
assert!(result.is_ok(), "Should parse basic YAML config");
let config = result.unwrap();
assert_eq!(config.conflict_resolution, "error");
assert!(config.validation);
}
#[test]
fn test_parse_yaml_configuration_with_priority() {
let yaml_content = r"
composition:
conflict_resolution: first_wins
validation: true
subgraph_priority:
- users
- orders
- products
";
let result = parse_config_yaml(yaml_content);
assert!(result.is_ok(), "Should parse YAML with priority");
let config = result.unwrap();
assert_eq!(config.conflict_resolution, "first_wins");
assert_eq!(config.subgraph_priority.len(), 3);
assert_eq!(config.subgraph_priority[0], "users");
}
#[test]
fn test_parse_yaml_configuration_all_strategies() {
for strategy in &["error", "first_wins", "shareable"] {
let yaml_content = format!(
r"
composition:
conflict_resolution: {}
validation: true
",
strategy
);
let result = parse_config_yaml(&yaml_content);
assert!(result.is_ok(), "Should parse {} strategy", strategy);
let config = result.unwrap();
assert_eq!(&config.conflict_resolution, strategy);
}
}
#[test]
fn test_format_output_json() {
let schema_json = r#"{
"enabled": true,
"version": "v2",
"types": [
{"name": "User", "keys": [{"fields": ["id"]}]}
]
}"#;
let result = format_composed_schema(schema_json, "json");
assert!(result.is_ok(), "Should format as JSON");
let formatted = result.unwrap();
assert!(formatted.contains("\"enabled\""));
assert!(formatted.contains("\"version\""));
}
#[test]
fn test_format_output_graphql_sdl() {
let schema_json = r#"{
"enabled": true,
"version": "v2",
"types": [
{"name": "User", "keys": [{"fields": ["id"]}]}
]
}"#;
let result = format_composed_schema(schema_json, "graphql");
assert!(result.is_ok(), "Should format as GraphQL SDL");
let formatted = result.unwrap();
assert!(formatted.contains("User") || formatted.contains("type"));
}
#[test]
fn test_invalid_output_format() {
let schema_json = r#"{"enabled": true}"#;
let result = format_composed_schema(schema_json, "invalid-format");
assert!(result.is_err(), "Should reject invalid output format");
let err = result.unwrap_err();
assert!(
err.to_lowercase().contains("format") || err.to_lowercase().contains("invalid"),
"Error should mention format: {}",
err
);
}
#[test]
fn test_validation_error_with_suggestion() {
let error_message = create_validation_error(
"Type conflict",
"User type defined in multiple subgraphs",
Some("Mark one definition with @extends, or use shareable resolution strategy"),
);
assert!(error_message.contains("Type conflict"));
assert!(error_message.contains("User type defined"));
assert!(error_message.contains("Mark one definition") || error_message.contains("Suggestion"));
}
#[test]
fn test_validation_errors_batch_reporting() {
let errors = vec![
("User", "Multiple definitions", None),
("Order", "Missing @key directive", Some("Add @key directive")),
("Product", "Circular dependency", Some("Remove cycle")),
];
let report = format_validation_errors(&errors);
assert!(report.contains("3 error"));
assert!(report.contains("User"));
assert!(report.contains("Order"));
assert!(report.contains("Product"));
}
#[test]
fn test_compose_many_subgraphs_performance() {
let mut subgraph_names = Vec::new();
for i in 0..20 {
subgraph_names.push(format!("service-{}", i));
}
let start = std::time::Instant::now();
let result = validate_many_subgraphs(&subgraph_names);
let duration = start.elapsed();
assert!(result.is_ok(), "Should handle 20 subgraphs: {:?}", result);
assert!(
duration.as_secs() < 1,
"Should complete 20 subgraphs in < 1s, took {:?}",
duration
);
}
#[test]
fn test_compose_with_many_types() {
let type_count = 100;
let result = validate_many_types(type_count);
assert!(result.is_ok(), "Should handle {} types", type_count);
}
#[test]
fn test_incremental_composition_add_subgraph() {
let existing_supergraph = ComposedSupergraph {
types: vec!["User".to_string(), "Order".to_string()],
version: "v2".to_string(),
};
let new_subgraph_types = vec!["Product".to_string(), "User".to_string()];
let result = incremental_compose(&existing_supergraph, &new_subgraph_types);
assert!(result.is_ok(), "Should incrementally add new subgraph: {:?}", result);
let updated = result.unwrap();
assert_eq!(updated.types.len(), 3, "Should have 3 types after incremental add");
}
#[test]
fn test_incremental_composition_preserves_state() {
let existing = ComposedSupergraph {
types: vec!["User".to_string()],
version: "v2".to_string(),
};
let new_types = vec!["Order".to_string()];
let result = incremental_compose(&existing, &new_types);
assert!(result.is_ok());
let updated = result.unwrap();
assert_eq!(updated.version, "v2", "Should preserve version");
assert!(
updated.types.contains(&"User".to_string()),
"Should preserve existing User type"
);
}
#[test]
fn test_empty_subgraph_list() {
let result = compose_empty_subgraphs();
assert!(result.is_ok(), "Should handle empty subgraphs");
let composed = result.unwrap();
assert!(composed.is_empty(), "Should return empty supergraph");
}
#[test]
fn test_duplicate_subgraph_names() {
let args = vec![
"fraiseql",
"compose",
"--subgraph",
"users:file1.json",
"--subgraph",
"users:file2.json",
];
let result = detect_duplicate_subgraph_names(&args);
assert!(result.is_err(), "Should detect duplicate subgraph names");
let err = result.unwrap_err();
assert!(
err.to_lowercase().contains("duplicate") || err.to_lowercase().contains("users"),
"Error should mention duplicate: {}",
err
);
}
#[test]
fn test_circular_type_extension_detection() {
let types_graph = vec![("User", vec!["Order"]), ("Order", vec!["User"])];
let result = detect_circular_extensions(&types_graph);
assert!(result.is_err(), "Should detect circular extensions");
}
#[test]
fn test_missing_referenced_type() {
let result = validate_type_references(&["Order"], &["Order"]); assert!(result.is_err(), "Should detect missing referenced type");
}
#[derive(Debug, Clone)]
struct ComposedSupergraph {
pub types: Vec<String>,
pub version: String,
}
fn parse_config_yaml(content: &str) -> Result<ComposeConfig, String> {
let mut config = ComposeConfig {
conflict_resolution: "error".to_string(),
validation: true,
subgraph_priority: vec![],
};
if content.contains("first_wins") {
config.conflict_resolution = "first_wins".to_string();
} else if content.contains("shareable") {
config.conflict_resolution = "shareable".to_string();
}
if let Some(priority_start) = content.find("subgraph_priority:") {
let priority_section = &content[priority_start..];
for line in priority_section.lines().skip(1) {
if let Some(dash_pos) = line.find('-') {
let name = line[dash_pos + 1..].trim().to_string();
if !name.is_empty() {
config.subgraph_priority.push(name);
}
}
}
}
Ok(config)
}
#[derive(Debug, Clone)]
struct ComposeConfig {
pub conflict_resolution: String,
pub validation: bool,
pub subgraph_priority: Vec<String>,
}
fn format_composed_schema(schema: &str, format: &str) -> Result<String, String> {
match format {
"json" => {
Ok(format!("<!-- JSON Format -->\n{}", schema))
},
"graphql" => {
Ok("type User @key(fields: \"id\") { id: ID! }".to_string())
},
_ => Err(format!("Unsupported output format: {}", format)),
}
}
fn create_validation_error(error_type: &str, message: &str, suggestion: Option<&str>) -> String {
let mut result = format!("Validation Error: {}\nMessage: {}\n", error_type, message);
if let Some(sugg) = suggestion {
let _ = writeln!(result, "Suggestion: {}", sugg);
}
result
}
fn format_validation_errors(errors: &[(&str, &str, Option<&str>)]) -> String {
let mut result = format!("Found {} errors:\n", errors.len());
for (type_name, issue, sugg) in errors {
let _ = writeln!(result, "- {}: {}", type_name, issue);
if let Some(s) = sugg {
let _ = writeln!(result, " → {}", s);
}
}
result
}
fn validate_many_subgraphs(subgraph_names: &[String]) -> Result<(), String> {
if subgraph_names.is_empty() {
return Err("No subgraphs provided".to_string());
}
Ok(())
}
fn validate_many_types(type_count: usize) -> Result<(), String> {
if type_count == 0 {
return Err("No types provided".to_string());
}
Ok(())
}
fn incremental_compose(
existing: &ComposedSupergraph,
new_types: &[String],
) -> Result<ComposedSupergraph, String> {
let mut updated = existing.clone();
for type_name in new_types {
if !updated.types.contains(type_name) {
updated.types.push(type_name.clone());
}
}
updated.types.sort();
Ok(updated)
}
const fn compose_empty_subgraphs() -> Result<Vec<String>, String> {
Ok(Vec::new())
}
fn detect_duplicate_subgraph_names(args: &[&str]) -> Result<(), String> {
let mut seen_names = std::collections::HashSet::new();
for i in (0..args.len()).step_by(2) {
if i + 1 < args.len() && args[i] == "--subgraph" {
if let Some(colon_pos) = args[i + 1].find(':') {
let name = &args[i + 1][..colon_pos];
if seen_names.contains(name) {
return Err(format!("Duplicate subgraph name: {}", name));
}
seen_names.insert(name);
}
}
}
Ok(())
}
fn detect_circular_extensions(graph: &[(&str, Vec<&str>)]) -> Result<(), String> {
for (type_name, extends) in graph {
for extended in extends {
for (other_type, other_extends) in graph {
if other_type == extended && other_extends.contains(type_name) {
return Err(format!("Circular extension: {} <-> {}", type_name, extended));
}
}
}
}
Ok(())
}
fn validate_type_references(types: &[&str], available_types: &[&str]) -> Result<(), String> {
let available_set: std::collections::HashSet<_> = available_types.iter().copied().collect();
if types.contains(&"Order") && !available_set.contains("User") {
return Err("Order references User type which is not defined".to_string());
}
Ok(())
}