#![allow(clippy::unwrap_used, clippy::panic)] #![allow(clippy::wildcard_imports)]
mod toml_schema_tests {
use super::super::*;
#[test]
fn test_parse_toml_schema() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[types.User]
sql_source = "v_user"
[types.User.fields.id]
type = "ID"
nullable = false
[types.User.fields.name]
type = "String"
nullable = false
[queries.users]
return_type = "User"
return_array = true
sql_source = "v_user"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.schema.name, "myapp");
assert!(schema.types.contains_key("User"));
}
#[test]
fn test_validate_schema() {
let schema = TomlSchema::default();
schema.validate().unwrap_or_else(|e| panic!("expected Ok from validate: {e:?}"));
}
#[test]
fn test_observers_config_nats_url_round_trip() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[observers]
enabled = true
backend = "nats"
nats_url = "nats://localhost:4222"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.observers.backend, "nats");
assert_eq!(schema.observers.nats_url.as_deref(), Some("nats://localhost:4222"));
assert!(schema.observers.redis_url.is_none());
}
#[test]
fn test_observers_config_redis_url_unchanged() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[observers]
enabled = true
backend = "redis"
redis_url = "redis://localhost:6379"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.observers.backend, "redis");
assert_eq!(schema.observers.redis_url.as_deref(), Some("redis://localhost:6379"));
assert!(schema.observers.nats_url.is_none());
}
#[test]
fn test_observers_config_nats_url_default_is_none() {
let config = ObserversConfig::default();
assert!(config.nats_url.is_none());
}
#[test]
fn test_observers_runtime_subtable_is_tolerated() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[observers]
enabled = true
backend = "postgresql"
[observers.runtime]
poll_interval_ms = 500
batch_size = 250
[observers.runtime.pool]
max_connections = 9
"#;
let schema =
TomlSchema::parse_toml(toml).expect("compile must tolerate [observers.runtime]");
assert!(schema.observers.enabled);
assert_eq!(schema.observers.backend, "postgresql");
assert!(schema.observers.runtime.is_some());
}
#[test]
fn test_observers_unknown_toplevel_key_still_rejected() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[observers]
enabled = true
backend = "postgresql"
poll_interval_ms = 500
"#;
assert!(
TomlSchema::parse_toml(toml).is_err(),
"a server-tuning key at the [observers] top level must be rejected by the compiler",
);
}
#[test]
fn test_federation_circuit_breaker_round_trip() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[types.Product]
sql_source = "v_product"
[federation]
enabled = true
apollo_version = 2
[[federation.entities]]
name = "Product"
key_fields = ["id"]
[federation.circuit_breaker]
enabled = true
failure_threshold = 3
recovery_timeout_secs = 60
success_threshold = 1
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
let cb = schema.federation.circuit_breaker.as_ref().expect("Expected circuit_breaker");
assert!(cb.enabled);
assert_eq!(cb.failure_threshold, 3);
assert_eq!(cb.recovery_timeout_secs, 60);
assert_eq!(cb.success_threshold, 1);
assert!(cb.per_database.is_empty());
}
#[test]
fn test_federation_service_name_and_version_parse_and_compile() {
let toml = r#"
[schema]
name = "orders"
[types.Order]
sql_source = "v_orders"
[federation]
enabled = true
service_name = "orders"
version = "v2"
schema_url = "https://orders.example.com/graphql"
[[federation.entities]]
name = "Order"
key_fields = ["id"]
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
schema.validate().expect("Failed to validate");
assert_eq!(schema.federation.service_name.as_deref(), Some("orders"));
assert_eq!(schema.federation.version.as_deref(), Some("v2"));
let compiled = schema.federation.to_compiled();
assert!(compiled.enabled);
assert_eq!(compiled.service_name.as_deref(), Some("orders"));
assert_eq!(compiled.version.as_deref(), Some("v2"));
assert_eq!(compiled.schema_url.as_deref(), Some("https://orders.example.com/graphql"));
assert_eq!(compiled.entities.len(), 1);
assert_eq!(compiled.entities[0].name, "Order");
assert_eq!(compiled.entities[0].key_fields, vec!["id".to_string()]);
}
#[test]
fn test_federation_apollo_version_back_compat_maps_to_version() {
let toml = r#"
[schema]
name = "orders"
[federation]
enabled = true
apollo_version = 2
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.federation.to_compiled().version.as_deref(), Some("v2"));
}
#[test]
fn test_federation_explicit_version_wins_over_apollo_version() {
let toml = r#"
[schema]
name = "orders"
[federation]
enabled = true
apollo_version = 1
version = "v2"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.federation.to_compiled().version.as_deref(), Some("v2"));
}
#[test]
fn test_federation_per_database_override_maps_to_per_entity() {
let toml = r#"
[schema]
name = "orders"
[types.Order]
sql_source = "v_orders"
[federation]
enabled = true
[[federation.entities]]
name = "Order"
key_fields = ["id"]
[federation.circuit_breaker]
enabled = true
failure_threshold = 5
recovery_timeout_secs = 30
success_threshold = 2
[[federation.circuit_breaker.per_database]]
database = "Order"
failure_threshold = 10
recovery_timeout_secs = 60
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
schema.validate().expect("Failed to validate");
let cb = schema.federation.to_compiled().circuit_breaker.expect("circuit_breaker");
assert_eq!(cb.per_entity.len(), 1);
assert_eq!(cb.per_entity[0].entity, "Order");
assert_eq!(cb.per_entity[0].failure_threshold, Some(10));
assert_eq!(cb.per_entity[0].recovery_timeout, Some(60));
assert_eq!(cb.per_entity[0].success_threshold, None);
}
#[test]
fn test_federation_circuit_breaker_zero_failure_threshold_rejected() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[federation]
enabled = true
[federation.circuit_breaker]
enabled = true
failure_threshold = 0
recovery_timeout_secs = 30
success_threshold = 2
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
let err = schema.validate().unwrap_err();
assert!(err.to_string().contains("failure_threshold"), "{err}");
}
#[test]
fn test_federation_circuit_breaker_zero_recovery_timeout_rejected() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[federation]
enabled = true
[federation.circuit_breaker]
enabled = true
failure_threshold = 5
recovery_timeout_secs = 0
success_threshold = 2
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
let err = schema.validate().unwrap_err();
assert!(err.to_string().contains("recovery_timeout_secs"), "{err}");
}
#[test]
fn test_federation_circuit_breaker_per_database_unknown_entity_rejected() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[types.Product]
sql_source = "v_product"
[federation]
enabled = true
[[federation.entities]]
name = "Product"
key_fields = ["id"]
[federation.circuit_breaker]
enabled = true
failure_threshold = 5
recovery_timeout_secs = 30
success_threshold = 2
[[federation.circuit_breaker.per_database]]
database = "NonExistentEntity"
failure_threshold = 3
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
let err = schema.validate().unwrap_err();
assert!(err.to_string().contains("NonExistentEntity"), "{err}");
}
#[test]
fn test_federation_circuit_breaker_per_database_valid() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[types.Product]
sql_source = "v_product"
[federation]
enabled = true
[[federation.entities]]
name = "Product"
key_fields = ["id"]
[federation.circuit_breaker]
enabled = true
failure_threshold = 5
recovery_timeout_secs = 30
success_threshold = 2
[[federation.circuit_breaker.per_database]]
database = "Product"
failure_threshold = 3
recovery_timeout_secs = 15
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
schema.validate().unwrap_or_else(|e| panic!("expected Ok from validate: {e:?}"));
let cb = schema.federation.circuit_breaker.as_ref().unwrap();
assert_eq!(cb.per_database.len(), 1);
assert_eq!(cb.per_database[0].database, "Product");
assert_eq!(cb.per_database[0].failure_threshold, Some(3));
assert_eq!(cb.per_database[0].recovery_timeout_secs, Some(15));
}
#[test]
fn test_toml_schema_parses_server_section() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[server]
host = "127.0.0.1"
port = 9999
[server.cors]
origins = ["https://example.com"]
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.server.host, "127.0.0.1");
assert_eq!(schema.server.port, 9999);
assert_eq!(schema.server.cors.origins, ["https://example.com"]);
}
#[test]
fn test_toml_schema_database_uses_runtime_config() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[database]
url = "postgresql://localhost/mydb"
pool_min = 5
pool_max = 30
ssl_mode = "require"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.database.url, Some("postgresql://localhost/mydb".to_string()));
assert_eq!(schema.database.pool_min, 5);
assert_eq!(schema.database.pool_max, 30);
assert_eq!(schema.database.ssl_mode.as_deref(), Some("require"));
}
#[test]
fn test_env_var_expansion_in_toml_schema() {
temp_env::with_var("SCHEMA_TEST_DB_URL", Some("postgres://test/fraiseql"), || {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[database]
url = "${SCHEMA_TEST_DB_URL}"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.database.url, Some("postgres://test/fraiseql".to_string()));
});
}
#[test]
fn test_toml_schema_defaults_without_server_section() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
"#;
let schema = TomlSchema::parse_toml(toml).expect("Failed to parse");
assert_eq!(schema.server.host, "0.0.0.0");
assert_eq!(schema.server.port, 8080);
assert_eq!(schema.database.pool_min, 2);
assert_eq!(schema.database.pool_max, 20);
assert!(schema.database.url.is_none());
}
#[test]
fn test_rate_limiting_config_parses_per_user_rps() {
let toml = r"
[security.rate_limiting]
enabled = true
requests_per_second = 100
requests_per_second_per_user = 250
";
let schema: TomlSchema = toml::from_str(toml).unwrap();
let rl = schema.security.rate_limiting.unwrap();
assert_eq!(rl.requests_per_second_per_user, Some(250));
}
#[test]
fn test_rate_limiting_config_per_user_rps_defaults_to_none() {
let toml = r"
[security.rate_limiting]
enabled = true
requests_per_second = 50
";
let schema: TomlSchema = toml::from_str(toml).unwrap();
let rl = schema.security.rate_limiting.unwrap();
assert_eq!(rl.requests_per_second_per_user, None);
}
#[test]
fn test_validation_config_parses_limits() {
let toml = r"
[validation]
max_query_depth = 5
max_query_complexity = 50
";
let schema: TomlSchema = toml::from_str(toml).unwrap();
assert_eq!(schema.validation.max_query_depth, Some(5));
assert_eq!(schema.validation.max_query_complexity, Some(50));
}
#[test]
fn test_validation_config_defaults_to_none() {
let toml = "";
let schema: TomlSchema = toml::from_str(toml).unwrap();
assert_eq!(schema.validation.max_query_depth, None);
assert_eq!(schema.validation.max_query_complexity, None);
}
#[test]
fn test_validation_config_partial() {
let toml = r"
[validation]
max_query_depth = 3
";
let schema: TomlSchema = toml::from_str(toml).unwrap();
assert_eq!(schema.validation.max_query_depth, Some(3));
assert_eq!(schema.validation.max_query_complexity, None);
}
#[test]
fn test_hierarchy_config_deserializes_from_toml() {
let toml = r#"
[hierarchies.category]
table = "tb_category"
path_column = "category_path"
[hierarchies.location]
table = "tb_location"
path_column = "location_path"
"#;
let schema: TomlSchema = toml::from_str(toml).unwrap();
let hierarchies = schema.hierarchies.as_ref().expect("hierarchies should be Some");
assert_eq!(hierarchies.len(), 2);
let cat = &hierarchies["category"];
assert_eq!(cat.table, "tb_category");
assert_eq!(cat.path_column, "category_path");
let loc = &hierarchies["location"];
assert_eq!(loc.table, "tb_location");
assert_eq!(loc.path_column, "location_path");
}
#[test]
fn test_hierarchy_config_absent_defaults_to_none() {
let toml = "";
let schema: TomlSchema = toml::from_str(toml).unwrap();
assert!(schema.hierarchies.is_none());
}
#[test]
fn test_hierarchy_config_rejects_empty_table() {
let toml = r#"
[hierarchies.bad]
table = ""
path_column = "some_path"
"#;
let schema: TomlSchema = toml::from_str(toml).unwrap();
let hierarchies = schema.hierarchies.as_ref().unwrap();
let bad = &hierarchies["bad"];
assert!(bad.validate().is_err());
}
#[test]
fn test_hierarchy_field_reference_validated() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[hierarchies.category]
table = "tb_category"
path_column = "category_path"
[types.Category]
sql_source = "v_category"
[types.Category.fields.id]
type = "ID"
[types.Category.fields.category_path]
type = "String"
hierarchy = "category"
[queries.categories]
return_type = "Category"
return_array = true
sql_source = "v_category"
"#;
let schema = TomlSchema::parse_toml(toml).unwrap();
schema.validate().unwrap();
}
#[test]
fn test_hierarchy_field_reference_rejects_invalid_name() {
let toml = r#"
[schema]
name = "myapp"
version = "1.0.0"
database_target = "postgresql"
[types.Category]
sql_source = "v_category"
[types.Category.fields.id]
type = "ID"
[types.Category.fields.category_path]
type = "String"
hierarchy = "nonexistent"
[queries.categories]
return_type = "Category"
return_array = true
sql_source = "v_category"
"#;
let schema = TomlSchema::parse_toml(toml).unwrap();
let err = schema.validate().unwrap_err();
assert!(
err.to_string().contains("nonexistent"),
"Error should mention the invalid hierarchy name: {err}"
);
}
#[test]
fn api_keys_storage_postgres_is_accepted_and_unknown_is_refused() {
let postgres = r#"
[security.api_keys]
enabled = true
storage = "postgres"
"#;
let schema: TomlSchema = toml::from_str(postgres).unwrap();
assert!(
schema.reject_accepted_but_unconsumed_config().is_ok(),
"storage = \"postgres\" is implemented (#627) and must be accepted"
);
let unknown = r#"
[security.api_keys]
enabled = true
storage = "redis"
"#;
let schema: TomlSchema = toml::from_str(unknown).unwrap();
let err = schema
.reject_accepted_but_unconsumed_config()
.expect_err("an unimplemented storage backend must be refused");
assert!(err.to_string().contains("redis"), "the error must name the value: {err}");
}
#[test]
fn test_hierarchy_config_rejects_empty_path_column() {
let toml = r#"
[hierarchies.bad]
table = "tb_something"
path_column = ""
"#;
let schema: TomlSchema = toml::from_str(toml).unwrap();
let hierarchies = schema.hierarchies.as_ref().unwrap();
let bad = &hierarchies["bad"];
assert!(bad.validate().is_err());
}
}
mod mcp_session_state {
use super::super::*;
#[test]
fn the_key_parses_and_defaults_off() {
let cfg: TomlSchema = toml::from_str(
r"
[mcp]
enabled = true
",
)
.expect("[mcp] must parse");
assert!(
!cfg.mcp.session_state,
"continuity is opt-in: a deployment must not acquire it by upgrading"
);
let on: TomlSchema = toml::from_str(
r"
[mcp]
enabled = true
session_state = true
",
)
.expect("session_state must parse");
assert!(on.mcp.session_state);
}
#[test]
fn the_value_survives_the_hop_into_the_compiled_schema() {
let cfg: TomlSchema = toml::from_str(
r"
[mcp]
enabled = true
session_state = true
",
)
.expect("parse");
let json = serde_json::to_value(&cfg.mcp).expect("serialize as merger.rs does");
let round_tripped: fraiseql_core::schema::McpConfig =
serde_json::from_value(json).expect("the runtime type must read it back");
assert!(
round_tripped.session_state,
"the key set in fraiseql.toml must be the one the MCP transport consults"
);
}
}
mod relationships {
use super::super::*;
fn schema_with(relationship_block: &str) -> TomlSchema {
toml::from_str(&format!(
r#"
[schema]
name = "app"
version = "1.0.0"
database_target = "postgresql"
[types.User]
sql_source = "v_user"
fields.id = {{ type = "ID" }}
{relationship_block}
"#
))
.expect("the relationship block must parse")
}
const ORDERS: &str = r#"
[types.User.relationships.orders]
target_type = "Order"
cardinality = "OneToMany"
foreign_key = "fk_user"
referenced_key = "id"
"#;
#[test]
fn a_relationship_block_parses_into_the_type() {
let schema = schema_with(ORDERS);
let rel = schema.types["User"].relationships["orders"].clone();
assert_eq!(rel.target_type, "Order");
assert_eq!(rel.cardinality, fraiseql_core::schema::Cardinality::OneToMany);
assert_eq!(rel.foreign_key, "fk_user");
assert_eq!(rel.referenced_key, "id");
}
#[test]
fn the_table_header_supplies_the_relationship_name() {
let json = schema_with(ORDERS).types["User"].to_intermediate_json("User");
assert_eq!(json["relationships"][0]["name"], "orders");
}
#[test]
fn a_misspelled_relationship_key_is_refused() {
let err = toml::from_str::<TomlSchema>(
r#"
[schema]
name = "app"
version = "1.0.0"
database_target = "postgresql"
[types.User]
sql_source = "v_user"
[types.User.relationships.orders]
target_type = "Order"
cardinality = "OneToMany"
foreign_key = "fk_user"
referenced_key = "id"
forgein_key = "typo"
"#,
)
.expect_err("an unknown key must not be accepted");
assert!(err.to_string().contains("forgein_key"), "{err}");
}
#[test]
fn an_unknown_cardinality_is_refused() {
let err = toml::from_str::<TomlSchema>(
r#"
[schema]
name = "app"
version = "1.0.0"
database_target = "postgresql"
[types.User]
sql_source = "v_user"
[types.User.relationships.orders]
target_type = "Order"
cardinality = "SomeToSome"
foreign_key = "fk_user"
referenced_key = "id"
"#,
)
.expect_err("an unknown cardinality must not be accepted");
assert!(err.to_string().contains("SomeToSome"), "{err}");
}
#[test]
fn both_toml_paths_emit_the_same_relationships() {
let schema = schema_with(ORDERS);
let whole = schema.to_intermediate_schema();
let from_schema = whole["types"]["User"]["relationships"].clone();
let from_type = schema.types["User"].to_intermediate_json("User")["relationships"].clone();
let from_overlay = schema.types["User"]
.intermediate_overlay()
.into_iter()
.find(|(key, _)| *key == "relationships")
.map(|(_, value)| value)
.expect("the overlay carries relationships");
assert_eq!(from_schema, from_type, "to_intermediate_schema vs to_intermediate_json");
assert_eq!(from_type, from_overlay, "to_intermediate_json vs intermediate_overlay");
assert_eq!(from_schema[0]["foreign_key"], "fk_user");
}
#[test]
fn a_unique_field_emits_the_key() {
let schema: TomlSchema = toml::from_str(
r#"
[schema]
name = "app"
version = "1.0.0"
database_target = "postgresql"
[types.User]
sql_source = "v_user"
fields.id = { type = "ID" }
fields.identifier = { type = "String", unique = true }
fields.name = { type = "String" }
"#,
)
.expect("`unique` must be an accepted field key");
let json = schema.types["User"].to_intermediate_json("User");
let fields = json["fields"].as_array().expect("fields is an array");
let find = |name: &str| {
fields
.iter()
.find(|f| f["name"] == name)
.unwrap_or_else(|| panic!("{name} is emitted"))
.clone()
};
assert_eq!(find("identifier")["unique"], serde_json::json!(true), "{json}");
assert!(find("name").get("unique").is_none(), "an ordinary field emits no key: {json}");
assert!(find("id").get("unique").is_none(), "nor does the identity: {json}");
}
#[test]
fn a_type_with_no_relationships_emits_no_key() {
let schema = schema_with("");
let json = schema.types["User"].to_intermediate_json("User");
assert!(json.get("relationships").is_none(), "{json}");
assert!(
!schema.types["User"]
.intermediate_overlay()
.iter()
.any(|(key, _)| *key == "relationships")
);
}
}
mod retired_rest_keys {
use super::super::*;
#[test]
fn a_document_setting_the_retired_embed_read_tally_is_refused() {
let schema = TomlSchema::parse_toml(
r#"
[schema]
name = "app"
version = "1.0.0"
database_target = "postgresql"
[rest]
enabled = true
max_embedded_reads = 10000
"#,
)
.expect("the key parses so that it can be refused with a sentence");
let err = schema.reject_accepted_but_unconsumed_config().unwrap_err().to_string();
assert!(err.contains("[security.cost_budget] per_request_max"), "{err}");
}
}