#![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)]
use fraiseql_server::{
error::{ErrorCode, ErrorResponse, GraphQLError},
routes::graphql::GraphQLRequest,
validation::RequestValidator,
};
#[test]
fn test_forbidden_error_has_generic_message() {
let error = GraphQLError::forbidden();
assert_eq!(error.code, ErrorCode::Forbidden);
assert_eq!(error.message, "Access denied");
assert!(!error.message.contains("field"));
assert!(!error.message.contains("permission"));
assert!(!error.message.contains("row"));
assert!(!error.message.contains("RBAC"));
assert!(!error.message.contains("RLS"));
}
#[test]
fn test_forbidden_error_with_path_preserves_location() {
let error =
GraphQLError::forbidden().with_path(vec!["user".to_string(), "sensitiveField".to_string()]);
assert_eq!(error.code, ErrorCode::Forbidden);
let path = error.path.as_ref().unwrap();
assert_eq!(path.len(), 2);
assert_eq!(path[0], "user");
assert_eq!(path[1], "sensitiveField");
assert_eq!(error.message, "Access denied");
}
#[test]
fn test_error_response_from_error_wraps_correctly() {
let error =
GraphQLError::forbidden().with_path(vec!["user".to_string(), "sensitiveField".to_string()]);
let response = ErrorResponse::from_error(error);
assert_eq!(response.errors.len(), 1);
assert_eq!(response.errors[0].code, ErrorCode::Forbidden);
assert_eq!(response.errors[0].message, "Access denied");
assert!(response.errors[0].path.is_some());
}
#[test]
fn test_error_response_serializes_to_graphql_spec() {
let error = GraphQLError::forbidden().with_path(vec!["query".to_string(), "user".to_string()]);
let response = ErrorResponse::from_error(error);
let json = serde_json::to_value(&response).unwrap();
assert!(json["errors"].is_array());
let first_error = &json["errors"][0];
assert!(first_error["message"].is_string());
assert_eq!(first_error["message"], "Access denied");
}
#[test]
fn test_validation_error_distinct_from_forbidden() {
let validation_error = GraphQLError::validation("Field 'foo' doesn't exist");
let forbidden_error = GraphQLError::forbidden();
assert_eq!(validation_error.code, ErrorCode::ValidationError);
assert_eq!(forbidden_error.code, ErrorCode::Forbidden);
assert!(validation_error.message.contains("foo"));
assert!(!forbidden_error.message.contains("foo"));
}
#[test]
fn test_validator_accepts_simple_query() {
let validator = RequestValidator::new();
let result = validator.validate_query("{ user { id name } }");
assert!(result.is_ok(), "Simple query should pass validation");
}
#[test]
fn test_validator_rejects_empty_query() {
let validator = RequestValidator::new();
let result = validator.validate_query("");
assert!(result.is_err(), "Empty query must be rejected");
let result = validator.validate_query(" ");
assert!(result.is_err(), "Whitespace-only query must be rejected");
}
#[test]
fn test_validator_rejects_malformed_query() {
let validator = RequestValidator::new()
.with_depth_validation(true)
.with_complexity_validation(true);
let malformed = vec![
"{ user { id", "not a query", "{ user { id } } extra", ];
for query in malformed {
let result = validator.validate_query(query);
let _ = result; }
}
#[test]
fn test_validator_enforces_depth_limit() {
let validator = RequestValidator::new().with_max_depth(3).with_depth_validation(true);
let shallow = "{ user { id name } }";
assert!(
validator.validate_query(shallow).is_ok(),
"Depth-2 query should pass depth-3 limit"
);
let deep = "{ user { posts { comments { replies { author { id } } } } } }";
let result = validator.validate_query(deep);
assert!(result.is_err(), "Depth-6 query should fail depth-3 limit: {result:?}");
}
#[test]
fn test_validator_enforces_complexity_limit() {
let validator = RequestValidator::new().with_max_complexity(5).with_complexity_validation(true);
let simple = "{ user { id } }";
assert!(
validator.validate_query(simple).is_ok(),
"Simple query should pass complexity-5 limit"
);
let complex = "{ user { id name email phone address bio avatar role createdAt updatedAt } }";
let result = validator.validate_query(complex);
assert!(result.is_err(), "10-field query should fail complexity-5 limit: {result:?}");
}
#[test]
fn test_validator_depth_disabled_allows_deep_queries() {
let validator = RequestValidator::new().with_max_depth(1).with_depth_validation(false);
let deep = "{ user { posts { comments { id } } } }";
assert!(
validator.validate_query(deep).is_ok(),
"Deep query should pass when depth validation is disabled"
);
}
#[test]
fn test_validator_accepts_mutations() {
let validator = RequestValidator::new();
let mutation = "mutation { createUser(input: { name: \"test\" }) { id } }";
assert!(validator.validate_query(mutation).is_ok(), "Mutation should pass validation");
}
#[test]
fn test_validator_accepts_query_with_variables() {
let validator = RequestValidator::new();
let query = "query GetUser($id: ID!) { user(id: $id) { id name email } }";
assert!(
validator.validate_query(query).is_ok(),
"Query with variables should pass validation"
);
}
#[test]
fn test_validator_accepts_fragments() {
let validator = RequestValidator::new();
let query = "query { users { ...UserFields } } fragment UserFields on User { id name }";
assert!(
validator.validate_query(query).is_ok(),
"Query with fragments should pass validation"
);
}
#[test]
fn test_validator_accepts_directives() {
let validator = RequestValidator::new();
let query =
"query GetUser($withEmail: Boolean!) { user { id name email @include(if: $withEmail) } }";
assert!(
validator.validate_query(query).is_ok(),
"Query with directives should pass validation"
);
}
#[test]
fn test_graphql_request_deserializes_from_json() {
let json = serde_json::json!({
"query": "query { user(id: \"123\") { id name email } }",
"variables": {"id": "123"},
"operationName": "GetUser"
});
let request: GraphQLRequest = serde_json::from_value(json).unwrap();
assert_eq!(request.query.as_deref(), Some("query { user(id: \"123\") { id name email } }"));
assert!(request.variables.is_some());
assert_eq!(request.operation_name, Some("GetUser".to_string()));
}
#[test]
fn test_graphql_request_minimal() {
let request = GraphQLRequest {
query: Some("{ user { id } }".to_string()),
variables: None,
operation_name: None,
extensions: None,
document_id: None,
};
let validator = RequestValidator::new();
validator
.validate_query(request.query.as_deref().unwrap())
.unwrap_or_else(|e| panic!("expected Ok validating minimal request query: {e}"));
}
#[test]
fn test_forbidden_error_does_not_leak_schema_info() {
let fields = vec!["password", "ssn", "secretKey", "internalId"];
for field in fields {
let error =
GraphQLError::forbidden().with_path(vec!["query".to_string(), field.to_string()]);
assert!(
!error.message.to_lowercase().contains(&field.to_lowercase()),
"Forbidden error leaks field name '{field}' in message: {}",
error.message
);
}
}
#[test]
fn test_error_response_multiple_errors() {
let errors = vec![
GraphQLError::forbidden().with_path(vec!["user".to_string(), "password".to_string()]),
GraphQLError::forbidden().with_path(vec!["user".to_string(), "ssn".to_string()]),
];
let response = ErrorResponse { errors };
assert_eq!(response.errors.len(), 2);
for error in &response.errors {
assert_eq!(error.message, "Access denied");
}
}