#![allow(clippy::unwrap_used)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::cast_lossless)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_errors_doc)] #![allow(missing_docs)] #![allow(clippy::items_after_statements)] #![allow(clippy::used_underscore_binding)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::match_same_arms)] #![allow(clippy::branches_sharing_code)] #![allow(clippy::undocumented_unsafe_blocks)]
use std::{io::Write, path::PathBuf};
use fraiseql_server::{CompiledSchemaLoader, ServerConfig};
use tempfile::NamedTempFile;
#[test]
fn test_default_config() {
let config = ServerConfig::default();
assert_eq!(config.graphql_path, "/graphql");
assert_eq!(config.health_path, "/health");
assert_eq!(config.introspection_path, "/introspection");
assert_eq!(config.schema_path, PathBuf::from("schema.compiled.json"));
assert!(config.cors_enabled);
assert!(!config.compression_enabled);
assert!(config.tracing_enabled);
}
#[test]
fn test_config_serialization() {
let config = ServerConfig::default();
let toml_str = toml::to_string(&config).expect("Failed to serialize config");
assert!(toml_str.contains("schema_path"));
assert!(toml_str.contains("bind_addr"));
}
#[test]
fn test_config_deserialization() {
let toml_str = r#"
schema_path = "custom_schema.json"
graphql_path = "/api/graphql"
health_path = "/api/health"
cors_enabled = false
compression_enabled = true
"#;
let config: ServerConfig = toml::from_str(toml_str).expect("Failed to deserialize config");
assert_eq!(config.schema_path, PathBuf::from("custom_schema.json"));
assert_eq!(config.graphql_path, "/api/graphql");
assert_eq!(config.health_path, "/api/health");
assert!(!config.cors_enabled);
assert!(config.compression_enabled);
}
#[tokio::test]
async fn test_schema_loader_missing_file() {
let loader = CompiledSchemaLoader::new("/nonexistent/schema.json");
let result = loader.load().await;
assert!(result.is_err(), "expected Err loading nonexistent schema, got Ok");
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found"), "expected 'not found' in error, got: {err_msg}");
}
#[tokio::test]
async fn test_schema_loader_invalid_json() {
let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
writeln!(temp_file, "{{invalid json").expect("Failed to write to temp file");
let loader = CompiledSchemaLoader::new(temp_file.path());
let result = loader.load().await;
assert!(result.is_err(), "expected Err loading invalid JSON schema, got Ok");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.to_lowercase().contains("parse") || err_msg.to_lowercase().contains("json"),
"expected 'parse' or 'json' in error, got: {err_msg}"
);
}
#[test]
fn test_schema_loader_path() {
let tmp = tempfile::NamedTempFile::with_suffix(".json").unwrap();
let loader = CompiledSchemaLoader::new(tmp.path());
assert_eq!(loader.path(), tmp.path());
}
#[test]
fn test_schema_loader_path_display() {
let path = "/home/user/schema.compiled.json";
let loader = CompiledSchemaLoader::new(path);
let path_display = loader.path().display().to_string();
assert_eq!(path_display, path);
}
#[test]
fn test_multiple_configs() {
let config1 = ServerConfig::default();
let config2 = ServerConfig {
schema_path: PathBuf::from("other.json"),
..ServerConfig::default()
};
assert_eq!(config1.schema_path, PathBuf::from("schema.compiled.json"));
assert_eq!(config2.schema_path, PathBuf::from("other.json"));
}
#[test]
fn test_config_custom_bind_addr() {
let config = ServerConfig {
bind_addr: "0.0.0.0:8080".parse().unwrap(),
..ServerConfig::default()
};
assert_eq!(config.bind_addr.ip().to_string(), "0.0.0.0");
assert_eq!(config.bind_addr.port(), 8080);
}
#[test]
fn pool_min_size_is_positive_in_default_config() {
let cfg = ServerConfig::default();
assert!(
cfg.pool_min_size > 0,
"default pool_min_size should be > 0, got {}",
cfg.pool_min_size
);
assert!(
cfg.pool_min_size <= cfg.pool_max_size,
"pool_min_size ({}) must not exceed pool_max_size ({})",
cfg.pool_min_size,
cfg.pool_max_size,
);
}
#[test]
fn pool_timeout_default_is_positive() {
let cfg = ServerConfig::default();
assert!(cfg.pool_timeout_secs > 0, "pool_timeout_secs should be > 0");
assert_eq!(cfg.pool_timeout_secs, 30, "expected default pool_timeout_secs = 30");
}
#[test]
fn test_config_feature_flags() {
let config = ServerConfig {
cors_enabled: false,
compression_enabled: false,
tracing_enabled: false,
apq_enabled: false,
cache_enabled: false,
..ServerConfig::default()
};
assert!(!config.cors_enabled);
assert!(!config.compression_enabled);
assert!(!config.tracing_enabled);
assert!(!config.apq_enabled);
assert!(!config.cache_enabled);
}
fn section_cases() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"auth",
"[auth]\nissuer = \"https://idp.test\"\n",
"[auth]\nisseur = \"https://idp.test\"\n",
),
(
"auth_hs256",
"[auth_hs256]\nsecret_env = \"S\"\nissuer = \"i\"\n",
"[auth_hs256]\nsecret_env = \"S\"\nisseur = \"i\"\n",
),
(
"tls",
"[tls]\nenabled = true\ncert_path = \"c.pem\"\nkey_path = \"k.pem\"\nrequire_client_cert = true\n",
"[tls]\nenabled = true\ncert_path = \"c.pem\"\nkey_path = \"k.pem\"\nrequire_client_certs = true\n",
),
(
"database_tls",
"[database_tls]\npostgres_ssl_mode = \"disable\"\n",
"[database_tls]\npostgres_ssl_mdoe = \"disable\"\n",
),
(
"rate_limiting",
"[rate_limiting]\nenabled = true\n",
"[rate_limiting]\nenabeld = true\n",
),
(
"admission_control",
"[admission_control]\nmax_concurrent = 4\n",
"[admission_control]\nmax_concurrrent = 4\n",
),
(
"pool_tuning",
"[pool_tuning]\nenabled = true\n",
"[pool_tuning]\nenabeld = true\n",
),
(
"usage",
"[usage]\nflush_interval_secs = 5\n",
"[usage]\nflush_interval_sec = 5\n",
),
(
"tenancy",
"[tenancy.runtime]\nenabled = true\n",
"[tenancy.runtime]\nenabeld = true\n",
),
#[cfg(feature = "sources")]
("sources", "[sources]\nenabled = true\n", "[sources]\nenabeld = true\n"),
]
}
#[test]
fn a_mistyped_key_is_refused_in_every_section() {
let mut served_silently = Vec::new();
for (section, correct, mistyped) in section_cases() {
assert!(
toml::from_str::<ServerConfig>(correct).is_ok(),
"[{section}]: the *correctly* spelled config must parse, or this case is \
measuring a broken fixture rather than the defect.\n{correct}"
);
if toml::from_str::<ServerConfig>(mistyped).is_ok() {
served_silently.push(section);
}
}
assert!(
served_silently.is_empty(),
"#1337: these sections accept a mistyped key and discard it silently, so the \
setting stays at its default and nothing says so — `[auth] require_jti`, \
`[tls] require_client_cert` and `[rate_limiting] enabled` are security \
switches. serde does not propagate `deny_unknown_fields` into nested structs, \
so each section struct needs its own: {served_silently:?}"
);
}
#[test]
fn the_refusal_names_the_offending_key() {
let err = toml::from_str::<ServerConfig>("[rate_limiting]\nenabeld = true\n")
.expect_err("a mistyped key must be refused");
let message = err.to_string();
assert!(
message.contains("enabeld"),
"the error must name the key the operator got wrong — 'unknown field' alone \
sends them looking through the whole section. Got: {message}"
);
}