#[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, pub query_responses: Vec>, pub execute_responses: Vec>, pub mocks: Vec, } #[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 = 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<&[Value]>) -> Result { println!("DEBUG SQL QUERY: {}", 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<&[Value]>) -> Result<(), String> { println!("DEBUG SQL EXECUTE: {}", 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 { Ok("00000000-0000-0000-0000-000000000000".to_string()) } fn timestamp(&self) -> Result { Ok("2026-03-10T00:00:00Z".to_string()) } #[cfg(test)] fn get_queries(&self) -> Vec { MOCK_STATE.with(|state| state.borrow().captured_queries.clone()) } #[cfg(test)] fn set_mocks(&self, mocks: Vec) { 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> { 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 let mut conditions = Vec::new(); if let Some(where_idx) = sql_upper.find(" WHERE ") { let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql.len()); if let Some(limit_idx) = sql_upper.find(" LIMIT ") { if limit_idx < where_end { where_end = limit_idx; } } let where_clause = &sql[where_idx + 7..where_end]; let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?; let parts = and_regex.split(where_clause); for part in parts { 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('\''); conditions.push((left.to_string(), right.to_string())); } else if part.to_uppercase().contains(" IS NULL") { let left = part[..part.to_uppercase().find(" IS NULL").unwrap()] .trim() .split('.') .last() .unwrap_or("") .replace('"', ""); // Remove quotes explicitly conditions.push((left, "null".to_string())); } } } // 3. Find matching mocks let mut matches = Vec::new(); 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; } } let mut matches_all = true; for (k, v) in &conditions { let mock_val_str = match mock_obj.get(k) { 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(), _ => { matches_all = false; break; } }; if mock_val_str != *v { matches_all = false; break; } } if matches_all { matches.push(mock.clone()); } } } Some(matches) }