#![cfg(feature = "federation")]
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::path::PathBuf;
use fraiseql_core::schema::{
ChangelogConfig, CompiledSchema, FederationConfig, FederationEntity, FieldDefinition,
FieldType, NamingConvention, QueryDefinition, TypeDefinition, inject_changelog,
};
fn service_sdl(schema: &CompiledSchema) -> String {
let raw = schema.raw_schema();
let meta = schema.federation_metadata().expect("subgraph has an enabled federation block");
fraiseql_core::federation::generate_service_sdl(&raw, &meta)
}
fn money_type() -> TypeDefinition {
TypeDefinition::new("Money", "public.v_money")
.with_field(FieldDefinition::new("amount", FieldType::Int))
.with_field(FieldDefinition::new("currency", FieldType::String))
}
fn subgraph_catalog() -> CompiledSchema {
let mut s = CompiledSchema::new();
s.naming_convention = NamingConvention::CamelCase;
s.types.push(money_type());
s.types.push(
TypeDefinition::new("Product", "public.v_product")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(FieldDefinition::new("name", FieldType::String))
.with_field(FieldDefinition::new("price", FieldType::Object("Money".into())))
.with_field(FieldDefinition::new("createdAt", FieldType::DateTime)),
);
s.queries.push(
QueryDefinition::new("products", "Product")
.returning_list()
.with_sql_source("public.v_product"),
);
s.federation = Some(FederationConfig {
enabled: true,
version: Some("v2".to_string()),
service_name: Some("catalog".to_string()),
entities: vec![FederationEntity {
name: "Product".to_string(),
key_fields: vec!["id".to_string()],
..Default::default()
}],
shareable_types: vec!["Money".to_string()],
..Default::default()
});
s.changelog = Some(ChangelogConfig {
expose: true,
..Default::default()
});
inject_changelog(&mut s);
s.build_indexes();
s
}
fn subgraph_reviews() -> CompiledSchema {
let mut s = CompiledSchema::new();
s.naming_convention = NamingConvention::CamelCase;
s.types.push(money_type());
s.types.push(
TypeDefinition::new("Review", "public.v_review")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(FieldDefinition::new("body", FieldType::String))
.with_field(FieldDefinition::new("rating", FieldType::Int)),
);
s.types.push(
TypeDefinition::new("Product", "public.v_product_reviews")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(FieldDefinition::new(
"reviews",
FieldType::List(Box::new(FieldType::Object("Review".into()))),
)),
);
s.queries.push(
QueryDefinition::new("reviews", "Review")
.returning_list()
.with_sql_source("public.v_review"),
);
s.federation = Some(FederationConfig {
enabled: true,
version: Some("v2".to_string()),
service_name: Some("reviews".to_string()),
entities: vec![
FederationEntity {
name: "Review".to_string(),
key_fields: vec!["id".to_string()],
..Default::default()
},
FederationEntity {
name: "Product".to_string(),
key_fields: vec!["id".to_string()],
extends: true,
external_fields: vec!["id".to_string()],
..Default::default()
},
],
shareable_types: vec!["Money".to_string()],
..Default::default()
});
s.build_indexes();
s
}
fn subgraph_reviews_with_changelog() -> CompiledSchema {
let mut s = subgraph_reviews();
s.changelog = Some(ChangelogConfig {
expose: true,
..Default::default()
});
inject_changelog(&mut s);
s.build_indexes();
s
}
#[test]
fn catalog_sdl_holds_every_federation_invariant() {
let sdl = service_sdl(&subgraph_catalog());
assert!(sdl.contains("extend schema @link("), "missing @link federation header");
assert!(sdl.contains("scalar _Any"), "missing _Any scalar");
assert!(sdl.contains("scalar DateTime"), "DateTime referenced but not declared (#495)");
assert!(sdl.contains("type Query {"), "no root Query type rendered (#495)");
assert!(sdl.contains("products"), "root query field missing from Query (#495)");
assert!(
sdl.contains("type Product @key(fields: \"id\")"),
"entity @key not rendered on Product (#496)"
);
assert!(
sdl.contains("type Money @shareable"),
"shareable value type missing @shareable (#496)"
);
assert!(sdl.contains("union _Entity = Product"), "Product missing from _Entity union");
assert!(!sdl.contains("Money |"), "keyless Money leaked into _Entity union (#496)");
assert!(!sdl.contains("| Money"), "keyless Money leaked into _Entity union (#496)");
assert!(sdl.contains("entityChangeLogs"), "change-log query not camelCased (#498)");
assert!(sdl.contains("pkEntityChangeLog"), "change-log field not camelCased (#498)");
assert!(!sdl.contains("entity_change_logs"), "snake_case change-log query leaked (#498)");
assert!(
!sdl.contains("pk_entity_change_log"),
"snake_case change-log field leaked (#498)"
);
}
#[test]
fn reviews_sdl_extends_the_borrowed_entity() {
let sdl = service_sdl(&subgraph_reviews());
assert!(
sdl.contains("extend type Product @key(fields: \"id\")"),
"extended entity not rendered (#496)"
);
assert!(sdl.contains("@external"), "external key field not marked @external (#496)");
assert!(sdl.contains("type Review @key(fields: \"id\")"), "owned entity @key missing");
assert!(sdl.contains("type Money @shareable"), "shareable value type missing @shareable");
assert!(sdl.contains("union _Entity ="), "no _Entity union rendered");
assert!(sdl.contains("Review"), "Review missing from _Entity union");
assert!(sdl.contains("Product"), "Product missing from _Entity union");
}
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/federation_compose")
.join(name)
}
#[test]
fn committed_sdl_fixtures_are_current() {
let cases = [
("catalog.graphql", subgraph_catalog()),
("reviews.graphql", subgraph_reviews()),
("reviews_conflict.graphql", subgraph_reviews_with_changelog()),
];
let bless = std::env::var_os("BLESS_FEDERATION_SDL").is_some();
for (file, schema) in cases {
let rendered = format!("{}\n", service_sdl(&schema).trim_end());
let path = fixture_path(file);
if bless {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, &rendered).unwrap();
continue;
}
let committed = std::fs::read_to_string(&path)
.expect("missing federation_compose SDL fixture; re-bless with BLESS_FEDERATION_SDL=1");
assert_eq!(
committed,
rendered,
"{} is stale; re-bless with BLESS_FEDERATION_SDL=1",
path.display()
);
}
}
#[test]
fn shared_value_type_is_identical_across_subgraphs() {
let catalog = service_sdl(&subgraph_catalog());
let reviews = service_sdl(&subgraph_reviews());
for sdl in [&catalog, &reviews] {
assert!(sdl.contains("type Money @shareable {"), "Money header drifted");
assert!(sdl.contains("amount: Int"), "Money.amount drifted");
assert!(sdl.contains("currency: String"), "Money.currency drifted");
}
}