#![allow(clippy::unwrap_used, clippy::panic)] #![allow(missing_docs)]
mod error_sanitization_tests {
use super::super::error_sanitization::*;
use crate::error::{ErrorCode, ErrorExtensions, GraphQLError};
fn enabled_sanitizer() -> ErrorSanitizer {
ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: true,
hide_implementation_details: true,
sanitize_database_errors: true,
custom_error_message: None,
})
}
fn disabled_sanitizer() -> ErrorSanitizer {
ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: false,
..ErrorSanitizationConfig::default()
})
}
#[test]
fn test_sanitizer_strips_db_error_when_enabled() {
let s = enabled_sanitizer();
let err = GraphQLError::database(r#"ERROR: relation "tb_users" does not exist"#);
let out = s.sanitize(err);
assert_eq!(out.message, "An internal error occurred");
}
#[test]
fn test_sanitizer_passes_through_when_disabled() {
let s = disabled_sanitizer();
let original = r#"ERROR: relation "tb_users" does not exist"#;
let err = GraphQLError::database(original);
let out = s.sanitize(err);
assert_eq!(out.message, original);
}
#[test]
fn a_client_triggerable_database_message_is_replaced() {
let s = enabled_sanitizer();
let fk = GraphQLError::new(
"Database error: Function call app.delete_order (with change-log outbox) failed: \
update or delete on table \"tb_order\" violates foreign key constraint \
\"tb_order_line_fk_order_fkey\" on table \"tb_order_line\"",
ErrorCode::ConstraintViolation,
);
let out = s.sanitize(fk);
assert_eq!(out.message, "The request conflicts with the current state of the data");
for leaked in ["tb_order", "app.delete_order", "fkey", "outbox"] {
assert!(!out.message.contains(leaked), "still leaks {leaked}: {}", out.message);
}
let cast = GraphQLError::new(
"Database error: Function call app.create_thing (with change-log outbox) failed: \
invalid input syntax for type inet: \"not-an-ip\"",
ErrorCode::BadUserInput,
);
let out = s.sanitize(cast);
assert_eq!(out.message, "The request contains an invalid value");
assert!(!out.message.contains("app.create_thing"), "{}", out.message);
}
#[test]
fn a_client_fault_is_not_described_as_an_internal_error() {
let s = enabled_sanitizer();
for code in [ErrorCode::BadUserInput, ErrorCode::ConstraintViolation] {
let out = s.sanitize(GraphQLError::new("raw db text", code));
assert_ne!(out.message, "raw db text", "{code:?} must be replaced at all");
assert!(
!out.message.contains("internal"),
"{code:?} replacement must not claim an internal error: {}",
out.message
);
}
let out = s.sanitize(GraphQLError::new("raw db text", ErrorCode::DatabaseError));
assert_eq!(out.message, "An internal error occurred");
}
#[test]
fn a_custom_message_covers_the_client_fault_classes_too() {
let s = ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: true,
sanitize_database_errors: true,
custom_error_message: Some("Contact support".to_string()),
..Default::default()
});
for code in [
ErrorCode::BadUserInput,
ErrorCode::ConstraintViolation,
ErrorCode::DatabaseError,
ErrorCode::InternalServerError,
] {
assert_eq!(
s.sanitize(GraphQLError::new("raw db text", code)).message,
"Contact support",
"{code:?}"
);
}
}
#[test]
fn sanitize_database_errors_false_still_passes_client_faults_through() {
let s = ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: true,
sanitize_database_errors: false,
..Default::default()
});
let raw = "Database error: violates foreign key constraint \"x_fkey\"";
for code in [ErrorCode::BadUserInput, ErrorCode::ConstraintViolation] {
assert_eq!(s.sanitize(GraphQLError::new(raw, code)).message, raw, "{code:?}");
}
}
#[test]
fn test_sanitizer_preserves_user_facing_errors() {
let s = enabled_sanitizer();
let cases = [
(ErrorCode::ValidationError, "field is required"),
(ErrorCode::Unauthenticated, "Authentication required"),
(ErrorCode::Forbidden, "Access denied"),
(ErrorCode::NotFound, "Resource not found"),
];
for (code, msg) in cases {
let err = GraphQLError::new(msg, code);
let out = s.sanitize(err);
assert_eq!(out.message, msg, "code {code:?} should not be sanitized");
}
}
#[test]
fn test_sanitizer_custom_message() {
let s = ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: true,
custom_error_message: Some("Contact support".to_string()),
..ErrorSanitizationConfig::default()
});
let err = GraphQLError::database("pg error detail");
assert_eq!(s.sanitize(err).message, "Contact support");
}
#[test]
fn test_sanitizer_strips_extensions_detail_when_hide_impl() {
let s = enabled_sanitizer();
let mut err = GraphQLError::internal("internal");
err.extensions = Some(ErrorExtensions {
category: None,
status: None,
request_id: None,
retry_after_secs: None,
detail: Some("panic at line 42".to_string()),
});
let out = s.sanitize(err);
assert!(
out.extensions.as_ref().and_then(|e| e.detail.as_ref()).is_none(),
"detail should be stripped when hide_implementation_details = true"
);
}
#[test]
fn test_sanitize_database_errors_false_allows_db_message_through() {
let s = ErrorSanitizer::new(ErrorSanitizationConfig {
enabled: true,
sanitize_database_errors: false,
..ErrorSanitizationConfig::default()
});
let err = GraphQLError::database("duplicate key value");
assert_eq!(s.sanitize(err).message, "duplicate key value");
}
}
mod pool_tuning_tests {
#[allow(clippy::wildcard_imports)]
use super::super::pool_tuning::*;
#[test]
fn test_default_config_is_disabled() {
let cfg = PoolPressureMonitorConfig::default();
assert!(!cfg.enabled, "pool pressure monitoring should be off by default");
}
#[test]
fn test_default_bounds_are_sensible() {
let cfg = PoolPressureMonitorConfig::default();
assert!(cfg.min_pool_size < cfg.max_pool_size);
assert!(cfg.scale_up_step > 0);
assert!(cfg.scale_down_step > 0);
assert!(cfg.tuning_interval_ms >= 1000);
}
#[test]
fn test_validate_passes_for_defaults() {
PoolPressureMonitorConfig::default()
.validate()
.unwrap_or_else(|e| panic!("default pool monitor config should pass validation: {e}"));
}
#[test]
fn test_validate_min_lt_max() {
let cfg = PoolPressureMonitorConfig {
min_pool_size: 10,
max_pool_size: 5,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"min >= max should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
fn test_validate_min_equals_max_is_invalid() {
let cfg = PoolPressureMonitorConfig {
min_pool_size: 10,
max_pool_size: 10,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"min == max should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
fn test_validate_idle_ratio_above_one() {
let cfg = PoolPressureMonitorConfig {
scale_down_idle_ratio: 1.5,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"idle ratio > 1.0 should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
fn test_validate_idle_ratio_negative() {
let cfg = PoolPressureMonitorConfig {
scale_down_idle_ratio: -0.1,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"idle ratio < 0.0 should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
fn test_validate_zero_scale_up_step() {
let cfg = PoolPressureMonitorConfig {
scale_up_step: 0,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"scale_up_step == 0 should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
fn test_validate_zero_scale_down_step() {
let cfg = PoolPressureMonitorConfig {
scale_down_step: 0,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"scale_down_step == 0 should be invalid, got: {:?}",
cfg.validate()
);
}
#[test]
#[allow(deprecated)] fn test_pool_tuning_config_alias_works() {
let _cfg: PoolTuningConfig = PoolTuningConfig::default();
}
#[test]
fn test_validate_interval_too_short() {
let cfg = PoolPressureMonitorConfig {
tuning_interval_ms: 50,
..Default::default()
};
assert!(
cfg.validate().is_err(),
"tuning_interval_ms < 100 should be invalid, got: {:?}",
cfg.validate()
);
}
}