Skip to main content

surreal_client/mocks/
engine.rs

1//! Mock SurrealDB Engine with Exact Request Matching
2//!
3//! Provides a simplified mock implementation that requires exact matching of method calls
4//! and parameters, making it predictable and easy to debug.
5
6use crate::{Engine, error::Result};
7use async_trait::async_trait;
8use ciborium::Value as CborValue;
9use serde_json::{Value, json};
10use std::collections::HashMap;
11
12/// A mock SurrealDB engine that requires exact matching of requests
13#[derive(Debug, Clone)]
14pub struct MockSurrealEngine {
15    /// Exact method+params combinations mapped to responses
16    exact_matches: HashMap<(String, Value), Value>,
17    /// Enable debug logging of queries
18    debug: bool,
19}
20
21impl MockSurrealEngine {
22    /// Create a new mock engine
23    pub fn new() -> Self {
24        Self {
25            exact_matches: HashMap::new(),
26            debug: false,
27        }
28    }
29
30    /// Enable debug logging of queries
31    pub fn with_debug(mut self, debug: bool) -> Self {
32        self.debug = debug;
33        self
34    }
35
36    /// Add an exact response for a specific method and parameters
37    pub fn with_exact_response(
38        mut self,
39        method: impl Into<String>,
40        params: Value,
41        response: Value,
42    ) -> Self {
43        self.exact_matches.insert((method.into(), params), response);
44        self
45    }
46
47    /// Add a response for a method with empty parameters
48    pub fn with_method_response(mut self, method: impl Into<String>, response: Value) -> Self {
49        self.exact_matches
50            .insert((method.into(), json!({})), response);
51        self
52    }
53
54    /// Add a response for a query method with specific query string
55    pub fn with_query_response(mut self, query: impl Into<String>, response: Value) -> Self {
56        let params = json!([query.into()]);
57        self.exact_matches
58            .insert(("query".to_string(), params), response);
59        self
60    }
61
62    /// Find exact matching response for a request or panic with descriptive error
63    fn find_response(&self, method: &str, params: &Value) -> Value {
64        if self.debug {
65            println!("MockSurrealEngine: method='{}', params={}", method, params);
66        }
67
68        let key = (method.to_string(), params.clone());
69
70        if let Some(response) = self.exact_matches.get(&key) {
71            if self.debug {
72                println!(
73                    "MockSurrealEngine: exact match found, returning {:?}",
74                    response
75                );
76            }
77            return response.clone();
78        }
79
80        // No exact match found - panic with descriptive error
81        let allowed_patterns: Vec<String> = self
82            .exact_matches
83            .keys()
84            .map(|(method, params)| format!("{}({})", method, params))
85            .collect();
86
87        panic!(
88            "MockSurrealEngine: executed method {}({}), but allowed patterns are: {}",
89            method,
90            params,
91            if allowed_patterns.is_empty() {
92                "NONE - no patterns configured!".to_string()
93            } else {
94                allowed_patterns.join(", ")
95            }
96        );
97    }
98}
99
100impl Default for MockSurrealEngine {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[async_trait]
107impl Engine for MockSurrealEngine {
108    async fn send_message(&mut self, method: &str, params: Value) -> Result<Value> {
109        Ok(self.find_response(method, &params))
110    }
111
112    async fn send_message_cbor(&mut self, method: &str, params: CborValue) -> Result<CborValue> {
113        let json_params = crate::cbor_convert::cbor_to_json(params);
114        let response = self.find_response(method, &json_params);
115        Ok(crate::cbor_convert::json_to_cbor(response))
116    }
117}
118
119/// Builder for creating mock SurrealDB instances with test data
120pub struct SurrealMockBuilder {
121    engine: MockSurrealEngine,
122    namespace: Option<String>,
123    database: Option<String>,
124}
125
126impl SurrealMockBuilder {
127    /// Create a new mock builder
128    pub fn new() -> Self {
129        Self {
130            engine: MockSurrealEngine::new(),
131            namespace: Some("test".to_string()),
132            database: Some("test".to_string()),
133        }
134    }
135
136    /// Set the namespace for the mock client
137    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
138        self.namespace = Some(namespace.into());
139        self
140    }
141
142    /// Set the database for the mock client
143    pub fn with_database(mut self, database: impl Into<String>) -> Self {
144        self.database = Some(database.into());
145        self
146    }
147
148    /// Enable debug logging
149    pub fn with_debug(mut self, debug: bool) -> Self {
150        self.engine = self.engine.with_debug(debug);
151        self
152    }
153
154    /// Add exact response for specific method and parameters
155    pub fn with_exact_response(
156        mut self,
157        method: impl Into<String>,
158        params: Value,
159        response: Value,
160    ) -> Self {
161        self.engine = self.engine.with_exact_response(method, params, response);
162        self
163    }
164
165    /// Add response for method with empty parameters
166    pub fn with_method_response(mut self, method: impl Into<String>, response: Value) -> Self {
167        self.engine = self.engine.with_method_response(method, response);
168        self
169    }
170
171    /// Add response for queries with specific query string
172    pub fn with_query_response(mut self, query: impl Into<String>, response: Value) -> Self {
173        self.engine = self.engine.with_query_response(query, response);
174        self
175    }
176
177    /// Build the SurrealClient instance with the configured mock engine
178    pub fn build(self) -> crate::SurrealClient {
179        crate::SurrealClient::new(Box::new(self.engine), self.namespace, self.database)
180    }
181}
182
183impl Default for SurrealMockBuilder {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use serde_json::json;
193
194    #[tokio::test]
195    #[should_panic(
196        expected = "executed method any_method({}), but allowed patterns are: NONE - no patterns configured!"
197    )]
198    async fn test_mock_engine_no_patterns_panics() {
199        let mut engine = MockSurrealEngine::new();
200        let _result = engine.send_message("any_method", json!({})).await.unwrap();
201    }
202
203    #[tokio::test]
204    async fn test_mock_engine_method_response() {
205        let mut engine =
206            MockSurrealEngine::new().with_method_response("query", json!([{"name": "John"}]));
207
208        let result = engine.send_message("query", json!({})).await.unwrap();
209        assert_eq!(result, json!([{"name": "John"}]));
210    }
211
212    #[tokio::test]
213    #[should_panic(expected = "executed method select({}), but allowed patterns are: query({})")]
214    async fn test_mock_engine_method_response_panics_on_unmatch() {
215        let mut engine =
216            MockSurrealEngine::new().with_method_response("query", json!([{"name": "John"}]));
217
218        let _result = engine.send_message("select", json!({})).await.unwrap();
219    }
220
221    #[tokio::test]
222    async fn test_mock_engine_exact_query_response() {
223        let mut engine = MockSurrealEngine::new()
224            .with_query_response("SELECT * FROM users", json!([{"type": "user"}]));
225
226        let result = engine
227            .send_message("query", json!(["SELECT * FROM users"]))
228            .await
229            .unwrap();
230        assert_eq!(result, json!([{"type": "user"}]));
231    }
232
233    #[tokio::test]
234    async fn test_mock_engine_exact_response() {
235        let mut engine = MockSurrealEngine::new().with_exact_response(
236            "custom",
237            json!({"param": "value"}),
238            json!({"result": "success"}),
239        );
240
241        let result = engine
242            .send_message("custom", json!({"param": "value"}))
243            .await
244            .unwrap();
245        assert_eq!(result, json!({"result": "success"}));
246
247        // This should panic because parameters don't match exactly
248        let should_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
249            tokio::runtime::Runtime::new().unwrap().block_on(async {
250                engine
251                    .send_message("custom", json!({"param": "different"}))
252                    .await
253            })
254        }));
255        assert!(should_panic.is_err());
256    }
257
258    #[tokio::test]
259    async fn test_surreal_mock_builder() {
260        let db = SurrealMockBuilder::new()
261            .with_query_response("SELECT * FROM users", json!([{"name": "Alice"}]))
262            .build();
263
264        // Test that we can execute the exact matching query
265        let result = db.query("SELECT * FROM users", None).await.unwrap();
266        assert_eq!(result, json!([{"name": "Alice"}]));
267    }
268
269    #[tokio::test]
270    #[should_panic(
271        expected = "executed method query([\"SELECT * FROM posts\"]), but allowed patterns are"
272    )]
273    async fn test_surreal_mock_builder_panics_on_unmatch() {
274        let db = SurrealMockBuilder::new()
275            .with_query_response("SELECT * FROM users", json!([{"name": "Alice"}]))
276            .build();
277
278        // This should panic because we're querying "posts" but only "users" is configured
279        let _result = db.query("SELECT * FROM posts", None).await.unwrap();
280    }
281
282    #[test]
283    fn test_exact_matching_only() {
284        let engine = MockSurrealEngine::new()
285            .with_query_response("SELECT name FROM users", json!([{"name": "Alice"}]))
286            .with_query_response(
287                "SELECT * FROM users",
288                json!([{"name": "Alice", "email": "alice@example.com"}]),
289            );
290
291        // Test that only exact matches work
292        let key1 = ("query".to_string(), json!(["SELECT name FROM users"]));
293        let key2 = ("query".to_string(), json!(["SELECT * FROM users"]));
294        let key3 = ("query".to_string(), json!(["SELECT name FROM posts"]));
295
296        assert!(engine.exact_matches.contains_key(&key1));
297        assert!(engine.exact_matches.contains_key(&key2));
298        assert!(!engine.exact_matches.contains_key(&key3));
299    }
300
301    #[test]
302    fn test_different_parameter_types() {
303        let engine = MockSurrealEngine::new()
304            .with_exact_response("method1", json!({}), json!("empty"))
305            .with_exact_response("method1", json!([]), json!("array"))
306            .with_exact_response("method1", json!({"key": "value"}), json!("object"));
307
308        // All three should be different keys
309        assert_eq!(engine.exact_matches.len(), 3);
310
311        let key1 = ("method1".to_string(), json!({}));
312        let key2 = ("method1".to_string(), json!([]));
313        let key3 = ("method1".to_string(), json!({"key": "value"}));
314
315        assert_eq!(engine.exact_matches.get(&key1), Some(&json!("empty")));
316        assert_eq!(engine.exact_matches.get(&key2), Some(&json!("array")));
317        assert_eq!(engine.exact_matches.get(&key3), Some(&json!("object")));
318    }
319}