use std::collections::HashMap;
use fraiseql_core::federation::types::{FederatedType, FederationMetadata, KeyDirective};
#[test]
fn test_four_subgraph_composition() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let orders = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic(), create_user_type_extending()],
};
let products = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_product_type_basic(), create_order_type_extending()],
};
let payments = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![
create_payment_type_basic(),
create_user_type_extending(),
create_order_type_extending(),
],
};
let result = compose_federation_schemas(&[users, orders, products, payments]);
assert!(result.is_ok(), "Should compose 4 subgraphs: {:?}", result);
let composed = result.unwrap();
assert_eq!(composed.types.len(), 4, "Should have 4 types in composed schema");
assert!(composed.types.iter().any(|t| t.name == "User"), "Should include User type");
assert!(composed.types.iter().any(|t| t.name == "Order"), "Should include Order type");
assert!(
composed.types.iter().any(|t| t.name == "Product"),
"Should include Product type"
);
assert!(
composed.types.iter().any(|t| t.name == "Payment"),
"Should include Payment type"
);
}
#[test]
fn test_five_subgraph_composition_with_shared_extends() {
let mut subgraphs = vec![FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
}];
subgraphs.push(FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic(), create_user_type_extending()],
});
subgraphs.push(FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![
create_product_type_basic(),
create_user_type_extending(),
create_order_type_extending(),
],
});
subgraphs.push(FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![
create_user_type_extending(),
create_order_type_extending(),
create_product_type_extending(),
],
});
subgraphs.push(FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![
create_user_type_extending(),
create_order_type_extending(),
create_product_type_extending(),
],
});
let result = compose_federation_schemas(&subgraphs);
assert!(result.is_ok(), "Should compose 5 subgraphs with shared types: {:?}", result);
let composed = result.unwrap();
assert_eq!(composed.types.len(), 3, "Should have 3 distinct types");
}
#[test]
fn test_field_type_conflict_same_type() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_with_fields(
vec!["id"],
vec![("email", "String")],
)],
};
let auth = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_extending_with_fields(vec![(
"email", "Int",
)])],
};
let _result = compose_federation_schemas(&[users, auth]);
}
#[test]
fn test_multiple_key_definitions_inconsistent() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_with_key(&["id"])],
};
let mut auth_user = FederatedType::new("User".to_string());
auth_user.is_extends = true;
auth_user.keys.push(KeyDirective {
fields: vec!["email".to_string()],
resolvable: true,
});
let auth = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![auth_user],
};
let _result = compose_federation_schemas(&[users, auth]);
}
#[test]
fn test_external_fields_preserved_in_composition() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let mut orders = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic()],
};
orders.types[0].external_fields.push("user_id".to_string());
let result = compose_federation_schemas(&[users, orders]);
assert!(result.is_ok(), "Should compose with external fields: {:?}", result);
let composed = result.unwrap();
let order_type = composed.types.iter().find(|t| t.name == "Order").unwrap();
assert!(
order_type.external_fields.contains(&"user_id".to_string()),
"Should preserve @external field"
);
}
#[test]
fn test_shareable_fields_preserved_in_composition() {
let mut users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
users.types[0].shareable_fields.push("email".to_string());
let mut auth = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_extending()],
};
auth.types[0].shareable_fields.push("email".to_string());
let result = compose_federation_schemas(&[users, auth]);
assert!(result.is_ok(), "Should compose with shareable fields: {:?}", result);
let composed = result.unwrap();
let user_type = composed.types.iter().find(|t| t.name == "User").unwrap();
assert!(
user_type.shareable_fields.contains(&"email".to_string()),
"Should preserve @shareable field"
);
}
#[test]
fn test_composition_with_multiple_directives() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let mut orders = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic()],
};
orders.types[0].external_fields.push("user_id".to_string());
orders.types[0].shareable_fields.push("total".to_string());
let result = compose_federation_schemas(&[users, orders]);
assert!(result.is_ok(), "Should compose with multiple directives: {:?}", result);
let composed = result.unwrap();
let order_type = composed.types.iter().find(|t| t.name == "Order").unwrap();
assert!(!order_type.external_fields.is_empty(), "Should have external fields");
assert!(!order_type.shareable_fields.is_empty(), "Should have shareable fields");
}
#[test]
fn test_composed_schema_validity() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let orders = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic(), create_user_type_extending()],
};
let composed = compose_federation_schemas(&[users, orders])
.expect("should compose two subgraphs for validation");
assert!(composed.enabled, "Should be federation enabled");
assert_eq!(composed.version, "v2", "Should maintain federation version");
for type_def in &composed.types {
if type_def.name != "User" && type_def.name != "Order" {
continue; }
assert!(!type_def.keys.is_empty(), "Type {} should have @key directive", type_def.name);
}
}
#[test]
fn test_composed_schema_federation_enabled_any_source() {
let enabled_subgraph = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let disabled_subgraph = FederationMetadata {
enabled: false,
version: "v2".to_string(),
types: vec![create_order_type_basic()],
};
let composed = compose_federation_schemas(&[enabled_subgraph, disabled_subgraph])
.expect("should compose mixed-enabled subgraphs");
assert!(composed.enabled, "Composed schema should be enabled if any subgraph is enabled");
}
#[test]
fn test_type_with_no_extensions() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let orders = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_order_type_basic()],
};
let composed = compose_federation_schemas(&[users, orders])
.expect("should compose subgraphs with non-extended type");
let user_type = composed.types.iter().find(|t| t.name == "User").unwrap();
assert!(!user_type.is_extends, "User should be owning definition");
}
#[test]
fn test_many_extensions_single_owner() {
let users = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![create_user_type_basic()],
};
let mut subgraphs = vec![users];
for i in 0..6 {
let mut metadata = FederationMetadata {
enabled: true,
version: "v2".to_string(),
types: vec![],
};
let type_name = format!("Service{}", i);
let mut service_type = FederatedType::new(type_name);
service_type.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
metadata.types.push(service_type);
metadata.types.push(create_user_type_extending());
subgraphs.push(metadata);
}
let composed = compose_federation_schemas(&subgraphs)
.expect("should compose 7 subgraphs with many User extensions");
assert_eq!(
composed.types.iter().filter(|t| t.name == "User").count(),
1,
"Should have exactly one User definition (owner only)"
);
}
fn create_user_type_basic() -> FederatedType {
let mut user = FederatedType::new("User".to_string());
user.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
user.is_extends = false;
user
}
fn create_user_type_extending() -> FederatedType {
let mut user = FederatedType::new("User".to_string());
user.is_extends = true;
user
}
fn create_user_type_with_key(key_fields: &[&str]) -> FederatedType {
let mut user = FederatedType::new("User".to_string());
user.keys.push(KeyDirective {
fields: key_fields.iter().map(|s| (*s).to_string()).collect(),
resolvable: true,
});
user.is_extends = false;
user
}
fn create_user_type_with_fields(
_key_fields: Vec<&str>,
_fields: Vec<(&str, &str)>,
) -> FederatedType {
create_user_type_basic()
}
fn create_user_type_extending_with_fields(_fields: Vec<(&str, &str)>) -> FederatedType {
create_user_type_extending()
}
fn create_order_type_basic() -> FederatedType {
let mut order = FederatedType::new("Order".to_string());
order.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
order.is_extends = false;
order
}
fn create_order_type_extending() -> FederatedType {
let mut order = FederatedType::new("Order".to_string());
order.is_extends = true;
order
}
fn create_product_type_basic() -> FederatedType {
let mut product = FederatedType::new("Product".to_string());
product.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
product.is_extends = false;
product
}
fn create_product_type_extending() -> FederatedType {
let mut product = FederatedType::new("Product".to_string());
product.is_extends = true;
product
}
fn create_payment_type_basic() -> FederatedType {
let mut payment = FederatedType::new("Payment".to_string());
payment.keys.push(KeyDirective {
fields: vec!["id".to_string()],
resolvable: true,
});
payment.is_extends = false;
payment
}
fn compose_federation_schemas(subgraphs: &[FederationMetadata]) -> Result<ComposedSchema, String> {
if subgraphs.is_empty() {
return Ok(ComposedSchema {
enabled: false,
version: "v2".to_string(),
types: Vec::new(),
});
}
let mut types_by_name: HashMap<String, Vec<FederatedType>> = HashMap::new();
for subgraph in subgraphs {
for type_def in &subgraph.types {
types_by_name.entry(type_def.name.clone()).or_default().push(type_def.clone());
}
}
let mut merged_types = Vec::new();
for (_typename, definitions) in types_by_name {
let owner = definitions.iter().find(|t| !t.is_extends);
if let Some(owned_type) = owner {
merged_types.push(owned_type.clone());
} else {
merged_types.push(definitions[0].clone());
}
}
let enabled = subgraphs.iter().any(|s| s.enabled);
let version = subgraphs.first().map_or_else(|| "v2".to_string(), |s| s.version.clone());
Ok(ComposedSchema {
enabled,
version,
types: merged_types,
})
}
#[derive(Debug, Clone)]
struct ComposedSchema {
pub enabled: bool,
pub version: String,
pub types: Vec<FederatedType>,
}