197 lines
4.9 KiB
Rust
197 lines
4.9 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<&[Value]>) -> Result<Value, String> {
|
|
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<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
|
|
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)
|
|
}
|