use geode_client::{Dsn, EdgeDirection, PatternBuilder, PredicateBuilder, QueryBuilder, Value};
use proptest::prelude::*;
use rust_decimal::Decimal;
use std::collections::HashMap;
use std::str::FromStr;
fn arb_value() -> impl Strategy<Value = Value> {
prop_oneof![
Just(Value::null()),
any::<bool>().prop_map(Value::bool),
any::<i64>().prop_map(Value::int),
".*".prop_map(Value::string),
(-1000000i64..1000000i64, 0u32..6u32).prop_map(|(n, scale)| {
let divisor = 10i64.pow(scale);
let decimal_str = format!(
"{}.{:0width$}",
n / divisor,
(n % divisor).abs(),
width = scale as usize
);
if let Ok(d) = Decimal::from_str(&decimal_str) {
Value::decimal(d)
} else {
Value::int(n)
}
}),
]
}
fn arb_nested_value() -> impl Strategy<Value = Value> {
arb_value().prop_recursive(
3, 64, 10, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..10).prop_map(Value::array),
prop::collection::hash_map("\\w{1,10}", inner, 0..10).prop_map(Value::object),
]
},
)
}
proptest! {
#[test]
fn null_is_null(_unused in 0..1i32) {
let v = Value::null();
prop_assert!(v.is_null());
}
#[test]
fn non_null_is_not_null(i in any::<i64>()) {
let v = Value::int(i);
prop_assert!(!v.is_null());
}
#[test]
fn int_roundtrip(i in any::<i64>()) {
let v = Value::int(i);
prop_assert_eq!(v.as_int().unwrap(), i);
}
#[test]
fn bool_roundtrip(b in any::<bool>()) {
let v = Value::bool(b);
prop_assert_eq!(v.as_bool().unwrap(), b);
}
#[test]
fn string_roundtrip(s in ".*") {
let v = Value::string(s.clone());
prop_assert_eq!(v.as_string().unwrap(), &s);
}
#[test]
fn decimal_access(n in -999999i64..999999i64) {
let d = Decimal::from(n);
let v = Value::decimal(d);
prop_assert_eq!(v.as_decimal().unwrap(), d);
}
#[test]
fn array_roundtrip(items in prop::collection::vec(any::<i64>(), 0..20)) {
let values: Vec<Value> = items.iter().map(|&i| Value::int(i)).collect();
let v = Value::array(values);
let arr = v.as_array().unwrap();
prop_assert_eq!(arr.len(), items.len());
for (i, &expected) in items.iter().enumerate() {
prop_assert_eq!(arr[i].as_int().unwrap(), expected);
}
}
#[test]
fn object_roundtrip(entries in prop::collection::hash_map("\\w{1,20}", any::<i64>(), 0..10)) {
let map: HashMap<String, Value> = entries.iter()
.map(|(k, &v)| (k.clone(), Value::int(v)))
.collect();
let v = Value::object(map);
let obj = v.as_object().unwrap();
prop_assert_eq!(obj.len(), entries.len());
for (k, &expected) in &entries {
prop_assert_eq!(obj.get(k).unwrap().as_int().unwrap(), expected);
}
}
#[test]
fn value_clone_preserves_data(v in arb_value()) {
let cloned = v.clone();
prop_assert_eq!(v.is_null(), cloned.is_null());
if let (Ok(a), Ok(b)) = (v.as_int(), cloned.as_int()) {
prop_assert_eq!(a, b);
}
if let (Ok(a), Ok(b)) = (v.as_string(), cloned.as_string()) {
prop_assert_eq!(a, b);
}
}
#[test]
fn to_json_never_panics(v in arb_nested_value()) {
let _ = v.to_json();
}
#[test]
fn from_json_never_panics(json_str in "\\{.*\\}|\\[.*\\]|\".*\"|[0-9]+|true|false|null") {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&json_str) {
let _ = Value::from_json(json); }
}
#[test]
fn simple_json_roundtrip(i in any::<i64>()) {
let original = Value::int(i);
let json = original.to_json();
let restored = Value::from_json(json).unwrap();
prop_assert!(!restored.is_null());
prop_assert!(restored.as_int().is_ok());
}
}
proptest! {
#[test]
fn query_builder_never_panics(
pattern in "\\(\\w+\\)",
condition in "\\w+ [><=] \\d+",
fields in prop::collection::vec("\\w+", 1..5),
limit in 1usize..1000
) {
let field_refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
let (query, _params) = QueryBuilder::new()
.match_pattern(&pattern)
.where_clause(&condition)
.return_(&field_refs)
.limit(limit)
.build();
prop_assert!(query.contains("MATCH"));
prop_assert!(query.contains("WHERE"));
prop_assert!(query.contains("RETURN"));
prop_assert!(query.contains("LIMIT"));
}
#[test]
fn query_builder_params_never_panic(
param_name in "\\w{1,20}",
param_value in any::<i64>()
) {
let (query, params) = QueryBuilder::new()
.match_pattern("(n:Node)")
.where_clause(&format!("n.value = ${}", param_name))
.with_param(¶m_name, param_value)
.return_(&["n"])
.build();
let param_pattern = format!("${}", param_name);
prop_assert!(query.contains(¶m_pattern));
prop_assert!(params.contains_key(¶m_name));
}
#[test]
fn query_builder_non_empty(pattern in "\\(\\w+\\)") {
let (query, _) = QueryBuilder::new()
.match_pattern(&pattern)
.return_(&["*"])
.build();
prop_assert!(!query.is_empty());
}
#[test]
fn query_builder_clauses_separated(
patterns in prop::collection::vec("\\(\\w+\\)", 1..4)
) {
let mut builder = QueryBuilder::new();
for pattern in &patterns {
builder = builder.match_pattern(pattern);
}
let (query, _) = builder.return_(&["*"]).build();
let match_count = query.matches("MATCH").count();
prop_assert_eq!(match_count, patterns.len());
}
}
proptest! {
#[test]
fn pattern_builder_never_panics(
var1 in "\\w{1,10}",
label1 in "\\w{1,20}",
edge_var in "\\w{1,10}",
edge_type in "\\w{1,20}",
var2 in "\\w{1,10}",
label2 in "\\w{1,20}"
) {
let pattern = PatternBuilder::new()
.node(&var1, &label1)
.edge(&edge_var, &edge_type, EdgeDirection::Outgoing)
.node(&var2, &label2)
.build();
prop_assert!(pattern.contains(&var1) || pattern.contains(&label1));
}
#[test]
fn pattern_builder_single_node(var in "\\w{1,10}", label in "\\w{1,20}") {
let pattern = PatternBuilder::new()
.node(&var, &label)
.build();
prop_assert!(pattern.starts_with('('));
prop_assert!(pattern.ends_with(')'));
}
#[test]
fn pattern_builder_edge_directions(
edge_var in "\\w{1,10}",
edge_type in "\\w{1,20}"
) {
let outgoing = PatternBuilder::new()
.node("a", "Node")
.edge(&edge_var, &edge_type, EdgeDirection::Outgoing)
.node("b", "Node")
.build();
let incoming = PatternBuilder::new()
.node("a", "Node")
.edge(&edge_var, &edge_type, EdgeDirection::Incoming)
.node("b", "Node")
.build();
let undirected = PatternBuilder::new()
.node("a", "Node")
.edge(&edge_var, &edge_type, EdgeDirection::Undirected)
.node("b", "Node")
.build();
prop_assert!(outgoing.contains("->"));
prop_assert!(incoming.contains("<-"));
prop_assert!(!undirected.contains("->") || !undirected.contains("<-"));
}
}
proptest! {
#[test]
fn predicate_builder_never_panics(
left in "\\w+\\.\\w+",
right in "\\d+"
) {
let pred = PredicateBuilder::new()
.greater_than(&left, &right)
.build_and();
prop_assert!(pred.contains(">"));
}
#[test]
fn predicate_builder_joins_with_and(
expr1 in "\\w+",
expr2 in "\\w+"
) {
let pred = PredicateBuilder::new()
.is_not_null(&expr1)
.is_not_null(&expr2)
.build_and();
if expr1 != expr2 {
prop_assert!(pred.contains("AND"));
}
}
}
proptest! {
#[test]
fn wrong_type_accessor_returns_error(i in any::<i64>()) {
let v = Value::int(i);
prop_assert!(v.as_string().is_err());
prop_assert!(v.as_bool().is_err());
prop_assert!(v.as_array().is_err());
prop_assert!(v.as_object().is_err());
prop_assert!(v.as_decimal().is_err());
}
#[test]
fn string_accessor_on_non_string_returns_error(b in any::<bool>()) {
let v = Value::bool(b);
prop_assert!(v.as_string().is_err());
prop_assert!(v.as_int().is_err());
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
#[test]
fn large_nested_array_no_overflow(depth in 1usize..100) {
let mut v = Value::int(42);
for _ in 0..depth {
v = Value::array(vec![v]);
}
let _ = v.to_json();
}
#[test]
fn large_object_handles_many_keys(num_keys in 1usize..500) {
let map: HashMap<String, Value> = (0..num_keys)
.map(|i| (format!("key_{}", i), Value::int(i as i64)))
.collect();
let v = Value::object(map);
let obj = v.as_object().unwrap();
prop_assert_eq!(obj.len(), num_keys);
}
}
proptest! {
#[test]
fn random_json_handled(bytes in prop::collection::vec(any::<u8>(), 0..1000)) {
if let Ok(s) = std::str::from_utf8(&bytes) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(s) {
let _ = Value::from_json(json);
}
}
}
#[test]
fn random_query_pattern_no_crash(pattern in ".*") {
let _ = QueryBuilder::new()
.match_pattern(&pattern)
.return_(&["*"])
.build();
}
#[test]
fn random_predicate_no_crash(left in ".*", right in ".*") {
let _ = PredicateBuilder::new()
.greater_than(&left, &right)
.build_and();
}
}
fn arb_dsn_string() -> impl Strategy<Value = String> {
(
prop_oneof![Just("quic"), Just("grpc")],
prop_oneof![
Just("localhost".to_string()),
Just("127.0.0.1".to_string()),
Just("host.example.com".to_string()),
Just("[::1]".to_string()),
Just("[2001:db8::1]".to_string()),
],
1u16..=65535u16,
prop::option::of("[a-zA-Z][a-zA-Z0-9_]{0,10}"),
prop::option::of("[a-zA-Z0-9]{1,10}"),
prop::option::of(1usize..=50_000usize),
prop::option::of("[a-zA-Z][a-zA-Z0-9_-]{0,12}"),
any::<bool>(),
any::<bool>(),
)
.prop_map(
|(scheme, host, port, user, pass, page_size, graph, tls_off, skip)| {
let mut s = format!("{}://", scheme);
match (&user, &pass) {
(Some(u), Some(p)) => s.push_str(&format!("{}:{}@", u, p)),
(Some(u), None) => s.push_str(&format!("{}@", u)),
_ => {}
}
s.push_str(&format!("{}:{}", host, port));
let mut params: Vec<String> = Vec::new();
if let Some(ps) = page_size {
params.push(format!("page_size={}", ps));
}
if let Some(g) = &graph {
params.push(format!("graph={}", g));
}
if tls_off {
params.push("tls=0".to_string());
}
if skip {
params.push("insecure_tls_skip_verify=true".to_string());
}
if user.is_none() {
if let Some(p) = &pass {
params.push(format!("pass={}", p));
}
}
if !params.is_empty() {
s.push('?');
s.push_str(¶ms.join("&"));
}
s
},
)
}
proptest! {
#[test]
fn dsn_format_parse_roundtrip(dsn_str in arb_dsn_string()) {
let parsed = Dsn::parse(&dsn_str)
.unwrap_or_else(|e| panic!("generated DSN {:?} failed to parse: {}", dsn_str, e));
let formatted = parsed.to_dsn();
let reparsed = Dsn::parse(&formatted)
.unwrap_or_else(|e| panic!("re-formatted DSN {:?} failed to parse: {}", formatted, e));
prop_assert_eq!(parsed, reparsed);
}
#[test]
fn dsn_to_dsn_has_scheme(dsn_str in arb_dsn_string()) {
let parsed = Dsn::parse(&dsn_str).unwrap();
let formatted = parsed.to_dsn();
prop_assert!(
formatted.starts_with("quic://") || formatted.starts_with("grpc://"),
"to_dsn output missing scheme: {}",
formatted
);
}
}