Files
jspg/src/database/executors/mock.rs
2026-04-02 21:55:57 -04:00

219 lines
5.5 KiB
Rust

#[cfg(test)]
use crate::database::executors::DatabaseExecutor;
#[cfg(test)]
use regex::Regex;
#[cfg(test)]
use serde_json::Value;
#[cfg(test)]
use std::cell::RefCell;
#[cfg(test)]
pub struct MockState {
pub captured_queries: Vec<String>,
pub query_responses: Vec<Result<Value, String>>,
pub execute_responses: Vec<Result<(), String>>,
pub mocks: Vec<Value>,
}
#[cfg(test)]
impl MockState {
pub fn new() -> Self {
Self {
captured_queries: Default::default(),
query_responses: Default::default(),
execute_responses: Default::default(),
mocks: Default::default(),
}
}
}
#[cfg(test)]
thread_local! {
pub static MOCK_STATE: RefCell<MockState> = RefCell::new(MockState::new());
}
#[cfg(test)]
pub struct MockExecutor {}
#[cfg(test)]
impl MockExecutor {
pub fn new() -> Self {
Self {}
}
}
#[cfg(test)]
impl DatabaseExecutor for MockExecutor {
fn query(&self, sql: &str, _args: Option<Vec<Value>>) -> Result<Value, String> {
println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string());
if !s.mocks.is_empty() {
if let Some(matches) = parse_and_match_mocks(sql, &s.mocks) {
if !matches.is_empty() {
return Ok(Value::Array(matches));
}
}
}
if s.query_responses.is_empty() {
return Ok(Value::Array(vec![]));
}
s.query_responses.remove(0)
})
}
fn execute(&self, sql: &str, _args: Option<Vec<Value>>) -> Result<(), String> {
println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string());
if s.execute_responses.is_empty() {
return Ok(());
}
s.execute_responses.remove(0)
})
}
fn auth_user_id(&self) -> Result<String, String> {
Ok("00000000-0000-0000-0000-000000000000".to_string())
}
fn timestamp(&self) -> Result<String, String> {
Ok("2026-03-10T00:00:00Z".to_string())
}
#[cfg(test)]
fn get_queries(&self) -> Vec<String> {
MOCK_STATE.with(|state| state.borrow().captured_queries.clone())
}
#[cfg(test)]
fn set_mocks(&self, mocks: Vec<Value>) {
MOCK_STATE.with(|state| {
state.borrow_mut().mocks = mocks;
});
}
#[cfg(test)]
fn reset_mocks(&self) {
MOCK_STATE.with(|state| {
let mut s = state.borrow_mut();
s.captured_queries.clear();
s.query_responses.clear();
s.execute_responses.clear();
s.mocks.clear();
});
}
}
#[cfg(test)]
fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
let sql_upper = sql.to_uppercase();
if !sql_upper.starts_with("SELECT") {
return None;
}
// 1. Extract table name
let table_regex = Regex::new(r#"(?i)\s+FROM\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#).ok()?;
let table = if let Some(caps) = table_regex.captures(sql) {
caps.get(1)?.as_str()
} else {
return None;
};
// 2. Extract WHERE conditions string
let mut where_clause = String::new();
if let Some(where_idx) = sql_upper.find(" WHERE ") {
let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql_upper.len());
if let Some(limit_idx) = sql_upper.find(" LIMIT ") {
if limit_idx < where_end {
where_end = limit_idx;
}
}
where_clause = sql[where_idx + 7..where_end].to_string();
}
// 3. Find matching mocks
let mut matches = Vec::new();
let or_regex = Regex::new(r"(?i)\s+OR\s+").ok()?;
let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?;
for mock in mocks {
if let Some(mock_obj) = mock.as_object() {
if let Some(t) = mock_obj.get("type") {
if t.as_str() != Some(table) {
continue;
}
}
if where_clause.is_empty() {
matches.push(mock.clone());
continue;
}
let or_parts = or_regex.split(&where_clause);
let mut any_branch_matched = false;
for or_part in or_parts {
let branch_str = or_part.replace('(', "").replace(')', "");
let mut branch_matches = true;
for part in and_regex.split(&branch_str) {
if let Some(eq_idx) = part.find('=') {
let left = part[..eq_idx]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let right = part[eq_idx + 1..].trim().trim_matches('\'');
let mock_val_str = match mock_obj.get(left) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(),
Some(Value::Bool(b)) => b.to_string(),
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != right {
branch_matches = false;
break;
}
} else if part.to_uppercase().contains(" IS NULL") {
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let mock_val_str = match mock_obj.get(left) {
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != "null" {
branch_matches = false;
break;
}
}
}
if branch_matches {
any_branch_matched = true;
break;
}
}
if any_branch_matched {
matches.push(mock.clone());
}
}
}
Some(matches)
}