use super::*;
use crate::graph::languages::cypher::tokenizer::{tokenize_cypher, CypherToken};
fn quote(name: &str) -> String {
format!("`{}`", name.replace('`', "``"))
}
fn graph_with_secrets() -> DirGraph {
let mut graph = DirGraph::new();
run_semantics_query(
&mut graph,
"CREATE (:Person {id: 1, title: 'ada'}), (:Person {id: 2, title: 'bob'}), \
(:Secret {id: 3, title: 'classified'})",
);
graph
}
fn run_semantics_query(graph: &mut DirGraph, query: &str) -> CypherResult {
let parsed = parser::parse_cypher(query)
.unwrap_or_else(|e| panic!("query failed to parse: {query}\n error: {e}"));
execute_mutable(
graph,
&parsed,
HashMap::new(),
crate::graph::algorithms::Interrupt::default(),
)
.unwrap_or_else(|e| panic!("query failed: {query}\n error: {e}"))
}
fn count(graph: &mut DirGraph, label: &str) -> i64 {
let result = run_semantics_query(graph, &format!("MATCH (n:{label}) RETURN count(n) AS c"));
match &result.rows[0][0] {
Value::Int64(n) => *n,
other => panic!("expected a count, got {other:?}"),
}
}
#[test]
fn doubled_backtick_is_one_literal_backtick() {
assert_eq!(
tokenize_cypher("`We``ird`").unwrap(),
vec![CypherToken::Identifier("We`ird".to_string())]
);
assert!(
!tokenize_cypher("`` ` ``").unwrap_err().is_empty(),
"an unterminated quoted identifier must still be an error"
);
assert_eq!(
tokenize_cypher("`a````b`").unwrap(),
vec![CypherToken::Identifier("a``b".to_string())]
);
assert_eq!(
tokenize_cypher("```a`").unwrap(),
vec![CypherToken::Identifier("`a".to_string())]
);
assert_eq!(
tokenize_cypher("`My Node`").unwrap(),
vec![CypherToken::Identifier("My Node".to_string())]
);
}
#[test]
fn an_unterminated_quoted_identifier_is_still_rejected() {
assert!(tokenize_cypher("`abc")
.unwrap_err()
.contains("Unterminated"));
assert!(tokenize_cypher("`a``b")
.unwrap_err()
.contains("Unterminated"));
}
#[test]
fn quote_then_tokenize_round_trips_every_hostile_identifier() {
for hostile in [
"Person`) DETACH DELETE n //",
"n` :Secret) RETURN n.title AS leaked //",
"`",
"``",
"a`b`c",
"plain",
"with space",
"with-hyphen.and.dots",
] {
let tokens = tokenize_cypher("e(hostile)).unwrap_or_else(|e| {
panic!("quoted {hostile:?} failed to tokenize: {e}");
});
assert_eq!(
tokens,
vec![CypherToken::Identifier(hostile.to_string())],
"quote-then-tokenize must round-trip {hostile:?} as ONE identifier"
);
}
}
#[test]
fn label_position_injection_is_inert_when_escaped() {
let mut graph = graph_with_secrets();
assert_eq!(count(&mut graph, "Person"), 2);
let label = "Person`) DETACH DELETE n //";
let query = format!("MATCH (n:{}) RETURN count(n) AS c", quote(label));
let result = run_semantics_query(&mut graph, &query);
assert_eq!(result.rows[0][0], Value::Int64(0));
assert_eq!(
count(&mut graph, "Person"),
2,
"the injected DETACH DELETE must not have run"
);
assert_eq!(count(&mut graph, "Secret"), 1);
}
#[test]
fn variable_position_injection_cannot_exfiltrate() {
let mut graph = graph_with_secrets();
let var = "n` :Secret) RETURN n.title AS leaked //";
let query = format!(
"MATCH ({}:Person) RETURN count({}) AS c",
quote(var),
quote(var)
);
let result = run_semantics_query(&mut graph, &query);
assert_eq!(result.columns, vec!["c"]);
assert_eq!(result.rows[0][0], Value::Int64(2));
}
#[test]
fn unescaped_injection_still_breaks_out() {
let mut graph = graph_with_secrets();
let label = "Person`) DETACH DELETE n //";
let naive = format!("MATCH (n:`{label}`) RETURN count(n) AS c");
run_semantics_query(&mut graph, &naive);
assert_eq!(
count(&mut graph, "Person"),
0,
"the raw-interpolation control must still be exploitable — otherwise \
the escaped cases above are not testing the escape"
);
}
#[test]
fn a_backtick_bearing_label_survives_the_pattern_round_trip() {
let mut graph = DirGraph::new();
let weird = "Od`d";
run_semantics_query(
&mut graph,
&format!("CREATE (:{} {{id: 1, title: 'x'}})", quote(weird)),
);
let result = run_semantics_query(
&mut graph,
&format!("MATCH (n:{}) RETURN n.title AS t", quote(weird)),
);
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::String("x".to_string()));
run_semantics_query(
&mut graph,
&format!(
"MATCH (n:{}) SET n.{} = 7",
quote(weird),
quote("we`ird key")
),
);
let result = run_semantics_query(
&mut graph,
&format!(
"MATCH (n:{}) WHERE n.{} = 7 RETURN count(n) AS c",
quote(weird),
quote("we`ird key")
),
);
assert_eq!(result.rows[0][0], Value::Int64(1));
}