#![allow(clippy::unwrap_used, clippy::panic)]
use serde_json::json;
use super::*;
fn encode(literal: &str) -> String {
let document = format!("{{ f(a: {literal}) {{ id }} }}");
let parsed =
crate::graphql::parse_query(&document).unwrap_or_else(|e| panic!("{literal}: {e}"));
parsed.selections[0].arguments[0].value_json.clone()
}
fn gql_string(s: &str) -> String {
serde_json::to_string(s).unwrap()
}
#[test]
fn strings_survive_the_round_trip_intact() {
let hostile = [
r"C:\Users\alice\notes.txt", "line one\nline two", "tab\there", "quote\"inside", "carriage\rreturn",
"null\u{0}byte",
"bell\u{7}",
"unicode ☃ snowman",
"emoji 🍓",
r#"{"looks":"like json"}"#,
r"\\",
"\\\"",
];
for original in hostile {
let encoded = encode(&gql_string(original));
let decoded = decode(&encoded)
.unwrap_or_else(|e| panic!("{original:?} encoded to invalid JSON {encoded:?}: {e}"));
assert_eq!(
decoded.as_str(),
Some(original),
"{original:?} did not survive the round trip (encoded as {encoded})"
);
}
}
#[test]
fn dollar_prefixed_literals_are_not_variable_references() {
let mut variables = HashMap::new();
variables.insert("100".to_string(), json!("LEAKED"));
variables.insert("price".to_string(), json!(42));
for literal in ["$100", "$price", "$", "$$", "$var"] {
let encoded = encode(&gql_string(literal));
let decoded = decode(&encoded).expect("decodes");
assert_eq!(variable_name(&decoded), None, "{literal:?} must not read as a variable");
let resolved = resolve_variables(decoded, &variables);
assert_eq!(
resolved,
json!(literal),
"{literal:?} must resolve to itself, not to a variable's value"
);
}
}
#[test]
fn variable_references_resolve_at_every_depth() {
let mut variables = HashMap::new();
variables.insert("limit".to_string(), json!(25));
variables.insert("tenant".to_string(), json!("acme"));
let top = decode(&encode("$limit")).unwrap();
assert_eq!(variable_name(&top), Some("limit"));
assert_eq!(resolve_variables(top, &variables), json!(25));
let decoded = decode(&encode("{tenantId: {eq: $tenant}}")).unwrap();
assert_eq!(
resolve_variables(decoded, &variables),
json!({"tenantId": {"eq": "acme"}}),
"a variable nested inside a where: argument must still resolve"
);
let decoded = decode(&encode(r#"[$tenant, "literal"]"#)).unwrap();
assert_eq!(resolve_variables(decoded, &variables), json!(["acme", "literal"]));
}
#[test]
fn an_undefined_variable_resolves_to_null() {
let decoded = decode(&encode("$missing")).unwrap();
assert_eq!(resolve_variables(decoded, &HashMap::new()), json!(null));
}
#[test]
fn to_graphql_escapes_strings_and_emits_variable_references() {
assert_eq!(to_graphql(&json!("plain")), r#""plain""#);
assert_eq!(to_graphql(&json!("has \"quote\"")), r#""has \"quote\"""#);
assert_eq!(to_graphql(&json!(r"back\slash")), r#""back\\slash""#);
assert_eq!(to_graphql(&json!("new\nline")), r#""new\nline""#);
assert_eq!(to_graphql(&variable_ref("limit")), "$limit");
assert_eq!(to_graphql(&json!({"field": "id"})), r#"{field: "id"}"#);
let rendered = to_graphql(&json!([{"field": "id", "direction": "ASC"}]));
assert!(
rendered.starts_with("[{") && rendered.ends_with("}]"),
"a list of objects must render as a GraphQL list of objects: {rendered}"
);
assert!(
rendered.contains(r#"field: "id""#) && rendered.contains(r#"direction: "ASC""#),
"object keys must be bare GraphQL names, not quoted JSON keys: {rendered}"
);
}
#[test]
fn a_malformed_stored_argument_errors_instead_of_being_dropped() {
let err = decode(r#"{"unterminated": "#).expect_err("malformed JSON must not decode");
assert!(
format!("{err}").contains("widen"),
"the error must say why dropping is unacceptable, got: {err}"
);
}
#[test]
fn nesting_past_the_cap_is_refused() {
let literal = format!("{}1{}", "[".repeat(MAX_DEPTH + 1), "]".repeat(MAX_DEPTH + 1));
let error = crate::graphql::parse_query(&format!("{{ f(a: {literal}) {{ id }} }}"))
.expect_err("a value past the depth cap must be refused");
assert!(error.to_string().contains("64-level"), "{error}");
}
#[test]
fn an_object_literal_keeps_its_written_key_order() {
assert_eq!(encode("{b: false, a: true}"), r#"{"b":false,"a":true}"#);
assert_eq!(encode("[{z: 1, y: {d: 1, c: 2}}]"), r#"[{"z":1,"y":{"d":1,"c":2}}]"#);
}