use regex::Regex;
use serde_json::json;
use crate::handlers::pxgraphql::*;
use crate::pxconfig::PXConfig;
fn test_conf() -> PXConfig {
PXConfig::default()
}
fn conf_with_keywords(patterns: &[&str]) -> PXConfig {
let mut conf = test_conf();
conf.graphql_keywords = patterns.iter().filter_map(|p| Regex::new(p).ok()).collect();
conf
}
fn extract_one(v: &serde_json::Value) -> PXGraphQLExtractedItem {
graphql_parse_and_extract(v, &test_conf()).unwrap_or_default()
}
#[test]
fn extract_from_object_with_query_field() {
let v = json!({ "query": "query Test { id }" });
let items = px_graphql_extract(&v, &test_conf());
assert_eq!(items.len(), 1);
assert_eq!(items.first().map(|i| i.op_type.as_str()), Some("query"));
assert_eq!(items.first().map(|i| i.name.as_str()), Some("Test"));
}
#[test]
fn extract_from_raw_string_body() {
let v = json!("query Test { id }");
let items = px_graphql_extract(&v, &test_conf());
assert_eq!(items.len(), 1);
assert_eq!(items.first().map(|i| i.op_type.as_str()), Some("query"));
assert_eq!(items.first().map(|i| i.name.as_str()), Some("Test"));
}
#[test]
fn object_without_query_field_returns_empty() {
let v = json!({ "variables": { "id": 1 } });
let item = extract_one(&v);
assert!(item.name.is_empty());
assert_eq!(item.op_type, "query");
assert_eq!(item.variables, vec!["id"]);
}
#[test]
fn object_with_null_query_returns_empty() {
let v = json!({ "query": null });
let item = extract_one(&v);
assert!(item.name.is_empty());
assert!(item.op_type.is_empty());
assert!(item.variables.is_empty());
}
#[test]
fn persisted_query_uses_operation_name_without_query_document() {
let v = json!({
"operationName": "test123",
"variables": {
"objectIds": ["83159105"],
"input": {
"cguid": "26d41c0e8f7ffe672ad97469b9a28117",
"appCode": "DESKTOP"
},
"filters": null
},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "bcba1c2fe2f11e68a3672bde013387220bf0f98c88efedb533d977a836eb1af0"
}
}
});
let item = extract_one(&v);
assert_eq!(item.name, "test123");
assert_eq!(item.op_type, "query");
let mut variables = item.variables;
variables.sort();
assert_eq!(
variables,
vec!["filters", "input.appCode", "input.cguid", "objectIds"]
);
}
#[test]
fn variable_object_keys_are_omitted_leaf_paths_reported() {
let v = json!({
"query": "query Test { id }",
"variables": {
"var1": "value1",
"var2": {
"var3": "value3"
}
}
});
let item = extract_one(&v);
let mut variables = item.variables;
variables.sort();
assert_eq!(variables, vec!["var1", "var2.var3"]);
}
#[test]
fn non_string_query_falls_back_to_operation_name() {
let v = json!({
"query": 123,
"operationName": "FallbackName",
"variables": {
"id": "abc"
}
});
let item = extract_one(&v);
assert_eq!(item.name, "FallbackName");
assert!(item.op_type.is_empty());
assert_eq!(item.variables, vec!["id"]);
}
#[test]
fn malformed_query_does_not_prevent_variable_extraction() {
let v = json!({
"query": "query Broken(",
"variables": {
"top": {
"nested": true
}
}
});
let item = extract_one(&v);
assert_eq!(item.variables, vec!["top.nested"]);
}
#[test]
fn scalar_json_values_return_default_item() {
for v in [json!(null), json!(true), json!(42), json!("not graphql")] {
let item = extract_one(&v);
assert!(item.name.is_empty());
assert!(item.op_type.is_empty());
assert!(item.variables.is_empty());
assert!(!item.sensitive);
}
}
#[test]
fn variables_that_are_not_objects_do_not_add_variable_paths() {
for variables in [json!(null), json!(["id"]), json!("id"), json!(123)] {
let v = json!({
"query": "query Test { id }",
"variables": variables
});
let item = extract_one(&v);
assert_eq!(item.name, "Test");
assert_eq!(item.op_type, "query");
assert!(item.variables.is_empty());
}
}
#[test]
fn batch_with_mixed_values_returns_one_item_per_payload() {
let v = json!([
{ "query": "query First { id }" },
{ "operationName": "PersistedOnly" },
null,
{ "query": null }
]);
let items = px_graphql_extract(&v, &test_conf());
assert_eq!(items.len(), 4);
assert_eq!(items.first().map(|i| i.name.as_str()), Some("First"));
assert_eq!(items.get(1).map(|i| i.name.as_str()), Some("PersistedOnly"));
assert_eq!(items.get(2).map(|i| i.name.as_str()), Some(""));
assert_eq!(items.get(3).map(|i| i.name.as_str()), Some(""));
}
#[test]
fn extract_keywords_preserves_duplicates_and_order() {
let query = r#"query GetUser {
user {
id
name
friends {
id
}
}
}"#;
let conf = conf_with_keywords(&["user", r"friends?", "id"]);
let v = json!({ "query": query });
let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();
let mut keywords = item.keywords;
keywords.sort();
assert_eq!(keywords, vec!["friends", "id", "id", "user"]);
}
#[test]
fn extract_keywords_empty_when_config_empty() {
let v = json!({ "query": "query Test { id id }" });
let item = graphql_parse_and_extract(&v, &test_conf()).unwrap_or_default();
assert!(item.keywords.is_empty());
assert_eq!(item.name, "Test");
}
#[test]
fn extract_keywords_truncates_per_keyword_length() {
let long = "x".repeat(150);
let query = format!("query Test {{ {long} }}");
let conf = conf_with_keywords(&["x+"]);
let v = json!({ "query": query });
let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();
assert_eq!(item.keywords.len(), 1);
assert_eq!(item.keywords.first().map(String::len), Some(100));
}
#[test]
fn extract_keywords_caps_total_count() {
let query = "query Test { ".to_owned() + &"id ".repeat(600) + "}";
let conf = conf_with_keywords(&["id"]);
let v = json!({ "query": query });
let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();
assert_eq!(item.keywords.len(), 500);
}
#[test]
fn extract_keywords_trims_whitespace() {
let conf = conf_with_keywords(&[r"\s+user\s+"]);
let v = json!({ "query": "query Test { user { id } }" });
let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();
assert_eq!(item.keywords, vec!["user"]);
}
#[test]
fn operation_name_fallback_honors_sensitive_operation_config() {
let mut conf = test_conf();
conf.sensitive_graphql_operation_names = vec!["SensitivePersisted".to_string()];
let v = json!({
"operationName": "SensitivePersisted",
"extensions": {
"persistedQuery": {
"version": 1
}
}
});
let item = graphql_parse_and_extract(&v, &conf).unwrap_or_default();
assert_eq!(item.name, "SensitivePersisted");
assert!(item.sensitive);
}