use std::collections::HashMap;
use serde_json::json;
use hyperi_rustlib::expression::{
ALLOWED_FUNCTIONS, DISALLOWED_FUNCTIONS, ExpressionError, ProfileConfig,
check_profile_with_config, compile, evaluate, evaluate_condition, validate,
};
#[test]
fn validate_valid_comparison() {
assert!(validate(r#"severity == "critical""#).is_empty());
}
#[test]
fn validate_valid_numeric() {
assert!(validate("amount > 10000").is_empty());
}
#[test]
fn validate_valid_logical() {
assert!(validate("a > 1 && b < 10").is_empty());
}
#[test]
fn validate_valid_membership() {
assert!(validate(r#"status in ["active", "pending"]"#).is_empty());
}
#[test]
fn validate_valid_string_function() {
assert!(validate(r#"msg.contains("error")"#).is_empty());
}
#[test]
fn validate_valid_starts_with() {
assert!(validate(r#"path.startsWith("/api/")"#).is_empty());
}
#[test]
fn validate_valid_ends_with() {
assert!(validate(r#"file.endsWith(".log")"#).is_empty());
}
#[test]
fn validate_matches_blocked_by_default() {
let errors = validate(r#"name.matches("^web-[0-9]+$")"#);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("matches()"));
}
#[test]
fn validate_matches_allowed_with_config() {
let config = ProfileConfig {
allow_regex: true,
..Default::default()
};
assert!(check_profile_with_config(r#"name.matches("^web-[0-9]+$")"#, &config).is_empty());
}
#[test]
fn validate_valid_has() {
assert!(validate("has(user.name)").is_empty());
}
#[test]
fn validate_valid_size() {
assert!(validate("size(tags) > 0").is_empty());
}
#[test]
fn validate_valid_ternary() {
assert!(validate("is_admin ? 95 : 50").is_empty());
}
#[test]
fn validate_valid_type_cast() {
assert!(validate("int(x) > 10").is_empty());
}
#[test]
fn validate_valid_arithmetic() {
assert!(validate("price * quantity > threshold").is_empty());
}
#[test]
fn validate_valid_boolean_literal() {
assert!(validate("enabled == true").is_empty());
}
#[test]
fn validate_valid_null_check() {
assert!(validate("x == null").is_empty());
}
#[test]
fn validate_valid_compound() {
assert!(validate(r#"severity == "critical" && amount > 10000 && !is_test"#).is_empty());
}
#[test]
fn validate_empty_expression() {
let errors = validate("");
assert_eq!(errors.len(), 1);
assert!(errors[0].to_lowercase().contains("empty"));
}
#[test]
fn validate_whitespace_only() {
let errors = validate(" ");
assert_eq!(errors.len(), 1);
assert!(errors[0].to_lowercase().contains("empty"));
}
#[test]
fn validate_disallowed_map() {
let errors = validate("[1,2,3].map(x, x * 2)");
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("map()"));
assert!(errors[0].contains("not allowed"));
}
#[test]
fn validate_disallowed_filter() {
let errors = validate("[1,2,3].filter(x, x > 1)");
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("filter()"));
}
#[test]
fn validate_disallowed_exists() {
let errors = validate("[1,2,3].exists(x, x > 2)");
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("exists()"));
}
#[test]
fn validate_disallowed_timestamp() {
let errors = validate(r#"timestamp("2024-01-01T00:00:00Z")"#);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("timestamp()"));
}
#[test]
fn validate_disallowed_duration() {
let errors = validate(r#"duration("1h")"#);
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("duration()"));
}
#[test]
fn evaluate_arithmetic() {
let data = HashMap::new();
let result = evaluate("1 + 2", &data).unwrap();
assert_eq!(result, 3.into());
}
#[test]
fn evaluate_string_comparison_true() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("critical"));
let result = evaluate(r#"severity == "critical""#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_string_comparison_false() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("low"));
let result = evaluate(r#"severity == "critical""#, &data).unwrap();
assert_eq!(result, false.into());
}
#[test]
fn evaluate_numeric_gt() {
let mut data = HashMap::new();
data.insert("amount".into(), json!(15000));
let result = evaluate("amount > 10000", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_numeric_gt_false() {
let mut data = HashMap::new();
data.insert("amount".into(), json!(5000));
let result = evaluate("amount > 10000", &data).unwrap();
assert_eq!(result, false.into());
}
#[test]
fn evaluate_logical_and() {
let mut data = HashMap::new();
data.insert("a".into(), json!(5));
data.insert("b".into(), json!(3));
let result = evaluate("a > 1 && b < 10", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_logical_or() {
let mut data = HashMap::new();
data.insert("a".into(), json!(5));
data.insert("b".into(), json!(3));
let result = evaluate("a > 100 || b < 10", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_logical_not() {
let mut data = HashMap::new();
data.insert("is_test".into(), json!(false));
let result = evaluate("!is_test", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_membership_in() {
let mut data = HashMap::new();
data.insert("status".into(), json!("active"));
let result = evaluate(r#"status in ["active", "pending"]"#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_membership_in_false() {
let mut data = HashMap::new();
data.insert("status".into(), json!("blocked"));
let result = evaluate(r#"status in ["active", "pending"]"#, &data).unwrap();
assert_eq!(result, false.into());
}
#[test]
fn evaluate_string_contains() {
let mut data = HashMap::new();
data.insert("msg".into(), json!("an error occurred"));
let result = evaluate(r#"msg.contains("error")"#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_string_starts_with() {
let mut data = HashMap::new();
data.insert("path".into(), json!("/api/v1/users"));
let result = evaluate(r#"path.startsWith("/api/")"#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_string_ends_with() {
let mut data = HashMap::new();
data.insert("file".into(), json!("app.log"));
let result = evaluate(r#"file.endsWith(".log")"#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_string_matches_blocked_by_default() {
let mut data = HashMap::new();
data.insert("name".into(), json!("web-42"));
let result = evaluate(r#"name.matches("^web-[0-9]+$")"#, &data);
assert!(result.is_err());
}
#[test]
fn evaluate_size_list() {
let mut data = HashMap::new();
data.insert("tags".into(), json!(["a", "b"]));
let result = evaluate("size(tags) > 0", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_ternary_true() {
let mut data = HashMap::new();
data.insert("is_admin".into(), json!(true));
let result = evaluate("is_admin ? 95 : 50", &data).unwrap();
assert_eq!(result, 95.into());
}
#[test]
fn evaluate_ternary_false() {
let mut data = HashMap::new();
data.insert("is_admin".into(), json!(false));
let result = evaluate("is_admin ? 95 : 50", &data).unwrap();
assert_eq!(result, 50.into());
}
#[test]
fn evaluate_boolean_true() {
let mut data = HashMap::new();
data.insert("enabled".into(), json!(true));
let result = evaluate("enabled == true", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_null_check() {
let mut data = HashMap::new();
data.insert("x".into(), json!(null));
let result = evaluate("x == null", &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_compound_condition() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("critical"));
data.insert("amount".into(), json!(15000));
let result = evaluate(r#"severity == "critical" && amount > 10000"#, &data).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn evaluate_missing_field_errors() {
let data = HashMap::new();
let result = evaluate(r#"severity == "critical""#, &data);
assert!(result.is_err());
}
#[test]
fn evaluate_invalid_expression_errors() {
let data = HashMap::new();
let result = evaluate("== broken", &data);
assert!(result.is_err());
}
#[test]
fn evaluate_disallowed_function_errors() {
let data = HashMap::new();
let result = evaluate("[1,2].map(x, x * 2)", &data);
assert!(result.is_err());
}
#[test]
fn condition_match() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("critical"));
assert!(evaluate_condition(r#"severity == "critical""#, &data));
}
#[test]
fn condition_no_match() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("low"));
assert!(!evaluate_condition(r#"severity == "critical""#, &data));
}
#[test]
fn condition_missing_field_returns_false() {
let data = HashMap::new();
assert!(!evaluate_condition(r#"severity == "critical""#, &data));
}
#[test]
fn condition_type_mismatch_returns_false() {
let mut data = HashMap::new();
data.insert("amount".into(), json!("not_a_number"));
assert!(!evaluate_condition("amount > 10", &data));
}
#[test]
fn condition_invalid_expression_returns_false() {
let data = HashMap::new();
assert!(!evaluate_condition("== broken", &data));
}
#[test]
fn condition_empty_expression_returns_false() {
let data = HashMap::new();
assert!(!evaluate_condition("", &data));
}
#[test]
fn condition_compound() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("critical"));
data.insert("amount".into(), json!(15000));
assert!(evaluate_condition(
r#"severity == "critical" && amount > 10000"#,
&data
));
}
#[test]
fn condition_compound_partial_match() {
let mut data = HashMap::new();
data.insert("severity".into(), json!("critical"));
data.insert("amount".into(), json!(5000));
assert!(!evaluate_condition(
r#"severity == "critical" && amount > 10000"#,
&data
));
}
#[test]
fn condition_in_membership() {
let mut data = HashMap::new();
data.insert("status".into(), json!("active"));
assert!(evaluate_condition(
r#"status in ["active", "pending"]"#,
&data
));
}
#[test]
fn condition_negated_in() {
let mut data = HashMap::new();
data.insert("status".into(), json!("active"));
assert!(evaluate_condition(
r#"!(status in ["blocked", "banned"])"#,
&data
));
}
#[test]
fn condition_numeric_comparison() {
let mut data = HashMap::new();
data.insert("amount".into(), json!(15000));
assert!(evaluate_condition("amount > 10000", &data));
}
#[test]
fn condition_ternary_truthy() {
let mut data = HashMap::new();
data.insert("is_admin".into(), json!(true));
assert!(evaluate_condition("is_admin ? 95 : 0", &data));
}
#[test]
fn condition_ternary_falsy() {
let mut data = HashMap::new();
data.insert("is_admin".into(), json!(true));
assert!(!evaluate_condition("is_admin ? 0 : 50", &data));
}
#[test]
fn compile_and_execute() {
let program = compile("price * quantity > threshold").unwrap();
let data = HashMap::from([
("price".into(), json!(10)),
("quantity".into(), json!(5)),
("threshold".into(), json!(40)),
]);
let ctx = hyperi_rustlib::expression::build_context(&data).unwrap();
let result = program.execute(&ctx).unwrap();
assert_eq!(result, true.into());
}
#[test]
fn compile_reuse() {
let program = compile(r#"severity == "critical""#).unwrap();
let data1 = HashMap::from([("severity".into(), json!("critical"))]);
let ctx1 = hyperi_rustlib::expression::build_context(&data1).unwrap();
assert_eq!(program.execute(&ctx1).unwrap(), true.into());
let data2 = HashMap::from([("severity".into(), json!("low"))]);
let ctx2 = hyperi_rustlib::expression::build_context(&data2).unwrap();
assert_eq!(program.execute(&ctx2).unwrap(), false.into());
}
#[test]
fn compile_invalid_raises() {
assert!(compile("== broken").is_err());
}
#[test]
fn compile_disallowed_raises() {
assert!(compile("[1,2].map(x, x*2)").is_err());
}
#[test]
fn compile_empty_raises() {
assert!(compile("").is_err());
}
#[test]
fn allowed_functions_contains_core() {
for f in &["contains", "startsWith", "endsWith", "size", "has"] {
assert!(ALLOWED_FUNCTIONS.contains(f), "missing allowed: {f}");
}
}
#[test]
fn matches_in_disallowed_not_allowed() {
assert!(DISALLOWED_FUNCTIONS.contains(&"matches"));
assert!(!ALLOWED_FUNCTIONS.contains(&"matches"));
}
#[test]
fn allowed_functions_contains_casts() {
for f in &["int", "uint", "double", "string", "bool"] {
assert!(ALLOWED_FUNCTIONS.contains(f), "missing allowed: {f}");
}
}
#[test]
fn disallowed_functions_present() {
for f in &[
"matches",
"map",
"filter",
"exists",
"all",
"exists_one",
"timestamp",
"duration",
] {
assert!(DISALLOWED_FUNCTIONS.contains(f), "missing disallowed: {f}");
}
}
#[test]
fn no_overlap_between_allowed_and_disallowed() {
for f in ALLOWED_FUNCTIONS {
assert!(
!DISALLOWED_FUNCTIONS.contains(f),
"overlap: {f} in both allowed and disallowed"
);
}
}
#[test]
fn validate_function_name_inside_string_not_flagged() {
assert!(validate(r#"msg.contains("filter")"#).is_empty());
}
#[test]
fn validate_timestamp_inside_string_not_flagged() {
assert!(validate(r#"label == "timestamp""#).is_empty());
}
#[test]
fn validate_matches_inside_string_not_flagged() {
assert!(validate(r#"msg.contains("matches")"#).is_empty());
}
#[test]
fn validate_real_call_after_string_caught() {
let errors = validate(r#""ok" + items.map(x, x)"#);
assert!(!errors.is_empty());
assert!(errors[0].contains("map()"));
}
#[test]
fn profile_config_default_blocks_all_restricted() {
let config = ProfileConfig::default();
let blocked = config.blocked_functions();
assert!(blocked.contains(&"matches"));
assert!(blocked.contains(&"map"));
assert!(blocked.contains(&"timestamp"));
}
#[test]
fn profile_config_selective_unlock() {
let config = ProfileConfig {
allow_regex: true,
..Default::default()
};
let blocked = config.blocked_functions();
assert!(!blocked.contains(&"matches"));
assert!(blocked.contains(&"map"));
assert!(blocked.contains(&"timestamp"));
}
#[test]
fn error_validation_display() {
let err = ExpressionError::Validation(vec!["error one".into(), "error two".into()]);
let msg = format!("{err}");
assert!(msg.contains("error one"));
assert!(msg.contains("error two"));
}
#[test]
fn error_compilation_display() {
let err = ExpressionError::Compilation("bad syntax".into());
let msg = format!("{err}");
assert!(msg.contains("bad syntax"));
}