use fraiseql_core::federation::{
DependencyGraph,
types::{
FederatedType, FederationMetadata, FieldFederationDirectives, FieldPathSelection,
KeyDirective,
},
};
#[test]
fn test_validate_requires_field_exists() {
let mut user_type = FederatedType::new("User".to_string());
user_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
user_type.set_field_directives(
"orders".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["email".to_string()],
typename: "User".to_string(),
}),
);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![user_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_ok(), "Should validate when @requires field exists");
}
#[test]
fn test_validate_requires_empty_path() {
let mut user_type = FederatedType::new("User".to_string());
user_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
let mut directives = FieldFederationDirectives::new();
directives.requires.push(FieldPathSelection {
path: vec![], typename: "User".to_string(),
});
user_type.set_field_directives("orders".to_string(), directives);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![user_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_err(), "Should fail when @requires has empty path");
let err = result.unwrap_err();
assert!(
err.to_lowercase().contains("empty"),
"Error message should mention empty path: {}",
err
);
}
#[test]
fn test_validate_requires_nested_field_path() {
let mut order_type = FederatedType::new("Order".to_string());
order_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
order_type.set_field_directives(
"shippingEstimate".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["user".to_string(), "email".to_string()],
typename: "Order".to_string(),
}),
);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![order_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_ok(), "Should support nested field paths in @requires");
}
#[test]
fn test_validate_provides_field_exists() {
let mut order_type = FederatedType::new("Order".to_string());
order_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
order_type.set_field_directives(
"shippingEstimate".to_string(),
FieldFederationDirectives::new().add_provides(FieldPathSelection {
path: vec!["weight".to_string()],
typename: "Order".to_string(),
}),
);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![order_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_ok(), "Should validate @provides declarations");
}
#[test]
fn test_validate_external_only_on_extends() {
let mut order_type = FederatedType::new("Order".to_string());
order_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
order_type.is_extends = true;
order_type
.set_field_directives("total".to_string(), FieldFederationDirectives::new().external());
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![order_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_ok(), "Should allow @external on @extends types");
}
#[test]
fn test_validate_external_only_on_extends_fails() {
let mut user_type = FederatedType::new("User".to_string());
user_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
user_type
.set_field_directives("email".to_string(), FieldFederationDirectives::new().external());
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![user_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_err(), "Should fail when @external used on non-extends type");
let err = result.unwrap_err();
assert!(
err.to_lowercase().contains("external") || err.to_lowercase().contains("extends"),
"Error should explain @external restriction: {}",
err
);
}
#[test]
fn test_validate_two_node_circular_requires() {
let mut user_type = FederatedType::new("User".to_string());
user_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
user_type.set_field_directives(
"orders".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["user".to_string()],
typename: "Order".to_string(),
}),
);
let mut order_type = FederatedType::new("Order".to_string());
order_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
order_type.set_field_directives(
"user".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["orders".to_string()],
typename: "User".to_string(),
}),
);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![user_type, order_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_err(), "Should fail when 2-node circular @requires detected");
let err = result.unwrap_err();
assert!(
err.to_lowercase().contains("circular")
|| err.to_lowercase().contains("cycle")
|| err.to_lowercase().contains("dependency"),
"Error should mention circular dependency: {}",
err
);
}
#[test]
fn test_validate_three_node_cycle() {
let mut type_a = FederatedType::new("A".to_string());
type_a.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
type_a.set_field_directives(
"f1".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["f2".to_string()],
typename: "B".to_string(),
}),
);
let mut type_b = FederatedType::new("B".to_string());
type_b.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
type_b.set_field_directives(
"f2".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["f3".to_string()],
typename: "C".to_string(),
}),
);
let mut type_c = FederatedType::new("C".to_string());
type_c.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
type_c.set_field_directives(
"f3".to_string(),
FieldFederationDirectives::new().add_requires(FieldPathSelection {
path: vec!["f1".to_string()],
typename: "A".to_string(),
}),
);
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![type_a, type_b, type_c],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_err(), "Should fail when 3-node circular dependency detected");
}
#[test]
fn test_validate_key_fields_exist() {
let mut user_type = FederatedType::new("User".to_string());
user_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
let metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![user_type],
};
let result = validate_federation_metadata(&metadata);
assert!(result.is_ok(), "Should allow valid @key declarations");
}
fn validate_federation_metadata(metadata: &FederationMetadata) -> Result<(), String> {
if !metadata.enabled {
return Ok(());
}
for federated_type in &metadata.types {
for (field_name, directives) in &federated_type.field_directives {
for (idx, required) in directives.requires.iter().enumerate() {
if required.path.is_empty() {
return Err(format!(
"Validation Error: Invalid @requires directive on {}.{}\n\
Position: @requires[{}]\n\
Issue: Field path cannot be empty\n\
Suggestion: Specify the required field name, e.g., @requires(fields: \"email\")",
federated_type.name, field_name, idx
));
}
}
for (idx, provided) in directives.provides.iter().enumerate() {
if provided.path.is_empty() {
return Err(format!(
"Validation Error: Invalid @provides directive on {}.{}\n\
Position: @provides[{}]\n\
Issue: Field path cannot be empty\n\
Suggestion: Specify the provided field name, e.g., @provides(fields: \"weight\")",
federated_type.name, field_name, idx
));
}
}
if directives.external && !federated_type.is_extends {
return Err(format!(
"Validation Error: @external directive on non-extended type\n\
Type: {}\n\
Field: {}\n\
Issue: @external can only be used on @extends types\n\
Suggestion: Add @extends directive to type {}, or remove @external from field",
federated_type.name, field_name, federated_type.name
));
}
}
}
let graph = DependencyGraph::build(metadata).map_err(|e| {
format!("Validation Error: Failed to build dependency graph\nReason: {}", e)
})?;
let cycles = graph.detect_cycles();
if !cycles.is_empty() {
let cycle_description = cycles
.iter()
.enumerate()
.map(|(i, cycle)| format!(" Cycle {}: {}", i + 1, cycle.join(" → ")))
.collect::<Vec<_>>()
.join("\n");
return Err(format!(
"Validation Error: Circular @requires dependencies detected\n\
Cycles found:\n{}\n\
Issue: Field requirements form circular dependency chain\n\
Suggestion: Remove one of the @requires directives to break the cycle",
cycle_description
));
}
Ok(())
}