mod initialization_tests {
use super::super::initialization::is_manifest_url_ssrf_blocked;
#[test]
fn field_encryption_marker_refuses_boot() {
use fraiseql_core::schema::{
CompiledSchema, FieldDefinition, FieldEncryptionConfig, FieldType, TypeDefinition,
};
use super::super::initialization::field_encryption_unsupported_check;
let mut user = TypeDefinition::new("User", "v_user");
user.fields
.push(FieldDefinition::new("email", FieldType::String).with_encryption(
FieldEncryptionConfig {
key_reference: "keys/user-email".to_string(),
algorithm: "AES-256-GCM".to_string(),
},
));
let schema = CompiledSchema {
types: vec![user],
..CompiledSchema::default()
};
let result = field_encryption_unsupported_check(&schema);
assert!(
matches!(&result, Err(crate::ServerError::ConfigError(msg)) if msg.contains("User.email")),
"a field marked for encryption must refuse to boot and name the field (H12): {result:?}"
);
}
#[test]
fn no_field_encryption_boots_fine() {
use fraiseql_core::schema::{CompiledSchema, FieldDefinition, FieldType, TypeDefinition};
use super::super::initialization::field_encryption_unsupported_check;
let mut user = TypeDefinition::new("User", "v_user");
user.fields.push(FieldDefinition::new("email", FieldType::String));
let schema = CompiledSchema {
types: vec![user],
..CompiledSchema::default()
};
assert!(
field_encryption_unsupported_check(&schema).is_ok(),
"a schema with no encryption-marked fields boots normally"
);
}
#[test]
fn persisted_queries_only_forces_strict_mode() {
use super::super::initialization::effective_trusted_doc_mode;
use crate::trusted_documents::TrustedDocumentMode;
assert_eq!(
effective_trusted_doc_mode("permissive", true),
TrustedDocumentMode::Strict,
"persisted_queries_only=true must force Strict over a permissive declared mode"
);
assert_eq!(effective_trusted_doc_mode("strict", true), TrustedDocumentMode::Strict);
assert_eq!(effective_trusted_doc_mode("strict", false), TrustedDocumentMode::Strict);
assert_eq!(
effective_trusted_doc_mode("permissive", false),
TrustedDocumentMode::Permissive,
"without the flag, a permissive schema stays permissive"
);
assert_eq!(effective_trusted_doc_mode("", false), TrustedDocumentMode::Permissive);
assert_eq!(effective_trusted_doc_mode("", true), TrustedDocumentMode::Strict);
}
#[test]
fn ssrf_blocks_localhost_by_name() {
assert!(is_manifest_url_ssrf_blocked("http://localhost/manifest.json"));
}
#[test]
fn ssrf_blocks_localhost_uppercase() {
assert!(is_manifest_url_ssrf_blocked("http://LOCALHOST/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv4_loopback() {
assert!(is_manifest_url_ssrf_blocked("http://127.0.0.1/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv4_private_192_168() {
assert!(is_manifest_url_ssrf_blocked("http://192.168.1.100/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv4_private_10_x() {
assert!(is_manifest_url_ssrf_blocked("http://10.0.0.1/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv4_private_172_16() {
assert!(is_manifest_url_ssrf_blocked("http://172.16.0.1/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv4_link_local() {
assert!(is_manifest_url_ssrf_blocked("http://169.254.1.1/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv6_loopback() {
assert!(is_manifest_url_ssrf_blocked("http://[::1]/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv6_unspecified() {
assert!(is_manifest_url_ssrf_blocked("http://[::]/manifest.json"));
}
#[test]
fn ssrf_blocks_ipv6_ula() {
assert!(is_manifest_url_ssrf_blocked("http://[fd00::1]/manifest.json"));
}
#[test]
fn ssrf_blocks_unparseable_url() {
assert!(is_manifest_url_ssrf_blocked("not a url at all"));
}
#[test]
fn ssrf_allows_public_https() {
assert!(!is_manifest_url_ssrf_blocked("https://cdn.example.com/manifest.json"));
}
#[test]
fn ssrf_allows_public_ipv4() {
assert!(!is_manifest_url_ssrf_blocked("http://93.184.216.34/manifest.json"));
}
#[test]
fn ssrf_allows_public_ipv6_global() {
assert!(!is_manifest_url_ssrf_blocked("http://[2001:db8::1]/manifest.json"));
}
#[cfg(feature = "auth")]
#[test]
fn pkce_without_state_encryption_is_fatal_in_production() {
use super::super::initialization::pkce_state_encryption_check;
let result = pkce_state_encryption_check(
false, true,
);
assert!(
result.is_err(),
"PKCE without state encryption must refuse to boot in production (#360)"
);
}
#[cfg(feature = "auth")]
#[test]
fn pkce_without_state_encryption_is_a_warning_in_development() {
use super::super::initialization::pkce_state_encryption_check;
assert!(
pkce_state_encryption_check(false, false).is_ok(),
"development mode downgrades the missing-state-encryption error to a warning"
);
}
#[cfg(feature = "auth")]
#[test]
fn pkce_with_state_encryption_is_always_ok() {
use super::super::initialization::pkce_state_encryption_check;
assert!(pkce_state_encryption_check(true, true).is_ok());
assert!(pkce_state_encryption_check(true, false).is_ok());
}
#[test]
fn error_sanitizer_secure_default_is_environment_aware() {
use super::super::initialization::build_error_sanitizer;
assert!(
build_error_sanitizer(None, true).is_enabled(),
"production must sanitize 5xx by default (H7)"
);
assert!(
!build_error_sanitizer(None, false).is_enabled(),
"development keeps verbose errors by default"
);
}
#[test]
fn explicit_error_sanitization_config_overrides_environment_default() {
use super::super::initialization::build_error_sanitizer;
use crate::config::error_sanitization::ErrorSanitizationConfig;
let off = ErrorSanitizationConfig {
enabled: false,
..ErrorSanitizationConfig::default()
};
assert!(!build_error_sanitizer(Some(off), true).is_enabled());
let on = ErrorSanitizationConfig {
enabled: true,
..ErrorSanitizationConfig::default()
};
assert!(build_error_sanitizer(Some(on), false).is_enabled());
}
use super::super::initialization::failed_login_lockout_check;
use crate::middleware::rate_limit::{
DEFAULT_FAILED_LOGIN_LOCKOUT_SECS, DEFAULT_FAILED_LOGIN_MAX_ATTEMPTS,
};
#[test]
fn failed_login_default_values_boot_silently_even_in_production() {
assert!(
failed_login_lockout_check(
DEFAULT_FAILED_LOGIN_MAX_ATTEMPTS,
DEFAULT_FAILED_LOGIN_LOCKOUT_SECS,
true,
)
.is_ok()
);
}
#[test]
fn failed_login_tuned_value_is_fatal_in_production() {
assert!(failed_login_lockout_check(5, DEFAULT_FAILED_LOGIN_LOCKOUT_SECS, true).is_err());
assert!(failed_login_lockout_check(DEFAULT_FAILED_LOGIN_MAX_ATTEMPTS, 60, true).is_err());
}
#[test]
fn failed_login_tuned_value_is_a_warning_in_development() {
assert!(failed_login_lockout_check(5, 60, false).is_ok());
}
#[cfg(feature = "observers")]
mod observer_transport {
use fraiseql_observers::config::TransportKind;
use crate::server::initialization::observer_transport_check;
#[test]
fn postgres_is_always_ok() {
assert!(observer_transport_check(TransportKind::Postgres, false, false, true).is_ok());
assert!(observer_transport_check(TransportKind::Postgres, false, false, false).is_ok());
}
#[test]
fn in_memory_is_always_ok() {
assert!(observer_transport_check(TransportKind::InMemory, false, false, true).is_ok());
}
#[test]
fn nats_not_compiled_in_is_fatal_in_production() {
assert!(observer_transport_check(TransportKind::Nats, false, true, true).is_err());
}
#[test]
fn nats_not_compiled_in_is_a_warning_in_development() {
assert!(observer_transport_check(TransportKind::Nats, false, true, false).is_ok());
}
#[test]
fn nats_without_url_is_fatal_in_production() {
assert!(observer_transport_check(TransportKind::Nats, true, false, true).is_err());
}
#[test]
fn nats_without_url_is_a_warning_in_development() {
assert!(observer_transport_check(TransportKind::Nats, true, false, false).is_ok());
}
#[test]
fn nats_compiled_with_url_is_ok() {
assert!(observer_transport_check(TransportKind::Nats, true, true, true).is_ok());
}
}
}
#[cfg(test)]
mod lifecycle_tests {
use std::time::Duration;
use super::super::lifecycle::drain_lifecycle_tasks;
#[tokio::test]
async fn drain_lifecycle_tasks_aborts_infinite_loops() {
let mut tasks: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
for _ in 0..3 {
tasks.spawn(async {
let mut ticker = tokio::time::interval(Duration::from_secs(60));
loop {
ticker.tick().await;
}
});
}
let drain =
tokio::time::timeout(Duration::from_secs(5), drain_lifecycle_tasks(tasks, 5)).await;
assert!(
drain.is_ok(),
"drain_lifecycle_tasks must abort infinite-loop tasks within the timeout"
);
}
#[tokio::test]
async fn drain_lifecycle_tasks_returns_quickly_for_empty_set() {
let tasks: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
let drain =
tokio::time::timeout(Duration::from_secs(1), drain_lifecycle_tasks(tasks, 5)).await;
assert!(drain.is_ok(), "drain on an empty JoinSet must be a no-op");
}
}